77
88 <!-- Note: dependencies can de updated using ./deps.sh script -->
99 < link href ="./deps_daisyui.min.css " rel ="stylesheet " type ="text/css " />
10- <!-- Note for daisyui: because we're using a subset of daisyui via CDN, many things won't be included -->
1110 < script src ="./deps_tailwindcss.js "> </ script >
1211 < style type ="text/tailwindcss ">
1312 .markdown {
1413 h1 , h2 , h3 , h4 , h5 , h6 , ul , ol , li { all : revert; }
1514 pre { @apply whitespace-pre-wrap; }
1615 /* TODO: fix markdown table */
1716 }
17+ /*
18+ Note for daisyui: because we're using a subset of daisyui via CDN, many things won't be included
19+ We can manually add the missing styles from https://cdnjs.cloudflare.com/ajax/libs/daisyui/4.12.14/full.css
20+ */
1821 .bg-base-100 {background-color : var (--fallback-b1 , oklch (var (--b1 )/ 1 ))}
1922 .bg-base-200 {background-color : var (--fallback-b2 , oklch (var (--b2 )/ 1 ))}
2023 .bg-base-300 {background-color : var (--fallback-b3 , oklch (var (--b3 )/ 1 ))}
24+ .text-base-content {color : var (--fallback-bc , oklch (var (--bc )/ 1 ))}
2125 .btn-mini {
2226 @apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
2327 }
2630
2731< body >
2832 < div id ="app " class ="flex flex-row ">
33+ <!-- sidebar -->
2934 < div class ="flex flex-col bg-black bg-opacity-5 w-64 py-8 px-4 h-screen overflow-y-auto ">
3035 < h2 class ="font-bold mb-4 ml-4 "> Conversations</ h2 >
36+
37+ <!-- list of conversations -->
3138 < div :class ="{
3239 'btn btn-ghost justify-start': true,
3340 'btn-active': messages.length === 0,
@@ -51,6 +58,8 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
5158 < div class ="grow text-2xl font-bold mt-8 mb-6 ">
5259 🦙 llama.cpp - chat
5360 </ div >
61+
62+ <!-- action buttons (top right) -->
5463 < div class ="flex items-center ">
5564 < button v-if ="messages.length > 0 " class ="btn mr-1 " @click ="deleteConv(viewingConvId) " :disabled ="isGenerating ">
5665 <!-- delete conversation button -->
@@ -119,9 +128,8 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
119128 <!-- textarea for editing message -->
120129 < template v-if ="editingMsg && editingMsg.id === msg.id ">
121130 < textarea
122- class ="textarea textarea-bordered w-96 "
123- v-model ="msg.content "
124- @keydown.enter ="editUserMsgAndRegenerate(msg) "> </ textarea >
131+ class ="textarea textarea-bordered bg-base-100 text-base-content w-96 "
132+ v-model ="msg.content "> </ textarea >
125133 < br />
126134 < button class ="btn btn-ghost mt-2 mr-2 " @click ="editingMsg = null "> Cancel</ button >
127135 < button class ="btn mt-2 " @click ="editUserMsgAndRegenerate(msg) "> Submit</ button >
@@ -147,7 +155,7 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
147155 </ div >
148156 </ div >
149157
150- <!-- pending assistant message -->
158+ <!-- pending (ongoing) assistant message -->
151159 < div id ="pending-msg " class ="chat chat-start ">
152160 < div v-if ="pendingMsg " class ="chat-bubble markdown ">
153161 < span v-if ="!pendingMsg.content " class ="loading loading-dots loading-md "> </ span >
@@ -160,13 +168,14 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
160168 < div class ="flex flex-row items-center mt-8 mb-6 ">
161169 < textarea
162170 class ="textarea textarea-bordered w-full "
163- placeholder ="Type a message... "
171+ placeholder ="Type a message (Shift+Enter to add a new line) "
164172 v-model ="inputMsg "
165- @keydown.enter ="sendMessage "
173+ @keydown.enter.exact.prevent ="sendMessage "
174+ @keydown.enter.shift.exact.prevent ="inputMsg += '\n' "
166175 :disabled ="isGenerating "
167176 id ="msg-input "
168177 > </ textarea >
169- < button v-if ="!isGenerating " class ="btn btn-primary ml-2 " @click ="sendMessage "> Send</ button >
178+ < button v-if ="!isGenerating " class ="btn btn-primary ml-2 " @click ="sendMessage " :disabled =" inputMsg.length === 0 " > Send</ button >
170179 < button v-else class ="btn btn-neutral ml-2 " @click ="stopGeneration "> Stop</ button >
171180 </ div >
172181 </ div >
@@ -175,7 +184,7 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
175184 < dialog class ="modal " :class ="{'modal-open': showConfigDialog} ">
176185 < div class ="modal-box ">
177186 < h3 class ="text-lg font-bold mb-6 "> Settings</ h3 >
178- < p class ="opacity-40 mb-6 "> Settings below are store in browser's localStorage</ p >
187+ < p class ="opacity-40 mb-6 "> Settings below are saved in browser's localStorage</ p >
179188 < label class ="form-control mb-2 ">
180189 < div class ="label "> System Message</ div >
181190 < textarea class ="textarea textarea-bordered h-24 " :placeholder ="'Default: ' + configDefault.systemMessage " v-model ="config.systemMessage "> </ textarea >
@@ -191,6 +200,8 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
191200 < div class ="label inline "> Custom JSON config (For more info, refer to < a class ="underline " href ="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md " target ="_blank " rel ="noopener noreferrer "> server documentation</ a > )</ div >
192201 < textarea class ="textarea textarea-bordered h-24 " placeholder ="Example: { "mirostat": 1, "min_p": 0.1 } " v-model ="config.custom "> </ textarea >
193202 </ label >
203+
204+ <!-- action buttons -->
194205 < button class ="btn mr-4 " @click ="config = {...configDefault} "> Reset to default</ button >
195206 < button class ="btn btn-primary " @click ="closeSaveAndConfigDialog "> Save and close</ button >
196207 </ div >
@@ -229,12 +240,12 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
229240 { props : [ "source" , "options" , "plugins" ] }
230241 ) ;
231242
232- // storage class
233243 // coversations is stored in localStorage
234244 // format: { [convId]: { id: string, lastModified: number, messages: [...] } }
235245 // convId is a string prefixed with 'conv-'
236- const Conversations = {
237- getAll ( ) {
246+ const StorageUtils = {
247+ // manage conversations
248+ getAllConversations ( ) {
238249 const res = [ ] ;
239250 for ( const key in localStorage ) {
240251 if ( key . startsWith ( 'conv-' ) ) {
@@ -245,13 +256,13 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
245256 return res ;
246257 } ,
247258 // can return null if convId does not exist
248- getOne ( convId ) {
259+ getOneConversation ( convId ) {
249260 return JSON . parse ( localStorage . getItem ( convId ) || 'null' ) ;
250261 } ,
251262 // if convId does not exist, create one
252263 appendMsg ( convId , msg ) {
253264 if ( msg . content === null ) return ;
254- const conv = Conversations . getOne ( convId ) || {
265+ const conv = StorageUtils . getOneConversation ( convId ) || {
255266 id : convId ,
256267 lastModified : Date . now ( ) ,
257268 messages : [ ] ,
@@ -267,12 +278,30 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
267278 localStorage . removeItem ( convId ) ;
268279 } ,
269280 filterAndKeepMsgs ( convId , predicate ) {
270- const conv = Conversations . getOne ( convId ) ;
281+ const conv = StorageUtils . getOneConversation ( convId ) ;
271282 if ( ! conv ) return ;
272283 conv . messages = conv . messages . filter ( predicate ) ;
273284 conv . lastModified = Date . now ( ) ;
274285 localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
275286 } ,
287+
288+ // manage config
289+ getConfig ( ) {
290+ return JSON . parse ( localStorage . getItem ( 'config' ) || 'null' ) || { ...CONFIG_DEFAULT } ;
291+ } ,
292+ setConfig ( config ) {
293+ localStorage . setItem ( 'config' , JSON . stringify ( config ) ) ;
294+ } ,
295+ getTheme ( ) {
296+ return localStorage . getItem ( 'theme' ) || 'auto' ;
297+ } ,
298+ setTheme ( theme ) {
299+ if ( theme === 'auto' ) {
300+ localStorage . removeItem ( 'theme' ) ;
301+ } else {
302+ localStorage . setItem ( 'theme' , theme ) ;
303+ }
304+ } ,
276305 } ;
277306
278307 // scroll to bottom of chat messages
@@ -287,15 +316,15 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
287316 } ,
288317 data ( ) {
289318 return {
290- conversations : Conversations . getAll ( ) ,
319+ conversations : StorageUtils . getAllConversations ( ) ,
291320 messages : [ ] , // { id: number, role: 'user' | 'assistant', content: string }
292- viewingConvId : Conversations . getNewConvId ( ) ,
321+ viewingConvId : StorageUtils . getNewConvId ( ) ,
293322 inputMsg : '' ,
294323 isGenerating : false ,
295324 pendingMsg : null , // the on-going message from assistant
296325 stopGeneration : ( ) => { } ,
297- selectedTheme : localStorage . getItem ( 'theme' ) || 'auto' ,
298- config : JSON . parse ( localStorage . getItem ( 'config' ) || 'null' ) || { ... CONFIG_DEFAULT } ,
326+ selectedTheme : StorageUtils . getTheme ( ) ,
327+ config : StorageUtils . getConfig ( ) ,
299328 showConfigDialog : false ,
300329 editingMsg : null ,
301330 // const
@@ -314,17 +343,12 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
314343 } ,
315344 methods : {
316345 setSelectedTheme ( theme ) {
317- if ( theme === 'auto' ) {
318- this . selectedTheme = 'auto' ;
319- localStorage . removeItem ( 'theme' ) ;
320- } else {
321- this . selectedTheme = theme ;
322- localStorage . setItem ( 'theme' , theme ) ;
323- }
346+ this . selectedTheme = theme ;
347+ StorageUtils . setTheme ( theme ) ;
324348 } ,
325349 newConversation ( ) {
326350 if ( this . isGenerating ) return ;
327- this . viewingConvId = Conversations . getNewConvId ( ) ;
351+ this . viewingConvId = StorageUtils . getNewConvId ( ) ;
328352 this . editingMsg = null ;
329353 this . fetchMessages ( ) ;
330354 chatScrollToBottom ( ) ;
@@ -339,9 +363,9 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
339363 deleteConv ( convId ) {
340364 if ( this . isGenerating ) return ;
341365 if ( window . confirm ( 'Are you sure to delete this conversation?' ) ) {
342- Conversations . remove ( convId ) ;
366+ StorageUtils . remove ( convId ) ;
343367 if ( this . viewingConvId === convId ) {
344- this . viewingConvId = Conversations . getNewConvId ( ) ;
368+ this . viewingConvId = StorageUtils . getNewConvId ( ) ;
345369 this . editingMsg = null ;
346370 }
347371 this . fetchConversation ( ) ;
@@ -362,13 +386,13 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
362386 }
363387 }
364388 this . showConfigDialog = false ;
365- localStorage . setItem ( 'config' , JSON . stringify ( this . config ) ) ;
389+ StorageUtils . setConfig ( this . config ) ;
366390 } ,
367391 async sendMessage ( ) {
368392 if ( ! this . inputMsg ) return ;
369393 const currConvId = this . viewingConvId ;
370394
371- Conversations . appendMsg ( currConvId , {
395+ StorageUtils . appendMsg ( currConvId , {
372396 id : Date . now ( ) ,
373397 role : 'user' ,
374398 content : this . inputMsg ,
@@ -419,14 +443,14 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
419443 }
420444 }
421445
422- Conversations . appendMsg ( currConvId , this . pendingMsg ) ;
446+ StorageUtils . appendMsg ( currConvId , this . pendingMsg ) ;
423447 this . fetchConversation ( ) ;
424448 this . fetchMessages ( ) ;
425449 setTimeout ( ( ) => document . getElementById ( 'msg-input' ) . focus ( ) , 1 ) ;
426450 } catch ( error ) {
427451 if ( error . name === 'AbortError' ) {
428452 // user stopped the generation via stopGeneration() function
429- Conversations . appendMsg ( currConvId , this . pendingMsg ) ;
453+ StorageUtils . appendMsg ( currConvId , this . pendingMsg ) ;
430454 this . fetchConversation ( ) ;
431455 this . fetchMessages ( ) ;
432456 } else {
@@ -444,9 +468,9 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
444468 // message actions
445469 regenerateMsg ( msg ) {
446470 if ( this . isGenerating ) return ;
447- // TODO: somehow keep old history (like how ChatGPT has different "tree")
471+ // 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
448472 const currConvId = this . viewingConvId ;
449- Conversations . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
473+ StorageUtils . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
450474 this . fetchConversation ( ) ;
451475 this . fetchMessages ( ) ;
452476 this . generateMessage ( currConvId ) ;
@@ -459,8 +483,8 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
459483 const currConvId = this . viewingConvId ;
460484 const newContent = msg . content ;
461485 this . editingMsg = null ;
462- Conversations . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
463- Conversations . appendMsg ( currConvId , {
486+ StorageUtils . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
487+ StorageUtils . appendMsg ( currConvId , {
464488 id : Date . now ( ) ,
465489 role : 'user' ,
466490 content : newContent ,
@@ -472,10 +496,10 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
472496
473497 // sync state functions
474498 fetchConversation ( ) {
475- this . conversations = Conversations . getAll ( ) ;
499+ this . conversations = StorageUtils . getAllConversations ( ) ;
476500 } ,
477501 fetchMessages ( ) {
478- this . messages = Conversations . getOne ( this . viewingConvId ) ?. messages ?? [ ] ;
502+ this . messages = StorageUtils . getOneConversation ( this . viewingConvId ) ?. messages ?? [ ] ;
479503 } ,
480504 } ,
481505 } ) . mount ( '#app' ) ;
0 commit comments