import $ from 'jquery'; import * as JSX from './jsx'; import * as api from './api'; import * as user from './user'; import * as util from './util'; import { Spinner, OkCancelPopup, addPopup, OkPopup } from './components'; let chat_id = ''; let chat_name = 'new chat'; // todo: use or rm export let msglog: Msg[] = []; export let file_list: MsgFile[] = []; let hljs = null; // todo: send done msg from server when tool limit is reached and dont append line // to prevent empty spinner let needs_append = false; let in_code_block = false; let in_reason_block = false; let was_in_reason = false; let reason_buf = ''; let code_block_buf = ''; let tool_buffer = ''; export interface MsgFile { name: string, type: string, content: string }; export interface Msg { content: string, role: string, timestamp: string, toolCall?: string, images?: string[], files?: MsgFile[], }; export interface ClientOptions { seed?: number, temperature?: number, }; export interface ChatReq { model: string, messages: Msg[], system: string, chatfile?: string, options?: ClientOptions, generateTitle: boolean, token: string }; export interface ToolCall { name: string, parameters: any }; interface ChatJsonRes { done: boolean, chunks?: string[], msgs?: Msg[], title?: string, err?: string, }; function getChatId() { const url = new URL( window.location.href ); const id = url.searchParams.get( 'id' ); if( !id ) { chat_id = ''; msglog = []; return null; } if( id == '0' ) { chat_id = ''; msglog = []; return null; } chat_id = id.slice(); return chat_id; } export async function createChat() { const res = await user.createChat(); chat_id = res.chatId; chat_name = 'new chat'; JSX.pushParams( { id: chat_id } ); await user.updatePrefs(); // todo : error handle this w text box when component made return getChatId(); } function isToolStr( str: string ) : boolean { const model = api.getModelFromName( user.settings.site_prefs.model || '' ); if( !model ) return false; const trimmed = str.replace( /\s+/g, '' ).toLowerCase(); for( let tool in model.capabilities ) { const str = `{"name":"${tool.toLowerCase()}"`; let matched = true; for( let i = 0; i < Math.min( trimmed.length, str.length ); ++i ) { if( trimmed[i] !== str[i] ) { matched = false; break; } } if( matched ) return true; } return false; } let chunk_buf = ''; function parseChatJson( rawjson: string, is_done: boolean ) : ChatJsonRes { let msgs = rawjson.split( '\n' ); let clean_done = false; let ret_msgs: Msg[] = []; let ret_chunks: string[] = []; let title: string = ''; for( let raw of msgs ) { if( !raw.length ) continue; let json = null; try { json = JSON.parse( chunk_buf + raw ); } catch( e: any ) { chunk_buf += raw; return { done: false }; } chunk_buf = ''; if( json.done ) clean_done = true; if( !clean_done && is_done ) return { done: true, err: "the stream was closed abruptly. please try again." }; else if( json.status == 'error' || json.status == 'busy' ) return { done: true, err: json.msg }; if( json.status != 'ok' ) return { done: true, err: json.msg || 'unknown error' }; let chunk: string = json.response; if( json.finalMsg && json.finalMsg.length > 0 ) { ret_msgs.push( { timestamp: (new Date()).toLocaleString(), role: 'assistant', content: json.finalMsg } ); if( json.title && !title.length ) title = json.title; } else if( json.tool ) { ret_msgs.push( { timestamp: (new Date()).toLocaleString(), role: 'tool', content: chunk } ); } else { ret_chunks.push( chunk ); } } return { done: clean_done || is_done, title: title.length > 0 ? title : null, msgs: ret_msgs, chunks: ret_chunks }; } function appendUserLine( msg: string, files?: MsgFile[] ) { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; $( "#terminal-inner" ).append(
{ user.settings.nickname }
{ msg }
{ files && }
); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } function appendModelLine() { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; $( "#terminal-inner" ).append(
{ user.settings.site_prefs.model || 'err' }
); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } // inserts a new text div function appendText() { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; $( ".chat-line" ).last().append(
); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } // inserts a new code div function appendCode( lang?: string ) { if( lang ) { $( ".chat-line" ).last().append(
        
