@@ -17,6 +17,11 @@ import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator';
17
17
18
18
const isDev = import . meta. env . MODE === 'development' ;
19
19
20
+ // types
21
+ /** @typedef {{ id: number, role: 'user' | 'assistant', content: string, timings: any } } Message */
22
+ /** @typedef {{ role: 'user' | 'assistant', content: string } } APIMessage */
23
+ /** @typedef {{ id: string, lastModified: number, messages: Array<Message> } } Conversation */
24
+
20
25
// utility functions
21
26
const isString = ( x ) => ! ! x . toLowerCase ;
22
27
const isBoolean = ( x ) => x === true || x === false ;
@@ -50,6 +55,8 @@ const CONFIG_DEFAULT = {
50
55
apiKey : '' ,
51
56
systemMessage : 'You are a helpful assistant.' ,
52
57
showTokensPerSecond : false ,
58
+ showThoughtInProgress : false ,
59
+ excludeThoughtOnReq : true ,
53
60
// make sure these default values are in sync with `common.h`
54
61
samplers : 'edkypmxt' ,
55
62
temperature : 0.8 ,
@@ -172,6 +179,7 @@ const MessageBubble = defineComponent({
172
179
config : Object ,
173
180
msg : Object ,
174
181
isGenerating : Boolean ,
182
+ showThoughtInProgress : Boolean ,
175
183
editUserMsgAndRegenerate : Function ,
176
184
regenerateMsg : Function ,
177
185
} ,
@@ -188,7 +196,31 @@ const MessageBubble = defineComponent({
188
196
prompt_per_second : this . msg . timings . prompt_n / ( this . msg . timings . prompt_ms / 1000 ) ,
189
197
predicted_per_second : this . msg . timings . predicted_n / ( this . msg . timings . predicted_ms / 1000 ) ,
190
198
} ;
191
- }
199
+ } ,
200
+ splitMsgContent ( ) {
201
+ const content = this . msg . content ;
202
+ if ( this . msg . role !== 'assistant' ) {
203
+ return { content } ;
204
+ }
205
+ let actualContent = '' ;
206
+ let cot = '' ;
207
+ let isThinking = false ;
208
+ let thinkSplit = content . split ( '<think>' , 2 ) ;
209
+ actualContent += thinkSplit [ 0 ] ;
210
+ while ( thinkSplit [ 1 ] !== undefined ) {
211
+ // <think> tag found
212
+ thinkSplit = thinkSplit [ 1 ] . split ( '</think>' , 2 ) ;
213
+ cot += thinkSplit [ 0 ] ;
214
+ isThinking = true ;
215
+ if ( thinkSplit [ 1 ] !== undefined ) {
216
+ // </think> closing tag found
217
+ isThinking = false ;
218
+ thinkSplit = thinkSplit [ 1 ] . split ( '<think>' , 2 ) ;
219
+ actualContent += thinkSplit [ 0 ] ;
220
+ }
221
+ }
222
+ return { content : actualContent , cot, isThinking } ;
223
+ } ,
192
224
} ,
193
225
methods : {
194
226
copyMsg ( ) {
@@ -208,7 +240,10 @@ const MessageBubble = defineComponent({
208
240
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }
209
241
// convId is a string prefixed with 'conv-'
210
242
const StorageUtils = {
211
- // manage conversations
243
+ /**
244
+ * manage conversations
245
+ * @returns {Array<Conversation> }
246
+ */
212
247
getAllConversations ( ) {
213
248
const res = [ ] ;
214
249
for ( const key in localStorage ) {
@@ -219,11 +254,19 @@ const StorageUtils = {
219
254
res . sort ( ( a , b ) => b . lastModified - a . lastModified ) ;
220
255
return res ;
221
256
} ,
222
- // can return null if convId does not exist
257
+ /**
258
+ * can return null if convId does not exist
259
+ * @param {string } convId
260
+ * @returns {Conversation | null }
261
+ */
223
262
getOneConversation ( convId ) {
224
263
return JSON . parse ( localStorage . getItem ( convId ) || 'null' ) ;
225
264
} ,
226
- // if convId does not exist, create one
265
+ /**
266
+ * if convId does not exist, create one
267
+ * @param {string } convId
268
+ * @param {Message } msg
269
+ */
227
270
appendMsg ( convId , msg ) {
228
271
if ( msg . content === null ) return ;
229
272
const conv = StorageUtils . getOneConversation ( convId ) || {
@@ -235,19 +278,36 @@ const StorageUtils = {
235
278
conv . lastModified = Date . now ( ) ;
236
279
localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
237
280
} ,
281
+ /**
282
+ * Get new conversation id
283
+ * @returns {string }
284
+ */
238
285
getNewConvId ( ) {
239
286
return `conv-${ Date . now ( ) } ` ;
240
287
} ,
288
+ /**
289
+ * remove conversation by id
290
+ * @param {string } convId
291
+ */
241
292
remove ( convId ) {
242
293
localStorage . removeItem ( convId ) ;
243
294
} ,
295
+ /**
296
+ * remove all conversations
297
+ * @param {string } convId
298
+ */
244
299
filterAndKeepMsgs ( convId , predicate ) {
245
300
const conv = StorageUtils . getOneConversation ( convId ) ;
246
301
if ( ! conv ) return ;
247
302
conv . messages = conv . messages . filter ( predicate ) ;
248
303
conv . lastModified = Date . now ( ) ;
249
304
localStorage . setItem ( convId , JSON . stringify ( conv ) ) ;
250
305
} ,
306
+ /**
307
+ * remove last message from conversation
308
+ * @param {string } convId
309
+ * @returns {Message | undefined }
310
+ */
251
311
popMsg ( convId ) {
252
312
const conv = StorageUtils . getOneConversation ( convId ) ;
253
313
if ( ! conv ) return ;
@@ -322,17 +382,20 @@ const mainApp = createApp({
322
382
data ( ) {
323
383
return {
324
384
conversations : StorageUtils . getAllConversations ( ) ,
325
- messages : [ ] , // { id: number, role: 'user' | 'assistant', content: string }
385
+ /** @type {Array<Message> } */
386
+ messages : [ ] ,
326
387
viewingConvId : StorageUtils . getNewConvId ( ) ,
327
388
inputMsg : '' ,
328
389
isGenerating : false ,
390
+ /** @type {Array<Message> | null } */
329
391
pendingMsg : null , // the on-going message from assistant
330
392
stopGeneration : ( ) => { } ,
331
393
selectedTheme : StorageUtils . getTheme ( ) ,
332
394
config : StorageUtils . getConfig ( ) ,
333
395
showConfigDialog : false ,
334
396
// const
335
397
themes : THEMES ,
398
+ /** @type {CONFIG_DEFAULT } */
336
399
configDefault : { ...CONFIG_DEFAULT } ,
337
400
configInfo : { ...CONFIG_INFO } ,
338
401
isDev,
@@ -425,42 +488,50 @@ const mainApp = createApp({
425
488
this . isGenerating = true ;
426
489
427
490
try {
491
+ /** @type {CONFIG_DEFAULT } */
492
+ const config = this . config ;
428
493
const abortController = new AbortController ( ) ;
429
494
this . stopGeneration = ( ) => abortController . abort ( ) ;
495
+ /** @type {Array<APIMessage> } */
496
+ let messages = [
497
+ { role : 'system' , content : config . systemMessage } ,
498
+ ...normalizeMsgsForAPI ( this . messages ) ,
499
+ ] ;
500
+ if ( config . excludeThoughtOnReq ) {
501
+ messages = filterThoughtFromMsgs ( messages ) ;
502
+ }
503
+ if ( isDev ) console . log ( { messages} ) ;
430
504
const params = {
431
- messages : [
432
- { role : 'system' , content : this . config . systemMessage } ,
433
- ...this . messages ,
434
- ] ,
505
+ messages,
435
506
stream : true ,
436
507
cache_prompt : true ,
437
- samplers : this . config . samplers ,
438
- temperature : this . config . temperature ,
439
- dynatemp_range : this . config . dynatemp_range ,
440
- dynatemp_exponent : this . config . dynatemp_exponent ,
441
- top_k : this . config . top_k ,
442
- top_p : this . config . top_p ,
443
- min_p : this . config . min_p ,
444
- typical_p : this . config . typical_p ,
445
- xtc_probability : this . config . xtc_probability ,
446
- xtc_threshold : this . config . xtc_threshold ,
447
- repeat_last_n : this . config . repeat_last_n ,
448
- repeat_penalty : this . config . repeat_penalty ,
449
- presence_penalty : this . config . presence_penalty ,
450
- frequency_penalty : this . config . frequency_penalty ,
451
- dry_multiplier : this . config . dry_multiplier ,
452
- dry_base : this . config . dry_base ,
453
- dry_allowed_length : this . config . dry_allowed_length ,
454
- dry_penalty_last_n : this . config . dry_penalty_last_n ,
455
- max_tokens : this . config . max_tokens ,
456
- timings_per_token : ! ! this . config . showTokensPerSecond ,
457
- ...( this . config . custom . length ? JSON . parse ( this . config . custom ) : { } ) ,
508
+ samplers : config . samplers ,
509
+ temperature : config . temperature ,
510
+ dynatemp_range : config . dynatemp_range ,
511
+ dynatemp_exponent : config . dynatemp_exponent ,
512
+ top_k : config . top_k ,
513
+ top_p : config . top_p ,
514
+ min_p : config . min_p ,
515
+ typical_p : config . typical_p ,
516
+ xtc_probability : config . xtc_probability ,
517
+ xtc_threshold : config . xtc_threshold ,
518
+ repeat_last_n : config . repeat_last_n ,
519
+ repeat_penalty : config . repeat_penalty ,
520
+ presence_penalty : config . presence_penalty ,
521
+ frequency_penalty : config . frequency_penalty ,
522
+ dry_multiplier : config . dry_multiplier ,
523
+ dry_base : config . dry_base ,
524
+ dry_allowed_length : config . dry_allowed_length ,
525
+ dry_penalty_last_n : config . dry_penalty_last_n ,
526
+ max_tokens : config . max_tokens ,
527
+ timings_per_token : ! ! config . showTokensPerSecond ,
528
+ ...( config . custom . length ? JSON . parse ( config . custom ) : { } ) ,
458
529
} ;
459
530
const chunks = sendSSEPostRequest ( `${ BASE_URL } /v1/chat/completions` , {
460
531
method : 'POST' ,
461
532
headers : {
462
533
'Content-Type' : 'application/json' ,
463
- ...( this . config . apiKey ? { 'Authorization' : `Bearer ${ this . config . apiKey } ` } : { } )
534
+ ...( config . apiKey ? { 'Authorization' : `Bearer ${ config . apiKey } ` } : { } )
464
535
} ,
465
536
body : JSON . stringify ( params ) ,
466
537
signal : abortController . signal ,
@@ -477,7 +548,7 @@ const mainApp = createApp({
477
548
} ;
478
549
}
479
550
const timings = chunk . timings ;
480
- if ( timings && this . config . showTokensPerSecond ) {
551
+ if ( timings && config . showTokensPerSecond ) {
481
552
// only extract what's really needed, to save some space
482
553
this . pendingMsg . timings = {
483
554
prompt_n : timings . prompt_n ,
@@ -598,3 +669,33 @@ try {
598
669
<button class="btn" onClick="localStorage.clear(); window.location.reload();">Clear localStorage</button>
599
670
</div>` ;
600
671
}
672
+
673
+ /**
674
+ * filter out redundant fields upon sending to API
675
+ * @param {Array<APIMessage> } messages
676
+ * @returns {Array<APIMessage> }
677
+ */
678
+ function normalizeMsgsForAPI ( messages ) {
679
+ return messages . map ( ( msg ) => {
680
+ return {
681
+ role : msg . role ,
682
+ content : msg . content ,
683
+ } ;
684
+ } ) ;
685
+ }
686
+
687
+ /**
688
+ * recommended for DeepsSeek-R1, filter out content between <think> and </think> tags
689
+ * @param {Array<APIMessage> } messages
690
+ * @returns {Array<APIMessage> }
691
+ */
692
+ function filterThoughtFromMsgs ( messages ) {
693
+ return messages . map ( ( msg ) => {
694
+ return {
695
+ role : msg . role ,
696
+ content : msg . role === 'assistant'
697
+ ? msg . content . split ( '</think>' ) . at ( - 1 ) . trim ( )
698
+ : msg . content ,
699
+ } ;
700
+ } ) ;
701
+ }
0 commit comments