From 636b0323075225c584b62719ed51e75521bb7ffb Mon Sep 17 00:00:00 2001 From: aura Date: Tue, 17 Feb 2026 22:39:42 +0100 Subject: push source --- moneyjsx/src/api.tsx | 80 +++ moneyjsx/src/ascii-art.tsx | 165 ++++++ moneyjsx/src/chat.tsx | 1023 +++++++++++++++++++++++++++++++++++++ moneyjsx/src/components.tsx | 238 +++++++++ moneyjsx/src/first-landing.tsx | 55 ++ moneyjsx/src/header.tsx | 98 ++++ moneyjsx/src/home.tsx | 90 ++++ moneyjsx/src/index-page.tsx | 32 ++ moneyjsx/src/index.html | 93 ++++ moneyjsx/src/jsx.tsx | 239 +++++++++ moneyjsx/src/login.tsx | 29 ++ moneyjsx/src/payment-success.tsx | 19 + moneyjsx/src/settings.tsx | 402 +++++++++++++++ moneyjsx/src/support-api.tsx | 214 ++++++++ moneyjsx/src/support-models.tsx | 114 +++++ moneyjsx/src/support-tos.tsx | 69 +++ moneyjsx/src/support-tutorial.tsx | 156 ++++++ moneyjsx/src/support.tsx | 18 + moneyjsx/src/terminal.tsx | 275 ++++++++++ moneyjsx/src/tsconfig.json | 121 +++++ moneyjsx/src/upgrade.tsx | 206 ++++++++ moneyjsx/src/user.tsx | 261 ++++++++++ moneyjsx/src/util.tsx | 83 +++ 23 files changed, 4080 insertions(+) create mode 100644 moneyjsx/src/api.tsx create mode 100644 moneyjsx/src/ascii-art.tsx create mode 100644 moneyjsx/src/chat.tsx create mode 100644 moneyjsx/src/components.tsx create mode 100644 moneyjsx/src/first-landing.tsx create mode 100644 moneyjsx/src/header.tsx create mode 100644 moneyjsx/src/home.tsx create mode 100644 moneyjsx/src/index-page.tsx create mode 100644 moneyjsx/src/index.html create mode 100644 moneyjsx/src/jsx.tsx create mode 100644 moneyjsx/src/login.tsx create mode 100644 moneyjsx/src/payment-success.tsx create mode 100644 moneyjsx/src/settings.tsx create mode 100644 moneyjsx/src/support-api.tsx create mode 100644 moneyjsx/src/support-models.tsx create mode 100644 moneyjsx/src/support-tos.tsx create mode 100644 moneyjsx/src/support-tutorial.tsx create mode 100644 moneyjsx/src/support.tsx create mode 100644 moneyjsx/src/terminal.tsx create mode 100644 moneyjsx/src/tsconfig.json create mode 100644 moneyjsx/src/upgrade.tsx create mode 100644 moneyjsx/src/user.tsx create mode 100644 moneyjsx/src/util.tsx (limited to 'moneyjsx/src') diff --git a/moneyjsx/src/api.tsx b/moneyjsx/src/api.tsx new file mode 100644 index 0000000..4ca79c2 --- /dev/null +++ b/moneyjsx/src/api.tsx @@ -0,0 +1,80 @@ +// todo: change this +export const url = "https://api.axonbox.net"; +export let models: Model[] = []; + +export interface ReqParams { + method: string, + body?: string, +} + +export interface Model { + name: string, + capabilities: any, + description: { + full: string, + short: string, + }, + license: string, + free: number +} + +export async function post( endpoint: string, body: Object ) { + return await req( endpoint, { + method: "POST", + body: JSON.stringify( body ), + } ); +} + +export async function req( endpoint: string, params: ReqParams ) { + const res = await fetch( `${url}/${endpoint}`, { + method: params.method, + headers: { + "Content-Type": "application/json", + }, + body: params.body, + } ); + + if( !res.ok ) { + let json = null; + try { + json = await res.json(); + } catch( e: any ) { + throw new Error( "error contacting server" ); + } + + throw new Error( json.msg ); + } + + const json = await res.json(); + if( json.status != 'ok' ) + throw new Error( json.msg ); + return json; +} + +export async function updateModels() { + parseModels(); + + try { + const res = await post( 'models', {} ); + models = res.models as Model[]; + localStorage.setItem( 'models', JSON.stringify( models ) ); + } catch( e: any ) { + throw new Error( e.message ); + } +} + +export function getModelFromName( name: string ) { + for( let model of models ) { + if( model.name === name ) + return model; + } + + return null; +} + +/** + * parses existing models from localStorage +**/ +export function parseModels() { + models = JSON.parse( localStorage.getItem( 'models' ) || '[]' ) as Model[]; +} diff --git a/moneyjsx/src/ascii-art.tsx b/moneyjsx/src/ascii-art.tsx new file mode 100644 index 0000000..d2f53b3 --- /dev/null +++ b/moneyjsx/src/ascii-art.tsx @@ -0,0 +1,165 @@ +import * as JSX from "./jsx"; +import $ from "jquery"; + +export default function AsciiArt() { + const ret =
+ { asciiArtStr } +
; + + setTimeout( doAsciiArt ); + return ret; +} + +let startTime = 0.0; +let anim = 0.0; + +function animFunc( a: number ) { + let ringRadius = anim; + let radius = a; + + let dist = ringRadius - radius; + + dist = Math.abs( dist ); + + let ret = Math.pow( dist, 0.1 + 0.8 * Math.pow( (anim + 0.5) / 2, 1.6 ) ); + let remain = ret % 0.12; + return ret - remain; +} + +function hexToRgb( hex: string ) { + let r = parseInt( hex.slice( 1, 3 ), 16 ); + let g = parseInt( hex.slice( 3, 5 ), 16 ); + let b = parseInt( hex.slice( 5, 7 ), 16 ); + return [ r, g, b ]; +} + +function lerpColor( c1: number[], c2: number[], t: number ) { + let r1 = c1[0], g1 = c1[1], b1 = c1[2]; + let r2 = c2[0], g2 = c2[1], b2 = c2[2]; + let r = r1 + ( r2 - r1 ) * t; + let g = g1 + ( g2 - g1 ) * t; + let b = b1 + ( b2 - b1 ) * t; + return [ Math.round( r ), Math.round( g ), Math.round( b ) ]; +} + +function getDist( x: number, y: number ) { + const x1 = 0.5 - x / 125; + const y1 = 0.5 - y / 63; + return Math.sqrt( x1 * x1 + y1 * y1 ); +} + +let isDoingAnim = false; +function doAsciiArt() { + let div = $( "#ascii-art" ); + if( !div.length ) { isDoingAnim = false; return; } + isDoingAnim = true; + + let deltaTime = ( Date.now() - startTime ) * 0.001; + startTime = Date.now(); + anim += .5 * deltaTime; + + if( anim > 2 ) + anim = -0.5; + + let rootColor = hexToRgb( $( ':root' ).css( '--front' ) ); + let innerStr = ""; + for( let i = 0; i < asciiArtStr.length; ++i ) { + let x = i % 125; + let y = Math.floor( i / 125 ); + + if( x == 124 ) { + continue; + } + if( asciiArtStr[i] == ',' || asciiArtStr[i] == ' ' || asciiArtStr[i] == '\t' ) { + innerStr += asciiArtStr[i]; + continue; + } + + let dist = getDist( x, y ); + if( dist < 0.133 ) { + innerStr += asciiArtStr[i]; + continue; + } + + let color = []; + if( asciiArtStr[i] == '#' ) { + let a = animFunc( dist * 2 ); + color = lerpColor( rootColor, [255, 255, 255, 1], a ); + color[3] = Math.max( Math.min( 1, Math.pow( dist, 2 ) + (1.0 - a) ), 0.8 ); + } + else { + color = [255, 255, 255, Math.max( 0.05, 1.0 - animFunc( (dist - 0.1) * 2 ) ) ]; + } + // appending string is faster than jsx + innerStr += `${asciiArtStr[i]}`; + } + + div.html( innerStr ); + setTimeout( doAsciiArt, 75 ); +} + +const asciiArtStr = ` + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # .. # # + # # .. # # + # # .### ### + # # ### ### .. + # # # # .... + # # # # .... + # # # # .. + # # # # + # # # # + .. ############################################## + ................................... ###.. ### ........... ### .........### + .................................. ####+############################################ + #######################################+# #.# + #####-# #.# + #################################### # # #.# + # # #.# + ######################################### #.#...................................... + ### #.#...............--..................... + ######################################### | ##################.###################### + # # ,---.. ,,---.,---.|---.,---.. , ### ### ###... + ################ ,---| >< | || || || | >< ################ ### #################### + ########################### .. ### \`---^' \`\`---'\` '\`---'\`---'' \` #.# ... ### ### + ################## #.# ... ####### + ######################### ... # # #.# . ################## + ############################. #.# #.# # # .............. + ###- #.# #-########################............... + ############################- # # #+# ...###...... ...... + .........................##############-# #-########################.... .. + .........................###. #-# #.# ....................... + ..............+########################################################## ...................... + .................###................... ### ### ...### .......... + .............. ############################################## + ....# #..... # # + ... ... # # # # + ................ # # # # + .................. # # # # + .......################## # # . + .....###### ### # # + ####+###################. # # + ######+ .............. # # + # # ................. # # + # # ...... # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # + # # # # +`; diff --git a/moneyjsx/src/chat.tsx b/moneyjsx/src/chat.tsx new file mode 100644 index 0000000..56798ab --- /dev/null +++ b/moneyjsx/src/chat.tsx @@ -0,0 +1,1023 @@ +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 +
+
+
+ +
+} diff --git a/moneyjsx/src/components.tsx b/moneyjsx/src/components.tsx new file mode 100644 index 0000000..ff317af --- /dev/null +++ b/moneyjsx/src/components.tsx @@ -0,0 +1,238 @@ +import $ from "jquery"; +import * as JSX from "./jsx"; +import { Header } from "./header" + +const popup_stack = []; +window.onclick = ( e: Event ) => { + if( !e.target ) return; + + if( popup_stack.length > 0 ) { + const last = popup_stack[popup_stack.length - 1]; + if( last.el[0] == e.target || last.el.find( e.target ).length != 0 ) return; + e.preventDefault(); + e.stopPropagation(); + if( last.fn ) + last.fn(); + + popup_stack.pop(); + last.el.remove(); + } +} + +/** + * appends an element to the DOM and saves it in the popup stack to be removed. +**/ +export function addPopup( element: JQuery ) { + document.body.appendChild( element[0] ); + setTimeout( () => popup_stack.push( { el: element, fn: null } ) ); +} + +/** + * closes the topmost popup +**/ +export function closePopup() { + if( popup_stack.length > 0 ) { + const last = popup_stack[popup_stack.length - 1]; + if( last.fn ) + last.fn(); + + popup_stack.pop(); + setTimeout( () => last.el.remove() ); + } +} + +/** + * sets a callback that will get executed once the current popup is closed + **/ +export function onPopupClosed( fn: Function ) { + setTimeout( () => { + if( popup_stack.length > 0 ) { + popup_stack[popup_stack.length - 1].fn = fn; + } + } ); +} + +/* + * accepts "folded" prop +**/ +export function RolldownListItem( props: any ) { + const ret =
+
+
>
+
{props.folded}
+
+
+
{props.children}
+
+
; + + ret.onclick = () => { + $( ret ).toggleClass( "active" ); + $( ret ).find( ".rolldown-expanded-container" ).toggleClass( 'active' ); + }; + + return ret; +} + +export function Spinner( props: any ) { + let spinner_steps = [ + '/', + '-', + '\\', + '|', + '/', + '-', + '\\', + '|' + ]; + + const id = props.id || ''; + + let el = $(
); + let i = 0; + let loop = () => { + el.text( spinner_steps[i++] ); + if( i > spinner_steps.length ) + i = 0; + + if( el[0].isConnected ) + setTimeout( loop, 100 ); + }; + + setTimeout( loop, 100 ); + return el[0]; +} + +/* + * accepts title prop +**/ +export function RolldownList( props: any ) { + return +
+ { props.children } +
+
+} + +/* + * accepts title prop and optional innerStyle +**/ +export function GroupBox( props: any ) { + return
+ + {props.title} + + + {props.children} + +
+} + +export function Page( props: any ) { + return <> +
+
+ {props.children} +
+ +} + +export function DropdownItem( props: any ) { + return +} + +/** + * supports innerStyle for styling the actual dropdown picker + **/ +export function Dropdown( props: any ) { + const children = props.children; + let title = props.title; + let inline = props.inline; + let onchange = props.onchange; + let classes = props.class || ''; + let style = props.style || ""; + let id = props.id || ''; + let innerStyle = props.innerStyle || ""; + + const showItems = ( e: Event ) => { + e.preventDefault(); + const target = $( e.target as HTMLElement ); + + const newDropdown = $( ); + target.parent().append( newDropdown[0] ); + setTimeout( () => popup_stack.push( { el: newDropdown, fn: null } ) ); + } + + if( inline ) { + return + } + else { + return <> + + + + } +} + +export function OkPopup( props: any ) { + const children = props.children; + const classes = props.class || ''; + const style = props.style || ''; + const id = props.id || ''; + + const onclick = ( e: Event ) => { + e.preventDefault(); + e.stopPropagation(); + if( props.onclick ) + props.onclick(); + + closePopup(); + } + + return
+ { children } +
+ +
+
+} + +export function OkCancelPopup( props: any ) { + const children = props.children; + const classes = props.class || ''; + const style = props.style || ''; + const id = props.id || ''; + + const onclick = ( e: Event ) => { + e.preventDefault(); + e.stopPropagation(); + if( props.onclick ) + props.onclick(); + + closePopup(); + } + + const oncancel = ( e: Event ) => { + e.preventDefault(); + e.stopPropagation(); + + closePopup(); + } + + return
+ { children } +
+ + +
+
+} diff --git a/moneyjsx/src/first-landing.tsx b/moneyjsx/src/first-landing.tsx new file mode 100644 index 0000000..8d01512 --- /dev/null +++ b/moneyjsx/src/first-landing.tsx @@ -0,0 +1,55 @@ +import * as JSX from './jsx'; +import * as user from './user'; + +import $ from 'jquery'; + +import { GroupBox, Page, Spinner } from './components'; + +async function saveNickname() { + const username = $( '#username-input' ).val() as string; + const spinner = $( ); + $( "#confirm-btn" ).append( spinner ); + + if( username.length > 1 ) { + try { + await user.savePrefs( { nickname: username } ); + localStorage.removeItem( 'needs-setup' ); + JSX.navigateParams( "/terminal", {} ); + } catch( e: any ) { + $( "#landing-error" ).text( e.message ); + $( "#landing-error" ).show(); + } + } + + spinner.remove(); +} + +export default function FirstLanding() { + if( user.settings && user.settings.nickname && user.settings.nickname.length > 1 ) + setTimeout( () => JSX.navigateParams( "/terminal", {} ) ); + + if( !user.is_loggedin ) { + setTimeout( () => JSX.navigate( "/" ) ); + return not logged in + } + + return + +
+ Welcome to axonbox.net +
+
+ Please input a username. This can be changed at any time. +
+ + +
+ +
+
+
+} diff --git a/moneyjsx/src/header.tsx b/moneyjsx/src/header.tsx new file mode 100644 index 0000000..e79e256 --- /dev/null +++ b/moneyjsx/src/header.tsx @@ -0,0 +1,98 @@ +import $ from 'jquery'; +import * as JSX from './jsx'; +import * as user from './user'; +import * as settings from './settings'; +import { Dropdown, DropdownItem, Spinner, addPopup } from './components'; + + +async function logout() { + await user.logout(); +} + +/** + * NavItem component + * takes in title and route, optionally "disabled" +**/ +function NavItem( props: any ) { + const url = new URL( window.location.href ); + var style = `height: 45px;`; + var onclick = () => JSX.navigate( props.route ); + if( props.disabled || url.pathname == props.route ) { + style += "color: #888;pointer-events: none;"; + onclick = null; + } + + return + [ { props.title } ] + +} + +function SettingsButton( props: any ) { + let openSettings = settings.openPopup; + let style = `height: 45px;`; + + if( props.disabled ) { + openSettings = null; + style += "color: #888; pointer-events: none;"; + } + + return + [ Settings ] + +} + +function EmailInput() { + const sendLink = async () => { + const email = $( "#login-email" ).val().toString(); + $( "#login-btn-wrapper" ).append( ); + + const res = await user.sendLoginLink( email ); + if( res === true ) { + $( "#header-login-dropdown" ).replaceWith( +
Success! Check your inbox for a login link.
+ ); + } else { + $( "#header-login-dropdown" ).replaceWith( +
Error sending email: { res }
+ ); + } + }; + + return
+
+ Log in + +
+ +
+
+
+} + +function showLoginPopup() { + addPopup( $( ) ); +} + +export function Header() { + return +} diff --git a/moneyjsx/src/home.tsx b/moneyjsx/src/home.tsx new file mode 100644 index 0000000..cd53eaf --- /dev/null +++ b/moneyjsx/src/home.tsx @@ -0,0 +1,90 @@ +import $ from "jquery"; +import * as JSX from "./jsx"; +import AsciiArt from "./ascii-art" +import { GroupBox, Page, RolldownList, RolldownListItem } from "./components"; + +const text_l = [ + "Have you considered how public AI tools might expose proprietary information?", + "What would the impact be if the sensitive data you have shared with AI tool was leaked?", + "How does your current AI provider handle security indicents? How transparent are they, really?", + "What happens to your data after it is shared with public AI tools?", + "Are you comfortable with the AI models that you interact with being retrained on the data you've shared with it?", +]; + +const text_r = [ + "Unrestricted prompt and tooling customization.", + "100% private LLM access with dedicated personal instances.", + "Frictionless access and removal for all of your shared information.", + "Long-term memory, web access, and notes provided with base configuration.", + "Custom embedded 'modules' available by direct contact via support page.", + "Unlimited API calls.", +]; + +function runTypewriter( messages: string[], elId: string ) { + const el = $( elId ).find( ".groupbody" ); + if( !el ) return; + + const type = ( el: JQuery, str: string ) => { + const span = $( ); + span.html( "> " ); + el.append( span ); + + let i = 0; + const typeWord = () => { + const words = str.split( ' ' ); + if( i < words.length ) { + span.html( span.html() + words[i] + ' ' ); + setTimeout( typeWord, 25 + Math.random() * 100 ); + } + ++i; + }; + typeWord(); + } + + for( let msg of messages ) { + type( el, msg ); + el.append(
); + } +} + +function HomeInfo() { + const ret =
+ + +
; + + setTimeout( () => { + runTypewriter( text_l, "#q2a" ); + runTypewriter( text_r, "#features" ); + } ); + return ret; +} + +function HomeFaq() { + return + + Unlike many of our competitors, we do not harvest or sell your personal data. We are hobbyists that simply want to provide the best possible service to assist you in achieving your goals. + + + We pride ourselves in the fact that we are small and tight-knit team of hobbyist developers with a passion to create and ship tools that can help reshape your daily life. + + + We believe that competition is a benefit in society. If we are successful in our growth, we can apply pressure towards others to stop data-brokering practices and instead focus on delivering real transparency and an ideal product. + + + At the moment, we utilize Stripe to accept payments. If you have issues with this, please reach out to us on Twitter. We gladly accept BTC, and would prefer it for any custom work we may perform for you. + + + The best form of contact will be through our support account here on Twitter. Alternatively, you can email us here. However, since we are a small team, Twitter would be the most convenient. + + +} + +export default function Home() { + return + +

Welcome your new second-in-command

+ + +
; +} diff --git a/moneyjsx/src/index-page.tsx b/moneyjsx/src/index-page.tsx new file mode 100644 index 0000000..a7130d2 --- /dev/null +++ b/moneyjsx/src/index-page.tsx @@ -0,0 +1,32 @@ +import * as JSX from "./jsx"; +import * as user from "./user"; + +import PaymentSuccess from "./payment-success"; +import FirstLanding from "./first-landing"; +import Tutorial from "./support-tutorial" +import Terminal from "./terminal"; +import Support from "./support"; +import Upgrade from "./upgrade"; +import Models from "./support-models"; +import Login from "./login"; +import Home from "./home"; +import ToS from "./support-tos"; +import Api from "./support-api"; + +JSX.addRoute( "/payment-success", () => ); +JSX.addRoute( "/first-landing", () => ); +JSX.addRoute( "/terminal", () => ); +JSX.addRoute( "/tutorial", () => ); +JSX.addRoute( "/support", () => ); +JSX.addRoute( "/upgrade", () => ); +JSX.addRoute( "/models", () => ); +JSX.addRoute( "/login", () => ); +JSX.addRoute( "/tos", () => ); +JSX.addRoute( "/api", () => ); +JSX.addRoute( "/", () => ); + +window.onpopstate = JSX.onPopState; +JSX.onPreNavigate( user.onNavigate ); + +const url = new URL( window.location.href ); +JSX.navigateParams( url.pathname, url.searchParams.entries() ); diff --git a/moneyjsx/src/index.html b/moneyjsx/src/index.html new file mode 100644 index 0000000..bebbed2 --- /dev/null +++ b/moneyjsx/src/index.html @@ -0,0 +1,93 @@ + + + + + + + axonbox.net + + + + + + + +
+
+ +
+
+ + + diff --git a/moneyjsx/src/jsx.tsx b/moneyjsx/src/jsx.tsx new file mode 100644 index 0000000..f167877 --- /dev/null +++ b/moneyjsx/src/jsx.tsx @@ -0,0 +1,239 @@ +import $ from 'jquery'; +const assetAttributeNames = new Set( ['data', 'srcset', 'src', 'href'] ); + +export interface Route { + path: string, + component: Function, +}; + +const routes = []; +let err404page = "/"; +let rootId = "moneyjsx-root"; +let onprenavigate: Function = () => {}; +let onpostnavigate: Function = () => {}; + +/** + * sets the id of the element to be replaced by the navigator + **/ +export function setRootId( rootId: string ) { + rootId = rootId; +} + +/** + * adds a route component to the routes list + * the component function must return either a jquery or a DOM element + **/ +export function addRoute( name: string, component: Function ) { + routes[name] = component; +} + +/** + * sets the route for a 404 page + **/ +export function set404Route( name: string ) { + err404page = name; +} + +/** + * sets the callback that will get called when a route changes + **/ +export function onPreNavigate( callback: Function ) { + onprenavigate = callback; +} + +/** + * sets the callback that will get called when a route changes + **/ +export function onPostNavigate( callback: Function ) { + onpostnavigate = callback; +} + +/** + * replaces the root element with the route component + **/ +export function navigate( route: string ) { + let url = new URL( window.location.href ); + url.pathname = route; + if( !routes[route] ) { + return navigate( err404page ); + } + window.history.pushState( {}, null, url.href ); + + onprenavigate(); + const el = $( routes[route]() ); + + $( `#${rootId}` ).children().remove(); + $( `#${rootId}` ).append( el ); + onpostnavigate(); +} + +/** + * navigate with params. see: navigate + **/ +export function navigateParams( route: string, params: any ) { + let url = new URL( window.location.href ); + let uparams = new URLSearchParams( params ); + url.pathname = route; + url.search = uparams.toString(); + if( !routes[route] ) { + return navigate( err404page ); + } + window.history.pushState( {}, null, url.href ); + + onprenavigate(); + const el = $( routes[route]() ); + + $( `#${rootId}` ).children().remove(); + $( `#${rootId}` ).append( el ); + onpostnavigate(); +} + +/** + * wrapper for history.pushState + **/ +export function pushParams( params: any ) { + const url = new URL( window.location.href ); + url.search = new URLSearchParams( params ).toString(); + + window.history.pushState( {}, null, url.href ); +} + +/** + * navigates without adding a history entry + * useful for e.g. re-rendering a page after waiting for a data callback +**/ +export function navigateParamsSilent( route: string, params: any ) { + let url = new URL( window.location.href ); + let uparams = new URLSearchParams( params ); + url.pathname = route; + url.search = uparams.toString(); + if( !routes[route] ) { + return navigateSilent( err404page ); + } + + onprenavigate(); + const el = $( routes[route]() ); + + $( `#${rootId}` ).children().remove(); + $( `#${rootId}` ).append( el ); + onpostnavigate(); +} + +/** + * see: navigateParamsSilent + **/ +export function navigateSilent( route: string ) { + let url = new URL( window.location.href ); + url.pathname = route; + if( !routes[route] ) { + return navigateSilent( err404page ); + } + + onprenavigate(); + const el = $( routes[route]() ); + + $( `#${rootId}` ).children().remove(); + $( `#${rootId}` ).append( el ); + onpostnavigate(); +} + +/** + * action when the back button is pressed +**/ +export function onPopState() { + let url = new URL( window.location.href ); + let uparams = new URLSearchParams( url.searchParams ); + url.search = uparams.toString(); + if( !routes[url.pathname] ) { + return navigateSilent( err404page ); + } + + onprenavigate(); + const el = $( routes[url.pathname]() ); + + $( `#${rootId}` ).children().remove(); + $( `#${rootId}` ).append( el ); + onpostnavigate(); +} + +export function getRoutes() : Route[] { + return routes; +} + + +// internal stuff below + +const originalAppendChild = Element.prototype.appendChild; +Element.prototype.appendChild = function( child: any ) { + if( Array.isArray( child ) ) { + for( const childArrayMember of child ) + this.appendChild( childArrayMember ); + + return child; + } + else if( typeof child === 'string' ) { + return originalAppendChild.call( this, document.createTextNode( child ) ); + } + else if( child ) { + return originalAppendChild.call( this, child ); + } +}; + +export function createElement( tag: any, props: any, ...children: any ) { + props = props || {}; + + if( typeof tag === "function" ) { + props.children = children; + return tag( props ); + } + + if( tag === 'raw-content' ) { + const dummy = document.createElement( 'div' ); + dummy.innerHTML = props.content; + return [...dummy.children]; + } + + const element = document.createElement( tag ); + + for( const [name, value] of Object.entries( props ) ) { + if( name.startsWith( 'on' ) ) { + const lowercaseName = name.toLowerCase(); + + if( lowercaseName in window ) { + element.addEventListener( lowercaseName.substring(2 ), value ); + continue; + } + } + + if( name == 'ref' ) { + ( value as any ).current = element; + continue; + } + + if( value === false ) + continue; + + if( value === true ) { + element.setAttribute( name, '' ); + continue; + } + + if( assetAttributeNames.has( name ) ) { + if( typeof value === 'string' ) { + element.setAttribute( name, value ); + } + continue; + } + + element.setAttribute( name, value ); + }; + + for( const child of children ) + element.appendChild( child ); + + return element; +} + +export function createFragment( props: any ) { + return props.children; +} diff --git a/moneyjsx/src/login.tsx b/moneyjsx/src/login.tsx new file mode 100644 index 0000000..0be6acc --- /dev/null +++ b/moneyjsx/src/login.tsx @@ -0,0 +1,29 @@ +import $ from 'jquery'; +import * as JSX from './jsx'; +import * as user from './user'; +import { GroupBox, Page } from './components'; + +export default function Login() { + if( user.is_loggedin ) + return JSX.navigateParams( "/", {} ); + + const url = new URL( window.location.href ); + const code = url.searchParams.get( "token" ); + let msg = "You should be redirected shortly..."; + if( !code ) { + msg = "The link you followed is invalid."; + } + else { + user.onLogin( code ).then( () => { + JSX.navigateParams( '/terminal', {} ); + } ).catch( ( e: any ) => { + $( "#login-msg" ).text( e.message ); + } ); + } + + return + + { msg } + + +} diff --git a/moneyjsx/src/payment-success.tsx b/moneyjsx/src/payment-success.tsx new file mode 100644 index 0000000..30316fb --- /dev/null +++ b/moneyjsx/src/payment-success.tsx @@ -0,0 +1,19 @@ +import * as JSX from './jsx'; +import { Page, GroupBox } from './components'; + +export default function PaymentSuccess( props: any ) { + setTimeout( () => JSX.navigateParams( "/terminal", {} ), 1000 ); + + return + +
+ Your payment was successful. You should be redirected shortly... +
+
+ +
+
+
+} diff --git a/moneyjsx/src/settings.tsx b/moneyjsx/src/settings.tsx new file mode 100644 index 0000000..ba39126 --- /dev/null +++ b/moneyjsx/src/settings.tsx @@ -0,0 +1,402 @@ +import $ from "jquery"; +import * as JSX from "./jsx"; +import * as api from "./api"; +import * as util from "./util"; +import * as user from "./user"; +import * as terminal from "./terminal"; +import { + addPopup, onPopupClosed, closePopup, + Dropdown, DropdownItem, + Spinner, + OkPopup, OkCancelPopup +} from "./components"; + +let old_font = ''; +const fonts = [ + 'Terminal', + 'Arial', + 'Monospace', + 'Sans-serif', + 'Serif', + 'Times' +]; + +function showDeleteTokenPopup( id: number ) { + const deleteToken = async() => { + try { + await user.deleteToken( id ); + $( `#token-${ id }` ).remove(); + } catch( e: any ) { + addPopup( $( + Error deleting token: { e.message } + ) ); + } + } + + addPopup( $( + + Are you sure you want to erase this API token? + + ) ); +} + +function showDeleteAllTokensPopup() { + const deleteTokens = async() => { + try { + await user.deleteAllTokens(); + closePopup(); + } catch( e: any ) { + addPopup( $( + Error deleting all tokens: { e.message } + ) ); + } + } + + addPopup( $( + + Are you sure you want to erase all API tokens? + + ) ); +} + +function TokenEntry( props: any ) { + const token = props.token; + + return
+ { token.value } + +
+} + +async function createNewToken( e: Event ) { + const btn = $( e.target ); + + e.preventDefault(); + e.stopPropagation(); + + const spinner = $( ); + btn.append( spinner ); + + try { + const token = await user.createToken(); + const tokens = await user.getTokens(); + // re-draw token popup + closePopup(); + addPopup( $( ) ); + + const copyTokenToClipboard = () => { navigator.clipboard.writeText( token ); } + addPopup( $( + + Your api token has been created.
+ Make sure to save it as it will not be shown to you again.
+
+ { token } +
+ + Press OK to copy the token to clipboard. +
+ ) ); + } catch( e: any ) { + addPopup( $( + + Error creating token: { e.message } + + ) ); + } + + spinner.remove(); +} + +function TokensPopup( props: any ) { + return
+ { props.tokens.length > 0 && +
+ { props.tokens.map( ( tok: any ) => { + return + } ) } +
+ } + { + props.tokens.length == 0 &&
+ No api tokens have been created. +
+ } +
+ + +
+
+} + +async function openTokensPopup() { + let spinner = $( ); + $( "#tokens-btn" ).append( spinner ); + try { + const tokens = await user.getTokens(); + spinner.remove(); + + return addPopup( $( ) ); + } catch( e: any ) { + spinner.remove(); + return addPopup( $( { e.message } ) ); + } +} + + +function showDeleteNotePopup( note_id: string ) { + const deleteNote = async() => { + try { + await user.deleteNote( note_id ); + $( `#note-${ note_id }` ).remove(); + } catch( e: any ) { + addPopup( $( + + Error deleting note
{ e.message } +
+ ) ); + } + } + + addPopup( $( + + Are you sure you want to delete this note? + + ) ); +} + +function showDeleteAllNotesPopup() { + addPopup( $( + { await user.deleteAllNotes(); closePopup(); } }> + Are you sure you want to delete all notes? + + ) ); +} + +function NotesPopup( props: any ) { + return
+
+ { props.notes.map( ( n: any ) => { + return
+ { n.content.split(' : ')[1] } + +
+ } ) } +
+
+ +
+
+} + +async function openNotesPopup() { + let spinner = $( ); + $( "#notes-btn" ).append( spinner ); + try { + const notes = await user.getNotes(); + spinner.remove(); + if( notes.length == 0 ) + return addPopup( $( No notes found ) ); + + return addPopup( $( ) ); + } catch( e: any ) { + spinner.remove(); + return addPopup( $( { e.message } ) ); + } +} + +function onSettingsClosed() { + if( old_font.length > 0 ) { + const style = document.documentElement.style; + style.setProperty( "--site-font", old_font ); + } +} + +function onFontChanged( e: Event ) { + const el = $( e.target ); + const font = el.text(); + if( !old_font.length ) + old_font = user.settings.site_prefs.font; + + user.settings.site_prefs.font = font; + const style = document.documentElement.style; + style.setProperty( "--site-font", font ); + + $( "#settings-font" ).text( font ); + closePopup(); +} + +function onModelChanged( e: Event ) { + const el = $( e.target ); + const model = el.text(); + + if( user.settings.plan.plan == 'free' ) { + for( let m of api.models ) { + if( m.name == model && !m.free ) { + const popup = $( JSX.navigateParams( "/upgrade", {} ) }> + This model is only available for paid users. + Please upgrade your plan to use it. + ); + + return addPopup( popup ); + } + } + } + + user.settings.site_prefs.model = model; + + $( "#settings-model" ).text( model ); + closePopup(); +} + +async function save() { + const newprefs = { + site_prefs: user.settings.site_prefs, + prompt_data: user.settings.prompt_data, + nickname: user.settings.nickname + }; + + const newprompt = $( "#system-prompt" ).val(); + newprefs.prompt_data.system = newprompt as string; + + const newname = $( "#uname-setting" ).val() as string; + if( newname.length > 1 ) + newprefs.nickname = newname; + + $( "#settings-spinner-wrapper" ).append( ); + try { + await user.savePrefs( newprefs ); + } catch( e: any ) { + $( "#settings-spinner-wrapper" ).empty(); + $( "#settings-spinner-wrapper" ).append(
{ e.message }
); + return; + } + + old_font = ''; + $( "#settings-spinner-wrapper" ).empty(); + $( "#username" ).text( user.settings.nickname ); + $( "#uname-setting" ).attr( "placeholder", user.settings.nickname ); + terminal.updateCapabilitiesDisplay(); +} + +function PlanDisplay() { + const el = $(
); + if( user.settings.plan.plan == 'free' ) { + el.append( + + Plan: free  + JSX.navigate( "/upgrade" ) }>[ upgrade ] + + ); + } else { + el.append( + + Plan: { user.settings.plan.plan } + + ); + + let days_left = ''; + if( user.settings.plan.endTime == -1 ) + days_left = 'inf'; + else + days_left = Math.floor( ( user.settings.plan.endTime - Date.now() ) / 60000 / 60 / 24 ).toString(); + el.append( + + Time left: { days_left.toString() } days + + ); + } + + return el[0]; +} + +async function downloadAllData() { + const spinner = $( ); + $( "#settings-spinner-wrapper" ).append( spinner ); + + try { + await user.downloadAllData(); + } catch( e: any ) { + addPopup( $( + + Error processing request: { e.message } + + ) ); + } + + spinner.remove(); +} + +async function invalidateAllSessions() { + const spinner = $( ); + $( "#settings-spinner-wrapper" ).append( spinner ); + + try { + await user.invalidateAllSessions(); + } catch( e: any ) { + addPopup( $( + + Error invalidating sessions: { e.message } + + ) ); + } + + spinner.remove(); +} + +function SettingsPopup() { + return
+
+
+
+ +
+
+
+ + +
+
+ + + { fonts.map( ( f ) => { f } ) } + +
+
+ + + { api.models.map( ( m ) => { + if( m.free || user.settings.plan.plan == 'paid' ) + return { m.name }; + return { m.name }; + } ) } + +
+
+ + +
+ +
+
+ +
+ + + +
+
+
; +} + +export function openPopup() { + if( !user.is_loggedin ) + return; + const el = $( ); + addPopup( el ); + onPopupClosed( onSettingsClosed ); +} diff --git a/moneyjsx/src/support-api.tsx b/moneyjsx/src/support-api.tsx new file mode 100644 index 0000000..abb4072 --- /dev/null +++ b/moneyjsx/src/support-api.tsx @@ -0,0 +1,214 @@ +import * as JSX from './jsx'; + +import $ from 'jquery'; + +import { Page, GroupBox } from './components'; + +export default function Api() { + return + +
+ +

POST /update-settings

+

Updates user settings based on the preferences provided.

+ +

Request

+

Body:

+
{ `{
+  "token": "JWT token",
+  "prefs": {
+    "nickname": "string",
+    "prompt_data": { "system": "string" },
+    "site_prefs": { "model": "string" }
+  }
+}` }
+ +

Responses

+
{ $( `{
+  "status": "ok" | "error",
+  "msg": "settings updated" | "error message",
+  "userprefs": {
+    "uuid": "string", /* the user uuid */
+    "nickname": "string", /* the user's set nickname */
+    "prompt_data": { "system": "string" }, /* the custom system prompt */
+    "site_prefs": { "model": "string" },  /* the user preferred model */
+    "chat_files": { "files": [ { "id": "string", "name": "string" } ] }, /* list of user's chat files */
+  }
+}` )[0] }
+
    +
  • 200 OK: Settings updated successfully
  • +
  • 400 Bad Request: Invalid value or nickname out of allowed range
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 500 Internal Server Error: General server error
  • +
+ +
+ +

POST /settings

+

Fetches user settings.

+ +

Request

+

Body:

+
{ `{
+  "token": "JWT token"
+}` }
+ +

Responses

+
{ $( `{
+  "uuid": "string", /* the user uuid */
+  "nickname": "string", /* the user's set nickname */
+  "prompt_data": { "system": "string" }, /* the custom system prompt */
+  "site_prefs": { "model": "string" },  /* the user preferred model */
+  "chat_files": { "files": [ { "id": "string", "name": "string" } ] }, /* list of user's chat files */
+}` )[0] }
+
    +
  • 200 OK: Returns user preferences
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 500 Internal Server Error: No user data found
  • +
+ +
+ +

POST /create-chat

+

Creates a new chat file for the user.

+ +

Request

+

Body:

+
{ `{
+  "token": "JWT token"
+}` }
+ +

Responses

+
{ $( `{
+  "status": "ok" | "nodata" | "error",
+  "msg": "chat created" | "error message",
+  "chatId": "string" /* the uuid/filename of the newly created chat. */
+}` )[0] }
+
    +
  • 200 OK: Chat created with ID and topic name
  • +
  • 400 Bad Request: Missing user data
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 500 Internal Server Error: Error creating chat file or updating database
  • +
+ +
+ +

POST /get-chat

+

Fetches the contents of a specified chat file.

+ +

Request

+

Body:

+
{ $( `{
+  "token": "JWT token",
+  "chatId": "string" /* chat file uuid */
+}` )[0] }
+ +

Responses

~ +
{ `{
+  "status": "ok" | "error",
+  "msg": "chat not found" | "error message",
+  "contents": [ { }, ... ]
+}` }
+
    +
  • 200 OK: Returns chat file contents
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 404 Not Found: Chat does not exist
  • +
  • 500 Internal Server Error: User data not found
  • +
+ +
+ +

POST /models

+

Lists available models for chat.

+ +

Request

+

Body:

+
{ `{
+  "token": "JWT token"
+}` }
+ +

Responses

+
    +
  • 200 OK: Returns list of models with their attributes
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
+ +
+ +

POST /generate

+

Generates text based on the input prompt. Can optionally be used for in-the-middle generation.

+ +

Request

+

Body:

+
{ $( `{
+  "token": "JWT token",
+  "prompt": "string", /* the text before the generated text */
+  "suffix": "string", /* optional - the text after the generated text */
+  "model": "string",
+  "options": { "seed": Number, "temperature": Number } /* optional */
+}` )[0] }
+ +

Response

+

Body:

+
{ $( `{
+  "status": "ok" | "error",
+  "response": "string",
+  "finalMsg": "string" /* optional - only at the end of chunked transmission */
+}` )[0] }
+ +
    +
  • 200 OK: Returns streamed chat response
  • +
  • 400 Bad Request: Missing required parameters
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 429 Too Many Requests: A request from this user is already being processed
  • +
  • 504 Gateway Timeout: No chat instance available
  • +
  • 500 Internal Server Error: Chat server error
  • +
+ +
+ +

POST /chat

+

Sends a message to the chat server and streams the response back to the user.

+ +

Request

+

Body:

+
{ $( `{
+  "token": "JWT token",
+  "model": "string",
+  "messages": [ {
+    "role": "user" | "assistant" | "tool" | "system",
+    "content": "string",
+    timestamp: "string"
+  } ],
+  "system": "string",  /* optional */
+  "options": { "seed": Number, "temperature": Number }, /* optional */
+  "chatfile": "string" /* optional - chat file uuid */
+}` )[0] }
+ +

Responses

+

Message:

+
{ `{
+  "status": "ok" | "error",
+  "msg": "string",
+  "done": false,
+  "tool": "string"
+}` }
+

End:

+
{ `{
+  "status": "ok" | "error",
+  "msg": "string",
+  "fullMsg": "full message returned by the model",
+  "done": true | false
+}` }
+
    +
  • 200 OK: Returns streamed chat response
  • +
  • 400 Bad Request: Missing required parameters
  • +
  • 401 Unauthorized: Invalid or expired token
  • +
  • 429 Too Many Requests: A request from this user is already being processed
  • +
  • 504 Gateway Timeout: No chat instance available
  • +
  • 500 Internal Server Error: Chat server error
  • +
+
+
+
+} diff --git a/moneyjsx/src/support-models.tsx b/moneyjsx/src/support-models.tsx new file mode 100644 index 0000000..2afd726 --- /dev/null +++ b/moneyjsx/src/support-models.tsx @@ -0,0 +1,114 @@ +import $ from "jquery"; +import * as JSX from './jsx'; +import * as API from "./api"; +import * as user from "./user"; +import * as util from "./util"; + +import { Page, GroupBox, Spinner, Dropdown, DropdownItem } from './components'; + +export default function Models() { + if( !API.models.length ) { + const modelTimeout = () => { + if( API.models.length ) + JSX.navigateSilent( "/models" ); + else + setTimeout( modelTimeout, 100 ); + } + + modelTimeout(); + + return +
+ +
+
+ } + + let model = API.models[0].name; + if( user.is_loggedin && user.settings.site_prefs && user.settings.site_prefs.model ) + model = user.settings.site_prefs.model!; + + return + +
+ + { API.models.map( ( m ) => { m.name } ) } + +
+ +
+
+
+} + +function changeModel( e: Event ) { + const el = $( e.target ); + const txt = el.text(); + console.log( txt ); + const m = API.getModelFromName( txt ); + console.log( m ); + + $( "#model-list" ).children().replaceWith( + + ); + + $( "#model-selector" ).text( m.name ); +} + + +function ModelInfo( props: any ) { + const m = props.model as API.Model; + return
+
+

+ { m.name } +

+
+
+
+ Web { m.capabilities.web ? '✔' : '✘' } + Notes { m.capabilities.notes ? '✔' : '✘' } + Vision { m.capabilities.vision ? '✔' : '✘' } + Memory Lookup { m.capabilities.remind ? '✔' : '✘' } +
+
+ { m.description.full } +
+
+
+
+ + { util.escapeHtml( m.license ) } + +
; +} + +let license_open = false; +function ModelLicense( props: any ) { + const collapseToggle = () => { + const el = $( "#model-license" ); + let list = el.find( '.tool-call-list' ); + let btn = el.find( '.tool-call-collapse' ); + + license_open = list.css( 'display' ) == 'block'; + if( !license_open ) { + list.css( 'display', 'block' ); + btn.text( '-' ); + } else { + list.css( 'display', 'none' ); + btn.text( '+' ); + } + }; + + let el =
+
+ License + { license_open ? '-' : '+' } +
+
+ { props.children } +
+
; + + return el; +} diff --git a/moneyjsx/src/support-tos.tsx b/moneyjsx/src/support-tos.tsx new file mode 100644 index 0000000..2aab52e --- /dev/null +++ b/moneyjsx/src/support-tos.tsx @@ -0,0 +1,69 @@ +import * as JSX from './jsx'; + +import { Page, GroupBox } from './components'; + +export default function ToS() { + return + + +1. Acceptance of Terms
+ Upon account creation, your agreement to these Terms of Service will be permanently stored in case of legal discourse.
+
+2. Definitions
+ "We", "us", and any first-person references, refer to axonbox as an organization, and the individuals that represent it.
+ "Service" refers to any service ( LLMs, API, module, website, etc. ) provided by axonbox.
+ "The user", "user", and "users" refers to any individual or entity that accesses or uses the Service.
+ "Content" refers to all information, messages, and data provided by Users or generated by the Service.
+
+3. User Responsibilities
+ The user agrees to use the Service only for lawful purposes.
+ The user will not use the Service to generate harmful, illegal, or offensive content.
+ The user must be at least 15 years of age to use the Service.
+ The user must abide by these terms, or else risk a suspension, or permanent ban, of access to our services.
+
+4. Prohibited Uses
+ Users agree not to use the Service in any of the following unintended ways:
+ Engaging in any activity that disrupts or interferes with the Service or its servers.
+ Attempting to gain unauthorized access to any portion of the Service or any other systems or networks connected to the Service.
+ Using the Service for any automated data collection, scraping, or similar activities.
+
+5. Intellectual Property
+ All content generated by the service is owned by the user unless otherwise stated.
+ If opted in to data sharing the user grants us a license to use it for the purpose of training future models, and only that.
+
+6. Account Management
+ Users must create an account to access the features and services provided by axonbox.
+ The user is responsible for maintaining the security of their own account information and for all activities that occur under said account. Any account disputes must be handled directly by axonbox support.
+
+7. Privacy Policy
+ The use of the Service is also governed by our Privacy Policy, which explains how we collect, use, and protect user information. Axonbox only performs collection of data necessary for axonbox services' critical functionality. If axonbox were to ever cease operations, all information related to users will be securely, permanently, and thoroughly destructed and destroyed.
+
+ Data collected by axonbox is limited to:
+ - User email
+ - User Service preferences
+ - Account creation timestamp
+ - IP Addresses of web requests
+ - Notes generated by chat models
+ - Chat logs until deletion ( encrypted with AES-256 )
+
+8. Dispute Resolution
+ Any disputes arising from these Terms will be resolved through binding arbitration in accordance with the rules of Texas law.
+
+9. Limitation of Liability
+ Axonbox is not liable for any direct, indirect, incidental, or consequential damages arising from use of the Service.
+
+10. Termination
+ We reserve the right to suspend or terminate access to the Service at our discretion at any time, without notice, for violations of these Terms.
+
+11. Modifications
+ We may update these Terms from time to time. We will notify users of any changes via the respective provided email or through the Service. Continued use of the Service constitutes acceptance of the updated Terms.
+
+12. Governing Law
+ These Terms shall be governed by the rules of Texas law.
+
+13. Contact Information
+ For any questions or concerns regarding these Terms, please contact us via our support account on X, accessible from the site map. +
+
+
; +} \ No newline at end of file diff --git a/moneyjsx/src/support-tutorial.tsx b/moneyjsx/src/support-tutorial.tsx new file mode 100644 index 0000000..a1d8a14 --- /dev/null +++ b/moneyjsx/src/support-tutorial.tsx @@ -0,0 +1,156 @@ +import $ from "jquery"; +import * as JSX from './jsx'; + +import { Dropdown, DropdownItem, Page, GroupBox } from './components'; + +interface TutorialPage { + title: string, + content: HTMLElement +} + +const pages: TutorialPage[ ] = [ + { + title: "Choosing the right model", + content: + + It would get quite wordy if we were to discuss every model in detail that we provide, + so our best suggestion is to check out our JSX.navigate( "/models" ) }>Models page. +
+ Here, you can see the various personalities our models come with by default, + the provided tooling, as well as licensing information for each respective model! +
+ }, + { + title: "Effective chatting", + content: + + Effective chatting best practices are also, as with most things, on a model-by-model basis. + However, there are some good standards to abide by when interacting with any LLM to increase effectiveness. + We'll list a few of our personally notable ones here, in no particular order: +
+ 1) Prompting : A good prompt can drastically change the output of a large language model, + effective prompts will be elaborated on in the selectPage( pages[ 2 ] ) }>next section. +

