12
12
.markdown {
13
13
h1 , h2 , h3 , h4 , h5 , h6 , ul , ol , li { all : revert; }
14
14
pre {
15
- @apply whitespace-pre-wrap my-4 rounded-lg p-2;
15
+ @apply whitespace-pre-wrap rounded-lg p-2;
16
16
border : 1px solid currentColor;
17
17
}
18
18
/* TODO: fix markdown table */
25
25
.bg-base-200 {background-color : var (--fallback-b2 , oklch (var (--b2 )/ 1 ))}
26
26
.bg-base-300 {background-color : var (--fallback-b3 , oklch (var (--b3 )/ 1 ))}
27
27
.text-base-content {color : var (--fallback-bc , oklch (var (--bc )/ 1 ))}
28
+ .show-on-hover {
29
+ @apply opacity-0 group-hover:opacity-100;
30
+ }
28
31
.btn-mini {
29
- @apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
32
+ @apply cursor-pointer hover:shadow-md;
30
33
}
31
34
.chat-screen { max-width : 900px ; }
32
35
/* because the default bubble color is quite dark, we will make a custom one using bg-base-300 */
@@ -152,14 +155,14 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
152
155
<!-- actions for each message -->
153
156
< div :class ="{'text-right': msg.role === 'user'} " class ="mx-4 mt-2 mb-2 ">
154
157
<!-- user message -->
155
- < button v-if ="msg.role === 'user' " class ="badge btn-mini " @click ="editingMsg = msg " :disabled ="isGenerating ">
158
+ < button v-if ="msg.role === 'user' " class ="badge btn-minishow-on-hover " @click ="editingMsg = msg " :disabled ="isGenerating ">
156
159
✍️ Edit
157
160
</ button >
158
161
<!-- assistant message -->
159
- < button v-if ="msg.role === 'assistant' " class ="badge btn-mini mr-2 " @click ="regenerateMsg(msg) " :disabled ="isGenerating ">
162
+ < button v-if ="msg.role === 'assistant' " class ="badge btn-mini show-on-hover mr-2 " @click ="regenerateMsg(msg) " :disabled ="isGenerating ">
160
163
🔄 Regenerate
161
164
</ button >
162
- < button v-if ="msg.role === 'assistant' " class ="badge btn-mini mr-2 " @click ="copyMsg(msg) " :disabled ="isGenerating ">
165
+ < button v-if ="msg.role === 'assistant' " class ="badge btn-mini show-on-hover mr-2 " @click ="copyMsg(msg) " :disabled ="isGenerating ">
163
166
📋 Copy
164
167
</ button >
165
168
</ div >
@@ -196,20 +199,21 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
196
199
< h3 class ="text-lg font-bold mb-6 "> Settings</ h3 >
197
200
< div class ="h-[calc(90vh-12rem)] overflow-y-auto ">
198
201
< p class ="opacity-40 mb-6 "> Settings below are saved in browser's localStorage</ p >
202
+ < settings-modal-short-input :config-key ="'apiKey' " :config-default ="configDefault " :config-info ="configInfo " v-model ="config.apiKey "> </ settings-modal-short-input >
199
203
< label class ="form-control mb-2 ">
200
204
< div class ="label "> System Message</ div >
201
205
< textarea class ="textarea textarea-bordered h-24 " :placeholder ="'Default: ' + configDefault.systemMessage " v-model ="config.systemMessage "> </ textarea >
202
206
</ label >
203
207
< template v-for ="configKey in ['temperature', 'top_k', 'top_p', 'min_p', 'max_tokens'] ">
204
- < settings-modal-numeric -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
208
+ < settings-modal-short -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
205
209
</ template >
206
210
<!-- TODO: add more sampling-related configs, please regroup them into different "collapse" sections -->
207
211
<!-- Section: Other sampler settings -->
208
212
< details class ="collapse collapse-arrow bg-base-200 mb-2 overflow-visible ">
209
213
< summary class ="collapse-title font-bold "> Other sampler settings</ summary >
210
214
< div class ="collapse-content ">
211
215
< template v-for ="configKey in ['dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold'] ">
212
- < settings-modal-numeric -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
216
+ < settings-modal-short -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
213
217
</ template >
214
218
</ div >
215
219
</ details >
@@ -218,7 +222,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
218
222
< summary class ="collapse-title font-bold "> Penalties settings</ summary >
219
223
< div class ="collapse-content ">
220
224
< template v-for ="configKey in ['repeat_last_n', 'repeat_penalty', 'presence_penalty', 'frequency_penalty', 'dry_multiplier', 'dry_base', 'dry_allowed_length', 'dry_penalty_last_n'] ">
221
- < settings-modal-numeric -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
225
+ < settings-modal-short -input :config-key ="configKey " :config-default ="configDefault " :config-info ="configInfo " v-model ="config[configKey] " />
222
226
</ template >
223
227
</ div >
224
228
</ details >
@@ -245,7 +249,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
245
249
</ div >
246
250
247
251
<!-- Template to be used by settings modal -->
248
- < template id ="settings-modal-numeric -input ">
252
+ < template id ="settings-modal-short -input ">
249
253
< label class ="input input-bordered join-item grow flex items-center gap-2 mb-2 ">
250
254
<!-- Show help message on hovering on the input label -->
251
255
< div class ="dropdown dropdown-hover ">
@@ -264,9 +268,13 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
264
268
import { createApp , defineComponent , shallowRef , computed , h } from './deps_vue.esm-browser.js' ;
265
269
import { llama } from './completion.js' ;
266
270
271
+ // utility functions
267
272
const isString = ( x ) => ! ! x . toLowerCase ;
268
273
const isNumeric = ( n ) => ! isString ( n ) && ! isNaN ( n ) ;
274
+ const escapeAttr = ( str ) => str . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) ;
275
+ const copyStr = ( str ) => navigator . clipboard . writeText ( str ) ;
269
276
277
+ // constants
270
278
const BASE_URL = localStorage . getItem ( 'base' ) // for debugging
271
279
|| ( new URL ( '.' , document . baseURI ) . href ) . toString ( ) ; // for production
272
280
const CONFIG_DEFAULT = {
@@ -295,7 +303,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
295
303
custom : '' , // custom json-stringified object
296
304
} ;
297
305
const CONFIG_INFO = {
298
- apiKey : '' ,
306
+ apiKey : 'Set the API Key if you are using --api-key option for the server. ' ,
299
307
systemMessage : 'The starting message that defines how model should behave.' ,
300
308
temperature : 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.' ,
301
309
dynatemp_range : 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.' ,
@@ -325,19 +333,28 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
325
333
// markdown support
326
334
const VueMarkdown = defineComponent (
327
335
( props ) => {
328
- const md = shallowRef ( new markdownit ( props . options ?? { breaks : true } ) ) ;
329
- for ( const plugin of props . plugins ?? [ ] ) {
330
- md . value . use ( plugin ) ;
331
- }
336
+ const md = shallowRef ( new markdownit ( { breaks : true } ) ) ;
337
+ const origFenchRenderer = md . value . renderer . rules . fence ;
338
+ md . value . renderer . rules . fence = ( tokens , idx , ...args ) => {
339
+ const content = tokens [ idx ] . content ;
340
+ const origRendered = origFenchRenderer ( tokens , idx , ...args ) ;
341
+ return `<div class="relative my-4">
342
+ <div class="text-right sticky top-4 mb-2 mr-2 h-0">
343
+ <button class="badge btn-mini" onclick="copyStr(${ escapeAttr ( JSON . stringify ( content ) ) } )">📋 Copy</button>
344
+ </div>
345
+ ${ origRendered }
346
+ </div>` ;
347
+ } ;
348
+ window . copyStr = copyStr ;
332
349
const content = computed ( ( ) => md . value . render ( props . source ) ) ;
333
350
return ( ) => h ( "div" , { innerHTML : content . value } ) ;
334
351
} ,
335
- { props : [ "source" , "options" , "plugins" ] }
352
+ { props : [ "source" ] }
336
353
) ;
337
354
338
355
// inout field to be used by settings modal
339
- const SettingsModalNumericInput = defineComponent ( {
340
- template : document . getElementById ( 'settings-modal-numeric -input' ) . innerHTML ,
356
+ const SettingsModalShortInput = defineComponent ( {
357
+ template : document . getElementById ( 'settings-modal-short -input' ) . innerHTML ,
341
358
props : [ 'configKey' , 'configDefault' , 'configInfo' , 'modelValue' ] ,
342
359
} ) ;
343
360
@@ -390,7 +407,11 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
390
407
if ( ! conv ) return ;
391
408
const msg = conv . messages . pop ( ) ;
392
409
conv . lastModified = Date . now ( ) ;
393
- localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
410
+ if ( conv . messages . length === 0 ) {
411
+ StorageUtils . remove ( convId ) ;
412
+ } else {
413
+ localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
414
+ }
394
415
return msg ;
395
416
} ,
396
417
@@ -431,7 +452,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
431
452
const mainApp = createApp ( {
432
453
components : {
433
454
VueMarkdown,
434
- SettingsModalNumericInput ,
455
+ SettingsModalShortInput ,
435
456
} ,
436
457
data ( ) {
437
458
return {
@@ -587,6 +608,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
587
608
this . isGenerating = false ;
588
609
this . stopGeneration = ( ) => { } ;
589
610
this . fetchMessages ( ) ;
611
+ chatScrollToBottom ( ) ;
590
612
} ,
591
613
592
614
// message actions
@@ -600,7 +622,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
600
622
this . generateMessage ( currConvId ) ;
601
623
} ,
602
624
copyMsg ( msg ) {
603
- navigator . clipboard . writeText ( msg . content ) ;
625
+ copyStr ( msg . content ) ;
604
626
} ,
605
627
editUserMsgAndRegenerate ( msg ) {
606
628
if ( this . isGenerating ) return ;
0 commit comments