1818 .bg-base-100 {background-color : var (--fallback-b1 , oklch (var (--b1 )/ 1 ))}
1919 .bg-base-200 {background-color : var (--fallback-b2 , oklch (var (--b2 )/ 1 ))}
2020 .bg-base-300 {background-color : var (--fallback-b3 , oklch (var (--b3 )/ 1 ))}
21+ .btn-mini {
22+ @apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
23+ }
2124 </ style >
2225</ head >
2326
@@ -49,14 +52,22 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
4952 🦙 llama.cpp - chat
5053 </ div >
5154 < div class ="flex items-center ">
52- < button v-if ="messages.length > 0 " class ="btn " @click ="deleteConv(viewingConvId) ">
55+ < button v-if ="messages.length > 0 " class ="btn mr-1 " @click ="deleteConv(viewingConvId) " :disabled =" isGenerating ">
5356 <!-- delete conversation button -->
5457 < svg xmlns ="http://www.w3.org/2000/svg " width ="16 " height ="16 " fill ="currentColor " class ="bi bi-trash " viewBox ="0 0 16 16 ">
5558 < path d ="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z "/>
5659 < path d ="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z "/>
5760 </ svg >
5861 </ button >
5962
63+ < button class ="btn " @click ="showConfigDialog = true " :disabled ="isGenerating ">
64+ <!-- edit config button -->
65+ < svg xmlns ="http://www.w3.org/2000/svg " width ="16 " height ="16 " fill ="currentColor " class ="bi bi-gear " viewBox ="0 0 16 16 ">
66+ < path d ="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0 "/>
67+ < path d ="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z "/>
68+ </ svg >
69+ </ button >
70+
6071 <!-- theme controller is copied from https://daisyui.com/components/theme-controller/ -->
6172 < div class ="dropdown dropdown-end dropdown-bottom ">
6273 < div tabindex ="0 " role ="button " class ="btn m-1 ">
@@ -67,7 +78,7 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
6778 </ div >
6879 < ul tabindex ="0 " class ="dropdown-content bg-base-300 rounded-box z-[1] w-52 p-2 shadow-2xl h-80 overflow-y-auto ">
6980 < li >
70- < button
81+ < button
7182 class ="btn btn-sm btn-block w-full btn-ghost justify-start "
7283 :class ="{ 'btn-active': selectedTheme === 'auto' } "
7384 @click ="setSelectedTheme('auto') ">
@@ -95,19 +106,44 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
95106 <!-- placeholder to shift the message to the bottom -->
96107 {{ messages.length === 0 ? 'Send a message to start' : '' }}
97108 </ div >
98- < div v-for ="msg in messages " :class ="{
99- 'chat group': true,
100- 'chat-start': msg.role !== 'user',
101- 'chat-end': msg.role === 'user',
102- } ">
109+ < div v-for ="msg in messages " class ="group ">
103110 < div :class ="{
104- 'chat-bubble markdown': true,
105- 'chat-bubble-primary': msg.role === 'user',
111+ 'chat': true,
112+ 'chat-start': msg.role !== 'user',
113+ 'chat-end': msg.role === 'user',
106114 } ">
107- < vue-markdown :source ="msg.content " />
115+ < div :class ="{
116+ 'chat-bubble markdown': true,
117+ 'chat-bubble-primary': msg.role === 'user',
118+ } ">
119+ <!-- textarea for editing message -->
120+ < template v-if ="editingMsg && editingMsg.id === msg.id ">
121+ < textarea
122+ class ="textarea textarea-bordered w-96 "
123+ v-model ="msg.content "
124+ @keydown.enter ="editUserMsgAndRegenerate(msg) "> </ textarea >
125+ < br />
126+ < button class ="btn btn-ghost mt-2 mr-2 " @click ="editingMsg = null "> Cancel</ button >
127+ < button class ="btn mt-2 " @click ="editUserMsgAndRegenerate(msg) "> Submit</ button >
128+ </ template >
129+ <!-- render message as markdown -->
130+ < vue-markdown v-else :source ="msg.content " />
131+ </ div >
108132 </ div >
109- < div v-if ="msg.role === 'user' " class ="badge cursor-pointer opacity-0 group-hover:opacity-100 ">
110- Edit
133+
134+ <!-- actions for each message -->
135+ < div :class ="{'text-right': msg.role === 'user'} " class ="mx-4 mt-2 mb-2 ">
136+ <!-- user message -->
137+ < button v-if ="msg.role === 'user' " class ="badge btn-mini " @click ="editingMsg = msg " :disabled ="isGenerating ">
138+ ✍️ Edit
139+ </ button >
140+ <!-- assistant message -->
141+ < button v-if ="msg.role === 'assistant' " class ="badge btn-mini mr-2 " @click ="regenerateMsg(msg) " :disabled ="isGenerating ">
142+ 🔄 Regenerate
143+ </ button >
144+ < button v-if ="msg.role === 'assistant' " class ="badge btn-mini mr-2 " @click ="copyMsg(msg) " :disabled ="isGenerating ">
145+ 📋 Copy
146+ </ button >
111147 </ div >
112148 </ div >
113149
@@ -127,12 +163,39 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
127163 placeholder ="Type a message... "
128164 v-model ="inputMsg "
129165 @keydown.enter ="sendMessage "
130- v-bind :disabled ="isGenerating "
166+ :disabled ="isGenerating "
131167 id ="msg-input "
132168 > </ textarea >
133- < button class ="btn btn-primary ml-2 " @click ="sendMessage " v-bind:disabled ="isGenerating "> Send</ button >
169+ < button v-if ="!isGenerating " class ="btn btn-primary ml-2 " @click ="sendMessage "> Send</ button >
170+ < button v-else class ="btn btn-neutral ml-2 " @click ="stopGeneration "> Stop</ button >
134171 </ div >
135172 </ div >
173+
174+ <!-- modal for editing config -->
175+ < dialog class ="modal " :class ="{'modal-open': showConfigDialog} ">
176+ < div class ="modal-box ">
177+ < 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 >
179+ < label class ="form-control mb-2 ">
180+ < div class ="label "> System Message</ div >
181+ < textarea class ="textarea textarea-bordered h-24 " :placeholder ="'Default: ' + configDefault.systemMessage " v-model ="config.systemMessage "> </ textarea >
182+ </ label >
183+ < template v-for ="key in Object.keys(config) ">
184+ < label v-if ="key != 'custom' && key != 'systemMessage' "
185+ class ="input input-bordered flex items-center gap-2 mb-2 ">
186+ < b > {{ key }}</ b >
187+ < input type ="text " class ="grow " :placeholder ="'Default: ' + (configDefault[key] || 'none') " v-model ="config[key] " />
188+ </ label >
189+ </ template >
190+ < label class ="form-control mb-2 ">
191+ < 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 >
192+ < textarea class ="textarea textarea-bordered h-24 " placeholder ="Example: { "mirostat": 1, "min_p": 0.1 } " v-model ="config.custom "> </ textarea >
193+ </ label >
194+ < button class ="btn mr-4 " @click ="config = {...configDefault} "> Reset to default</ button >
195+ < button class ="btn btn-primary " @click ="closeSaveAndConfigDialog "> Save and close</ button >
196+ </ div >
197+ < div class ="modal-backdrop " @click ="closeSaveAndConfigDialog "> </ div >
198+ </ dialog >
136199 </ div >
137200
138201 < script src ="./deps_markdown-it.js "> </ script >
@@ -142,6 +205,16 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
142205
143206 const BASE_URL = localStorage . getItem ( 'base' ) // for debugging
144207 || ( new URL ( '.' , document . baseURI ) . href ) . toString ( ) ; // for production
208+ const CONFIG_DEFAULT = {
209+ apiKey : '' ,
210+ systemMessage : 'You are a helpful assistant.' ,
211+ temperature : 0.8 ,
212+ top_k : 40 ,
213+ top_p : 0.95 ,
214+ max_tokens : - 1 ,
215+ custom : '' , // custom json object
216+ } ;
217+ const THEMES = [ 'light' , 'dark' , 'cupcake' , 'bumblebee' , 'emerald' , 'corporate' , 'synthwave' , 'retro' , 'cyberpunk' , 'valentine' , 'halloween' , 'garden' , 'forest' , 'aqua' , 'lofi' , 'pastel' , 'fantasy' , 'wireframe' , 'black' , 'luxury' , 'dracula' , 'cmyk' , 'autumn' , 'business' , 'acid' , 'lemonade' , 'night' , 'coffee' , 'winter' , 'dim' , 'nord' , 'sunset' ] ;
145218
146219 // markdown support
147220 const VueMarkdown = defineComponent (
@@ -177,6 +250,7 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
177250 } ,
178251 // if convId does not exist, create one
179252 appendMsg ( convId , msg ) {
253+ if ( msg . content === null ) return ;
180254 const conv = Conversations . getOne ( convId ) || {
181255 id : convId ,
182256 lastModified : Date . now ( ) ,
@@ -192,6 +266,13 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
192266 remove ( convId ) {
193267 localStorage . removeItem ( convId ) ;
194268 } ,
269+ filterAndKeepMsgs ( convId , predicate ) {
270+ const conv = Conversations . getOne ( convId ) ;
271+ if ( ! conv ) return ;
272+ conv . messages = conv . messages . filter ( predicate ) ;
273+ conv . lastModified = Date . now ( ) ;
274+ localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
275+ } ,
195276 } ;
196277
197278 // scroll to bottom of chat messages
@@ -212,10 +293,14 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
212293 inputMsg : '' ,
213294 isGenerating : false ,
214295 pendingMsg : null , // the on-going message from assistant
215- abortController : null ,
296+ stopGeneration : ( ) => { } ,
216297 selectedTheme : localStorage . getItem ( 'theme' ) || 'auto' ,
298+ config : JSON . parse ( localStorage . getItem ( 'config' ) || 'null' ) || { ...CONFIG_DEFAULT } ,
299+ showConfigDialog : false ,
300+ editingMsg : null ,
217301 // const
218- themes : [ 'light' , 'dark' , 'cupcake' , 'bumblebee' , 'emerald' , 'corporate' , 'synthwave' , 'retro' , 'cyberpunk' , 'valentine' , 'halloween' , 'garden' , 'forest' , 'aqua' , 'lofi' , 'pastel' , 'fantasy' , 'wireframe' , 'black' , 'luxury' , 'dracula' , 'cmyk' , 'autumn' , 'business' , 'acid' , 'lemonade' , 'night' , 'coffee' , 'winter' , 'dim' , 'nord' , 'sunset' ] ,
302+ themes : THEMES ,
303+ configDefault : { ...CONFIG_DEFAULT } ,
219304 }
220305 } ,
221306 computed : { } ,
@@ -240,12 +325,14 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
240325 newConversation ( ) {
241326 if ( this . isGenerating ) return ;
242327 this . viewingConvId = Conversations . getNewConvId ( ) ;
328+ this . editingMsg = null ;
243329 this . fetchMessages ( ) ;
244330 chatScrollToBottom ( ) ;
245331 } ,
246332 setViewingConv ( convId ) {
247333 if ( this . isGenerating ) return ;
248334 this . viewingConvId = convId ;
335+ this . editingMsg = null ;
249336 this . fetchMessages ( ) ;
250337 chatScrollToBottom ( ) ;
251338 } ,
@@ -255,11 +342,28 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
255342 Conversations . remove ( convId ) ;
256343 if ( this . viewingConvId === convId ) {
257344 this . viewingConvId = Conversations . getNewConvId ( ) ;
345+ this . editingMsg = null ;
258346 }
259347 this . fetchConversation ( ) ;
260348 this . fetchMessages ( ) ;
261349 }
262350 } ,
351+ closeSaveAndConfigDialog ( ) {
352+ try {
353+ if ( this . config . custom . length ) JSON . parse ( this . config . custom ) ;
354+ } catch ( error ) {
355+ alert ( 'Invalid JSON for custom config. Please either fix it or leave it empty.' ) ;
356+ return ;
357+ }
358+ for ( const key of [ 'temperature' , 'top_k' , 'top_p' , 'max_tokens' ] ) {
359+ if ( isNaN ( this . config [ key ] ) ) {
360+ alert ( 'Invalid number for ' + key ) ;
361+ return ;
362+ }
363+ }
364+ this . showConfigDialog = false ;
365+ localStorage . setItem ( 'config' , JSON . stringify ( this . config ) ) ;
366+ } ,
263367 async sendMessage ( ) {
264368 if ( ! this . inputMsg ) return ;
265369 const currConvId = this . viewingConvId ;
@@ -271,20 +375,34 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
271375 } ) ;
272376 this . fetchConversation ( ) ;
273377 this . fetchMessages ( ) ;
274-
275378 this . inputMsg = '' ;
379+ this . editingMsg = null ;
380+ this . generateMessage ( currConvId ) ;
381+ } ,
382+ async generateMessage ( currConvId ) {
383+ if ( this . isGenerating ) return ;
276384 this . pendingMsg = { id : Date . now ( ) + 1 , role : 'assistant' , content : null } ;
277385 this . isGenerating = true ;
386+ this . editingMsg = null ;
278387
279388 try {
280- this . abortController = new AbortController ( ) ;
389+ const abortController = new AbortController ( ) ;
390+ this . stopGeneration = ( ) => abortController . abort ( ) ;
281391 const params = {
282- messages : this . messages ,
392+ messages : [
393+ { role : 'system' , content : this . config . systemMessage } ,
394+ ...this . messages ,
395+ ] ,
283396 stream : true ,
284397 cache_prompt : true ,
398+ temperature : this . config . temperature ,
399+ top_k : this . config . top_k ,
400+ top_p : this . config . top_p ,
401+ max_tokens : this . config . max_tokens ,
402+ ...( this . config . custom . length ? JSON . parse ( this . config . custom ) : { } ) ,
285403 } ;
286404 const config = {
287- controller : this . abortController ,
405+ controller : abortController ,
288406 api_url : BASE_URL ,
289407 endpoint : '/chat/completions' ,
290408 } ;
@@ -304,15 +422,52 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
304422 Conversations . appendMsg ( currConvId , this . pendingMsg ) ;
305423 this . fetchConversation ( ) ;
306424 this . fetchMessages ( ) ;
307- this . pendingMsg = null ;
308- this . isGenerating = false ;
309425 setTimeout ( ( ) => document . getElementById ( 'msg-input' ) . focus ( ) , 1 ) ;
310426 } catch ( error ) {
311- console . error ( error ) ;
312- alert ( error ) ;
313- this . pendingMsg = null ;
314- this . isGenerating = false ;
427+ if ( error . name === 'AbortError' ) {
428+ // user stopped the generation via stopGeneration() function
429+ Conversations . appendMsg ( currConvId , this . pendingMsg ) ;
430+ this . fetchConversation ( ) ;
431+ this . fetchMessages ( ) ;
432+ } else {
433+ console . error ( error ) ;
434+ alert ( error ) ;
435+ this . inputMsg = this . pendingMsg . content || '' ;
436+ }
315437 }
438+
439+ this . pendingMsg = null ;
440+ this . isGenerating = false ;
441+ this . stopGeneration = ( ) => { } ;
442+ } ,
443+
444+ // message actions
445+ regenerateMsg ( msg ) {
446+ if ( this . isGenerating ) return ;
447+ // TODO: somehow keep old history (like how ChatGPT has different "tree")
448+ const currConvId = this . viewingConvId ;
449+ Conversations . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
450+ this . fetchConversation ( ) ;
451+ this . fetchMessages ( ) ;
452+ this . generateMessage ( currConvId ) ;
453+ } ,
454+ copyMsg ( msg ) {
455+ navigator . clipboard . writeText ( msg . content ) ;
456+ } ,
457+ editUserMsgAndRegenerate ( msg ) {
458+ if ( this . isGenerating ) return ;
459+ const currConvId = this . viewingConvId ;
460+ const newContent = msg . content ;
461+ this . editingMsg = null ;
462+ Conversations . filterAndKeepMsgs ( currConvId , ( m ) => m . id < msg . id ) ;
463+ Conversations . appendMsg ( currConvId , {
464+ id : Date . now ( ) ,
465+ role : 'user' ,
466+ content : newContent ,
467+ } ) ;
468+ this . fetchConversation ( ) ;
469+ this . fetchMessages ( ) ;
470+ this . generateMessage ( currConvId ) ;
316471 } ,
317472
318473 // sync state functions
0 commit comments