+ 2) Clarify : As the saying "garbage in, garbage out" goes with computing, it continues here to an extraordinary extent. + One of, if not, the most effective methods of improving LLM responses is by providing and continually improving: + queries, instructions, sample code, or other data you feed into the language model that fits your current need. +

+ 3) Reminders : LLMs, just like humans, need reminders sometimes. If you are sending large amounts of data, or + if you are having a model make many web requests, it's very likely you will leave the model's + context window. This can lead to unpredictable and unexplainable results, so do with that information + what you will. +

+ 4) Tooling : Luckily for you, utilizing our provided selectPage( pages[ 4 ] ) }>tooling is not something that you will have to initiate. + Models equipped with tooling functionality will automatically use our provided tools to better complete your requests. + All this being said, you will have to "manage" your tooling. For web requests, as mentioned above, they can quickly + devour your avaiable context window and cause erroneous responses to occur. Notes as well, when above capacity, can + cause the same. We do provide a way to manage notes, and we can only suggest to keep web requests to a minimum. +
+
+ }, + { + title: "Prompting", + content: + + We feel like this cannot be understated, so we will mention it again. + Each model is different, and will respond differently to the same query, prompt, image, etc. + This is what we describe as "personalities". +
+ However, as with chatting, we can provide general advice that applies across the board. + As well, we will attach some resources below that we have used in our journey in this process. + We feel these are of good quality and dig into a low level in layman's terms to help you + understand easily. +
+
+ 1) Prompt format matters : As seen in the resources below, there are many types of prompting. + Using the wrong style of prompt for the wrong purpose could hinder + service performance rather than improve it. Make sure you have a + rudimentary understanding of what prompting is and what it does before + embarking on your quest. +

