diff options
| author | aura <nw@moneybot.cc> | 2026-02-17 22:39:42 +0100 |
|---|---|---|
| committer | aura <nw@moneybot.cc> | 2026-02-17 22:39:42 +0100 |
| commit | 636b0323075225c584b62719ed51e75521bb7ffb (patch) | |
| tree | 61b02271b6d0695a4beffc23fb6eb062a7da22c3 /moneyjsx/src/terminal.tsx | |
push source
Diffstat (limited to 'moneyjsx/src/terminal.tsx')
| -rw-r--r-- | moneyjsx/src/terminal.tsx | 275 |
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> +} |
