summaryrefslogtreecommitdiff
path: root/moneyjsx/src
diff options
context:
space:
mode:
authoraura <nw@moneybot.cc>2026-02-17 22:39:42 +0100
committeraura <nw@moneybot.cc>2026-02-17 22:39:42 +0100
commit636b0323075225c584b62719ed51e75521bb7ffb (patch)
tree61b02271b6d0695a4beffc23fb6eb062a7da22c3 /moneyjsx/src
push source
Diffstat (limited to 'moneyjsx/src')
-rw-r--r--moneyjsx/src/api.tsx80
-rw-r--r--moneyjsx/src/ascii-art.tsx165
-rw-r--r--moneyjsx/src/chat.tsx1023
-rw-r--r--moneyjsx/src/components.tsx238
-rw-r--r--moneyjsx/src/first-landing.tsx55
-rw-r--r--moneyjsx/src/header.tsx98
-rw-r--r--moneyjsx/src/home.tsx90
-rw-r--r--moneyjsx/src/index-page.tsx32
-rw-r--r--moneyjsx/src/index.html93
-rw-r--r--moneyjsx/src/jsx.tsx239
-rw-r--r--moneyjsx/src/login.tsx29
-rw-r--r--moneyjsx/src/payment-success.tsx19
-rw-r--r--moneyjsx/src/settings.tsx402
-rw-r--r--moneyjsx/src/support-api.tsx214
-rw-r--r--moneyjsx/src/support-models.tsx114
-rw-r--r--moneyjsx/src/support-tos.tsx69
-rw-r--r--moneyjsx/src/support-tutorial.tsx156
-rw-r--r--moneyjsx/src/support.tsx18
-rw-r--r--moneyjsx/src/terminal.tsx275
-rw-r--r--moneyjsx/src/tsconfig.json121
-rw-r--r--moneyjsx/src/upgrade.tsx206
-rw-r--r--moneyjsx/src/user.tsx261
-rw-r--r--moneyjsx/src/util.tsx83
23 files changed, 4080 insertions, 0 deletions
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 = <div id="ascii-art">
+ { asciiArtStr }
+ </div>;
+
+ 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 += `<span style='color: rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})'>${asciiArtStr[i]}</span>`;
+ }
+
+ 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(
+ <div class="chat-line">
+ <div class="chat-role-user">
+ { user.settings.nickname }
+ <span style="color:#fff">:&nbsp;</span>
+ </div>
+ <div class="chat-content">
+ { msg }
+ </div>
+ { files && <ChatFiles id={ msglog.length.toString() } files={ files } noremove /> }
+ </div>
+ );
+
+ 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( <div class="chat-line">
+ <div class="chat-role-model">
+ { user.settings.site_prefs.model || 'err' }
+ <span style="color:#fff">:&nbsp;</span>
+ </div>
+ <div class="chat-content">
+ <Spinner style="display: inline" />
+ </div>
+ </div> );
+
+ 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( <div class="chat-content" /> );
+
+ if( isbottom )
+ terminal.scrollTop( terminal[0].scrollHeight );
+}
+
+// inserts a new code div
+function appendCode( lang?: string ) {
+ if( lang ) {
+ $( ".chat-line" ).last().append(
+ <pre>
+ <div class="chat-code-line">{ lang }</div>
+ <code class={ `language-${ lang }` }></code>
+ </pre>
+ );
+ } else {
+ $( ".chat-line" ).last().append( <pre><code></code></pre> );
+ }
+}
+
+
+function ToolInfo( props: any ) {
+ return <div class="tool-call-title-wrapper">
+ <div class="tool-call-title">
+ { props.title }
+ </div>
+ <div class="tool-call-collapse">
+ { props.collapsed ? '+' : '-' }
+ </div>
+ </div>
+}
+
+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 = $( <div class="tool-call" onclick={ collapseToggle } collapsed /> );
+ const ti = $( <ToolInfo title="Reasoning... " /> );
+ ti.find( ".tool-call-title" ).append( <Spinner style="display: inline" /> );
+ el.append( ti );
+ el.append( <div class="reason-output" style="display: none"></div> );
+
+ $( ".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 = $( <div class="tool-call" onclick={ collapseToggle } /> );
+ const trimmed = tool.name.replace( /\s+/g, '' );
+ switch( trimmed.toLowerCase() ) {
+ case 'notes':
+ el.append( <ToolInfo title="Note saved" /> );
+ el.append( <div class="tool-call-list">{ tool.parameters.contents || '' }</div> );
+ break;
+ case 'web':
+ el.append( <ToolInfo title="Searching the web..." /> );
+ el.append( <div class="tool-call-list">{ tool.parameters.url || '' }</div> );
+ if( appendLine ) needs_append = true;
+ break;
+ case 'remind':
+ el.append( <ToolInfo title="Searching the conversation..." /> );
+ el.append( <div class="tool-call-list">{ tool.parameters.keywords ? tool.parameters.keywords.join( ' ' ) : '' }</div> );
+ 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( <div class="chat-error">{ e }</div> );
+
+ 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 = '<think>';
+ 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 = '</think>';
+ if( buf == wanted_end || (buf.length > wanted_end.length && buf.startsWith( wanted_end )) ) {
+ $( ".chat-line" ).last().append( <div class="chat-content">{ '\n' }</div> );
+ $( ".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(
+ <div class="chat-code-line">
+ { lang }
+ </div>
+ );
+ }
+ }
+ 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( <ChatList /> );
+ }).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( <ChatInput noupdate /> );
+
+ 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( <ChatFiles id="cmd-files" files={ file_list } /> );
+}
+
+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 <div id={ props.id } class="cmd-file">
+ <span class="cmd-file-name">{ props.name.toString() }</span>&nbsp;
+ <span class="cmd-file-size">{ util.sizeHumanReadable( props.size ) }</span>&nbsp;
+ { props.key !== undefined &&
+ <a class="cmd-file-remove"
+ onclick={ () => removeFile( props.key ) }
+ href="#"
+ >
+ remove
+ </a>
+ }
+ </div>
+}
+
+function ChatFiles( props: any ) {
+ if( !props.files.length )
+ return <div />
+
+ const show_remove = props.noremove === undefined;
+
+ return <div id={ props.id } class='cmd-files'>
+ <div class="cmd-files-header">
+ Attachments:
+ </div>
+ { props.files.map( ( f: MsgFile, i: number ) => {
+ if( !show_remove )
+ return <ChatFile id={ `${props.id}-${i}` } size={ f.content.length } name={ f.name } />
+ return <ChatFile id={ `${props.id}-${i}` } size={ f.content.length } name={ f.name } key={i} />
+ } ) }
+ </div>
+}
+
+export function ChatCmdLine() {
+ return <div id="cmd-input" contenteditable spellcheck="false"
+ onkeydown={ onInputKeyPress }
+ onpaste={ onPaste }
+ >
+ <span id="input-content"><br /></span>
+ </div>
+}
+
+export function ChatInput( props: any ) {
+ const task = async () => {
+ const chat_id = getChatId();
+ if( !chat_id ) {
+ return;
+ }
+
+ disableInput();
+ $( "#terminal-inner" ).empty();
+ const spinner = $( <Spinner class="chat-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 = $( <div class="chat-line" id="chat-input">
+ <div id="cmd">
+ <div class="chat-role-user" style="width: fit-content; float: left">
+ { user.settings.nickname }<span style="color: #fff">:</span>&nbsp;
+ </div>
+ <ChatCmdLine />
+ <div style="clear: both" />
+ </div>
+ </div> );
+
+ 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( <ChatList /> );
+ }
+
+ const popup = $( <OkPopup>
+ Chat deleted.
+ </OkPopup> );
+
+ addPopup( popup );
+ } catch( e: any ) {
+ const popup = $( <OkPopup>
+ Error deleting chat: { e.message }
+ </OkPopup> );
+
+ addPopup( popup );
+ }
+}
+
+function showDeletePopup( id: string, name: string ) {
+ const popup = $( <OkCancelPopup onclick={ () => { deleteChat( id ) } }>
+ <div>
+ Are you sure you want to delete {name}?
+ </div>
+ </OkCancelPopup> );
+
+ addPopup( popup );
+}
+
+function ChatListEntry( props: any ) {
+ let title = props.title;
+ if( title.length > 20 ) {
+ title = title.substring( 0, 20 ) + '...';
+ }
+
+ return <div class="chat-item" id={ 'chatbtn-' + props.id } style={ props.style }>
+ <div class="chat-name" onclick={ () => JSX.navigateParams( "/terminal", props.id != '0' ? { id: props.id } : {} ) }>
+ <a>{ title }</a>
+ </div>
+ {
+ (() => {
+ if( props.id != 0 )
+ return <div class="chat-delete">
+ <a onclick={ () => showDeletePopup( props.id, props.title ) }>🗑</a>
+ </div>
+ })()
+ }
+ </div>
+}
+
+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 <ChatListEntry id={ file.id } title={ file.name } />
+ } );
+ for( let e of entries )
+ $( "#chat-selection-sidebar" ).append( e );
+ } );
+
+ return <div id="chat-list">
+ <div id="chat-selection">
+ current chat
+ <div id="chat-selection-inner" onmouseenter={ onSidebarHover } ontouchstart={ onSidebarHover }>
+ <div>
+ <span id="chat-selection-current">new chat</span>
+ </div>
+ </div>
+ </div>
+ <div id="chat-selection-sidebar" style="display: none" onmouseleave={ onSidebarLeave } ontouchend={ onSidebarLeave } ontouchcancel={ onSidebarLeave }>
+ <ChatListEntry id={ '0' } title={ "new chat [*]" } style="padding-top: 5px" />
+ </div>
+ </div>
+}
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 = <div class="rolldown">
+ <div class="rolldown-collapsed-container">
+ <div class="rolldown-icon-container">&gt;</div>
+ <div class="rolldown-collapsed">{props.folded}</div>
+ </div>
+ <div class="rolldown-expanded-container">
+ <div class="rolldown-expanded">{props.children}</div>
+ </div>
+ </div>;
+
+ 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 = $( <div class="spinner" id={ id } style={ props.style || '' } /> );
+ 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 <GroupBox title={props.title} style={props.style} innerStyle="margin: 0px; width: 100%">
+ <div class="rolldownlist">
+ { props.children }
+ </div>
+ </GroupBox>
+}
+
+/*
+ * accepts title prop and optional innerStyle
+**/
+export function GroupBox( props: any ) {
+ return <div class="groupbox" id={props.id || ''} style={props.style || ''}>
+ <span class="grouptitle">
+ {props.title}
+ </span>
+ <span class="groupbody" style={props.innerStyle || ''}>
+ {props.children}
+ </span>
+ </div>
+}
+
+export function Page( props: any ) {
+ return <>
+ <Header />
+ <div id="page-main">
+ {props.children}
+ </div>
+ </>
+}
+
+export function DropdownItem( props: any ) {
+ return <div class="dropdown-inner" style={ props.style || "" } onclick={ props.onclick || null }>
+ {props.children}
+ </div>
+}
+
+/**
+ * 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 = $( <div class="dropdown-wrapper" style={ innerStyle }>
+ { children.map( ( child: HTMLElement ) => {
+ if( !child.onclick ) child.onclick = onchange; return child;
+ } ) }
+ </div> );
+ target.parent().append( newDropdown[0] );
+ setTimeout( () => popup_stack.push( { el: newDropdown, fn: null } ) );
+ }
+
+ if( inline ) {
+ return <button class={ 'dropdown' + ' ' + classes } style={ style } onclick={ showItems } id={ id }>
+ { title }
+ </button>
+ }
+ else {
+ return <>
+ <label>{ title }</label>
+ <button class={ 'dropdown' + ' ' + classes } style={ style } onclick={ showItems } id={ id }>
+ </button>
+ </>
+ }
+}
+
+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 <div class={ "popup-msg " + classes } id={ id } style={ style }>
+ { children }
+ <div style="display: flex; justify-content: center">
+ <button onclick={ onclick }>Ok</button>
+ </div>
+ </div>
+}
+
+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 <div class={ "popup-msg " + classes } id={ id } style={ style }>
+ { children }
+ <div style="display: flex; justify-content: center">
+ <button onclick={ onclick } style="margin-right: 10px">Ok</button>
+ <button onclick={ oncancel }>Cancel</button>
+ </div>
+ </div>
+}
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 = $( <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 <Page>not logged in</Page>
+ }
+
+ return <Page>
+ <GroupBox title="First configuration">
+ <div>
+ Welcome to axonbox.net
+ </div>
+ <div>
+ Please input a username. This can be changed at any time.
+ </div>
+ <div id="landing-error" style="display: none"></div>
+ <input type="text" id="username-input" maxlength="15" placeholder="username" />
+ <div style="width: 100%; text-align: right; margin-top: -20px">
+ <button style="width: 150px; margin-right: 5px; display: inline; height: 20px"
+ class="settings-btn"
+ id="confirm-btn"
+ onclick={ saveNickname }
+ >Confirm</button>
+ </div>
+ </GroupBox>
+ </Page>
+}
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 <DropdownItem onclick={ onclick } style={ style }>
+ <a>[ { props.title } ]</a>
+ </DropdownItem>
+}
+
+function SettingsButton( props: any ) {
+ let openSettings = settings.openPopup;
+ let style = `height: 45px;`;
+
+ if( props.disabled ) {
+ openSettings = null;
+ style += "color: #888; pointer-events: none;";
+ }
+
+ return <DropdownItem onclick={ openSettings } style={ style }>
+ <a>[ Settings ]</a>
+ </DropdownItem>
+}
+
+function EmailInput() {
+ const sendLink = async () => {
+ const email = $( "#login-email" ).val().toString();
+ $( "#login-btn-wrapper" ).append( <Spinner /> );
+
+ const res = await user.sendLoginLink( email );
+ if( res === true ) {
+ $( "#header-login-dropdown" ).replaceWith(
+ <div style="padding: 5px">Success! Check your inbox for a login link.</div>
+ );
+ } else {
+ $( "#header-login-dropdown" ).replaceWith(
+ <div style="padding: 5px">Error sending email: { res }</div>
+ );
+ }
+ };
+
+ return <div id="header-dropdown">
+ <div id="header-login-dropdown">
+ Log in
+ <input type="text" placeholder="email" id="login-email" style="margin-bottom: 5px" />
+ <div id="login-btn-wrapper" style="display: flex; justify-content: space-between; margin-top: 5px;">
+ <button onclick={ sendLink }>
+ [ get code ]
+ </button>
+ </div>
+ </div>
+ </div>
+}
+
+function showLoginPopup() {
+ addPopup( $( <EmailInput /> ) );
+}
+
+export function Header() {
+ return <header id="header">
+ <div id="header-user">
+ { user.is_loggedin && <><b>User: </b><b id="username">{ user.settings.nickname }</b>&nbsp;</> }
+ { user.is_loggedin && <button onclick={ logout }>[ Log out ]</button> }
+ { !user.is_loggedin && <button onclick={ showLoginPopup }>[ Log in ]</button> }
+ </div>
+ <div id="header-ctrls">
+ <a style="padding-right: 10px" onclick={ () => JSX.navigate( "/support" ) }>Site Map</a>
+ <Dropdown title="≡" inline innerStyle="top: 24px; margin-left: 0" style="width: 48px" id="header-dropdown-btn">
+ {
+ user.is_loggedin ? <NavItem title="Terminal" route="/terminal" /> : <NavItem title="Terminal" route="/" disabled />
+ }
+ {
+ user.is_loggedin ? <SettingsButton /> : <SettingsButton disabled />
+ }
+ <NavItem title="Home" route="/" />
+ </Dropdown>
+ </div>
+ </header>
+}
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></span> );
+ span.html( "&gt; " );
+ 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( <br/> );
+ }
+}
+
+function HomeInfo() {
+ const ret = <div id="homepagemain">
+ <GroupBox title="Questions to ask" id="q2a"/>
+ <GroupBox title="Features" id="features" />
+ </div>;
+
+ setTimeout( () => {
+ runTypewriter( text_l, "#q2a" );
+ runTypewriter( text_r, "#features" );
+ } );
+ return ret;
+}
+
+function HomeFaq() {
+ return <RolldownList title="FAQ" style="min-width: 333px; max-width: 600px; width: calc( 100% - 12px )">
+ <RolldownListItem folded="What makes our service the ideal choice?">
+ 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.
+ </RolldownListItem>
+ <RolldownListItem folded="What do we offer that others can't?">
+ 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.
+ </RolldownListItem>
+ <RolldownListItem folded="How do you plan to shape the AI sector?">
+ 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.
+ </RolldownListItem>
+ <RolldownListItem folded="What forms of payment do we accept?">
+ 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.
+ </RolldownListItem>
+ <RolldownListItem folded="How can I get in touch?">
+ The best form of contact will be through our support account <a href="https://twitter.com/axonbox">here on Twitter</a>. Alternatively, you can email us <a href="mailto:support@axonbox.net">here</a>. However, since we are a small team, Twitter would be the most convenient.
+ </RolldownListItem>
+ </RolldownList>
+}
+
+export default function Home() {
+ return <Page>
+ <AsciiArt />
+ <h3 style="margin-top: 15px">Welcome your new second-in-command</h3>
+ <HomeInfo />
+ <HomeFaq />
+ </Page>;
+}
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", () => <PaymentSuccess /> );
+JSX.addRoute( "/first-landing", () => <FirstLanding /> );
+JSX.addRoute( "/terminal", () => <Terminal /> );
+JSX.addRoute( "/tutorial", () => <Tutorial /> );
+JSX.addRoute( "/support", () => <Support /> );
+JSX.addRoute( "/upgrade", () => <Upgrade /> );
+JSX.addRoute( "/models", () => <Models /> );
+JSX.addRoute( "/login", () => <Login /> );
+JSX.addRoute( "/tos", () => <ToS /> );
+JSX.addRoute( "/api", () => <Api /> );
+JSX.addRoute( "/", () => <Home /> );
+
+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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="description" content=" ">
+ <meta name="viewport" content="width=device-width,user-scalable=no">
+ <title>axonbox.net</title>
+ <link rel="stylesheet" href="/static/main.css">
+ <link href="/static/highlight.css" rel="preload" as="style" onload="this.rel='stylesheet'">
+ <link rel="apple-touch-icon" href="files/globe.ico">
+ <link rel="icon" href="files/globe.ico" type="image/x-icon">
+ <link rel="shortcut icon" href="files/globe.ico" type="image/x-icon">
+ </head>
+ <body>
+ <div id="moneyjsx-root">
+ <div id="homepage" style="padding-top: 0px">
+ <noscript>
+ <!--- for browsers with noscript !--->
+ <div id="ascii-art">
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # .. # #
+ # # .. # #
+ # # .### ###
+ # # ### ### ..
+ # # # # ....
+ # # # # ....
+ # # # # ..
+ # # # #
+ # # # #
+ .. ##############################################
+ ................................... ###.. ### ........... ### .........###
+ .................................. ####+############################################
+ #######################################+# #.#
+ #####-# #.#
+ #################################### # # #.#
+ # # #.#
+ ######################################### #.#......................................
+ ### #.#...............--.....................
+ ######################################### | ##################.######################
+ # # ,---.. ,,---.,---.|---.,---.. , ### ### ###...
+ ################ ,---| &rt;&lt; | || || || | &rt;&lt ################ ### ####################
+ ########################### .. ### `---^' ``---'` '`---'`---'' ` #.# ... ### ###
+ ################## #.# ... #######
+ ######################### ... # # #.# . ##################
+ ############################. #.# #.# # # ..............
+ ###- #.# #-########################...............
+ ############################- # # #+# ...###...... ......
+ .........................##############-# #-########################.... ..
+ .........................###. #-# #.# .......................
+ ..............+########################################################## ......................
+ .................###................... ### ### ...### ..........
+ .............. ##############################################
+ ....# #..... # #
+ ... ... # # # #
+ ................ # # # #
+ .................. # # # #
+ .......################## # # .
+ .....###### ### # #
+ ####+###################. # #
+ ######+ .............. # #
+ # # ................. # #
+ # # ...... # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ # # # #
+ </div>
+ <h3 style="margin-top: 5px;">axonbox.net</h3>
+ <div>
+ you need javascript to use this website
+ </div>
+ </noscript>
+ </div>
+ </div>
+ </body>
+</html>
+
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 <Page>
+ <GroupBox title="Login">
+ <span id="login-msg">{ msg }</span>
+ </GroupBox>
+ </Page>
+}
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 <Page>
+ <GroupBox title="Payment successful">
+ <div style="padding: 5px">
+ Your payment was successful. You should be redirected shortly...
+ </div>
+ <div style="width: 100%; text-align: right;">
+ <button style="margin-right: 5px" onclick={ () => JSX.navigateParams( "/terminal", {} ) }>
+ go to terminal
+ </button>
+ </div>
+ </GroupBox>
+ </Page>
+}
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( $( <OkPopup>
+ Error deleting token: { e.message }
+ </OkPopup> ) );
+ }
+ }
+
+ addPopup( $(
+ <OkCancelPopup onclick={ deleteToken }>
+ Are you sure you want to erase this API token?
+ </OkCancelPopup>
+ ) );
+}
+
+function showDeleteAllTokensPopup() {
+ const deleteTokens = async() => {
+ try {
+ await user.deleteAllTokens();
+ closePopup();
+ } catch( e: any ) {
+ addPopup( $( <OkPopup>
+ Error deleting all tokens: { e.message }
+ </OkPopup> ) );
+ }
+ }
+
+ addPopup( $(
+ <OkCancelPopup onclick={ deleteTokens }>
+ Are you sure you want to erase all API tokens?
+ </OkCancelPopup>
+ ) );
+}
+
+function TokenEntry( props: any ) {
+ const token = props.token;
+
+ return <div class="tokens-entry" id={ `token-${ token.id }` }>
+ <span>{ token.value }</span>
+ <button class="tokens-delete" onclick={ () => showDeleteTokenPopup( token.id ) }>🗑</button>
+ </div>
+}
+
+async function createNewToken( e: Event ) {
+ const btn = $( e.target );
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ const spinner = $( <Spinner /> );
+ btn.append( spinner );
+
+ try {
+ const token = await user.createToken();
+ const tokens = await user.getTokens();
+ // re-draw token popup
+ closePopup();
+ addPopup( $( <TokensPopup tokens={ tokens } /> ) );
+
+ const copyTokenToClipboard = () => { navigator.clipboard.writeText( token ); }
+ addPopup( $(
+ <OkCancelPopup
+ style="max-width: 330px; width: auto; text-align: center; display: flex; justify-content: center;"
+ onclick={ copyTokenToClipboard }
+ >
+ Your api token has been created.<br />
+ Make sure to save it as it will not be shown to you again.<br />
+ <div style="background-color: #000; width: 100%; overflow: scroll; padding: 5px">
+ { token }
+ </div>
+
+ Press OK to copy the token to clipboard.
+ </OkCancelPopup>
+ ) );
+ } catch( e: any ) {
+ addPopup( $(
+ <OkPopup>
+ Error creating token: { e.message }
+ </OkPopup>
+ ) );
+ }
+
+ spinner.remove();
+}
+
+function TokensPopup( props: any ) {
+ return <div id="tokens-popup">
+ { props.tokens.length > 0 &&
+ <div id="tokens-inner">
+ { props.tokens.map( ( tok: any ) => {
+ return <TokenEntry token={tok} />
+ } ) }
+ </div>
+ }
+ {
+ props.tokens.length == 0 && <div>
+ No api tokens have been created.
+ </div>
+ }
+ <div style="display: flex; margin-top: 10px;">
+ <button class="settings-btn" style="width: 150px; margin-right: 5px" onclick={ createNewToken }>Create new token</button>
+ <button class="settings-btn" style="width: 150px" onclick={ showDeleteAllTokensPopup }>Delete all tokens</button>
+ </div>
+ </div>
+}
+
+async function openTokensPopup() {
+ let spinner = $( <Spinner /> );
+ $( "#tokens-btn" ).append( spinner );
+ try {
+ const tokens = await user.getTokens();
+ spinner.remove();
+
+ return addPopup( $( <TokensPopup tokens={ tokens } /> ) );
+ } catch( e: any ) {
+ spinner.remove();
+ return addPopup( $( <OkPopup>{ e.message }</OkPopup> ) );
+ }
+}
+
+
+function showDeleteNotePopup( note_id: string ) {
+ const deleteNote = async() => {
+ try {
+ await user.deleteNote( note_id );
+ $( `#note-${ note_id }` ).remove();
+ } catch( e: any ) {
+ addPopup( $(
+ <OkPopup>
+ Error deleting note<br />{ e.message }
+ </OkPopup>
+ ) );
+ }
+ }
+
+ addPopup( $(
+ <OkCancelPopup onclick={ deleteNote }>
+ Are you sure you want to delete this note?
+ </OkCancelPopup>
+ ) );
+}
+
+function showDeleteAllNotesPopup() {
+ addPopup( $(
+ <OkCancelPopup onclick={ async() => { await user.deleteAllNotes(); closePopup(); } }>
+ Are you sure you want to delete all notes?
+ </OkCancelPopup>
+ ) );
+}
+
+function NotesPopup( props: any ) {
+ return <div id="notes-popup">
+ <div id="notes-inner">
+ { props.notes.map( ( n: any ) => {
+ return <div class="notes-entry" id={ `note-${ n.id }` }>
+ <span>{ n.content.split(' : ')[1] }</span>
+ <button class="notes-delete" onclick={ () => showDeleteNotePopup( n.id ) }>🗑</button>
+ </div>
+ } ) }
+ </div>
+ <div style="display: flex; margin-top: 10px;">
+ <button class="settings-btn" style="width: 200px" onclick={ showDeleteAllNotesPopup }>Delete all notes</button>
+ </div>
+ </div>
+}
+
+async function openNotesPopup() {
+ let spinner = $( <Spinner /> );
+ $( "#notes-btn" ).append( spinner );
+ try {
+ const notes = await user.getNotes();
+ spinner.remove();
+ if( notes.length == 0 )
+ return addPopup( $( <OkPopup>No notes found</OkPopup> ) );
+
+ return addPopup( $( <NotesPopup notes={ notes } /> ) );
+ } catch( e: any ) {
+ spinner.remove();
+ return addPopup( $( <OkPopup>{ e.message }</OkPopup> ) );
+ }
+}
+
+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 = $( <OkCancelPopup onclick={ () => JSX.navigateParams( "/upgrade", {} ) }>
+ This model is only available for paid users.
+ Please upgrade your plan to use it.
+ </OkCancelPopup> );
+
+ 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( <Spinner /> );
+ try {
+ await user.savePrefs( newprefs );
+ } catch( e: any ) {
+ $( "#settings-spinner-wrapper" ).empty();
+ $( "#settings-spinner-wrapper" ).append( <div class="settings-err">{ e.message }</div> );
+ 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 = $( <div style="margin-top: 10px; display: flex; justify-content: space-evenly" /> );
+ if( user.settings.plan.plan == 'free' ) {
+ el.append(
+ <span>
+ Plan: <b>free</b>&nbsp;
+ <a onclick={ () => JSX.navigate( "/upgrade" ) }>[ upgrade ]</a>
+ </span>
+ );
+ } else {
+ el.append(
+ <span>
+ Plan: <b>{ user.settings.plan.plan }</b>
+ </span>
+ );
+
+ 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(
+ <span>
+ Time left: { days_left.toString() } days
+ </span>
+ );
+ }
+
+ return el[0];
+}
+
+async function downloadAllData() {
+ const spinner = $( <Spinner /> );
+ $( "#settings-spinner-wrapper" ).append( spinner );
+
+ try {
+ await user.downloadAllData();
+ } catch( e: any ) {
+ addPopup( $(
+ <OkPopup>
+ Error processing request: { e.message }
+ </OkPopup>
+ ) );
+ }
+
+ spinner.remove();
+}
+
+async function invalidateAllSessions() {
+ const spinner = $( <Spinner /> );
+ $( "#settings-spinner-wrapper" ).append( spinner );
+
+ try {
+ await user.invalidateAllSessions();
+ } catch( e: any ) {
+ addPopup( $(
+ <OkPopup>
+ Error invalidating sessions: { e.message }
+ </OkPopup>
+ ) );
+ }
+
+ spinner.remove();
+}
+
+function SettingsPopup() {
+ return <div id="settings-popup">
+ <div id="settings-top">
+ <div>
+ <label>System prompt</label><br/>
+ <textarea maxlength="1024" placeholder="..." id="system-prompt">
+ { util.escapeHtml( user.settings.prompt_data.system || '' ) }
+ </textarea>
+ </div>
+ <div>
+ <div class="chat-column">
+ <label style="margin-bottom: 1px">Username</label>
+ <input type="text" placeholder={ user.settings.nickname || '' } id="uname-setting" />
+ </div>
+ <div class="chat-column">
+ <label>Site font</label>
+ <Dropdown title={ user.settings.site_prefs.font } id="settings-font" inline>
+ { fonts.map( ( f ) => <DropdownItem onclick={ onFontChanged }>{ f }</DropdownItem> ) }
+ </Dropdown>
+ </div>
+ <div class="chat-column">
+ <label>Chat model</label>
+ <Dropdown title={ user.settings.site_prefs.model! } id="settings-model" inline>
+ { api.models.map( ( m ) => {
+ if( m.free || user.settings.plan.plan == 'paid' )
+ return <DropdownItem onclick={ onModelChanged }>{ m.name }</DropdownItem>;
+ return <DropdownItem onclick={ onModelChanged } style="color: #888">{ m.name }</DropdownItem>;
+ } ) }
+ </Dropdown>
+ </div>
+ <div style="display: flex; justify-content: space-between; margin-top: 10px">
+ <button class="settings-btn" id="notes-btn" onclick={ openNotesPopup }>View notes</button>
+ <button class="settings-btn" id="tokens-btn" onclick={ openTokensPopup }>View tokens</button>
+ </div>
+ <PlanDisplay />
+ </div>
+ </div>
+
+ <div style="display: flex; flex-direction: column;">
+ <button style="margin: 2px" onclick={ downloadAllData }>download all my data</button>
+ <button style="margin: 2px" onclick={ invalidateAllSessions }>sign out all devices</button>
+ <button style="margin: 2px" onclick={ save }>Save</button>
+ <div id="settings-spinner-wrapper" />
+ </div>
+ </div>;
+}
+
+export function openPopup() {
+ if( !user.is_loggedin )
+ return;
+ const el = $( <SettingsPopup /> );
+ 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 <Page>
+ <GroupBox title="API" style="width: 85%">
+ <div id="support-container" style="text-align: left; width: 100%">
+
+ <h2>POST /update-settings</h2>
+ <p>Updates user settings based on the preferences provided.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre><code>{ `{
+ "token": "JWT token",
+ "prefs": {
+ "nickname": "string",
+ "prompt_data": { "system": "string" },
+ "site_prefs": { "model": "string" }
+ }
+}` }</code></pre>
+
+ <h3>Responses</h3>
+<pre>{ $( `<code>{
+ "status": "ok" | "error",
+ "msg": "settings updated" | "error message",
+ "userprefs": {
+ "uuid": "string", <span class="comment">/* the user uuid */</span>
+ "nickname": "string", <span class="comment">/* the user's set nickname */</span>
+ "prompt_data": { "system": "string" }, <span class="comment">/* the custom system prompt */</span>
+ "site_prefs": { "model": "string" }, <span class="comment">/* the user preferred model */</span>
+ "chat_files": { "files": [ { "id": "string", "name": "string" } ] }, <span class="comment">/* list of user's chat files */</span>
+ }
+}</code>` )[0] }</pre>
+ <ul>
+ <li><span class="status">200 OK</span>: Settings updated successfully</li>
+ <li><span class="status">400 Bad Request</span>: Invalid value or nickname out of allowed range</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">500 Internal Server Error</span>: General server error</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /settings</h2>
+ <p>Fetches user settings.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre><code>{ `{
+ "token": "JWT token"
+}` }</code></pre>
+
+ <h3>Responses</h3>
+<pre>{ $( `<code>{
+ "uuid": "string", <span class="comment">/* the user uuid */</span>
+ "nickname": "string", <span class="comment">/* the user's set nickname */</span>
+ "prompt_data": { "system": "string" }, <span class="comment">/* the custom system prompt */</span>
+ "site_prefs": { "model": "string" }, <span class="comment">/* the user preferred model */</span>
+ "chat_files": { "files": [ { "id": "string", "name": "string" } ] }, <span class="comment">/* list of user's chat files */</span>
+}</code>` )[0] }</pre>
+ <ul>
+ <li><span class="status">200 OK</span>: Returns user preferences</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">500 Internal Server Error</span>: No user data found</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /create-chat</h2>
+ <p>Creates a new chat file for the user.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre><code>{ `{
+ "token": "JWT token"
+}` }</code></pre>
+
+ <h3>Responses</h3>
+<pre>{ $( `<code>{
+ "status": "ok" | "nodata" | "error",
+ "msg": "chat created" | "error message",
+ "chatId": "string" <span class="comment">/* the uuid/filename of the newly created chat. */</span>
+}</code>` )[0] }</pre>
+ <ul>
+ <li><span class="status">200 OK</span>: Chat created with ID and topic name</li>
+ <li><span class="status">400 Bad Request</span>: Missing user data</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">500 Internal Server Error</span>: Error creating chat file or updating database</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /get-chat</h2>
+ <p>Fetches the contents of a specified chat file.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre>{ $( `<code>{
+ "token": "JWT token",
+ "chatId": "string" <span class="comment">/* chat file uuid */</span>
+}</code>` )[0] }</pre>
+
+ <h3>Responses</h3>~
+<pre><code>{ `{
+ "status": "ok" | "error",
+ "msg": "chat not found" | "error message",
+ "contents": [ { }, ... ]
+}` }</code></pre>
+ <ul>
+ <li><span class="status">200 OK</span>: Returns chat file contents</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">404 Not Found</span>: Chat does not exist</li>
+ <li><span class="status">500 Internal Server Error</span>: User data not found</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /models</h2>
+ <p>Lists available models for chat.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre><code>{ `{
+ "token": "JWT token"
+}` }</code></pre>
+
+ <h3>Responses</h3>
+ <ul>
+ <li><span class="status">200 OK</span>: Returns list of models with their attributes</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /generate</h2>
+ <p>Generates text based on the input prompt. Can optionally be used for in-the-middle generation.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre>{ $( `<code>{
+ "token": "JWT token",
+ "prompt": "string", <span class="comment">/* the text before the generated text */</span>
+ "suffix": "string", <span class="comment">/* optional - the text after the generated text */</span>
+ "model": "string",
+ "options": { "seed": Number, "temperature": Number } <span class="comment">/* optional */</span>
+}</code>` )[0] }</pre>
+
+ <h3>Response</h3>
+ <p><strong>Body:</strong></p>
+<pre>{ $( `<code>{
+ "status": "ok" | "error",
+ "response": "string",
+ "finalMsg": "string" <span class="comment">/* optional - only at the end of chunked transmission */</span>
+}</code>` )[0] }</pre>
+
+ <ul>
+ <li><span class="status">200 OK</span>: Returns streamed chat response</li>
+ <li><span class="status">400 Bad Request</span>: Missing required parameters</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">429 Too Many Requests</span>: A request from this user is already being processed</li>
+ <li><span class="status">504 Gateway Timeout</span>: No chat instance available</li>
+ <li><span class="status">500 Internal Server Error</span>: Chat server error</li>
+ </ul>
+
+ <hr />
+
+ <h2>POST /chat</h2>
+ <p>Sends a message to the chat server and streams the response back to the user.</p>
+
+ <h3>Request</h3>
+ <p><strong>Body:</strong></p>
+<pre>{ $( `<code>{
+ "token": "JWT token",
+ "model": "string",
+ "messages": [ {
+ "role": "user" | "assistant" | "tool" | "system",
+ "content": "string",
+ timestamp: "string"
+ } ],
+ "system": "string", <span class="comment">/* optional */</span>
+ "options": { "seed": Number, "temperature": Number }, <span class="comment">/* optional */</span>
+ "chatfile": "string" <span class="comment">/* optional - chat file uuid */</span>
+}</code>` )[0] }</pre>
+
+ <h3>Responses</h3>
+ <p><strong>Message:</strong></p>
+<pre><code>{ `{
+ "status": "ok" | "error",
+ "msg": "string",
+ "done": false,
+ "tool": "string"
+}` }</code></pre>
+ <p><strong>End:</strong></p>
+<pre><code>{ `{
+ "status": "ok" | "error",
+ "msg": "string",
+ "fullMsg": "full message returned by the model",
+ "done": true | false
+}` }</code></pre>
+ <ul>
+ <li><span class="status">200 OK</span>: Returns streamed chat response</li>
+ <li><span class="status">400 Bad Request</span>: Missing required parameters</li>
+ <li><span class="status">401 Unauthorized</span>: Invalid or expired token</li>
+ <li><span class="status">429 Too Many Requests</span>: A request from this user is already being processed</li>
+ <li><span class="status">504 Gateway Timeout</span>: No chat instance available</li>
+ <li><span class="status">500 Internal Server Error</span>: Chat server error</li>
+ </ul>
+ </div>
+ </GroupBox>
+ </Page>
+}
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 <Page>
+ <div>
+ <Spinner />
+ </div>
+ </Page>
+ }
+
+ 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 <Page>
+ <GroupBox title="Models" style="width: 85%">
+ <div id="selector-wrapper">
+ <Dropdown title={ model } id="model-selector" inline>
+ { API.models.map( ( m ) => <DropdownItem onclick={ changeModel }>{ m.name }</DropdownItem> ) }
+ </Dropdown>
+ </div>
+
+ <div id="model-list"><ModelInfo model={ API.getModelFromName( model ) } /></div>
+ </GroupBox>
+ </Page>
+}
+
+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(
+ <ModelInfo model={ m } />
+ );
+
+ $( "#model-selector" ).text( m.name );
+}
+
+
+function ModelInfo( props: any ) {
+ const m = props.model as API.Model;
+ return <div class="model-item">
+ <div class="model-header" style="text-align: left">
+ <h2 style="padding: 0px 10px 20px; margin: 0;">
+ { m.name }
+ </h2>
+ </div>
+ <div class="model-body">
+ <div class="model-capabilities">
+ <span class={ m.capabilities.web ? "green" : "red" }>Web { m.capabilities.web ? '✔' : '✘' }</span>
+ <span class={ m.capabilities.notes ? "green" : "red" }>Notes { m.capabilities.notes ? '✔' : '✘' }</span>
+ <span class={ m.capabilities.vision ? "green" : "red" }>Vision { m.capabilities.vision ? '✔' : '✘' }</span>
+ <span class={ m.capabilities.remind ? "green" : "red" }>Memory Lookup { m.capabilities.remind ? '✔' : '✘' }</span>
+ </div>
+ <div class="model-description">
+ { m.description.full }
+ </div>
+ <div id="model-license-wrapper"></div>
+ <br />
+ </div>
+ <ModelLicense>
+ { util.escapeHtml( m.license ) }
+ </ModelLicense>
+ </div>;
+}
+
+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 = <div class='tool-call' id="model-license" onclick={ collapseToggle }>
+ <div class='tool-call-title-wrapper'>
+ <span class='tool-call-title'>License</span>
+ <span class='tool-call-collapse'>{ license_open ? '-' : '+' }</span>
+ </div>
+ <div class='tool-call-list' style={ `display: ${ license_open ? 'block' : 'none' }; white-space: pre-line; text-align: left` }>
+ { props.children }
+ </div>
+ </div>;
+
+ 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 <Page>
+ <GroupBox title="ToS" style="width: 85%;">
+ <span id="tos-text">
+1. Acceptance of Terms<br />
+ Upon account creation, your agreement to these Terms of Service will be permanently stored in case of legal discourse.<br />
+<br />
+2. Definitions<br />
+ "We", "us", and any first-person references, refer to axonbox as an organization, and the individuals that represent it.<br />
+ "Service" refers to any service ( LLMs, API, module, website, etc. ) provided by axonbox.<br />
+ "The user", "user", and "users" refers to any individual or entity that accesses or uses the Service.<br />
+ "Content" refers to all information, messages, and data provided by Users or generated by the Service.<br />
+<br />
+3. User Responsibilities<br />
+ The user agrees to use the Service only for lawful purposes.<br />
+ The user will not use the Service to generate harmful, illegal, or offensive content.<br />
+ The user must be at least 15 years of age to use the Service.<br />
+ The user must abide by these terms, or else risk a suspension, or permanent ban, of access to our services.<br />
+<br />
+4. Prohibited Uses<br />
+ Users agree not to use the Service in any of the following unintended ways:<br />
+ Engaging in any activity that disrupts or interferes with the Service or its servers.<br />
+ Attempting to gain unauthorized access to any portion of the Service or any other systems or networks connected to the Service.<br />
+ Using the Service for any automated data collection, scraping, or similar activities.<br />
+<br />
+5. Intellectual Property<br />
+ All content generated by the service is owned by the user unless otherwise stated.<br />
+ 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.<br />
+<br />
+6. Account Management<br />
+ Users must create an account to access the features and services provided by axonbox.<br />
+ 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.<br />
+<br />
+7. Privacy Policy<br />
+ 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.<br />
+<br />
+ Data collected by axonbox is limited to:<br />
+ - User email<br />
+ - User Service preferences<br />
+ - Account creation timestamp<br />
+ - IP Addresses of web requests<br />
+ - Notes generated by chat models<br />
+ - Chat logs until deletion ( encrypted with AES-256 )<br />
+<br />
+8. Dispute Resolution<br />
+ Any disputes arising from these Terms will be resolved through binding arbitration in accordance with the rules of Texas law.<br />
+<br />
+9. Limitation of Liability<br />
+ Axonbox is not liable for any direct, indirect, incidental, or consequential damages arising from use of the Service.<br />
+<br />
+10. Termination<br />
+ 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.<br />
+<br />
+11. Modifications<br />
+ 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.<br />
+<br />
+12. Governing Law<br />
+ These Terms shall be governed by the rules of Texas law.<br />
+<br />
+13. Contact Information<br />
+ For any questions or concerns regarding these Terms, please contact us via our support account on X, accessible from the site map.
+ </span>
+ </GroupBox>
+ </Page>;
+} \ 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:
+ <span>
+ 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 <a onclick={ () => JSX.navigate( "/models" ) }>Models</a> page.
+ <br />
+ 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!
+ </span>
+ },
+ {
+ title: "Effective chatting",
+ content:
+ <span>
+ 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:
+ <div style="padding: 10px 10px 0;">
+ <b>1) Prompting :</b> A good prompt can drastically change the output of a large language model,
+ effective prompts will be elaborated on in the <a onclick={ () => selectPage( pages[ 2 ] ) }>next</a> section.
+ <br /><br />
+ <b>2) Clarify :</b> 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.
+ <br /><br />
+ <b>3) Reminders :</b> 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.
+ <br /><br />
+ <b>4) Tooling :</b> Luckily for you, utilizing our provided <a onclick={ () => selectPage( pages[ 4 ] ) }>tooling</a> 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.
+ </div>
+ </span>
+ },
+ {
+ title: "Prompting",
+ content:
+ <span>
+ 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".
+ <br />
+ 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.
+ <br />
+ <div style="padding: 10px 10px 0;">
+ <b>1) Prompt format matters :</b> 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.
+ <br /><br />
+ <b>2) Prompt interferes with context window :</b> 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.
+ <br /><br />
+ <b>3) Test, test, and test again :</b> 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.
+ </div>
+ <br />
+ The aforementioned resources are as follows, in an order that we'd suggest approaching this topic with :
+ <div style="padding: 10px 10px 0;">
+ 1) <a href="https://github.blog/ai-and-ml/generative-ai/prompt-engineering-guide-generative-ai-llms/" target="_blank">What is prompting</a>
+ <br />
+ 2) <a href="https://github.com/tghurair/agentic-prompting/tree/main/techniques" target="_blank">Basic prompting examples</a>
+ <br />
+ 3) <a href="https://www.promptingguide.ai/models" target="_blank">Model-specific prompting tips</a>
+ </div>
+ </span>
+ },
+ {
+ title: "Modules",
+ content:
+ <span>
+ 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 <a onclick={ () => selectPage( pages[ 5 ] ) }>api</a>, and still create a lively environment for any
+ chatbot, website, video game dialog, or any other endeavour only the mind can uncover.
+ <br />
+ At the moment, our pricing for module implementation to your service starts at $250 USD, and of course goes up with project scope.
+ </span>
+ },
+ {
+ title: "Tools",
+ content:
+ <span>
+ To not repeat ourselves too much here, we'd suggest reading #4 of <a onclick={ () => selectPage( pages[ 1 ] ) }>effective chatting</a> before this portion.
+ <br /><br />
+ 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.
+ </span>
+ },
+ {
+ title: "API",
+ content:
+ <span>
+ 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 <a onclick={ () => JSX.navigate( "/api" ) }>this</a> page.
+ </span>
+ }
+];
+
+let selected_page: TutorialPage = pages[ 0 ];
+export default function Tutorial() {
+ return <Page>
+ <GroupBox title="Tutorial" style="width: 85%; display: flex;">
+ <div id="page-selector-wrapper">
+ <Dropdown title={ selected_page.title } inline id="tutorial-links-box">
+ { pages.map( ( p ) => <DropdownItem onclick={ () => selectPage( p ) }> { p.title } </DropdownItem> ) }
+ </Dropdown>
+ </div>
+
+ <div class="tutorial-section">
+ <span id="tutorial-content">
+ { selected_page.content }
+ </span>
+ </div>
+ </GroupBox>
+ </Page>
+}
+
+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 <Page>
+ <GroupBox title="Support pages" style="width: 60%">
+ <div style="display: flex; flex-direction: column">
+ { /* note day: u have to actually add those routes in index-page.tsx */ }
+ <a onclick={ () => JSX.navigate( "/models" ) }>Models</a>
+ <a onclick={ () => JSX.navigate( "/tutorial" ) }>Tutorial</a>
+ <a onclick={ () => JSX.navigate( "/api" ) }>API</a>
+ <a onclick={ () => JSX.navigate( "/tos" ) }>ToS</a>
+ <a href="https://twitter.com/axonbox">Contact</a>
+ </div>
+ </GroupBox>
+ </Page>
+}
diff --git a/moneyjsx/src/terminal.tsx b/moneyjsx/src/terminal.tsx
new file mode 100644
index 0000000..bc4657d
--- /dev/null
+++ b/moneyjsx/src/terminal.tsx
@@ -0,0 +1,275 @@
+import $ from 'jquery';
+import * as JSX from './jsx';
+import * as api from './api';
+import * as user from './user';
+import * as chat from './chat';
+
+import { Page } from './components';
+import { ChatInput, ChatList } from './chat';
+
+let start_w = 0, start_h = 0;
+let start_mx = 0, start_my = 0;
+let is_resizing = false;
+let has_listener = false;
+
+function startResize( e: MouseEvent ) {
+ const terminal = $( "#terminal" );
+ start_mx = e.pageX;
+ start_my = e.pageY;
+
+ if( terminal[0].style.width )
+ start_w = parseInt( terminal[0].style.width );
+ else
+ start_w = terminal[0].clientWidth;
+
+ if( terminal[0].style.height )
+ start_h = parseInt( terminal[0].style.height );
+ else
+ start_h = terminal[0].clientHeight * 1.05;
+
+ window.addEventListener( "touchmove", resize );
+ window.addEventListener( "touchend", saveSize );
+ window.addEventListener( "touchcancel", saveSize );
+ window.addEventListener( "mousemove", resize );
+ window.addEventListener( "mouseup", saveSize );
+ is_resizing = true;
+}
+
+function resize( e: MouseEvent ) {
+ if( !is_resizing )
+ return;
+
+ let new_w = start_w + ( e.pageX - start_mx ) * 2.0;
+ let new_h = start_h + ( ( e.pageY - start_my ) * 1.05 );
+
+ const body = $( "body" );
+ const max_h = body.outerHeight() - 60;
+
+ if( new_h > max_h )
+ new_h = max_h;
+
+ const terminal = $( "#terminal" );
+
+ terminal.css( `width`, `${ new_w }px` );
+ terminal.css( `height`, `${ new_h }px` );
+}
+
+function saveSize() {
+ if( !is_resizing )
+ return;
+
+ const terminal = $( "#terminal" );
+ let size = {
+ width: terminal[0].style.width,
+ height: terminal[0].style.height,
+ };
+
+ is_resizing = false;
+ localStorage.setItem( "terminal-size", JSON.stringify( size ) );
+
+ window.removeEventListener( "touchmove", resize );
+ window.removeEventListener( "touchend", saveSize );
+ window.removeEventListener( "touchcancel", saveSize );
+ window.removeEventListener( "mousemove", resize );
+ window.removeEventListener( "mouseup", saveSize );
+}
+
+function getStyleForSize() {
+ let style = "";
+ if( window.innerWidth > 768 ) {
+ const size_settings = localStorage.getItem( "terminal-size" );
+ if( size_settings ) {
+ const parsed = JSON.parse( size_settings );
+ style = `width: ${ parsed.width }; height: ${ parsed.height };`;
+ }
+ }
+ else {
+ style = `width: 95%; height: ${Math.floor( window.innerHeight - 130 )}px`;
+ }
+
+ return style;
+}
+
+function onWindowResize() {
+ const terminal = $( "#terminal" );
+ const style = getStyleForSize();
+ terminal.attr( "style", style );
+
+ if( window.innerWidth < 768 )
+ $( "#terminal-resizer" ).hide();
+ else
+ $( "#terminal-resizer" ).show();
+}
+
+function focusInput( e: Event ) {
+ const sel = window.getSelection();
+ if( sel && sel.type == 'Range' )
+ return;
+
+ const input = $( "#cmd-input" );
+ const content = input.find( "#input-content" );
+ if( !input.length || !content.length )
+ return;
+
+ for( let iclass of ( e.target as HTMLElement )?.classList ) {
+ if( iclass.startsWith( "tool-call" ) )
+ return;
+ }
+
+ input[0].focus();
+
+ // move cursor to the end of text
+ if( sel.anchorNode != content[0] && sel.anchorNode.parentElement != content[0] ) {
+ const range = document.createRange();
+ if( content.length ) {
+ const child = content[0].firstChild;
+ if( child ) {
+ range.setStart( child, 0 );
+ range.setEnd( child, child.textContent.length );
+ }
+ else {
+ range.selectNodeContents( content[0] );
+ }
+ range.collapse( false );
+
+ sel.removeAllRanges();
+ sel.addRange( range );
+ }
+ }
+}
+
+function TerminalResizer( props: any ) {
+ return <div id="terminal-resizer"
+ onmousedown={ startResize }
+ ontouchstart={ startResize }
+ style={ props.style || '' }
+ />
+}
+
+let promptc = 0;
+function writePrompt( promptTxt: string ) {
+ let el = $(
+ <div id={ `prompt-${promptc++}` } onclick={ () => inputPrompt( promptTxt ) }>
+ <a href="#"></a>
+ </div>
+ );
+
+ let writeChar = ( str: string, i: number ) => {
+ if( i >= str.length )
+ return;
+
+ let char = str.charAt( i );
+ let link = el.find( 'a' );
+ let text = link.text();
+ link.text( text + char );
+ setTimeout( () => writeChar( str, i + 1 ), 50 );
+ };
+
+ writeChar( promptTxt, 0 );
+ $( '#suggested-prompts' ).append( el );
+}
+
+async function getPrompts() {
+ const prompts_req = await fetch( `${window.location.origin}/static/prompts.json` );
+ const data = await prompts_req.json();
+ const { prompts } = data;
+
+ const shuffled = prompts.sort( () => Math.random() - 0.5 );
+ const random_prompts = shuffled.slice( 0, 5 );
+
+ random_prompts.forEach( ( p: string ) => {
+ writePrompt( p );
+ } );
+}
+
+function inputPrompt( txt: string ) {
+ const input = $( "#cmd-input" );
+ const el = input[0] as HTMLInputElement;
+
+ input.text( txt );
+ input[0].focus();
+
+ let range = document.createRange()
+ let sel = window.getSelection()
+
+ range.setStart( el.childNodes[0], txt.length );
+ range.collapse( true )
+
+ sel.removeAllRanges()
+ sel.addRange( range )
+}
+
+function SuggestedPrompts() {
+ setTimeout( () => {
+ if( !chat.msglog.length )
+ getPrompts();
+ } );
+
+ return <div id="suggested-prompts">
+ </div>
+}
+
+function TerminalWindow() {
+ if( !has_listener ) {
+ window.onresize = onWindowResize;
+ has_listener = true;
+ }
+
+ const style = getStyleForSize();
+ return <div id="terminal" style={ style } onclick={ focusInput }>
+ <div id="terminal-header">
+ <div>
+ <div id="chat-name">new chat</div>
+ <a href="#" onclick={ () => JSX.navigateParams( "/terminal", {} ) }>X</a>
+ </div>
+ </div>
+ <div id="terminal-inner">
+ <ChatInput />
+ <SuggestedPrompts />
+ </div>
+ <div>
+ <TerminalResizer style={ window.innerWidth <= 768 ? "display: none" : "" } />
+ </div>
+ </div>
+}
+
+function ModelCapabilities() {
+ const model = api.getModelFromName( user.settings.site_prefs.model! );
+ if( !model )
+ return <div />
+
+ const capabilities = model.capabilities;
+ if( !capabilities )
+ return <div />
+
+ let model_str = `${model.name} | `;
+ model_str += `vision ${capabilities.vision ? '✔' : '✘'} | `;
+ model_str += `web ${capabilities.web ? '✔' : '✘'} | `;
+ model_str += `notes ${capabilities.notes ? '✔' : '✘'} | `;
+ model_str += `memory lookup ${capabilities.remind ? '✔' : '✘'}`;
+ // todo: later
+ // model_str += ` | reasoning ${capabilities.reasoning ? '✔' : '✘'}`;
+
+ return <div id="model-capabilities">
+ { model_str }
+ </div>
+}
+
+export function updateCapabilitiesDisplay() {
+ const div = $( "#model-capabilities" );
+ if( div.length > 0 )
+ div.replaceWith( <ModelCapabilities /> );
+}
+
+export default function Terminal() {
+ if( !user.is_loggedin ) {
+ setTimeout( () => JSX.navigate( "/" ) );
+ return <Page>not logged in</Page>
+ }
+
+ return <Page>
+ <TerminalWindow />
+ <ModelCapabilities />
+ <ChatList />
+ </Page>
+}
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 '<reference>'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 <div style="display: flex">
+ { params.checked &&
+ <input type="radio"
+ name="product"
+ value={ params.value }
+ onChange={ onProductChange }
+ checked="checked"
+ />
+ }
+ { !params.checked &&
+ <input type="radio"
+ name="product"
+ value={ params.value }
+ onChange={ onProductChange }
+ />
+ }
+ <label class="radio-label" style="margin-left: 5px">
+ { params.text } <span style="color: var(--front)">-- ${ params.price }</span>
+ </label>
+ </div>
+}
+
+function UpgradeLoaded() {
+ return <GroupBox
+ title="Account upgrade"
+ style="width: 90%; max-width: 550px; display: none"
+ innerStyle="width: calc( 100% - 10px )"
+ id="upgrade-inner"
+ >
+ <div style="border-bottom: 1px solid var(--front);">
+ <h2 style="padding: 8px 0px; margin: 0px; font-size: 28px">Summary</h2>
+ <div style="width: 60%">
+ <h3 style="margin: 5px 0;">
+ Product:
+ </h3>
+ <h4 style="margin: 5px 0">
+ <span id="product-name">Axonbox premium account upgrade</span><br />
+ <SelectionRadio text="1 week" price="2.70" value="1"/>
+ <SelectionRadio text="1 month" price="10" value="2" checked="checked"/>
+ <SelectionRadio text="1 year" price="100" value="3"/>
+ </h4>
+ </div>
+ </div>
+ <div>
+ <h3 style="margin: 5px 0">
+ Payment information
+ </h3>
+ </div>
+ <form id="upgrade-form" method="POST" action="" onsubmit={ onFormSubmit }>
+ <div>
+ <label for="name-element">Contact information</label>
+ <div id="name-element"></div>
+ <label for="card-element">Credit or debit card</label>
+ <div id="card-element" style="padding: 10px 0"></div>
+ <div id="card-errors" role="alert"></div>
+ </div>
+ <div>
+ <span style="display:none" id='payment-error'></span>
+ </div>
+ <button type="submit">Submit</button>
+ </form>
+ </GroupBox>
+}
+
+export default function Upgrade() {
+ setTimeout( () => {
+ loadStripe().then( () => {
+ $( <UpgradeLoaded /> ).insertAfter( "#upgrade-loading" );
+ initStripe();
+ waitForStripe();
+ } );
+ } );
+
+ return <Page>
+ <GroupBox title="Account upgrade" id="upgrade-loading">
+ <span>Loading... &nbsp;<Spinner style="display: inline" /></span>
+ </GroupBox>
+ </Page>
+}
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 = <a style="display:none" href={url} download="userdata.zip"/>;
+ $( '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 = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#39;',
+ '/': '&#x2F;',
+ '`': '&#x60;',
+ '=': '&#x3D;'
+ };
+
+ 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';
+}
+