{ lang }
); } else { $( ".chat-line" ).last().append(
); } } function ToolInfo( props: any ) { return
{ props.title }
{ props.collapsed ? '+' : '-' }
} function appendReason() { const collapseToggle = () => { let list = el.find( '.reason-output' ); let btn = el.find( '.tool-call-collapse' ); const open = list.css( 'display' ) == 'block'; if( !open ) { list.css( 'display', 'block' ); btn.text( '-' ); } else { list.css( 'display', 'none' ); btn.text( '+' ); } }; const el = $(
); const ti = $( ); ti.find( ".tool-call-title" ).append( ); el.append( ti ); el.append( ); $( ".chat-line" ).last().find( ".chat-content" ).replaceWith( el ); } function outputReason( buf: string ) { const el = $( ".reason-output" ).last(); const txt = el.text(); el.text( txt + buf ); } function outputTool( tool: ToolCall, appendLine?: boolean ) { const content = $( ".chat-line" ).last().find( ".chat-content" ); if( needs_append || content.text().length > 1 ) appendModelLine(); const collapseToggle = () => { let list = el.find( '.tool-call-list' ); let btn = el.find( '.tool-call-collapse' ); const open = list.css( 'display' ) == 'block'; if( !open ) { list.css( 'display', 'block' ); btn.text( '-' ); } else { list.css( 'display', 'none' ); btn.text( '+' ); } }; const el = $(
); const trimmed = tool.name.replace( /\s+/g, '' ); switch( trimmed.toLowerCase() ) { case 'notes': el.append( ); el.append(
{ tool.parameters.contents || '' }
); break; case 'web': el.append( ); el.append(
{ tool.parameters.url || '' }
); if( appendLine ) needs_append = true; break; case 'remind': el.append( ); el.append(
{ tool.parameters.keywords ? tool.parameters.keywords.join( ' ' ) : '' }
); if( appendLine ) needs_append = true; break; } if( in_reason_block ) { $( ".chat-line" ).last().find( ".tool-call" ).find( ".spinner" ).remove(); $( ".chat-line" ).last().append( el ); appendModelLine(); needs_append = false; appendReason(); } else { $( ".chat-line" ).last().find( ".chat-content" ).replaceWith( el ); } } function outputError( e: string ) { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; if( needs_append ) { appendModelLine(); needs_append = false; } let el = $( ".chat-line" ).last(); el.empty(); el.append(
{ e }
); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } function outputChunk( chunk: string ) { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; if( needs_append ) { appendModelLine(); needs_append = false; } let el = $( ".chat-line" ).last().find( ".chat-content" ).last(); el.find( '.spinner' ).remove(); el.text( el.text() + chunk ); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } function outputCode( chunk: string ) { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; if( needs_append ) { appendModelLine(); needs_append = false; } let el = $( ".chat-line" ).last().find( "code" ).last(); el.find( '.spinner' ).remove(); el.text( el.text() + chunk ); if( hljs ) hljs.highlightElement( el[0] ); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } function checkToolBuffer( chunk: string ) { tool_buffer += chunk; let tool: ToolCall | null = null; if( isToolStr( tool_buffer ) ) { try { tool = JSON.parse( tool_buffer ); outputTool( tool, true ); tool_buffer = ''; } catch( e: any ) {} return true; } let buf = tool_buffer.slice(); tool_buffer = ''; return buf; } /// returns 1 and sets in_reason_block if the msg starts with a reason token /// returns -1 if the msg does not match a reason token /// returns 0 if the reason token is partially completed function checkReasonBuffer( chunk: string ) { const buf = chunk.toLowerCase(); const wanted_begin = ''; if( !in_reason_block ) { if( buf == wanted_begin || (buf.length > wanted_begin.length && buf.startsWith( wanted_begin )) ) { appendReason(); in_reason_block = true; return 1; } if( wanted_begin.startsWith( buf ) ) return 0; return -1; } const wanted_end = ''; if( buf == wanted_end || (buf.length > wanted_end.length && buf.startsWith( wanted_end )) ) { $( ".chat-line" ).last().append(
{ '\n' }
); $( ".tool-call" ).last().find( ".spinner" ).remove(); in_reason_block = false; was_in_reason = true; return 1; } if( wanted_end.endsWith( buf ) ) return 0; return -1; } // todo: clean, static return type. maybe nullable string function checkCodeBuffer( chunk: string ) { const backtick = chunk.search( '`' ); if( backtick != -1 ) { code_block_buf += chunk.slice( backtick ); return true; } let buf = chunk; let parts = chunk.split( '\n' ); if( code_block_buf.startsWith( '```' ) ) { if( parts.length <= 1 ) return true; buf = parts[1]; if( !in_code_block ) { appendCode(); const lang = parts[0].substring( 3 ); if( lang.length > 0 ) { $( ".chat-line" ).last().find( "code" ).insertBefore(
{ lang }
); } } else appendText(); in_code_block = !in_code_block; code_block_buf = ''; } return buf; } function resetBuffers() { in_reason_block = false; in_code_block = false; was_in_reason = false; needs_append = false; reason_buf = ''; tool_buffer = ''; code_block_buf = ''; } // returns true when stream completes function handleChatJson( res: ChatJsonRes ) : boolean { const model = api.getModelFromName( user.settings.site_prefs.model! ); tool_buffer = ''; if( res.err ) { outputError( res.err ); } else if( res.chunks ) { for( let chunk of res.chunks ) { let buf = checkToolBuffer( chunk ); if( buf === true ) continue; if( model.capabilities.thinker ) { const reason = checkReasonBuffer( buf ); if( reason > -1 ) continue; if( in_reason_block ) { outputReason( buf ); continue; } } if( was_in_reason ) { while( buf.startsWith( '\n' ) ) buf = buf.slice( 1 ); } // todo: fix, this is str8 fkn voodoo . buf = checkCodeBuffer( buf ); if( buf === true ) continue; if( in_code_block ) outputCode( buf ); else outputChunk( buf ); } } if( res.msgs ) { for( let msg of res.msgs ) { msglog.push( msg ); } } if( res.title ) { user.updatePrefs().then( () => { $( "#chat-list" ).replaceWith( ); }).catch( ( e: any ) => { outputError( e.msg ); }); updateChatTitle( res.title ); } return res.done; } function makeChatReq() : ChatReq { return { model: user.settings.site_prefs.model!, messages: msglog, system: user.settings.prompt_data.system || '', chatfile: getChatId(), token: localStorage.getItem( 'session' ), generateTitle: true }; } export async function sendChatReq( cur_chat: string ) { let res = null; try { const req = makeChatReq(); res = await fetch( `${api.url}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( req ) } ); } catch( e: any ) { return outputError( e.message ); } if( !res.ok ) { let json = null; try{ json = await res.json(); } catch { return outputError( `error contacting server (${res.statusCode}). are you connected to the internet?` ); } return outputError( json.msg ); } if( chat_id != cur_chat ) return; const reader = res.body?.getReader(); const decoder = new TextDecoder(); if( !reader ) return outputError( "can't read response" ); resetBuffers(); return new Promise( ( resolve ) => { const readLoop = async() => { if( chat_id != cur_chat ) return resolve( true ); const { value, done } = await reader.read(); const raw = decoder.decode( value, { stream: true } ); const parsed = parseChatJson( raw, done ); if( handleChatJson( parsed ) ) return resolve( true ); setTimeout( readLoop, 50 ); } setTimeout( readLoop, 50 ); } ); } function disableInput() { $( "#chat-input" ).remove(); } function appendInput() { const terminal = $( "#terminal-inner" ); const isbottom = terminal[0].scrollTop == (terminal[0] as any).scrollTopMax; $( "#terminal-inner" ).append( ); if( isbottom ) terminal.scrollTop( terminal[0].scrollHeight ); } export async function onChat( msg: string ) { disableInput(); appendUserLine( msg, file_list ); appendModelLine(); let id = getChatId(); if( !id ) { try { id = await createChat() } catch( e: any ) { return outputError( "error creating chat: " + e.message ); } } if( file_list.length ) { msglog.push( { timestamp: (new Date()).toLocaleString(), content: msg, role: 'user', files: file_list } ); } else { msglog.push( { timestamp: (new Date()).toLocaleString(), content: msg, role: 'user' } ); } file_list = []; await sendChatReq( id ); if( getChatId() == id ) appendInput(); } async function getChatHistory() { if( !getChatId() ) throw new Error( 'chat_id not set' ); try { const res = await api.post( 'get-chat', { token: localStorage.getItem( 'session' ), chatId: chat_id } ); return res; } catch( e: any ) {} // todo: handle error ? return null; } function updateChatTitle( name: string ) { $( "#chat-name" ).text( name.substring( 0, 500 ) ); let cur_name = name; if( cur_name.length > 25 ) { cur_name = cur_name.substring( 0, 25 ) + "..."; } $( "#chat-selection-current" ).text( cur_name ); } function outputChatTextForHistory( msg: string ) { let chunks = msg.split( "```" ); let in_code = false; for( let chunk of chunks ) { if( !in_code ) { appendText(); outputChunk( chunk ); } else { const newl = chunk.indexOf( "\n" ); let lang = null; if( newl != -1 ) { lang = chunk.substring( 0, newl ); chunk = chunk.substring( newl + 1 ); } appendCode( lang ); outputCode( chunk ); } in_code = !in_code; } } function outputToolForHistory( msg: Msg ) { let cur_idx = 0; let open = -1; let close = msg.content.lastIndexOf( '}' ); open = msg.content.indexOf( "{", cur_idx ); // why is this assigned here & not on def ? also, cur_idx will never not be 0 ??? if( open == -1 || close == -1 ) { outputChatTextForHistory( msg.content ); return; } let slice = msg.content.substring( open, close + 1 ); try { const parsed = JSON.parse( slice ); if( open != 0 ) outputChatTextForHistory( msg.content.substring( 0, open ) ); if( parsed.name ) outputTool( parsed ); else outputChatTextForHistory( msg.content ); } catch( e ) { outputChatTextForHistory( msg.content ); } } function outputChatHistory( history: Msg[] ) { for( let msg of history ) { if( msg.role == 'assistant' ) { appendModelLine(); const content = $( ".chat-line" ).last().find( ".chat-content" ); content.empty(); if( msg.toolCall ) { outputToolForHistory( msg ); } else { outputChatTextForHistory( msg.content ); } } else if( msg.role == 'user' ) { appendUserLine( msg.content, msg.files ); } } setTimeout( () => { const inner = $( "#terminal-inner" ); inner.scrollTop( inner.prop( "scrollTopMax" ) ); if( history.length > 0 ) { $( "#suggested-prompts" ).remove(); } } ); } // don't ask me what the fuck is happening here, this is some javascript voodoo // - nave async function onInputKeyPress( e: KeyboardEvent ) { let self = $( e.target as HTMLElement ); const text = self[0].innerText; $( "#suggested-prompts" ).remove(); if( e.key == 'Enter' ) { if( e.shiftKey ) return; e.preventDefault(); e.stopPropagation(); if( text.length > 0 ) { await onChat( text ); } } } function handleAttachments() { $( "#cmd-files" ).remove(); $( "#cmd" ).append( ); } function onFileReadEvent( e: Event, img: boolean, name: string ) { let contents = (e.target as any).result; let b64 = contents.split( 'base64,' )[1]; if( img ) { return; } let txt = atob( b64 ); if( txt.length > 1024 * 20 ) // todo: arbitrary change prolly return; file_list.push( { name, type: 'text', content: txt } ); handleAttachments(); } async function onPaste( e: ClipboardEvent ) { const model = api.getModelFromName( user.settings.site_prefs.model ); if( !model ) return; const vision = !!model.capabilities.vision; const items = e.clipboardData.items; if( !items || !items.length ) return; for( let it of items ) { if( it.type == 'text/html' ) continue; if( it.kind.startsWith( 'string' ) ) { let s = e.clipboardData.getData( 'text' ); if( s.length > 512 ) { e.preventDefault(); file_list.push( { name: `file${file_list.length}.txt`, content: s, type: 'text' } ); handleAttachments(); } } else { let isImg = false; if( it.type.startsWith( 'image' ) ) { if( !vision ) continue; isImg = true; } let blob = it.getAsFile(); let reader = new FileReader(); reader.onload = ( e ) => onFileReadEvent( e, isImg, blob.name ); reader.readAsDataURL( blob ); e.preventDefault(); } } } function removeFile( key: number ) { file_list.splice( key, 1 ); handleAttachments(); } function ChatFile( props: any ) { return
{ props.name.toString() }  { util.sizeHumanReadable( props.size ) }  { props.key !== undefined && removeFile( props.key ) } href="#" > remove }
} function ChatFiles( props: any ) { if( !props.files.length ) return
const show_remove = props.noremove === undefined; return
Attachments:
{ props.files.map( ( f: MsgFile, i: number ) => { if( !show_remove ) return return } ) }
} export function ChatCmdLine() { return

} export function ChatInput( props: any ) { const task = async () => { const chat_id = getChatId(); if( !chat_id ) { return; } disableInput(); $( "#terminal-inner" ).empty(); const spinner = $( ); $( "#terminal-inner" ).append( spinner ); const chat = await getChatHistory(); if( !chat ) { msglog = []; spinner.remove(); return appendInput(); } try { msglog = JSON.parse( chat.contents ); outputChatHistory( JSON.parse( chat.contents ) ); } catch( e: any ) { msglog = []; } updateChatTitle( chat.name ); spinner.remove(); setTimeout( appendInput ); }; if( !props.noupdate ) setTimeout( task ); const ret = $(
{ user.settings.nickname }: 
); return ret[0]; } function onSidebarHover() { $( "#chat-selection-sidebar" ).show(); } function onSidebarLeave() { $( "#chat-selection-sidebar" ).hide(); } async function deleteChat( id: string ) { try { await user.deleteChat( id ); await user.updatePrefs(); if( id == getChatId() ) JSX.navigateParams( "/terminal", {} ); else { $( "#chat-list" ).replaceWith( ); } const popup = $( Chat deleted. ); addPopup( popup ); } catch( e: any ) { const popup = $( Error deleting chat: { e.message } ); addPopup( popup ); } } function showDeletePopup( id: string, name: string ) { const popup = $( { deleteChat( id ) } }>
Are you sure you want to delete {name}?
); addPopup( popup ); } function ChatListEntry( props: any ) { let title = props.title; if( title.length > 20 ) { title = title.substring( 0, 20 ) + '...'; } return
JSX.navigateParams( "/terminal", props.id != '0' ? { id: props.id } : {} ) }> { title }
{ (() => { if( props.id != 0 ) return })() }
} let hljs_imported = 0; async function importHlJs() { if( hljs ) return; if( hljs_imported ) return; hljs_imported = 1; hljs = ( await import( 'highlight.js' ) ).default; $( ".chat-line.code" ).each( ( e ) => { hljs.highlightElement( e[0] ); } ); } export function ChatList() { if( !hljs ) { importHlJs(); } setTimeout( () => { if( !user.settings.chat_files || !user.settings.chat_files.files ) return; const files = user.settings.chat_files.files; const entries = files.reverse().map( ( file: user.ChatFile ) => { return } ); for( let e of entries ) $( "#chat-selection-sidebar" ).append( e ); } ); return
current chat
new chat
}