summaryrefslogtreecommitdiff
path: root/moneyjsx/src/terminal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'moneyjsx/src/terminal.tsx')
-rw-r--r--moneyjsx/src/terminal.tsx275
1 files changed, 275 insertions, 0 deletions
diff --git a/moneyjsx/src/terminal.tsx b/moneyjsx/src/terminal.tsx
new file mode 100644
index 0000000..bc4657d
--- /dev/null
+++ b/moneyjsx/src/terminal.tsx
@@ -0,0 +1,275 @@
+import $ from 'jquery';
+import * as JSX from './jsx';
+import * as api from './api';
+import * as user from './user';
+import * as chat from './chat';
+
+import { Page } from './components';
+import { ChatInput, ChatList } from './chat';
+
+let start_w = 0, start_h = 0;
+let start_mx = 0, start_my = 0;
+let is_resizing = false;
+let has_listener = false;
+
+function startResize( e: MouseEvent ) {
+ const terminal = $( "#terminal" );
+ start_mx = e.pageX;
+ start_my = e.pageY;
+
+ if( terminal[0].style.width )
+ start_w = parseInt( terminal[0].style.width );
+ else
+ start_w = terminal[0].clientWidth;
+
+ if( terminal[0].style.height )
+ start_h = parseInt( terminal[0].style.height );
+ else
+ start_h = terminal[0].clientHeight * 1.05;
+
+ window.addEventListener( "touchmove", resize );
+ window.addEventListener( "touchend", saveSize );
+ window.addEventListener( "touchcancel", saveSize );
+ window.addEventListener( "mousemove", resize );
+ window.addEventListener( "mouseup", saveSize );
+ is_resizing = true;
+}
+
+function resize( e: MouseEvent ) {
+ if( !is_resizing )
+ return;
+
+ let new_w = start_w + ( e.pageX - start_mx ) * 2.0;
+ let new_h = start_h + ( ( e.pageY - start_my ) * 1.05 );
+
+ const body = $( "body" );
+ const max_h = body.outerHeight() - 60;
+
+ if( new_h > max_h )
+ new_h = max_h;
+
+ const terminal = $( "#terminal" );
+
+ terminal.css( `width`, `${ new_w }px` );
+ terminal.css( `height`, `${ new_h }px` );
+}
+
+function saveSize() {
+ if( !is_resizing )
+ return;
+
+ const terminal = $( "#terminal" );
+ let size = {
+ width: terminal[0].style.width,
+ height: terminal[0].style.height,
+ };
+
+ is_resizing = false;
+ localStorage.setItem( "terminal-size", JSON.stringify( size ) );
+
+ window.removeEventListener( "touchmove", resize );
+ window.removeEventListener( "touchend", saveSize );
+ window.removeEventListener( "touchcancel", saveSize );
+ window.removeEventListener( "mousemove", resize );
+ window.removeEventListener( "mouseup", saveSize );
+}
+
+function getStyleForSize() {
+ let style = "";
+ if( window.innerWidth > 768 ) {
+ const size_settings = localStorage.getItem( "terminal-size" );
+ if( size_settings ) {
+ const parsed = JSON.parse( size_settings );
+ style = `width: ${ parsed.width }; height: ${ parsed.height };`;
+ }
+ }
+ else {
+ style = `width: 95%; height: ${Math.floor( window.innerHeight - 130 )}px`;
+ }
+
+ return style;
+}
+
+function onWindowResize() {
+ const terminal = $( "#terminal" );
+ const style = getStyleForSize();
+ terminal.attr( "style", style );
+
+ if( window.innerWidth < 768 )
+ $( "#terminal-resizer" ).hide();
+ else
+ $( "#terminal-resizer" ).show();
+}
+
+function focusInput( e: Event ) {
+ const sel = window.getSelection();
+ if( sel && sel.type == 'Range' )
+ return;
+
+ const input = $( "#cmd-input" );
+ const content = input.find( "#input-content" );
+ if( !input.length || !content.length )
+ return;
+
+ for( let iclass of ( e.target as HTMLElement )?.classList ) {
+ if( iclass.startsWith( "tool-call" ) )
+ return;
+ }
+
+ input[0].focus();
+
+ // move cursor to the end of text
+ if( sel.anchorNode != content[0] && sel.anchorNode.parentElement != content[0] ) {
+ const range = document.createRange();
+ if( content.length ) {
+ const child = content[0].firstChild;
+ if( child ) {
+ range.setStart( child, 0 );
+ range.setEnd( child, child.textContent.length );
+ }
+ else {
+ range.selectNodeContents( content[0] );
+ }
+ range.collapse( false );
+
+ sel.removeAllRanges();
+ sel.addRange( range );
+ }
+ }
+}
+
+function TerminalResizer( props: any ) {
+ return <div id="terminal-resizer"
+ onmousedown={ startResize }
+ ontouchstart={ startResize }
+ style={ props.style || '' }
+ />
+}
+
+let promptc = 0;
+function writePrompt( promptTxt: string ) {
+ let el = $(
+ <div id={ `prompt-${promptc++}` } onclick={ () => inputPrompt( promptTxt ) }>
+ <a href="#"></a>
+ </div>
+ );
+
+ let writeChar = ( str: string, i: number ) => {
+ if( i >= str.length )
+ return;
+
+ let char = str.charAt( i );
+ let link = el.find( 'a' );
+ let text = link.text();
+ link.text( text + char );
+ setTimeout( () => writeChar( str, i + 1 ), 50 );
+ };
+
+ writeChar( promptTxt, 0 );
+ $( '#suggested-prompts' ).append( el );
+}
+
+async function getPrompts() {
+ const prompts_req = await fetch( `${window.location.origin}/static/prompts.json` );
+ const data = await prompts_req.json();
+ const { prompts } = data;
+
+ const shuffled = prompts.sort( () => Math.random() - 0.5 );
+ const random_prompts = shuffled.slice( 0, 5 );
+
+ random_prompts.forEach( ( p: string ) => {
+ writePrompt( p );
+ } );
+}
+
+function inputPrompt( txt: string ) {
+ const input = $( "#cmd-input" );
+ const el = input[0] as HTMLInputElement;
+
+ input.text( txt );
+ input[0].focus();
+
+ let range = document.createRange()
+ let sel = window.getSelection()
+
+ range.setStart( el.childNodes[0], txt.length );
+ range.collapse( true )
+
+ sel.removeAllRanges()
+ sel.addRange( range )
+}
+
+function SuggestedPrompts() {
+ setTimeout( () => {
+ if( !chat.msglog.length )
+ getPrompts();
+ } );
+
+ return <div id="suggested-prompts">
+ </div>
+}
+
+function TerminalWindow() {
+ if( !has_listener ) {
+ window.onresize = onWindowResize;
+ has_listener = true;
+ }
+
+ const style = getStyleForSize();
+ return <div id="terminal" style={ style } onclick={ focusInput }>
+ <div id="terminal-header">
+ <div>
+ <div id="chat-name">new chat</div>
+ <a href="#" onclick={ () => JSX.navigateParams( "/terminal", {} ) }>X</a>
+ </div>
+ </div>
+ <div id="terminal-inner">
+ <ChatInput />
+ <SuggestedPrompts />
+ </div>
+ <div>
+ <TerminalResizer style={ window.innerWidth <= 768 ? "display: none" : "" } />
+ </div>
+ </div>
+}
+
+function ModelCapabilities() {
+ const model = api.getModelFromName( user.settings.site_prefs.model! );
+ if( !model )
+ return <div />
+
+ const capabilities = model.capabilities;
+ if( !capabilities )
+ return <div />
+
+ let model_str = `${model.name} | `;
+ model_str += `vision ${capabilities.vision ? '✔' : '✘'} | `;
+ model_str += `web ${capabilities.web ? '✔' : '✘'} | `;
+ model_str += `notes ${capabilities.notes ? '✔' : '✘'} | `;
+ model_str += `memory lookup ${capabilities.remind ? '✔' : '✘'}`;
+ // todo: later
+ // model_str += ` | reasoning ${capabilities.reasoning ? '✔' : '✘'}`;
+
+ return <div id="model-capabilities">
+ { model_str }
+ </div>
+}
+
+export function updateCapabilitiesDisplay() {
+ const div = $( "#model-capabilities" );
+ if( div.length > 0 )
+ div.replaceWith( <ModelCapabilities /> );
+}
+
+export default function Terminal() {
+ if( !user.is_loggedin ) {
+ setTimeout( () => JSX.navigate( "/" ) );
+ return <Page>not logged in</Page>
+ }
+
+ return <Page>
+ <TerminalWindow />
+ <ModelCapabilities />
+ <ChatList />
+ </Page>
+}