2024-12-15 12:55:54 +01:00
import './styles.scss' ;
2024-12-03 19:38:44 +01:00
import { createApp , defineComponent , shallowRef , computed , h } from 'vue/dist/vue.esm-bundler.js' ;
import MarkdownIt from 'markdown-it' ;
2024-12-11 20:52:14 +01:00
import TextLineStream from 'textlinestream' ;
2024-12-15 12:55:54 +01:00
// math formula rendering
import 'katex/dist/katex.min.css' ;
2024-12-17 09:52:09 +01:00
import markdownItKatexGpt from './katex-gpt' ;
2024-12-15 12:55:54 +01:00
import markdownItKatexNormal from '@vscode/markdown-it-katex' ;
// code highlighting
import hljs from './highlight-config' ;
import daisyuiThemes from 'daisyui/src/theming/themes' ;
2024-12-17 09:52:09 +01:00
// ponyfill for missing ReadableStream asyncIterator on Safari
2024-12-20 14:12:06 +01:00
import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator' ;
2024-12-17 09:52:09 +01:00
2024-12-11 20:52:14 +01:00
const isDev = import . meta . env . MODE === 'development' ;
2024-12-03 19:38:44 +01:00
// utility functions
const isString = ( x ) => ! ! x . toLowerCase ;
2024-12-11 20:52:14 +01:00
const isBoolean = ( x ) => x === true || x === false ;
const isNumeric = ( n ) => ! isString ( n ) && ! isNaN ( n ) && ! isBoolean ( n ) ;
2024-12-03 19:38:44 +01:00
const escapeAttr = ( str ) => str . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
2024-12-20 14:12:06 +01:00
const copyStr = ( textToCopy ) => {
// Navigator clipboard api needs a secure context (https)
if ( navigator . clipboard && window . isSecureContext ) {
navigator . clipboard . writeText ( textToCopy ) ;
} else {
// Use the 'out of viewport hidden text area' trick
const textArea = document . createElement ( 'textarea' ) ;
textArea . value = textToCopy ;
// Move textarea out of the viewport so it's not visible
textArea . style . position = 'absolute' ;
textArea . style . left = '-999999px' ;
document . body . prepend ( textArea ) ;
textArea . select ( ) ;
document . execCommand ( 'copy' ) ;
}
} ;
2024-12-03 19:38:44 +01:00
// constants
2024-12-15 12:55:54 +01:00
const BASE _URL = isDev
? ( localStorage . getItem ( 'base' ) || 'https://localhost:8080' ) // for debugging
: ( new URL ( '.' , document . baseURI ) . href ) . toString ( ) . replace ( /\/$/ , '' ) ; // for production
console . log ( { BASE _URL } ) ;
2024-12-03 19:38:44 +01:00
const CONFIG _DEFAULT = {
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value.
apiKey : '' ,
systemMessage : 'You are a helpful assistant.' ,
2024-12-11 20:52:14 +01:00
showTokensPerSecond : false ,
2024-12-03 19:38:44 +01:00
// make sure these default values are in sync with `common.h`
2024-12-16 11:31:14 +01:00
samplers : 'edkypmxt' ,
2024-12-03 19:38:44 +01:00
temperature : 0.8 ,
dynatemp _range : 0.0 ,
dynatemp _exponent : 1.0 ,
top _k : 40 ,
top _p : 0.95 ,
min _p : 0.05 ,
xtc _probability : 0.0 ,
xtc _threshold : 0.1 ,
typical _p : 1.0 ,
repeat _last _n : 64 ,
repeat _penalty : 1.0 ,
presence _penalty : 0.0 ,
frequency _penalty : 0.0 ,
dry _multiplier : 0.0 ,
dry _base : 1.75 ,
dry _allowed _length : 2 ,
dry _penalty _last _n : - 1 ,
max _tokens : - 1 ,
custom : '' , // custom json-stringified object
} ;
const CONFIG _INFO = {
apiKey : 'Set the API Key if you are using --api-key option for the server.' ,
systemMessage : 'The starting message that defines how model should behave.' ,
samplers : 'The order at which samplers are applied, in simplified way. Default is "dkypmxt": dry->top_k->typ_p->top_p->min_p->xtc->temperature' ,
temperature : 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.' ,
dynatemp _range : 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.' ,
dynatemp _exponent : 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.' ,
top _k : 'Keeps only k top tokens.' ,
top _p : 'Limits tokens to those that together have a cumulative probability of at least p' ,
min _p : 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.' ,
xtc _probability : 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.' ,
xtc _threshold : 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.' ,
typical _p : 'Sorts and limits tokens based on the difference between log-probability and entropy.' ,
repeat _last _n : 'Last n tokens to consider for penalizing repetition' ,
repeat _penalty : 'Controls the repetition of token sequences in the generated text' ,
presence _penalty : 'Limits tokens based on whether they appear in the output or not.' ,
frequency _penalty : 'Limits tokens based on how often they appear in the output.' ,
dry _multiplier : 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.' ,
dry _base : 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.' ,
dry _allowed _length : 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.' ,
dry _penalty _last _n : 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.' ,
max _tokens : 'The maximum number of token per output.' ,
custom : '' , // custom json-stringified object
} ;
// config keys having numeric value (i.e. temperature, top_k, top_p, etc)
const CONFIG _NUMERIC _KEYS = Object . entries ( CONFIG _DEFAULT ) . filter ( e => isNumeric ( e [ 1 ] ) ) . map ( e => e [ 0 ] ) ;
// list of themes supported by daisyui
2024-12-15 12:55:54 +01:00
const THEMES = [ 'light' , 'dark' ]
// make sure light & dark are always at the beginning
. concat ( Object . keys ( daisyuiThemes ) . filter ( t => t !== 'light' && t !== 'dark' ) ) ;
2024-12-03 19:38:44 +01:00
// markdown support
const VueMarkdown = defineComponent (
( props ) => {
2024-12-15 12:55:54 +01:00
const md = shallowRef ( new MarkdownIt ( {
breaks : true ,
highlight : function ( str , lang ) { // Add highlight.js
if ( lang && hljs . getLanguage ( lang ) ) {
try {
return '<pre><code class="hljs">' +
hljs . highlight ( str , { language : lang , ignoreIllegals : true } ) . value +
'</code></pre>' ;
} catch ( _ _ ) { }
}
return '<pre><code class="hljs">' + md . value . utils . escapeHtml ( str ) + '</code></pre>' ;
}
} ) ) ;
// support latex with double dollar sign and square brackets
md . value . use ( markdownItKatexGpt , {
delimiters : [
{ left : '\\[' , right : '\\]' , display : true } ,
{ left : '\\(' , right : '\\)' , display : false } ,
{ left : '$$' , right : '$$' , display : false } ,
// do not add single dollar sign here, other wise it will confused with dollar used for money symbol
] ,
throwOnError : false ,
} ) ;
// support latex with single dollar sign
md . value . use ( markdownItKatexNormal , { throwOnError : false } ) ;
// add copy button to code blocks
2024-12-03 19:38:44 +01:00
const origFenchRenderer = md . value . renderer . rules . fence ;
md . value . renderer . rules . fence = ( tokens , idx , ... args ) => {
const content = tokens [ idx ] . content ;
const origRendered = origFenchRenderer ( tokens , idx , ... args ) ;
return ` <div class="relative my-4">
< div class = "text-right sticky top-4 mb-2 mr-2 h-0" >
< button class = "badge btn-mini" onclick = "copyStr(${escapeAttr(JSON.stringify(content))})" > 📋 Copy < / b u t t o n >
< / d i v >
$ { origRendered }
< / d i v > ` ;
} ;
window . copyStr = copyStr ;
const content = computed ( ( ) => md . value . render ( props . source ) ) ;
2024-12-20 14:12:06 +01:00
return ( ) => h ( 'div' , { innerHTML : content . value } ) ;
2024-12-03 19:38:44 +01:00
} ,
2024-12-20 14:12:06 +01:00
{ props : [ 'source' ] }
2024-12-03 19:38:44 +01:00
) ;
// input field to be used by settings modal
const SettingsModalShortInput = defineComponent ( {
template : document . getElementById ( 'settings-modal-short-input' ) . innerHTML ,
props : {
label : { type : String , required : false } ,
configKey : String ,
configDefault : Object ,
configInfo : Object ,
modelValue : [ Object , String , Number ] ,
} ,
} ) ;
2024-12-11 20:52:14 +01:00
// message bubble component
const MessageBubble = defineComponent ( {
components : {
VueMarkdown
} ,
template : document . getElementById ( 'message-bubble' ) . innerHTML ,
props : {
config : Object ,
msg : Object ,
isGenerating : Boolean ,
editUserMsgAndRegenerate : Function ,
regenerateMsg : Function ,
} ,
data ( ) {
return {
editingContent : null ,
} ;
} ,
computed : {
timings ( ) {
if ( ! this . msg . timings ) return null ;
return {
... this . msg . timings ,
prompt _per _second : this . msg . timings . prompt _n / ( this . msg . timings . prompt _ms / 1000 ) ,
predicted _per _second : this . msg . timings . predicted _n / ( this . msg . timings . predicted _ms / 1000 ) ,
} ;
}
} ,
methods : {
copyMsg ( ) {
copyStr ( this . msg . content ) ;
} ,
editMsg ( ) {
this . editUserMsgAndRegenerate ( {
... this . msg ,
content : this . editingContent ,
} ) ;
this . editingContent = null ;
} ,
} ,
} ) ;
2024-12-03 19:38:44 +01:00
// coversations is stored in localStorage
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }
// convId is a string prefixed with 'conv-'
const StorageUtils = {
// manage conversations
getAllConversations ( ) {
const res = [ ] ;
for ( const key in localStorage ) {
if ( key . startsWith ( 'conv-' ) ) {
res . push ( JSON . parse ( localStorage . getItem ( key ) ) ) ;
}
}
res . sort ( ( a , b ) => b . lastModified - a . lastModified ) ;
return res ;
} ,
// can return null if convId does not exist
getOneConversation ( convId ) {
return JSON . parse ( localStorage . getItem ( convId ) || 'null' ) ;
} ,
// if convId does not exist, create one
appendMsg ( convId , msg ) {
if ( msg . content === null ) return ;
const conv = StorageUtils . getOneConversation ( convId ) || {
id : convId ,
lastModified : Date . now ( ) ,
messages : [ ] ,
} ;
conv . messages . push ( msg ) ;
conv . lastModified = Date . now ( ) ;
localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
} ,
getNewConvId ( ) {
return ` conv- ${ Date . now ( ) } ` ;
} ,
remove ( convId ) {
localStorage . removeItem ( convId ) ;
} ,
filterAndKeepMsgs ( convId , predicate ) {
const conv = StorageUtils . getOneConversation ( convId ) ;
if ( ! conv ) return ;
conv . messages = conv . messages . filter ( predicate ) ;
conv . lastModified = Date . now ( ) ;
localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
} ,
popMsg ( convId ) {
const conv = StorageUtils . getOneConversation ( convId ) ;
if ( ! conv ) return ;
const msg = conv . messages . pop ( ) ;
conv . lastModified = Date . now ( ) ;
if ( conv . messages . length === 0 ) {
StorageUtils . remove ( convId ) ;
} else {
localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
}
return msg ;
} ,
// manage config
getConfig ( ) {
const savedVal = JSON . parse ( localStorage . getItem ( 'config' ) || '{}' ) ;
// to prevent breaking changes in the future, we always provide default value for missing keys
return {
... CONFIG _DEFAULT ,
... savedVal ,
} ;
} ,
setConfig ( config ) {
localStorage . setItem ( 'config' , JSON . stringify ( config ) ) ;
} ,
getTheme ( ) {
return localStorage . getItem ( 'theme' ) || 'auto' ;
} ,
setTheme ( theme ) {
if ( theme === 'auto' ) {
localStorage . removeItem ( 'theme' ) ;
} else {
localStorage . setItem ( 'theme' , theme ) ;
}
} ,
} ;
// scroll to bottom of chat messages
// if requiresNearBottom is true, only auto-scroll if user is near bottom
const chatScrollToBottom = ( requiresNearBottom ) => {
const msgListElem = document . getElementById ( 'messages-list' ) ;
const spaceToBottom = msgListElem . scrollHeight - msgListElem . scrollTop - msgListElem . clientHeight ;
if ( ! requiresNearBottom || ( spaceToBottom < 100 ) ) {
setTimeout ( ( ) => msgListElem . scrollTo ( { top : msgListElem . scrollHeight } ) , 1 ) ;
}
} ;
2024-12-11 20:52:14 +01:00
// wrapper for SSE
async function * sendSSEPostRequest ( url , fetchOptions ) {
const res = await fetch ( url , fetchOptions ) ;
const lines = res . body
. pipeThrough ( new TextDecoderStream ( ) )
. pipeThrough ( new TextLineStream ( ) ) ;
2024-12-17 09:52:09 +01:00
for await ( const line of asyncIterator ( lines ) ) {
2024-12-11 20:52:14 +01:00
if ( isDev ) console . log ( { line } ) ;
if ( line . startsWith ( 'data:' ) && ! line . endsWith ( '[DONE]' ) ) {
const data = JSON . parse ( line . slice ( 5 ) ) ;
yield data ;
} else if ( line . startsWith ( 'error:' ) ) {
const data = JSON . parse ( line . slice ( 6 ) ) ;
throw new Error ( data . message || 'Unknown error' ) ;
}
}
} ;
2024-12-03 19:38:44 +01:00
const mainApp = createApp ( {
components : {
VueMarkdown ,
SettingsModalShortInput ,
2024-12-11 20:52:14 +01:00
MessageBubble ,
2024-12-03 19:38:44 +01:00
} ,
data ( ) {
return {
conversations : StorageUtils . getAllConversations ( ) ,
messages : [ ] , // { id: number, role: 'user' | 'assistant', content: string }
viewingConvId : StorageUtils . getNewConvId ( ) ,
inputMsg : '' ,
isGenerating : false ,
pendingMsg : null , // the on-going message from assistant
stopGeneration : ( ) => { } ,
selectedTheme : StorageUtils . getTheme ( ) ,
config : StorageUtils . getConfig ( ) ,
showConfigDialog : false ,
// const
themes : THEMES ,
configDefault : { ... CONFIG _DEFAULT } ,
configInfo : { ... CONFIG _INFO } ,
2024-12-15 12:55:54 +01:00
isDev ,
2024-12-03 19:38:44 +01:00
}
} ,
computed : { } ,
mounted ( ) {
document . getElementById ( 'app' ) . classList . remove ( 'opacity-0' ) ; // show app
// scroll to the bottom when the pending message height is updated
const pendingMsgElem = document . getElementById ( 'pending-msg' ) ;
const resizeObserver = new ResizeObserver ( ( ) => {
if ( this . isGenerating ) chatScrollToBottom ( true ) ;
} ) ;
resizeObserver . observe ( pendingMsgElem ) ;
2024-12-15 12:55:54 +01:00
this . setSelectedTheme ( this . selectedTheme ) ;
2024-12-03 19:38:44 +01:00
} ,
2024-12-11 20:52:14 +01:00
watch : {
viewingConvId : function ( val , oldVal ) {
if ( val != oldVal ) {
this . fetchMessages ( ) ;
chatScrollToBottom ( ) ;
this . hideSidebar ( ) ;
}
}
} ,
2024-12-03 19:38:44 +01:00
methods : {
hideSidebar ( ) {
document . getElementById ( 'toggle-drawer' ) . checked = false ;
} ,
setSelectedTheme ( theme ) {
this . selectedTheme = theme ;
2024-12-15 12:55:54 +01:00
document . body . setAttribute ( 'data-theme' , theme ) ;
document . body . setAttribute ( 'data-color-scheme' , daisyuiThemes [ theme ] ? . [ 'color-scheme' ] ? ? 'auto' ) ;
2024-12-03 19:38:44 +01:00
StorageUtils . setTheme ( theme ) ;
} ,
newConversation ( ) {
if ( this . isGenerating ) return ;
this . viewingConvId = StorageUtils . getNewConvId ( ) ;
} ,
setViewingConv ( convId ) {
if ( this . isGenerating ) return ;
this . viewingConvId = convId ;
} ,
deleteConv ( convId ) {
if ( this . isGenerating ) return ;
if ( window . confirm ( 'Are you sure to delete this conversation?' ) ) {
StorageUtils . remove ( convId ) ;
if ( this . viewingConvId === convId ) {
this . viewingConvId = StorageUtils . getNewConvId ( ) ;
}
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
}
} ,
downloadConv ( convId ) {
const conversation = StorageUtils . getOneConversation ( convId ) ;
if ( ! conversation ) {
alert ( 'Conversation not found.' ) ;
return ;
}
const conversationJson = JSON . stringify ( conversation , null , 2 ) ;
const blob = new Blob ( [ conversationJson ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = ` conversation_ ${ convId } .json ` ;
document . body . appendChild ( a ) ;
a . click ( ) ;
document . body . removeChild ( a ) ;
URL . revokeObjectURL ( url ) ;
} ,
async sendMessage ( ) {
if ( ! this . inputMsg ) return ;
const currConvId = this . viewingConvId ;
StorageUtils . appendMsg ( currConvId , {
id : Date . now ( ) ,
role : 'user' ,
content : this . inputMsg ,
} ) ;
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
this . inputMsg = '' ;
this . generateMessage ( currConvId ) ;
chatScrollToBottom ( ) ;
} ,
async generateMessage ( currConvId ) {
if ( this . isGenerating ) return ;
this . pendingMsg = { id : Date . now ( ) + 1 , role : 'assistant' , content : null } ;
this . isGenerating = true ;
try {
const abortController = new AbortController ( ) ;
this . stopGeneration = ( ) => abortController . abort ( ) ;
const params = {
messages : [
{ role : 'system' , content : this . config . systemMessage } ,
... this . messages ,
] ,
stream : true ,
cache _prompt : true ,
samplers : this . config . samplers ,
temperature : this . config . temperature ,
dynatemp _range : this . config . dynatemp _range ,
dynatemp _exponent : this . config . dynatemp _exponent ,
top _k : this . config . top _k ,
top _p : this . config . top _p ,
min _p : this . config . min _p ,
typical _p : this . config . typical _p ,
xtc _probability : this . config . xtc _probability ,
xtc _threshold : this . config . xtc _threshold ,
repeat _last _n : this . config . repeat _last _n ,
repeat _penalty : this . config . repeat _penalty ,
presence _penalty : this . config . presence _penalty ,
frequency _penalty : this . config . frequency _penalty ,
dry _multiplier : this . config . dry _multiplier ,
dry _base : this . config . dry _base ,
dry _allowed _length : this . config . dry _allowed _length ,
dry _penalty _last _n : this . config . dry _penalty _last _n ,
max _tokens : this . config . max _tokens ,
2024-12-11 20:52:14 +01:00
timings _per _token : ! ! this . config . showTokensPerSecond ,
2024-12-03 19:38:44 +01:00
... ( this . config . custom . length ? JSON . parse ( this . config . custom ) : { } ) ,
} ;
2024-12-11 20:52:14 +01:00
const chunks = sendSSEPostRequest ( ` ${ BASE _URL } /v1/chat/completions ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
2024-12-18 15:00:07 +01:00
... ( this . config . apiKey ? { 'Authorization' : ` Bearer ${ this . config . apiKey } ` } : { } )
2024-12-11 20:52:14 +01:00
} ,
body : JSON . stringify ( params ) ,
signal : abortController . signal ,
} ) ;
for await ( const chunk of chunks ) {
const stop = chunk . stop ;
const addedContent = chunk . choices [ 0 ] . delta . content ;
2024-12-03 19:38:44 +01:00
const lastContent = this . pendingMsg . content || '' ;
if ( addedContent ) {
this . pendingMsg = {
id : this . pendingMsg . id ,
role : 'assistant' ,
content : lastContent + addedContent ,
} ;
}
2024-12-11 20:52:14 +01:00
const timings = chunk . timings ;
if ( timings && this . config . showTokensPerSecond ) {
// only extract what's really needed, to save some space
this . pendingMsg . timings = {
prompt _n : timings . prompt _n ,
prompt _ms : timings . prompt _ms ,
predicted _n : timings . predicted _n ,
predicted _ms : timings . predicted _ms ,
} ;
}
2024-12-03 19:38:44 +01:00
}
StorageUtils . appendMsg ( currConvId , this . pendingMsg ) ;
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
setTimeout ( ( ) => document . getElementById ( 'msg-input' ) . focus ( ) , 1 ) ;
} catch ( error ) {
if ( error . name === 'AbortError' ) {
// user stopped the generation via stopGeneration() function
StorageUtils . appendMsg ( currConvId , this . pendingMsg ) ;
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
} else {
console . error ( error ) ;
alert ( error ) ;
// pop last user message
const lastUserMsg = StorageUtils . popMsg ( currConvId ) ;
this . inputMsg = lastUserMsg ? lastUserMsg . content : '' ;
}
}
this . pendingMsg = null ;
this . isGenerating = false ;
this . stopGeneration = ( ) => { } ;
this . fetchMessages ( ) ;
chatScrollToBottom ( ) ;
} ,
// message actions
regenerateMsg ( msg ) {
if ( this . isGenerating ) return ;
// TODO: somehow keep old history (like how ChatGPT has different "tree"). This can be done by adding "sub-conversations" with "subconv-" prefix, and new message will have a list of subconvIds
const currConvId = this . viewingConvId ;
StorageUtils . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
this . generateMessage ( currConvId ) ;
} ,
editUserMsgAndRegenerate ( msg ) {
if ( this . isGenerating ) return ;
const currConvId = this . viewingConvId ;
const newContent = msg . content ;
StorageUtils . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
StorageUtils . appendMsg ( currConvId , {
id : Date . now ( ) ,
role : 'user' ,
content : newContent ,
} ) ;
this . fetchConversation ( ) ;
this . fetchMessages ( ) ;
this . generateMessage ( currConvId ) ;
} ,
// settings dialog methods
closeAndSaveConfigDialog ( ) {
try {
if ( this . config . custom . length ) JSON . parse ( this . config . custom ) ;
} catch ( error ) {
alert ( 'Invalid JSON for custom config. Please either fix it or leave it empty.' ) ;
return ;
}
for ( const key of CONFIG _NUMERIC _KEYS ) {
if ( isNaN ( this . config [ key ] ) || this . config [ key ] . toString ( ) . trim ( ) . length === 0 ) {
alert ( ` Invalid number for ${ key } (expected an integer or a float) ` ) ;
return ;
}
this . config [ key ] = parseFloat ( this . config [ key ] ) ;
}
this . showConfigDialog = false ;
StorageUtils . setConfig ( this . config ) ;
} ,
closeAndDiscardConfigDialog ( ) {
this . showConfigDialog = false ;
this . config = StorageUtils . getConfig ( ) ;
} ,
resetConfigDialog ( ) {
if ( window . confirm ( 'Are you sure to reset all settings?' ) ) {
this . config = { ... CONFIG _DEFAULT } ;
}
} ,
// sync state functions
fetchConversation ( ) {
this . conversations = StorageUtils . getAllConversations ( ) ;
} ,
fetchMessages ( ) {
this . messages = StorageUtils . getOneConversation ( this . viewingConvId ) ? . messages ? ? [ ] ;
} ,
2024-12-15 12:55:54 +01:00
// debug functions
async debugImportDemoConv ( ) {
const res = await fetch ( '/demo-conversation.json' ) ;
const demoConv = await res . json ( ) ;
StorageUtils . remove ( demoConv . id ) ;
for ( const msg of demoConv . messages ) {
StorageUtils . appendMsg ( demoConv . id , msg ) ;
}
this . fetchConversation ( ) ;
}
2024-12-03 19:38:44 +01:00
} ,
} ) ;
mainApp . config . errorHandler = alert ;
try {
mainApp . mount ( '#app' ) ;
} catch ( err ) {
console . error ( err ) ;
document . getElementById ( 'app' ) . innerHTML = ` <div style="margin:2em auto">
Failed to start app . Please try clearing localStorage and try again . < br / >
< br / >
< button class = "btn" onClick = "localStorage.clear(); window.location.reload();" > Clear localStorage < / b u t t o n >
< / d i v > ` ;
}