+ 2) Prompt interferes with context window : This is a good one to keep in mind. Though it will + likely never be an issue for the average user, advanced + power users may find a point in which a lengthy prompt + causes a model to *only* output erroneous data. Keeping + your prompt clear and concise should be the goal of any + power user. +

+ 3) Test, test, and test again : This is the most important step. The true essence of trial and error + is found here. Attempting different variations, structures, and wordings + of the same prompt can lead to vastly different outcomes, all coinciding + with the previously discussed points. +
+
+ The aforementioned resources are as follows, in an order that we'd suggest approaching this topic with : + +
+ }, + { + title: "Modules", + content: + + Modules are a product that we offer outside of being a convenient host and frontend for a wide array of hand-picked open source LLMs. + They can be created using a fraction of our available selectPage( pages[ 5 ] ) }>api, and still create a lively environment for any + chatbot, website, video game dialog, or any other endeavour only the mind can uncover. +
+ At the moment, our pricing for module implementation to your service starts at $250 USD, and of course goes up with project scope. +
+ }, + { + title: "Tools", + content: + + To not repeat ourselves too much here, we'd suggest reading #4 of selectPage( pages[ 1 ] ) }>effective chatting before this portion. +

+ As stated there, our provided tooling is initiated automatically by the will of the selected model, but you are in charge + of managing its resource consumption. If you notice it taking a lot of notes, it will be worthwhile to clean out notes that + aren't so important. If your current conversation has a lot of web requests in it's history, it could prove resourceful to + start a new chat to initiate a new context window to refreshing the models "short-term memory", while carrying over useful + notes from past conversations. These nuanced tweaks will become frictionless and scale overtime to further hone in the model + to become tailored to you, the user. +
+ }, + { + title: "API", + content: + + Our OpenAI-compatible API is the core that powers this entire site. + As you can see for yourself in the inspector, this site is written completely with an in-house framework. + None of the nonsense that plagues the modern industry. We understand the importance of simplicty. + What makes this all possible, outside of our ability to "Make It Work" as developers, is our robust API. + + To dig deeper and become a power user, please visit JSX.navigate( "/api" ) }>this page. + + } +]; + +let selected_page: TutorialPage = pages[ 0 ]; +export default function Tutorial() { + return + +
+ + { pages.map( ( p ) => selectPage( p ) }> { p.title } ) } + +
+ +
+ + { selected_page.content } + +
+
+
+} + +function selectPage( page: TutorialPage ) { + selected_page = page; + $( "#tutorial-links-box" ).text( page.title ); // moneyjsx todo @nave : FIX PLEASE, I SHOULDN'T HAVE TO DO THIS ( IT'S BROKEN ON MODEL PAGE AS WELL ) -- intuitive functionality is that it rerenders when the selected_page var is modified + $( "#tutorial-content" ).html( page.content ); // if we could add a reactive body to that as well that responds when selection changed, we could remove this entire need for onclick, but that's also probably a fuck ton of work . +} diff --git a/moneyjsx/src/support.tsx b/moneyjsx/src/support.tsx new file mode 100644 index 0000000..2b718be --- /dev/null +++ b/moneyjsx/src/support.tsx @@ -0,0 +1,18 @@ +import * as JSX from './jsx'; + +import { Page, GroupBox } from './components'; + +export default function Support() { + return + + + + +} 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
+} + +let promptc = 0; +function writePrompt( promptTxt: string ) { + let el = $( +
inputPrompt( promptTxt ) }> + +
+ ); + + 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
+
+} + +function TerminalWindow() { + if( !has_listener ) { + window.onresize = onWindowResize; + has_listener = true; + } + + const style = getStyleForSize(); + return
+ +
+ + +
+
+ +
+
+} + +function ModelCapabilities() { + const model = api.getModelFromName( user.settings.site_prefs.model! ); + if( !model ) + return
+ + const capabilities = model.capabilities; + if( !capabilities ) + return
+ + 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
+ { model_str } +
+} + +export function updateCapabilitiesDisplay() { + const div = $( "#model-capabilities" ); + if( div.length > 0 ) + div.replaceWith( ); +} + +export default function Terminal() { + if( !user.is_loggedin ) { + setTimeout( () => JSX.navigate( "/" ) ); + return not logged in + } + + return + + + + +} diff --git a/moneyjsx/src/tsconfig.json b/moneyjsx/src/tsconfig.json new file mode 100644 index 0000000..b964594 --- /dev/null +++ b/moneyjsx/src/tsconfig.json @@ -0,0 +1,121 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + "moduleResolution": "node", + /* Language and Environment */ + "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "ES2017", + "DOM", + "DOM.Iterable", + "ScriptHost" + ], + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "react", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "jsxFactory": "JSX.createElement", + "jsxFragmentFactory": "JSX.createFragment", + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2020", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + "typeRoots": [ + "./node_modules/@types", + "./types" + ], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ + "strictFunctionTypes": false, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": false, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ + "strictBuiltinIteratorReturn": false, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + "noImplicitThis": false, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/moneyjsx/src/upgrade.tsx b/moneyjsx/src/upgrade.tsx new file mode 100644 index 0000000..ab29e34 --- /dev/null +++ b/moneyjsx/src/upgrade.tsx @@ -0,0 +1,206 @@ +import * as JSX from './jsx'; +import * as user from './user'; +import * as api from './api'; +import $ from 'jquery'; + +import { Page, GroupBox, Spinner } from './components'; + +let stripe = null; +let card = null; +let selected_product = 2; + +async function loadStripe() { + return new Promise( function (resolve, reject ) { + const s = document.createElement( 'script' ); + s.src = 'https://js.stripe.com/v3/'; + s.onload = resolve; + s.onerror = reject; + document.head.appendChild( s ); + } ); +} + +function showError( text: string ) { + $( "#payment-error" ).show(); + $( "#payment-error" ).text( text ); +} + +async function onFormSubmit( e: Event ) { + e.preventDefault(); + const { paymentMethod, error } = await stripe.createPaymentMethod( { + type: 'card', + card + } ); + + if( error ) + return showError( error.message ); + + const res = await api.post( 'create-payment-intent', { + token: localStorage.getItem( 'session' ), + paymentMethodId: paymentMethod.id, + subLength: selected_product + } ); + + if( res.status == 'ok' ) { + const intent = res.paymentIntent; + if( intent.status == 'succeeded' ) + return JSX.navigateParams( '/payment-success', {} ); + else + return showError( "Payment failed. Contact support if the issue persists." ); + } + else if( res.msg ) + return showError( "Error: " + res.msg ); + else + return showError( "Payment failed. Contact support if the issue persists." ); +} + +function waitForStripe() { + const iframes = $( 'iframe' ); + if( !iframes.length ) + return setTimeout( waitForStripe, 50 ); + + for( let iframe of iframes ) { + if( iframe.attributes.getNamedItem( 'name' ).value == 'hcaptcha-invisible' ) { + $( '#upgrade-loading' ).remove(); + $( '#upgrade-inner' ).show(); + + return; + } + } + + setTimeout( waitForStripe, 50 ); +} + +async function initStripe() { + stripe = window['Stripe']( 'pk_live_51Q9JG602rmv2yeiGYNNfwFkqp5ntpXIDIMIoiwDoEOrB5IsC75WDy3XOqekvuwY6PecviSSo5ERt0umrdBdUtqhd00FyYZe5p3' ); + const font = user.settings.site_prefs.font; + + const params = { + mode: 'billing', + style: { + base: { + color: '#fff', + fontFamily: font, + fontSize: '15px', + '::placeholder': { + color: '#ccc', + }, + }, + }, + } + + const el = getComputedStyle( document.body ); + const back = el.getPropertyValue( '--back' ); + const front = el.getPropertyValue( '--front' ); + const appearance = { + theme: 'stripe', + disableAnimations: true, + + variables: { + colorPrimary: front, + colorBackground: back, + colorText: '#fff', + fontFamily: font, + fontSizeBase: '15px', + fontSizeSm: '13px', + spacingUnit: '2px', + borderRadius: '0px', + }, + rules: { + ".Input": { + border: 'none' + } + } + } + + const elements = stripe.elements( { appearance } ); + const name = elements.create( 'address', { mode: 'billing', appearance } ); + name.mount( '#name-element' ); + + card = elements.create( 'card', params ); + card.mount( '#card-element' ); +} + +function onProductChange( e: Event ) { + selected_product = parseInt( (e.target as HTMLInputElement).value ); +} + +function SelectionRadio( params: any ) { + return
+ { params.checked && + + } + { !params.checked && + + } + +
+} + +function UpgradeLoaded() { + return +} + +export default function Upgrade() { + setTimeout( () => { + loadStripe().then( () => { + $( ).insertAfter( "#upgrade-loading" ); + initStripe(); + waitForStripe(); + } ); + } ); + + return + + Loading...   + + +} diff --git a/moneyjsx/src/user.tsx b/moneyjsx/src/user.tsx new file mode 100644 index 0000000..7755670 --- /dev/null +++ b/moneyjsx/src/user.tsx @@ -0,0 +1,261 @@ +import $ from 'jquery'; + +import * as JSX from './jsx'; +import * as api from './api'; + +export interface ChatFile { + id: string, + name: string +} + +export interface UserSettings { + uuid: string, + nickname: string, + site_prefs: { + font: string, + model?: string + }, + prompt_data: { + system?: string + }, + chat_files?: { + files?: ChatFile[] + }, + plan: { + endTime: number, + plan: string + } +}; + +export let is_loggedin = false; +export let settings: UserSettings = null; + +let on_prefs_updated: Function = () => {}; + +export async function onPrefsUpdated( fn: Function ) { + on_prefs_updated = fn; +} + +/** + * polls settings from the server +**/ +export async function updatePrefs() { + if( !is_loggedin ) { + return; + } + + let res = null; + try { + res = await fetch( `${api.url}/settings`, { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( { + token: localStorage.getItem( 'session' ), + } ) + } ); + + if( !res.ok ) { + try { + const json = await res.json(); + if( json.status == 'nodata' ) { + localStorage.setItem( 'needs-setup', 'true' ); + if( window.location.pathname != '/first-landing' ) + return JSX.navigate( '/first-landing' ); + else { + settings = null; + localStorage.setItem( 'settings', '{}' ); + } + } else if( json.status != 'ok' ) { + is_loggedin = false; + localStorage.clear(); + } + + throw new Error( json.msg ); + } catch( e: any ) { + throw new Error( "error contacting server" ); + } + } + + res = await res.json(); + } catch( e: any ) { + throw new Error( e.message ); + } + + settings = res.userprefs; + localStorage.setItem( 'settings', JSON.stringify( settings ) ); + on_prefs_updated(); +} + +/** + * saves prefs on the server +**/ +export async function savePrefs( prefs: any ) { + const res = await api.post( `update-settings`, { token: localStorage.getItem( 'session' ), prefs } ); + settings = res.userprefs; + localStorage.setItem( 'settings', JSON.stringify( settings ) ); +} + +export async function getNotes() { + const res = await api.post( `get-notes`, { token: localStorage.getItem( 'session' ) } ); + return res.notes; +} + +export async function deleteNote( note_id: string ) { + await api.post( `delete-note`, { token: localStorage.getItem( 'session' ), noteId: note_id } ); +} + +export async function deleteAllNotes() { + await api.post( `delete-notes`, { token: localStorage.getItem( 'session' ) } ); +} + +export async function getTokens() { + const res = await api.post( `get-tokens`, { token: localStorage.getItem( 'session' ) } ); + return res.tokens; +} + +export async function createToken() { + const res = await api.post( `create-token`, { token: localStorage.getItem( 'session' ) } ); + return res.token; +} + +export async function deleteToken( token_id: number ) { + await api.post( `delete-token`, { id: token_id, token: localStorage.getItem( 'session' ) } ); +} + +export async function deleteAllTokens() { + await api.post( `delete-tokens`, { token: localStorage.getItem( 'session' ) } ); +} + +export async function createChat() { + const res = await api.post( `create-chat`, { token: localStorage.getItem( 'session' ) } ); + return res; +} + +export async function deleteChat( chatId: string ) { + await api.post( `delete-chat`, { chatId, token: localStorage.getItem( 'session' ) } ); +} + +export async function downloadAllData() { + let res = await fetch( `${api.url}/getalldata`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { token: localStorage.getItem( 'session' ) } ) + } ); + + let contentType = res.headers.get( 'content-type' ); + if( contentType && contentType.includes( 'application/zip' ) ) { + let blob = await res.blob(); + let url = window.URL.createObjectURL( blob ); + const a = ; + $( 'body' ).append( a ); + a.onclick = ( e: Event ) => { e.stopPropagation(); }; + a.click(); + window.URL.revokeObjectURL( url ); + } + else { + let json = null; + try { + json = await res.json(); + } catch( e: any ) { + throw new Error( "Error contacting server" ); + } + + if( json.status != 'ok' ) + throw new Error( json.msg ); + } +} + +export async function invalidateAllSessions() { + const res = await api.post( `invalidate-all-sessions`, { token: localStorage.getItem( 'session' ) } ); + localStorage.setItem( 'session', res.session ); +} + +function updateFont() { + const style = document.documentElement.style; + if( settings.site_prefs && settings.site_prefs.font != null ) + style.setProperty( "--site-font", settings.site_prefs.font ); +} + +async function onNavigateAsync() { + try { + if( is_loggedin ) { + if( localStorage.getItem( 'needs-setup' ) && window.location.pathname !== "/first-landing" ) + setTimeout( () => JSX.navigate( "/first-landing" ) ); + else + await updatePrefs(); + // update it again in case it changed in prefs + updateFont(); + } + + await api.updateModels(); + } catch( e: any ) { + // todo: display message box w error + console.log( e ); + } +} + +export function onNavigate() { + settings = JSON.parse( localStorage.getItem( 'settings' ) || '{}' ) as UserSettings; + api.parseModels(); + if( localStorage.getItem( 'session' ) ) + is_loggedin = true; + + if( is_loggedin ) + updateFont(); + + onNavigateAsync(); +} + +/** + * returns true on success, error message on failure +**/ +export async function sendLoginLink( email: string ) { + let res = null; + try { + res = await api.post( `send-login-link`, { email } ); + } catch( e: any ) { + return e.message; + } + + return true; +} + +export async function onLogin( code: string ) { + let json = null; + try { + const res = await fetch( `${api.url}/login?token=${code}` ); + json = await res.json(); + } catch( e: any ) { + throw new Error( "error contacting server" ); + } + + if( json.status != 'ok' ) + throw new Error( json.msg ); + + localStorage.setItem( 'session', json.session ); + is_loggedin = true; + + try { + await updatePrefs(); + } catch( e: any ) { + throw new Error( e.msg ); + } +} + +export async function logout() { + if( !is_loggedin ) + return; + + try { + await api.post( 'invalidate-session', { token: localStorage.getItem( 'session' ) } ); + } catch( e: any ) {} + + localStorage.clear(); + is_loggedin = false; + JSX.navigate( '/' ); + window.location.reload(); +} diff --git a/moneyjsx/src/util.tsx b/moneyjsx/src/util.tsx new file mode 100644 index 0000000..097502e --- /dev/null +++ b/moneyjsx/src/util.tsx @@ -0,0 +1,83 @@ +import * as api from './api'; +import * as chat from './chat'; + +export function escapeHtml( html: string ) { + const entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }; + + return String( html ).replace( /[&<>"'`=\/]/g, ( s ) => { + return entityMap[s]; + } ); +} + +export function parseJWT( token: string ) : any { + const parts = token.split( '.' ); + let encoded = parts[1]; + encoded = encoded.replace(/-/g, '+').replace(/_/g, '/'); + const pad = encoded.length % 4; + if( pad === 1 ) + throw new Error( 'what the fuck' ); + if( pad > 1 ) + encoded += new Array( 5 - pad ).join( '=' ); + + const payload = JSON.parse( atob( encoded ) ); + return payload; +} + +export function isToolStr( buf: string, model: api.Model ) { + const trimmed = buf.replace( /\s+/g, '' ).toLowerCase(); + const capabilities = model.capabilities; + + for( let tool in capabilities ) { + let name = tool.toLowerCase(); + let str = `{"name":"${name}"`; + + let not_matched = false; + for( let i = 0; i < Math.min( trimmed.length, str.length ); i++ ) { + if( trimmed[i] !== str[i] ) { + not_matched = true; + break; + } + } + + if( !not_matched ) + return true; + } + + return false; +} + +export function getToolCall( msg: chat.Msg ) { + let first_bracket = msg.content.indexOf( '{' ); + let last_bracket = msg.content.lastIndexOf( '}' ); + + if( first_bracket == -1 || last_bracket == -1 ) + return null; + + let call = msg.content.substring( first_bracket, last_bracket + 1 ); + let json = null; + try { json = JSON.parse( call ); } + catch( e ) { return null; } + + return json; +} + +export function sizeHumanReadable( size: number ) { + if( size < 1024 ) + return size + ' B'; + else if( size < 1024 * 1024 ) + return ( size / 1024 ).toFixed( 2 ) + ' KB'; + else if( size < 1024 * 1024 * 1024 ) + return ( size / 1024 / 1024 ).toFixed( 2 ) + ' MB'; + else + return ( size / 1024 / 1024 / 1024 ).toFixed( 2 ) + ' GB'; +} + -- cgit v1.2.3