dsh-model-billing 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/client.js +2345 -2308
- package/package.json +68 -68
package/client.js
CHANGED
|
@@ -1,2308 +1,2345 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: 'dsh-model-billing',
|
|
3
|
-
factory(require) {
|
|
4
|
-
const React = require('react')
|
|
5
|
-
const ReactDOM = require('react-dom')
|
|
6
|
-
const h = React.createElement
|
|
7
|
-
const {
|
|
8
|
-
Fragment,
|
|
9
|
-
useEffect,
|
|
10
|
-
useId,
|
|
11
|
-
useLayoutEffect,
|
|
12
|
-
useMemo,
|
|
13
|
-
useRef,
|
|
14
|
-
useState,
|
|
15
|
-
useSyncExternalStore,
|
|
16
|
-
} = React
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------
|
|
19
|
-
// Locale
|
|
20
|
-
// ---------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const STRINGS = {
|
|
23
|
-
zh: {
|
|
24
|
-
nav: '模型与费用',
|
|
25
|
-
title: '模型与费用',
|
|
26
|
-
intro: '调整已配置模型的顺序,并填写每百万 tokens 的价格。金额根据会话日志估算所有模型;DeepSeek 使用内置峰谷价。',
|
|
27
|
-
loading: '正在加载模型目录…',
|
|
28
|
-
loadFailed: '模型目录加载失败',
|
|
29
|
-
retry: '重试',
|
|
30
|
-
modelsCount: '{count} 个模型',
|
|
31
|
-
save: '保存',
|
|
32
|
-
saving: '保存中…',
|
|
33
|
-
saved: '已保存',
|
|
34
|
-
unsaved: '有未保存的修改',
|
|
35
|
-
undo: '撤销修改',
|
|
36
|
-
orderHint: '用 ↑ / ↓ 调整顺序;顺序直接决定对话框右下角模型选择器中的排列,也用于费用估算。',
|
|
37
|
-
noModels: '当前没有可用的已配置模型。',
|
|
38
|
-
up: '上移',
|
|
39
|
-
down: '下移',
|
|
40
|
-
cacheHitPrice: '输入 · 缓存命中',
|
|
41
|
-
cacheMissPrice: '输入 · 缓存未命中',
|
|
42
|
-
outputPrice: '输出',
|
|
43
|
-
perMillion: '元 / 百万 tokens',
|
|
44
|
-
deepseekTag: 'DeepSeek',
|
|
45
|
-
deepseekNote: '默认使用官方峰谷价;填写完整的自定义价格后,将优先按自定义价格估算。',
|
|
46
|
-
officialPrice: '官方定价',
|
|
47
|
-
priceSourceOfficial: '官方价',
|
|
48
|
-
priceSourceCustom: '自定义',
|
|
49
|
-
customizePrice: '自定义价格',
|
|
50
|
-
useOfficialPrice: '恢复官方价',
|
|
51
|
-
customPriceNote: '自定义价格优先于官方峰谷价;三项留空并保存即可恢复官方价。',
|
|
52
|
-
peak: '高峰',
|
|
53
|
-
offPeak: '空闲',
|
|
54
|
-
deepseekSchedule: '高峰为北京时间周一至周五 9:00–12:00、14:00–18:00(法定节假日除外);其余时段为空闲。',
|
|
55
|
-
pricePlaceholder: '未设置',
|
|
56
|
-
priceUnset: '未定价',
|
|
57
|
-
priceHitShort: '命中',
|
|
58
|
-
priceMissShort: '未命中',
|
|
59
|
-
priceOutShort: '输出',
|
|
60
|
-
priceUnit: '元/M',
|
|
61
|
-
invalidPrice: '价格必须是非负数字,或留空表示未设置。',
|
|
62
|
-
saveFailed: '保存失败:{message}',
|
|
63
|
-
formula: '估算公式:缓存命中输入 × 命中价 +(未缓存输入 + 缓存写入)× 未命中价 + 输出 × 输出价。',
|
|
64
|
-
formulaNote: '价格按每百万 tokens 计。缓存写入按缓存未命中价计算;未配置价格的模型不产生金额。',
|
|
65
|
-
singlePrice: '价格',
|
|
66
|
-
settingsOpen: '在设置中编辑',
|
|
67
|
-
costEstimate: '费用估算',
|
|
68
|
-
costDetails: '输入框下方费用估算',
|
|
69
|
-
close: '关闭',
|
|
70
|
-
thisTurn: '本轮',
|
|
71
|
-
sessionTotal: '本会话',
|
|
72
|
-
cacheHit: '缓存命中率',
|
|
73
|
-
tokens: '输入 / 输出',
|
|
74
|
-
turn: '轮次',
|
|
75
|
-
content: '内容',
|
|
76
|
-
time: '时间',
|
|
77
|
-
model: '使用模型',
|
|
78
|
-
cost: '费用',
|
|
79
|
-
byModel: '按模型',
|
|
80
|
-
excludedShort: '不计入',
|
|
81
|
-
partial: '部分未定价',
|
|
82
|
-
partialShort: '部分未定价',
|
|
83
|
-
partialNote: '有非 DeepSeek 调用尚未配置完整价格,合计只包含已定价部分。',
|
|
84
|
-
showUnpricedNotice: '显示未定价提示',
|
|
85
|
-
unpricedRoutes: '未定价路由',
|
|
86
|
-
addPrice: '补充价格',
|
|
87
|
-
priceSaved: '价格已保存',
|
|
88
|
-
archiveSection: '历史价格',
|
|
89
|
-
archiveHint: '已移除模型的调用仍按这里保存的价格计价,不需要把它们加回模型列表。',
|
|
90
|
-
archiveEmpty: '暂无历史价格',
|
|
91
|
-
addArchive: '添加历史价格',
|
|
92
|
-
archiveProvider: '提供商',
|
|
93
|
-
archiveModel: '模型',
|
|
94
|
-
archiveConflict: '该路由仍在当前模型列表中,请直接编辑上方的模型价格。',
|
|
95
|
-
invalidRoute: '提供商和模型都不能为空。',
|
|
96
|
-
remove: '删除',
|
|
97
|
-
estimateNote: '金额由会话日志中的 token 用量和缓存命中估算;DeepSeek 按事件时间使用内置峰谷价,实际账单以提供方为准。',
|
|
98
|
-
webToolNote: '含 DeepSeek Search/Fetch 按次费 ≈ {amount}(搜索 {searchBilled}/{searchCalls} 次,抓取 {fetchBilled}/{fetchCalls} 次)。',
|
|
99
|
-
listSep: '、',
|
|
100
|
-
turnCost: '费用',
|
|
101
|
-
turnTokens: '本轮 Token',
|
|
102
|
-
displaySection: '轮尾显示',
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.
|
|
510
|
-
if (generation !== slot.generation) return
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (value
|
|
575
|
-
if (value
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return h('div', { className: 'mb-picker-prices' },
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
.mb-
|
|
635
|
-
.mb-
|
|
636
|
-
.mb-
|
|
637
|
-
.mb-
|
|
638
|
-
.mb-
|
|
639
|
-
.mb-
|
|
640
|
-
.mb-
|
|
641
|
-
.mb-
|
|
642
|
-
.mb-
|
|
643
|
-
.mb-
|
|
644
|
-
.mb-
|
|
645
|
-
.mb-
|
|
646
|
-
.mb-
|
|
647
|
-
.mb-
|
|
648
|
-
.mb-
|
|
649
|
-
.mb-
|
|
650
|
-
.mb-
|
|
651
|
-
.mb-
|
|
652
|
-
.mb-
|
|
653
|
-
.mb-
|
|
654
|
-
.mb-
|
|
655
|
-
.mb-
|
|
656
|
-
.mb-
|
|
657
|
-
.mb-
|
|
658
|
-
.mb-
|
|
659
|
-
.mb-
|
|
660
|
-
.mb-
|
|
661
|
-
.mb-
|
|
662
|
-
.mb-
|
|
663
|
-
.mb-
|
|
664
|
-
.mb-
|
|
665
|
-
.mb-
|
|
666
|
-
.mb-
|
|
667
|
-
.mb-
|
|
668
|
-
.mb-
|
|
669
|
-
.mb-
|
|
670
|
-
.mb-
|
|
671
|
-
.mb-
|
|
672
|
-
.mb-
|
|
673
|
-
.mb-
|
|
674
|
-
.mb-
|
|
675
|
-
.mb-
|
|
676
|
-
.mb-
|
|
677
|
-
.mb-
|
|
678
|
-
.mb-
|
|
679
|
-
.mb-
|
|
680
|
-
.mb-
|
|
681
|
-
.mb-
|
|
682
|
-
.mb-
|
|
683
|
-
.mb-
|
|
684
|
-
.mb-
|
|
685
|
-
.mb-
|
|
686
|
-
.mb-
|
|
687
|
-
.mb-
|
|
688
|
-
.mb-
|
|
689
|
-
.mb-
|
|
690
|
-
.mb-
|
|
691
|
-
.mb-
|
|
692
|
-
.mb-
|
|
693
|
-
.mb-
|
|
694
|
-
.mb-
|
|
695
|
-
.mb-
|
|
696
|
-
.mb-
|
|
697
|
-
.mb-
|
|
698
|
-
.mb-
|
|
699
|
-
.mb-
|
|
700
|
-
.mb-
|
|
701
|
-
.mb-
|
|
702
|
-
.mb-
|
|
703
|
-
.mb-
|
|
704
|
-
.mb-
|
|
705
|
-
.mb-
|
|
706
|
-
.mb-
|
|
707
|
-
.mb-
|
|
708
|
-
.mb-
|
|
709
|
-
.mb-
|
|
710
|
-
.mb-
|
|
711
|
-
.mb-
|
|
712
|
-
.mb-
|
|
713
|
-
.mb-
|
|
714
|
-
.mb-
|
|
715
|
-
.mb-
|
|
716
|
-
.mb-
|
|
717
|
-
.mb-
|
|
718
|
-
.mb-
|
|
719
|
-
.mb-
|
|
720
|
-
.mb-
|
|
721
|
-
.mb-
|
|
722
|
-
.mb-
|
|
723
|
-
.mb-
|
|
724
|
-
.mb-
|
|
725
|
-
.mb-
|
|
726
|
-
.mb-
|
|
727
|
-
.mb-
|
|
728
|
-
.mb-
|
|
729
|
-
.mb-
|
|
730
|
-
.mb-
|
|
731
|
-
.mb-
|
|
732
|
-
.mb-
|
|
733
|
-
.mb-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
.mb-
|
|
738
|
-
.mb-
|
|
739
|
-
.mb-
|
|
740
|
-
.mb-
|
|
741
|
-
.mb-
|
|
742
|
-
.mb-
|
|
743
|
-
.mb-
|
|
744
|
-
.mb-
|
|
745
|
-
.mb-
|
|
746
|
-
.mb-
|
|
747
|
-
.mb-
|
|
748
|
-
.mb-
|
|
749
|
-
.mb-
|
|
750
|
-
.mb-
|
|
751
|
-
.mb-
|
|
752
|
-
.mb-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
.mb-picker-
|
|
760
|
-
.mb-picker-
|
|
761
|
-
.mb-picker-
|
|
762
|
-
.mb-picker-
|
|
763
|
-
.mb-picker-
|
|
764
|
-
.mb-picker-
|
|
765
|
-
.mb-picker-
|
|
766
|
-
.mb-picker-
|
|
767
|
-
.mb-picker-
|
|
768
|
-
.mb-picker-
|
|
769
|
-
.mb-picker-
|
|
770
|
-
.mb-picker-
|
|
771
|
-
.mb-picker-
|
|
772
|
-
.mb-picker-
|
|
773
|
-
.mb-picker-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
.mb-picker-
|
|
779
|
-
.mb-picker-
|
|
780
|
-
.mb-picker-
|
|
781
|
-
.mb-picker-
|
|
782
|
-
|
|
783
|
-
.mb-picker-
|
|
784
|
-
.mb-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
setDirty(true)
|
|
965
|
-
setStatus(null)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
h('div', { className: 'mb-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
h('div', { className: 'mb-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
h('
|
|
1506
|
-
|
|
1507
|
-
h('
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
h('
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
h('
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
h('
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
:
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
const
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
useEffect(() => {
|
|
1832
|
-
if (
|
|
1833
|
-
const timer = setTimeout(() =>
|
|
1834
|
-
return () => clearTimeout(timer)
|
|
1835
|
-
}, [
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
}, [
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}, [
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
return
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
if (
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
}))
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
'
|
|
2200
|
-
'
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-model-billing',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react')
|
|
5
|
+
const ReactDOM = require('react-dom')
|
|
6
|
+
const h = React.createElement
|
|
7
|
+
const {
|
|
8
|
+
Fragment,
|
|
9
|
+
useEffect,
|
|
10
|
+
useId,
|
|
11
|
+
useLayoutEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
useSyncExternalStore,
|
|
16
|
+
} = React
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------
|
|
19
|
+
// Locale
|
|
20
|
+
// ---------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const STRINGS = {
|
|
23
|
+
zh: {
|
|
24
|
+
nav: '模型与费用',
|
|
25
|
+
title: '模型与费用',
|
|
26
|
+
intro: '调整已配置模型的顺序,并填写每百万 tokens 的价格。金额根据会话日志估算所有模型;DeepSeek 使用内置峰谷价。',
|
|
27
|
+
loading: '正在加载模型目录…',
|
|
28
|
+
loadFailed: '模型目录加载失败',
|
|
29
|
+
retry: '重试',
|
|
30
|
+
modelsCount: '{count} 个模型',
|
|
31
|
+
save: '保存',
|
|
32
|
+
saving: '保存中…',
|
|
33
|
+
saved: '已保存',
|
|
34
|
+
unsaved: '有未保存的修改',
|
|
35
|
+
undo: '撤销修改',
|
|
36
|
+
orderHint: '用 ↑ / ↓ 调整顺序;顺序直接决定对话框右下角模型选择器中的排列,也用于费用估算。',
|
|
37
|
+
noModels: '当前没有可用的已配置模型。',
|
|
38
|
+
up: '上移',
|
|
39
|
+
down: '下移',
|
|
40
|
+
cacheHitPrice: '输入 · 缓存命中',
|
|
41
|
+
cacheMissPrice: '输入 · 缓存未命中',
|
|
42
|
+
outputPrice: '输出',
|
|
43
|
+
perMillion: '元 / 百万 tokens',
|
|
44
|
+
deepseekTag: 'DeepSeek',
|
|
45
|
+
deepseekNote: '默认使用官方峰谷价;填写完整的自定义价格后,将优先按自定义价格估算。',
|
|
46
|
+
officialPrice: '官方定价',
|
|
47
|
+
priceSourceOfficial: '官方价',
|
|
48
|
+
priceSourceCustom: '自定义',
|
|
49
|
+
customizePrice: '自定义价格',
|
|
50
|
+
useOfficialPrice: '恢复官方价',
|
|
51
|
+
customPriceNote: '自定义价格优先于官方峰谷价;三项留空并保存即可恢复官方价。',
|
|
52
|
+
peak: '高峰',
|
|
53
|
+
offPeak: '空闲',
|
|
54
|
+
deepseekSchedule: '高峰为北京时间周一至周五 9:00–12:00、14:00–18:00(法定节假日除外);其余时段为空闲。',
|
|
55
|
+
pricePlaceholder: '未设置',
|
|
56
|
+
priceUnset: '未定价',
|
|
57
|
+
priceHitShort: '命中',
|
|
58
|
+
priceMissShort: '未命中',
|
|
59
|
+
priceOutShort: '输出',
|
|
60
|
+
priceUnit: '元/M',
|
|
61
|
+
invalidPrice: '价格必须是非负数字,或留空表示未设置。',
|
|
62
|
+
saveFailed: '保存失败:{message}',
|
|
63
|
+
formula: '估算公式:缓存命中输入 × 命中价 +(未缓存输入 + 缓存写入)× 未命中价 + 输出 × 输出价。',
|
|
64
|
+
formulaNote: '价格按每百万 tokens 计。缓存写入按缓存未命中价计算;未配置价格的模型不产生金额。',
|
|
65
|
+
singlePrice: '价格',
|
|
66
|
+
settingsOpen: '在设置中编辑',
|
|
67
|
+
costEstimate: '费用估算',
|
|
68
|
+
costDetails: '输入框下方费用估算',
|
|
69
|
+
close: '关闭',
|
|
70
|
+
thisTurn: '本轮',
|
|
71
|
+
sessionTotal: '本会话',
|
|
72
|
+
cacheHit: '缓存命中率',
|
|
73
|
+
tokens: '输入 / 输出',
|
|
74
|
+
turn: '轮次',
|
|
75
|
+
content: '内容',
|
|
76
|
+
time: '时间',
|
|
77
|
+
model: '使用模型',
|
|
78
|
+
cost: '费用',
|
|
79
|
+
byModel: '按模型',
|
|
80
|
+
excludedShort: '不计入',
|
|
81
|
+
partial: '部分未定价',
|
|
82
|
+
partialShort: '部分未定价',
|
|
83
|
+
partialNote: '有非 DeepSeek 调用尚未配置完整价格,合计只包含已定价部分。',
|
|
84
|
+
showUnpricedNotice: '显示未定价提示',
|
|
85
|
+
unpricedRoutes: '未定价路由',
|
|
86
|
+
addPrice: '补充价格',
|
|
87
|
+
priceSaved: '价格已保存',
|
|
88
|
+
archiveSection: '历史价格',
|
|
89
|
+
archiveHint: '已移除模型的调用仍按这里保存的价格计价,不需要把它们加回模型列表。',
|
|
90
|
+
archiveEmpty: '暂无历史价格',
|
|
91
|
+
addArchive: '添加历史价格',
|
|
92
|
+
archiveProvider: '提供商',
|
|
93
|
+
archiveModel: '模型',
|
|
94
|
+
archiveConflict: '该路由仍在当前模型列表中,请直接编辑上方的模型价格。',
|
|
95
|
+
invalidRoute: '提供商和模型都不能为空。',
|
|
96
|
+
remove: '删除',
|
|
97
|
+
estimateNote: '金额由会话日志中的 token 用量和缓存命中估算;DeepSeek 按事件时间使用内置峰谷价,实际账单以提供方为准。',
|
|
98
|
+
webToolNote: '含 DeepSeek Search/Fetch 按次费 ≈ {amount}(搜索 {searchBilled}/{searchCalls} 次,抓取 {fetchBilled}/{fetchCalls} 次)。',
|
|
99
|
+
listSep: '、',
|
|
100
|
+
turnCost: '费用',
|
|
101
|
+
turnTokens: '本轮 Token',
|
|
102
|
+
displaySection: '轮尾显示',
|
|
103
|
+
displayFields: '显示项目',
|
|
104
|
+
displayLook: '外观',
|
|
105
|
+
modelsSection: '模型顺序与单价',
|
|
106
|
+
modelsHint: '↑ / ↓ 决定选择器里的排列;单价按每百万 tokens 填写,三项留空即恢复官方价。',
|
|
107
|
+
displayTurnTail: '启用轮尾显示',
|
|
108
|
+
displayDock: '显示输入框下方费用估算',
|
|
109
|
+
dockSection: '输入框下方费用估算',
|
|
110
|
+
dockHint: '控制输入框下方的费用条与费用估算弹层。',
|
|
111
|
+
displayHint: '控制每轮结束后在对话尾部显示的内容;修改后点“保存”生效。',
|
|
112
|
+
displayModels: '使用模型',
|
|
113
|
+
displayProvider: '模型提供商',
|
|
114
|
+
displayCost: '本轮费用',
|
|
115
|
+
displayTokens: 'Token 用量',
|
|
116
|
+
displayCacheHit: '缓存命中率',
|
|
117
|
+
displayLabels: '显示文字标签',
|
|
118
|
+
displayFontSize: '字号',
|
|
119
|
+
displayDensity: '间距',
|
|
120
|
+
displayWeight: '字重',
|
|
121
|
+
fontSizeSmall: '小',
|
|
122
|
+
fontSizeNormal: '标准',
|
|
123
|
+
fontSizeLarge: '大',
|
|
124
|
+
densityCompact: '紧凑',
|
|
125
|
+
densityComfortable: '舒适',
|
|
126
|
+
weightNormal: '常规',
|
|
127
|
+
weightBold: '加粗',
|
|
128
|
+
pickerSection: '模型选择器',
|
|
129
|
+
takeoverPicker: '接管模型选择器',
|
|
130
|
+
takeoverHint: '开启后使用本插件按自定义顺序排列的模型选择器;关闭后恢复 DSH 内置选择器。',
|
|
131
|
+
chooseModel: '选择模型',
|
|
132
|
+
loadingModels: '加载模型…',
|
|
133
|
+
searchModel: '搜索模型',
|
|
134
|
+
noMatch: '没有匹配的模型',
|
|
135
|
+
pickerEmpty: '暂无可用模型',
|
|
136
|
+
reasoningEffort: '推理强度',
|
|
137
|
+
providerDefault: '提供方默认',
|
|
138
|
+
switching: '切换中…',
|
|
139
|
+
pickerRetry: '重新加载',
|
|
140
|
+
pickerAria: '选择模型与推理强度',
|
|
141
|
+
pickerUnavailable: '模型目录尚未就绪,请刷新页面',
|
|
142
|
+
},
|
|
143
|
+
en: {
|
|
144
|
+
nav: 'Models & Cost',
|
|
145
|
+
title: 'Models & Cost',
|
|
146
|
+
intro: 'Reorder configured models and enter prices per million tokens. Amounts estimate every model from the session log; DeepSeek uses its built-in peak/off-peak prices.',
|
|
147
|
+
loading: 'Loading the model catalog…',
|
|
148
|
+
loadFailed: 'Could not load the model catalog',
|
|
149
|
+
retry: 'Retry',
|
|
150
|
+
modelsCount: '{count} models',
|
|
151
|
+
save: 'Save',
|
|
152
|
+
saving: 'Saving…',
|
|
153
|
+
saved: 'Saved',
|
|
154
|
+
unsaved: 'Unsaved changes',
|
|
155
|
+
undo: 'Discard changes',
|
|
156
|
+
orderHint: 'Use ↑ / ↓ to reorder. This order is used directly by the model picker at the bottom-right of the composer, and by the cost details.',
|
|
157
|
+
noModels: 'No configured models are available.',
|
|
158
|
+
up: 'Move up',
|
|
159
|
+
down: 'Move down',
|
|
160
|
+
cacheHitPrice: 'Input · cache hit',
|
|
161
|
+
cacheMissPrice: 'Input · cache miss',
|
|
162
|
+
outputPrice: 'Output',
|
|
163
|
+
perMillion: 'CNY / million tokens',
|
|
164
|
+
deepseekTag: 'DeepSeek',
|
|
165
|
+
deepseekNote: 'Official peak/off-peak prices are used by default; a complete custom price set takes precedence.',
|
|
166
|
+
officialPrice: 'Official prices',
|
|
167
|
+
priceSourceOfficial: 'Official',
|
|
168
|
+
priceSourceCustom: 'Custom',
|
|
169
|
+
customizePrice: 'Set custom prices',
|
|
170
|
+
useOfficialPrice: 'Restore official prices',
|
|
171
|
+
customPriceNote: 'Custom prices override the official peak/off-peak table. Leave all three blank and save to restore official prices.',
|
|
172
|
+
peak: 'Peak',
|
|
173
|
+
offPeak: 'Off-peak',
|
|
174
|
+
deepseekSchedule: 'Peak: Beijing time Mon–Fri 09:00–12:00 and 14:00–18:00, excluding Chinese statutory holidays; all other times are off-peak.',
|
|
175
|
+
pricePlaceholder: 'Not set',
|
|
176
|
+
priceUnset: 'Not priced',
|
|
177
|
+
priceHitShort: 'hit',
|
|
178
|
+
priceMissShort: 'miss',
|
|
179
|
+
priceOutShort: 'out',
|
|
180
|
+
priceUnit: 'CNY/M',
|
|
181
|
+
invalidPrice: 'Prices must be non-negative numbers, or blank when unset.',
|
|
182
|
+
saveFailed: 'Save failed: {message}',
|
|
183
|
+
formula: 'Estimate: cached input × hit price + (uncached input + cache write) × miss price + output × output price.',
|
|
184
|
+
formulaNote: 'Prices are per million tokens. Cache writes use the cache-miss price; models without a full price set produce no amount.',
|
|
185
|
+
singlePrice: 'Prices',
|
|
186
|
+
settingsOpen: 'Edit in Settings',
|
|
187
|
+
costEstimate: 'Cost estimate',
|
|
188
|
+
costDetails: 'Composer cost estimate',
|
|
189
|
+
close: 'Close',
|
|
190
|
+
thisTurn: 'Turn',
|
|
191
|
+
sessionTotal: 'Session',
|
|
192
|
+
cacheHit: 'Cache hit rate',
|
|
193
|
+
tokens: 'Input / output',
|
|
194
|
+
turn: 'Turn',
|
|
195
|
+
content: 'Content',
|
|
196
|
+
time: 'Time',
|
|
197
|
+
model: 'Model',
|
|
198
|
+
cost: 'Cost',
|
|
199
|
+
byModel: 'By model',
|
|
200
|
+
excludedShort: 'Excluded',
|
|
201
|
+
partial: 'partly unpriced',
|
|
202
|
+
partialShort: 'partly unpriced',
|
|
203
|
+
partialNote: 'Some non-DeepSeek calls do not have a complete price set; the total includes only priced calls.',
|
|
204
|
+
showUnpricedNotice: 'Show unpriced notices',
|
|
205
|
+
unpricedRoutes: 'Unpriced routes',
|
|
206
|
+
addPrice: 'Add price',
|
|
207
|
+
priceSaved: 'Price saved',
|
|
208
|
+
archiveSection: 'Historical prices',
|
|
209
|
+
archiveHint: 'Calls to removed models stay priced from this book; you do not need to add them back to the model list.',
|
|
210
|
+
archiveEmpty: 'No historical prices yet',
|
|
211
|
+
addArchive: 'Add historical price',
|
|
212
|
+
archiveProvider: 'Provider',
|
|
213
|
+
archiveModel: 'Model',
|
|
214
|
+
archiveConflict: 'This route is still in the configured list; edit its price above instead.',
|
|
215
|
+
invalidRoute: 'Provider and model are both required.',
|
|
216
|
+
remove: 'Remove',
|
|
217
|
+
estimateNote: 'Amounts are estimated from token usage and cache hits in the session log; DeepSeek uses built-in peak/off-peak prices at the event time, and the provider bill is authoritative.',
|
|
218
|
+
webToolNote: 'Includes DeepSeek Search/Fetch per-request fees ≈ {amount} (search {searchBilled}/{searchCalls}, fetch {fetchBilled}/{fetchCalls}).',
|
|
219
|
+
listSep: ', ',
|
|
220
|
+
turnCost: 'Cost',
|
|
221
|
+
turnTokens: 'Turn tokens',
|
|
222
|
+
displaySection: 'Turn-tail display',
|
|
223
|
+
displayFields: 'Shown items',
|
|
224
|
+
displayLook: 'Appearance',
|
|
225
|
+
modelsSection: 'Model order and prices',
|
|
226
|
+
modelsHint: 'The arrows set the picker order; prices are per million tokens, and leaving all three blank restores the official table.',
|
|
227
|
+
displayTurnTail: 'Show the turn tail',
|
|
228
|
+
displayDock: 'Show the composer cost estimate',
|
|
229
|
+
dockSection: 'Composer cost estimate',
|
|
230
|
+
dockHint: 'Controls the cost bar below the composer and its detail panel.',
|
|
231
|
+
displayHint: 'Choose what appears below each completed turn; click Save to apply.',
|
|
232
|
+
displayModels: 'Models used',
|
|
233
|
+
displayProvider: 'Model provider',
|
|
234
|
+
displayCost: 'Turn cost',
|
|
235
|
+
displayTokens: 'Token usage',
|
|
236
|
+
displayCacheHit: 'Cache hit rate',
|
|
237
|
+
displayLabels: 'Show text labels',
|
|
238
|
+
displayFontSize: 'Font size',
|
|
239
|
+
displayDensity: 'Spacing',
|
|
240
|
+
displayWeight: 'Font weight',
|
|
241
|
+
fontSizeSmall: 'Small',
|
|
242
|
+
fontSizeNormal: 'Normal',
|
|
243
|
+
fontSizeLarge: 'Large',
|
|
244
|
+
densityCompact: 'Compact',
|
|
245
|
+
densityComfortable: 'Comfortable',
|
|
246
|
+
weightNormal: 'Regular',
|
|
247
|
+
weightBold: 'Bold',
|
|
248
|
+
pickerSection: 'Model picker',
|
|
249
|
+
takeoverPicker: 'Take over the model picker',
|
|
250
|
+
takeoverHint: 'On: use this plugin’s picker with your custom model order. Off: restore the built-in DSH picker.',
|
|
251
|
+
chooseModel: 'Select model',
|
|
252
|
+
loadingModels: 'Loading models…',
|
|
253
|
+
searchModel: 'Search models',
|
|
254
|
+
noMatch: 'No matching model',
|
|
255
|
+
pickerEmpty: 'No model available',
|
|
256
|
+
reasoningEffort: 'Reasoning effort',
|
|
257
|
+
providerDefault: 'Provider default',
|
|
258
|
+
switching: 'Switching…',
|
|
259
|
+
pickerRetry: 'Reload',
|
|
260
|
+
pickerAria: 'Select model and reasoning effort',
|
|
261
|
+
pickerUnavailable: 'The model directory is not ready; reload the page',
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const t = (key, values) => {
|
|
266
|
+
const active = localeService?.getSnapshot?.().active ?? 'zh'
|
|
267
|
+
const dict = active.toLowerCase().startsWith('en') ? STRINGS.en : STRINGS.zh
|
|
268
|
+
const template = dict[key] ?? STRINGS.zh[key] ?? key
|
|
269
|
+
return values === undefined
|
|
270
|
+
? template
|
|
271
|
+
: template.replace(/\{(\w+)\}/g, (match, name) => values[name] === undefined ? match : String(values[name]))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let localeService
|
|
275
|
+
let modelDirectories
|
|
276
|
+
let modelDirectoryRevision = 0
|
|
277
|
+
const modelDirectoryListeners = new Set()
|
|
278
|
+
|
|
279
|
+
function publishModelDirectories(next) {
|
|
280
|
+
if (next === undefined || next === modelDirectories) return
|
|
281
|
+
modelDirectories = next
|
|
282
|
+
modelDirectoryRevision += 1
|
|
283
|
+
for (const listener of [...modelDirectoryListeners]) listener()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function subscribeModelDirectories(listener) {
|
|
287
|
+
modelDirectoryListeners.add(listener)
|
|
288
|
+
return () => modelDirectoryListeners.delete(listener)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function useModelDirectoriesRevision() {
|
|
292
|
+
return useSyncExternalStore(
|
|
293
|
+
subscribeModelDirectories,
|
|
294
|
+
() => modelDirectoryRevision,
|
|
295
|
+
() => 0,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function subscribeLocale(listener) {
|
|
300
|
+
return localeService?.subscribe?.(listener) ?? (() => {})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function localeRevision() {
|
|
304
|
+
return localeService?.getSnapshot?.().revision ?? 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function useT() {
|
|
308
|
+
useSyncExternalStore(subscribeLocale, localeRevision, () => 0)
|
|
309
|
+
return t
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const DISPLAY_DEFAULTS = Object.freeze({
|
|
313
|
+
showTurnTail: true,
|
|
314
|
+
showDock: true,
|
|
315
|
+
showModels: true,
|
|
316
|
+
showProvider: false,
|
|
317
|
+
showCost: true,
|
|
318
|
+
showTokens: false,
|
|
319
|
+
showCacheHit: false,
|
|
320
|
+
showLabels: true,
|
|
321
|
+
showUnpricedNotice: true,
|
|
322
|
+
takeoverPicker: true,
|
|
323
|
+
fontSize: 'normal',
|
|
324
|
+
density: 'comfortable',
|
|
325
|
+
fontWeight: 'normal',
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const DISPLAY_STORAGE_KEY = 'dsh-model-billing.display'
|
|
329
|
+
|
|
330
|
+
function clearStoredDisplay() {
|
|
331
|
+
try {
|
|
332
|
+
localStorage.removeItem(DISPLAY_STORAGE_KEY)
|
|
333
|
+
} catch {
|
|
334
|
+
// Ignore unavailable storage.
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Older builds mirrored display settings into localStorage, which made the
|
|
339
|
+
// browser a second source of truth that could never sync across devices.
|
|
340
|
+
// Clear the legacy key once; the Host state is now the only copy.
|
|
341
|
+
clearStoredDisplay()
|
|
342
|
+
|
|
343
|
+
function readDisplay(data) {
|
|
344
|
+
const source = data?.display
|
|
345
|
+
return source !== null && typeof source === 'object' && !Array.isArray(source)
|
|
346
|
+
? { ...DISPLAY_DEFAULTS, ...source }
|
|
347
|
+
: { ...DISPLAY_DEFAULTS }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------
|
|
351
|
+
// Shared catalog state
|
|
352
|
+
// ---------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
const catalog = {
|
|
355
|
+
snap: { status: 'idle', data: null, error: null },
|
|
356
|
+
listeners: new Set(),
|
|
357
|
+
inflight: null,
|
|
358
|
+
generation: 0,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const REQUEST_TIMEOUT_MS = 8000
|
|
362
|
+
|
|
363
|
+
async function requestJson(url, options) {
|
|
364
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
365
|
+
const timeout = controller === null
|
|
366
|
+
? null
|
|
367
|
+
: setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
368
|
+
let response
|
|
369
|
+
try {
|
|
370
|
+
response = await fetch(url, {
|
|
371
|
+
credentials: 'same-origin',
|
|
372
|
+
cache: 'no-store',
|
|
373
|
+
...options,
|
|
374
|
+
signal: options?.signal ?? controller?.signal ?? null,
|
|
375
|
+
headers: {
|
|
376
|
+
accept: 'application/json',
|
|
377
|
+
...(options?.headers ?? {}),
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (timeout !== null) clearTimeout(timeout)
|
|
382
|
+
throw controller !== null && error?.name === 'AbortError'
|
|
383
|
+
? new Error(`timeout after ${REQUEST_TIMEOUT_MS}ms`)
|
|
384
|
+
: error
|
|
385
|
+
}
|
|
386
|
+
if (timeout !== null) clearTimeout(timeout)
|
|
387
|
+
let body
|
|
388
|
+
try {
|
|
389
|
+
body = await response.json()
|
|
390
|
+
} catch {
|
|
391
|
+
throw new Error(`HTTP ${response.status}`)
|
|
392
|
+
}
|
|
393
|
+
if (!response.ok || body?.ok === false) {
|
|
394
|
+
throw new Error(body?.message ?? `HTTP ${response.status}`)
|
|
395
|
+
}
|
|
396
|
+
return body
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function emitCatalog() {
|
|
400
|
+
for (const listener of [...catalog.listeners]) listener()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function setCatalog(draft) {
|
|
404
|
+
catalog.snap = { ...catalog.snap, ...draft }
|
|
405
|
+
emitCatalog()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function ensureCatalog(force = false) {
|
|
409
|
+
if (!force && catalog.snap.status === 'ready') return catalog.snap.data
|
|
410
|
+
if (catalog.inflight !== null && !force) return catalog.inflight
|
|
411
|
+
catalog.generation += 1
|
|
412
|
+
const generation = catalog.generation
|
|
413
|
+
setCatalog({ status: 'loading', error: null })
|
|
414
|
+
const operation = requestJson('/model-billing/catalog')
|
|
415
|
+
.then((data) => {
|
|
416
|
+
if (generation !== catalog.generation) return data
|
|
417
|
+
setCatalog({ status: 'ready', data, error: null })
|
|
418
|
+
return data
|
|
419
|
+
})
|
|
420
|
+
.catch((error) => {
|
|
421
|
+
if (generation === catalog.generation) {
|
|
422
|
+
setCatalog({ status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
423
|
+
}
|
|
424
|
+
return null
|
|
425
|
+
})
|
|
426
|
+
.finally(() => {
|
|
427
|
+
if (catalog.inflight === operation) catalog.inflight = null
|
|
428
|
+
})
|
|
429
|
+
catalog.inflight = operation
|
|
430
|
+
return operation
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function subscribeCatalog(listener) {
|
|
434
|
+
catalog.listeners.add(listener)
|
|
435
|
+
if (catalog.snap.status === 'idle') void ensureCatalog()
|
|
436
|
+
return () => catalog.listeners.delete(listener)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function catalogSnapshot() {
|
|
440
|
+
return catalog.snap
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function useCatalog() {
|
|
444
|
+
return useSyncExternalStore(subscribeCatalog, catalogSnapshot, catalogSnapshot)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---------------------------------------------------------------------
|
|
448
|
+
// Shared session usage state
|
|
449
|
+
// ---------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
const usageSlots = new Map()
|
|
452
|
+
const USAGE_DEBOUNCE_MS = 300
|
|
453
|
+
|
|
454
|
+
function usageSlot(sessionId) {
|
|
455
|
+
let slot = usageSlots.get(sessionId)
|
|
456
|
+
if (slot === undefined) {
|
|
457
|
+
slot = {
|
|
458
|
+
snap: { status: 'idle', data: null, error: null },
|
|
459
|
+
listeners: new Set(),
|
|
460
|
+
inflight: null,
|
|
461
|
+
generation: 0,
|
|
462
|
+
revision: null,
|
|
463
|
+
pending: null,
|
|
464
|
+
refs: 0,
|
|
465
|
+
rowsByTurn: new Map(),
|
|
466
|
+
}
|
|
467
|
+
usageSlots.set(sessionId, slot)
|
|
468
|
+
}
|
|
469
|
+
slot.refs += 1
|
|
470
|
+
return slot
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function releaseSlot(sessionId, slot) {
|
|
474
|
+
slot.refs = Math.max(0, slot.refs - 1)
|
|
475
|
+
if (slot.listeners.size > 0 || slot.refs > 0) return
|
|
476
|
+
if (slot.pending !== null) {
|
|
477
|
+
clearTimeout(slot.pending)
|
|
478
|
+
slot.pending = null
|
|
479
|
+
}
|
|
480
|
+
usageSlots.delete(sessionId)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function emitUsage(slot) {
|
|
484
|
+
for (const listener of [...slot.listeners]) listener()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function ensureUsage(sessionId, revision, force = false) {
|
|
488
|
+
const slot = usageSlot(sessionId)
|
|
489
|
+
if (!force && slot.revision === revision && slot.snap.status !== 'error') return
|
|
490
|
+
if (slot.inflight !== null && !force) return
|
|
491
|
+
slot.revision = revision
|
|
492
|
+
if (slot.pending !== null) clearTimeout(slot.pending)
|
|
493
|
+
// Collapse the token-counter updates a streaming reply produces into one
|
|
494
|
+
// trailing request instead of one request per token.
|
|
495
|
+
slot.pending = setTimeout(() => {
|
|
496
|
+
slot.pending = null
|
|
497
|
+
void fetchUsage(sessionId, revision)
|
|
498
|
+
}, USAGE_DEBOUNCE_MS)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function fetchUsage(sessionId, revision) {
|
|
502
|
+
const slot = usageSlots.get(sessionId)
|
|
503
|
+
if (slot === undefined) return
|
|
504
|
+
slot.generation += 1
|
|
505
|
+
const generation = slot.generation
|
|
506
|
+
slot.snap = { ...slot.snap, status: 'loading', error: null }
|
|
507
|
+
emitUsage(slot)
|
|
508
|
+
const operation = requestJson(`/model-billing/usage?sessionId=${encodeURIComponent(sessionId)}`)
|
|
509
|
+
.then((data) => {
|
|
510
|
+
if (generation !== slot.generation) return
|
|
511
|
+
// Index once per fetch rather than scanning the row list on every
|
|
512
|
+
// turn render.
|
|
513
|
+
slot.snap = { status: 'ready', data, rowsByTurn: indexRowsByTurn(data), error: null }
|
|
514
|
+
slot.rowsByTurn = slot.snap.rowsByTurn
|
|
515
|
+
emitUsage(slot)
|
|
516
|
+
})
|
|
517
|
+
.catch((error) => {
|
|
518
|
+
if (generation !== slot.generation) return
|
|
519
|
+
slot.snap = {
|
|
520
|
+
status: 'error',
|
|
521
|
+
data: slot.snap.data,
|
|
522
|
+
rowsByTurn: slot.snap.rowsByTurn,
|
|
523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
524
|
+
}
|
|
525
|
+
emitUsage(slot)
|
|
526
|
+
})
|
|
527
|
+
.finally(() => {
|
|
528
|
+
if (slot.inflight === operation) slot.inflight = null
|
|
529
|
+
})
|
|
530
|
+
slot.inflight = operation
|
|
531
|
+
return operation
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function indexRowsByTurn(data) {
|
|
535
|
+
const index = new Map()
|
|
536
|
+
if (Array.isArray(data?.usage?.turns)) {
|
|
537
|
+
for (const row of data.usage.turns) index.set(row.turn, row)
|
|
538
|
+
}
|
|
539
|
+
return index
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function useSessionUsage(sessionId, revision) {
|
|
543
|
+
const slot = usageSlot(sessionId)
|
|
544
|
+
useEffect(() => () => releaseSlot(sessionId, slot), [sessionId, slot])
|
|
545
|
+
const snap = useSyncExternalStore(
|
|
546
|
+
(listener) => {
|
|
547
|
+
slot.listeners.add(listener)
|
|
548
|
+
return () => slot.listeners.delete(listener)
|
|
549
|
+
},
|
|
550
|
+
() => slot.snap,
|
|
551
|
+
() => slot.snap,
|
|
552
|
+
)
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
if (sessionId !== undefined && revision !== null) ensureUsage(sessionId, revision)
|
|
555
|
+
}, [sessionId, revision])
|
|
556
|
+
return snap
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function tokenUsageRevision(tokenUsage) {
|
|
560
|
+
if (tokenUsage === undefined || tokenUsage === null) return 'none'
|
|
561
|
+
return [
|
|
562
|
+
tokenUsage.uncachedInputTokens ?? '',
|
|
563
|
+
tokenUsage.outputTokens ?? '',
|
|
564
|
+
tokenUsage.cacheReadTokens ?? '',
|
|
565
|
+
tokenUsage.cacheWriteTokens ?? '',
|
|
566
|
+
].join(':')
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------
|
|
570
|
+
// Formatting
|
|
571
|
+
// ---------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
function formatMoney(value) {
|
|
574
|
+
if (value === null || value === undefined || !Number.isFinite(value)) return '—'
|
|
575
|
+
if (value === 0) return '¥0'
|
|
576
|
+
const fixed = value.toFixed(2)
|
|
577
|
+
return fixed === '0.00' ? '<¥0.01' : `¥${fixed}`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function formatTokens(value) {
|
|
581
|
+
if (!Number.isFinite(value)) return '—'
|
|
582
|
+
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`
|
|
583
|
+
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`
|
|
584
|
+
return String(Math.round(value))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function formatPercent(value) {
|
|
588
|
+
return value === null || value === undefined ? '—' : `${(value * 100).toFixed(1)}%`
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function formatPrice(value) {
|
|
592
|
+
if (value === null || value === undefined) return '—'
|
|
593
|
+
return String(Number(value.toFixed(6)))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function PriceLines({ pricing, translate, showSource = true }) {
|
|
597
|
+
const source = showSource === false ? null : pricing?.source === 'official'
|
|
598
|
+
? translate('priceSourceOfficial')
|
|
599
|
+
: pricing?.source === 'user' ? translate('priceSourceCustom') : null
|
|
600
|
+
const sourceLine = source === null ? null : h('div', { className: 'mb-picker-price-source' }, source)
|
|
601
|
+
if (pricing?.builtin === true) {
|
|
602
|
+
const line = (label, prices) =>
|
|
603
|
+
`${label} ${translate('priceHitShort')} ${formatPrice(prices?.cacheHit)} · ${translate('priceMissShort')} ${formatPrice(prices?.cacheMiss)} · ${translate('priceOutShort')} ${formatPrice(prices?.output)} ${translate('priceUnit')}`
|
|
604
|
+
return h('div', { className: 'mb-picker-prices' },
|
|
605
|
+
sourceLine,
|
|
606
|
+
h('div', { className: 'mb-picker-price-line' }, line(translate('peak'), pricing.peak)),
|
|
607
|
+
h('div', { className: 'mb-picker-price-line' }, line(translate('offPeak'), pricing.offPeak)))
|
|
608
|
+
}
|
|
609
|
+
const hasAny = pricing !== null && pricing !== undefined
|
|
610
|
+
&& [pricing.cacheHit, pricing.cacheMiss, pricing.output].some((value) => typeof value === 'number')
|
|
611
|
+
if (!hasAny) {
|
|
612
|
+
return h('div', { className: 'mb-picker-prices' }, translate('priceUnset'))
|
|
613
|
+
}
|
|
614
|
+
return h('div', { className: 'mb-picker-prices' },
|
|
615
|
+
sourceLine,
|
|
616
|
+
h('div', { className: 'mb-picker-price-line' },
|
|
617
|
+
`${translate('priceHitShort')} ${formatPrice(pricing.cacheHit)} · ${translate('priceMissShort')} ${formatPrice(pricing.cacheMiss)} · ${translate('priceOutShort')} ${formatPrice(pricing.output)} ${translate('priceUnit')}`))
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function timeText(timestamp) {
|
|
621
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) return ''
|
|
622
|
+
try {
|
|
623
|
+
return new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
624
|
+
} catch {
|
|
625
|
+
return ''
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ---------------------------------------------------------------------
|
|
630
|
+
// Styles
|
|
631
|
+
// ---------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
const CSS = `
|
|
634
|
+
.mb-page { max-width: 780px; padding: 6px 2px 48px; color: var(--dsw-alias-label-primary, #e8e8ea); font: 13px/1.5 var(--ds-font-family, system-ui, -apple-system, "Segoe UI", sans-serif); }
|
|
635
|
+
.mb-page * { box-sizing: border-box; }
|
|
636
|
+
.mb-hero { display: flex; align-items: flex-start; gap: 14px; margin: 0 0 16px; }
|
|
637
|
+
.mb-mark { flex: none; display: grid; place-items: center; width: 42px; height: 42px; border-radius: 11px; overflow: hidden; line-height: 0; }
|
|
638
|
+
.mb-mark svg { width: 42px; height: 42px; display: block; }
|
|
639
|
+
.mb-hero h2 { margin: 0 0 4px; font-size: 20px; line-height: 1.25; font-weight: 650; }
|
|
640
|
+
.mb-hero p { margin: 0; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12.5px; }
|
|
641
|
+
.mb-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 2px; }
|
|
642
|
+
.mb-bar-count { color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12px; }
|
|
643
|
+
.mb-muted { color: var(--dsw-alias-label-secondary, #a5a5ad); }
|
|
644
|
+
.mb-spacer { flex: 1; }
|
|
645
|
+
.mb-btn { appearance: none; height: 30px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #3a3a3d); border-radius: 7px; background: var(--dsw-alias-bg-layer-1, #202024); color: var(--dsw-alias-label-primary, #e8e8ea); font: inherit; font-size: 12px; cursor: pointer; }
|
|
646
|
+
.mb-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2, #26262a); }
|
|
647
|
+
.mb-btn:disabled { opacity: 1; color: var(--dsw-alias-label-secondary, #a5a5ad); border-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, .1)); background: var(--dsw-alias-bg-layer-1, #202024); cursor: default; }
|
|
648
|
+
.mb-btn-primary { background: var(--dsw-alias-brand-primary, #3b82f6); border-color: var(--dsw-alias-brand-primary, #3b82f6); color: var(--dsw-alias-bg-base, #14141a); font-weight: 600; }
|
|
649
|
+
.mb-btn-primary:hover:not(:disabled) { opacity: .9; }
|
|
650
|
+
.mb-btn-primary:disabled { background: var(--dsw-alias-bg-layer-2, #26262a); border-color: var(--dsw-alias-border-l2, #3a3a3d); color: var(--dsw-alias-label-secondary, #a5a5ad); }
|
|
651
|
+
.mb-btn-icon { width: 28px; height: 28px; padding: 0; font-size: 13px; }
|
|
652
|
+
.mb-status { min-height: 18px; margin: 2px 2px 0; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12px; }
|
|
653
|
+
.mb-status[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #f85149); }
|
|
654
|
+
.mb-status[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #3fb950); }
|
|
655
|
+
.mb-group { margin-top: 16px; border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .1)); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #202024); overflow: hidden; }
|
|
656
|
+
.mb-group-head { padding: 13px 16px 11px; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .08)); }
|
|
657
|
+
.mb-group-head h3 { margin: 0; font-size: 13.5px; font-weight: 600; }
|
|
658
|
+
.mb-group-head p { margin: 3px 0 0; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12px; }
|
|
659
|
+
.mb-group-body { padding: 0 16px; }
|
|
660
|
+
.mb-row { display: flex; align-items: center; gap: 18px; min-height: 52px; padding: 10px 0; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); }
|
|
661
|
+
.mb-row:first-child { border-top: 0; }
|
|
662
|
+
.mb-row.is-disabled { opacity: .48; }
|
|
663
|
+
.mb-row-copy { flex: 1; min-width: 0; }
|
|
664
|
+
.mb-row-label { font-size: 12.8px; font-weight: 500; }
|
|
665
|
+
.mb-row-desc { margin-top: 2px; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11.5px; }
|
|
666
|
+
.mb-row-control { flex: none; display: flex; align-items: center; gap: 6px; }
|
|
667
|
+
.mb-subhead { padding: 12px 0 2px; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); }
|
|
668
|
+
.mb-subhead:first-child { border-top: 0; }
|
|
669
|
+
.mb-switch { position: relative; width: 38px; height: 22px; padding: 0; border: 1px solid var(--dsw-alias-border-l2, #3a3a3d); border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #26262a); cursor: pointer; transition: background .14s ease, border-color .14s ease; }
|
|
670
|
+
.mb-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); transition: transform .14s ease; }
|
|
671
|
+
.mb-switch[aria-checked="true"] { border-color: transparent; background: var(--dsw-alias-brand-primary, #3b82f6); }
|
|
672
|
+
.mb-switch[aria-checked="true"]::after { transform: translateX(16px); background: var(--dsw-alias-bg-base, #14141a); }
|
|
673
|
+
.mb-switch:disabled { opacity: .38; cursor: not-allowed; }
|
|
674
|
+
.mb-switch:focus-visible, .mb-btn:focus-visible, .mb-select:focus-visible, .mb-input:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #3b82f6); outline-offset: 2px; }
|
|
675
|
+
.mb-select { max-width: 230px; height: 28px; padding: 0 28px 0 9px; border: 1px solid var(--dsw-alias-border-l2, #3a3a3d); border-radius: 7px; background: var(--dsw-alias-bg-layer-2, #26262a); color: var(--dsw-alias-label-primary, #e8e8ea); font: inherit; font-size: 12px; cursor: pointer; }
|
|
676
|
+
.mb-select:disabled { cursor: not-allowed; opacity: .5; }
|
|
677
|
+
.mb-model { padding: 12px 0; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); }
|
|
678
|
+
.mb-model:first-child { border-top: 0; }
|
|
679
|
+
.mb-model-head { display: flex; align-items: flex-start; gap: 10px; }
|
|
680
|
+
.mb-index { flex: none; display: grid; place-items: center; width: 22px; height: 22px; margin-top: 1px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #26262a); color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
681
|
+
.mb-name { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12.8px; font-weight: 500; }
|
|
682
|
+
.mb-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
683
|
+
.mb-sub { margin-top: 2px; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11.5px; overflow-wrap: anywhere; }
|
|
684
|
+
.mb-head-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: none; }
|
|
685
|
+
.mb-tag { display: inline-block; padding: 1px 6px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #26262a); color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 10.5px; font-weight: 500; white-space: nowrap; }
|
|
686
|
+
.mb-tag-excluded { color: var(--dsw-alias-state-warn-primary); }
|
|
687
|
+
.mb-model-body { margin: 10px 0 0 32px; display: grid; gap: 10px; }
|
|
688
|
+
.mb-price-group { border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .08)); border-radius: 9px; padding: 10px 12px 12px; }
|
|
689
|
+
.mb-price-group-title { display: flex; gap: 8px; align-items: baseline; margin-bottom: 8px; font-size: 12.5px; font-weight: 600; }
|
|
690
|
+
.mb-price-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
691
|
+
.mb-card-actions { margin-top: 10px; display: flex; justify-content: flex-end; }
|
|
692
|
+
.mb-field { display: grid; gap: 4px; min-width: 0; }
|
|
693
|
+
.mb-field-label { color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11px; }
|
|
694
|
+
.mb-input { width: 100%; height: 30px; border: 1px solid var(--dsw-alias-border-l2, #3a3a3d); border-radius: 7px; background: var(--dsw-alias-bg-layer-2, #26262a); color: var(--dsw-alias-label-primary, #e8e8ea); padding: 0 9px; font: inherit; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
695
|
+
.mb-input:focus { border-color: var(--dsw-alias-brand-primary, #3b82f6); }
|
|
696
|
+
.mb-note { margin: 8px 0 0; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12px; }
|
|
697
|
+
.mb-warn { margin: 6px 0 0; color: var(--dsw-alias-state-warn-primary, #d29922); font-size: 12px; }
|
|
698
|
+
.mb-empty { border: 1px dashed var(--dsw-alias-border-l2, #3a3a3d); border-radius: 10px; padding: 22px; text-align: center; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 12px; }
|
|
699
|
+
.mb-footer-note { margin: 12px 2px 0; color: var(--dsw-alias-label-secondary, #a5a5ad); font-size: 11.5px; }
|
|
700
|
+
.mb-archive-row { padding: 11px 0; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); display: grid; gap: 8px; }
|
|
701
|
+
.mb-archive-row:first-child { border-top: 0; }
|
|
702
|
+
.mb-archive-head { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 8px; align-items: center; }
|
|
703
|
+
.mb-archive-actions { display: flex; gap: 8px; padding: 11px 0; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); }
|
|
704
|
+
.mb-route-editor { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; padding: 10px 0 0; border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .07)); }
|
|
705
|
+
.mb-route-editor .mb-input { flex: 1 1 104px; width: auto; min-width: 84px; }
|
|
706
|
+
.mb-route-editor .mb-btn { flex: none; }
|
|
707
|
+
.mb-dock { align-self: center; display: flex; flex-direction: column; align-items: flex-start; color: var(--dsw-alias-label-tertiary); font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px); line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); }
|
|
708
|
+
.mb-dock-trigger { appearance: none; border: 0; background: transparent; color: inherit; font: inherit; line-height: inherit; padding: 1px 8px; border-radius: var(--dsw-radius-sm); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; text-align: left; max-width: 100%; flex-wrap: nowrap; }
|
|
709
|
+
.mb-dock-trigger:hover, .mb-dock-trigger[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
|
|
710
|
+
.mb-dock-amount { color: var(--dsw-alias-label-secondary); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
711
|
+
.mb-dock-sep { opacity: .55; }
|
|
712
|
+
.mb-dock-icon { flex: none; width: 14px; height: 14px; }
|
|
713
|
+
.mb-dock-caret { font-size: 10px; opacity: .7; }
|
|
714
|
+
.mb-panel { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-menu-surface-fill, var(--dsw-alias-bg-layer-1)); border-radius: var(--dsw-radius-lg, 10px); padding: 10px 12px; color: var(--dsw-alias-label-primary); }
|
|
715
|
+
.mb-panel-floating { position: fixed; z-index: 1200; max-width: calc(100vw - 24px); max-height: min(430px, calc(100vh - 96px)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l2, var(--dsw-alias-border-l2)) transparent; box-shadow: var(--dsw-elevation-prominent); backdrop-filter: var(--dsw-menu-backdrop-filter, none); }
|
|
716
|
+
.mb-panel-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
717
|
+
.mb-panel-title { font-weight: 600; }
|
|
718
|
+
.mb-panel-close { margin-left: auto; width: 24px; height: 24px; border: 0; border-radius: var(--dsw-radius-sm); background: 0 0; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 18px; line-height: 20px; cursor: pointer; }
|
|
719
|
+
.mb-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
720
|
+
.mb-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 10px; }
|
|
721
|
+
.mb-summary-item { background: var(--dsw-alias-bg-layer-2); border-radius: 7px; padding: 7px 9px; min-width: 0; }
|
|
722
|
+
.mb-summary-key { color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
723
|
+
.mb-summary-value { font-variant-numeric: tabular-nums; font-weight: 600; overflow-wrap: anywhere; }
|
|
724
|
+
.mb-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
725
|
+
.mb-table th, .mb-table td { text-align: left; padding: 4px 5px; border-bottom: 1px solid var(--dsw-alias-border-l1); vertical-align: top; }
|
|
726
|
+
.mb-table th { color: var(--dsw-alias-label-secondary); font-weight: 500; font-size: 11px; }
|
|
727
|
+
.mb-table td.num, .mb-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
728
|
+
.mb-table tr:last-child td { border-bottom: 0; }
|
|
729
|
+
.mb-turn-table { table-layout: fixed; }
|
|
730
|
+
.mb-turn-table th, .mb-turn-table td, .mb-turn-table th.num, .mb-turn-table td.num { text-align: left; }
|
|
731
|
+
.mb-turn-table th:nth-child(1), .mb-turn-table td:nth-child(1) { width: 5%; }
|
|
732
|
+
.mb-turn-table th:nth-child(2), .mb-turn-table td:nth-child(2) { width: 25.5%; }
|
|
733
|
+
.mb-turn-table th:nth-child(3), .mb-turn-table td:nth-child(3) { width: 12.5%; }
|
|
734
|
+
.mb-turn-table th:nth-child(4), .mb-turn-table td:nth-child(4) { width: 21%; }
|
|
735
|
+
.mb-turn-table th:nth-child(5), .mb-turn-table td:nth-child(5) { width: 10%; }
|
|
736
|
+
.mb-turn-table th:nth-child(6), .mb-turn-table td:nth-child(6) { width: 16%; }
|
|
737
|
+
.mb-turn-table th:nth-child(7), .mb-turn-table td:nth-child(7) { width: 10%; }
|
|
738
|
+
.mb-turn-preview, .mb-turn-model { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
739
|
+
.mb-turn-model { color: var(--dsw-alias-label-primary); }
|
|
740
|
+
.mb-panel-note { margin: 9px 0 0; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
741
|
+
.mb-turn { color: var(--dsw-alias-label-secondary); font-size: 12px; font-variant-numeric: tabular-nums; display: inline-flex; flex-wrap: wrap; align-items: baseline; column-gap: 10px; row-gap: 2px; max-width: 100%; }
|
|
742
|
+
.mb-turn[data-size="small"] { font-size: 11px; }
|
|
743
|
+
.mb-turn[data-size="large"] { font-size: 13px; }
|
|
744
|
+
.mb-turn[data-density="compact"] { column-gap: 6px; }
|
|
745
|
+
.mb-turn-item { white-space: nowrap; }
|
|
746
|
+
.mb-turn strong { color: inherit; font-weight: 600; }
|
|
747
|
+
.mb-turn[data-weight="normal"] { font-weight: 400; }
|
|
748
|
+
.mb-turn[data-weight="normal"] strong { font-weight: 400; }
|
|
749
|
+
.mb-turn[data-weight="bold"] { font-weight: 600; }
|
|
750
|
+
.mb-turn[data-weight="bold"] strong { font-weight: 600; }
|
|
751
|
+
.mb-turn-partial { color: var(--dsw-alias-state-warn-primary); }
|
|
752
|
+
.mb-tag.is-muted { color: var(--dsw-alias-label-secondary); }
|
|
753
|
+
@media (max-width: 720px) {
|
|
754
|
+
.mb-group-head { padding: 11px 12px 9px; }
|
|
755
|
+
.mb-group-body { padding: 0 12px; }
|
|
756
|
+
.mb-model-body { margin-left: 0; }
|
|
757
|
+
}
|
|
758
|
+
.mb-picker { position: relative; display: flex; min-width: 0; }
|
|
759
|
+
.mb-picker-trigger { border-radius: var(--dsw-radius-sm); min-width: 0; max-width: min(360px, 45cqw); height: 28px; color: var(--dsw-alias-label-secondary); cursor: pointer; background: 0 0; border: none; outline: none; align-items: center; gap: 4px; padding: 0 4px 0 8px; font-size: 13px; font-weight: 400; line-height: 20px; display: flex; }
|
|
760
|
+
.mb-picker-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
761
|
+
.mb-picker-trigger:focus-visible { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
762
|
+
.mb-picker-trigger:disabled { color: var(--dsw-alias-label-dimmed, var(--dsw-alias-label-secondary)); cursor: default; }
|
|
763
|
+
.mb-picker-icon { display: var(--dsh-composer-model-icon-display, none); flex: none; }
|
|
764
|
+
.mb-picker-name { color: inherit; text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; display: var(--dsh-composer-model-text-display, block); }
|
|
765
|
+
.mb-picker-trigger-effort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption, var(--dsw-alias-label-secondary)); flex-shrink: 1000; overflow: hidden; display: var(--dsh-composer-model-text-display, block); }
|
|
766
|
+
.mb-picker-chevron { color: var(--dsw-alias-label-caption, var(--dsw-alias-label-secondary)); flex: none; transition: transform .12s; }
|
|
767
|
+
.mb-picker-chevron-open { transform: rotate(180deg); }
|
|
768
|
+
.mb-picker-menu { box-sizing: border-box; z-index: 1100; width: min(360px, calc(100vw - 32px)); max-width: min(360px, calc(100vw - 32px)); max-height: min(480px, calc(100vh - 96px)); box-shadow: var(--dsw-elevation-prominent); background: var(--dsw-menu-surface-fill, var(--dsw-alias-bg-layer-1)); backdrop-filter: var(--dsw-menu-backdrop-filter, none); color: var(--dsw-alias-label-primary); border: 0; border-radius: var(--dsw-radius-lg, 10px); flex-direction: column; padding: 10px; display: flex; position: fixed; overflow: hidden; }
|
|
769
|
+
.mb-picker-search { box-sizing: border-box; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); height: 32px; margin: 0 0 6px; padding: 0 9px; font: inherit; font-size: 13px; color: var(--dsw-alias-label-primary); width: 100%; flex: none; }
|
|
770
|
+
.mb-picker-search::placeholder { color: var(--dsw-alias-label-caption, var(--dsw-alias-label-secondary)); }
|
|
771
|
+
.mb-picker-search:focus { border-color: var(--dsw-alias-state-business-primary); outline: none; }
|
|
772
|
+
.mb-picker-list { min-height: 0; overflow-y: auto; padding: 0 2px; display: flex; flex-direction: column; }
|
|
773
|
+
.mb-picker-group { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); background: var(--dsw-alias-menu-group-header-fill, var(--dsw-menu-surface-fill, var(--dsw-alias-bg-layer-1))); position: sticky; top: 0; z-index: 1; margin-top: 2px; padding: 5px 10px; font-size: 11px; font-weight: 500; line-height: 16px; }
|
|
774
|
+
/* The first group header opens the list, so the menu's own padding already
|
|
775
|
+
separates it from the top edge; it only drops the inter-group lead-in.
|
|
776
|
+
A negative margin cannot do this: the list is a scroll container, and the
|
|
777
|
+
overflow clip eats any lead-in that leaves its scrollport. */
|
|
778
|
+
.mb-picker-group:first-child { margin-top: 0; }
|
|
779
|
+
.mb-picker-row { box-sizing: border-box; border-radius: var(--dsw-radius-md); width: auto; min-width: 100%; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: none; outline: none; align-items: center; gap: 8px; padding: 10px; font: inherit; display: flex; }
|
|
780
|
+
.mb-picker-row + .mb-picker-row { margin-top: 2px; }
|
|
781
|
+
.mb-picker-row:hover:not(:disabled), .mb-picker-row:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
782
|
+
.mb-picker-row:disabled { color: var(--dsw-alias-label-dimmed, var(--dsw-alias-label-secondary)); cursor: default; }
|
|
783
|
+
.mb-picker-option-copy { flex-direction: column; flex: 1; min-width: 0; gap: 4px; display: flex; }
|
|
784
|
+
.mb-picker-model { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 20px; overflow: hidden; }
|
|
785
|
+
.mb-picker-prices { color: var(--dsw-alias-label-caption, var(--dsw-alias-label-secondary)); font-size: 11px; line-height: 17px; font-variant-numeric: tabular-nums; }
|
|
786
|
+
.mb-picker-price-line { white-space: normal; overflow-wrap: anywhere; }
|
|
787
|
+
.mb-picker-price-source { margin-bottom: 2px; color: var(--dsw-alias-label-secondary); font-weight: 600; }
|
|
788
|
+
.mb-picker-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; font-size: 12px; }
|
|
789
|
+
.mb-picker-check svg { width: 14px; height: 14px; }
|
|
790
|
+
.mb-picker-empty { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
791
|
+
.mb-picker-divider { border-top: 1px solid var(--dsw-alias-border-l1); margin: 3px 0; }
|
|
792
|
+
.mb-picker-efforts { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; padding: 2px 4px 4px; }
|
|
793
|
+
.mb-picker-efforts-label { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); font-size: 11px; margin-right: 2px; }
|
|
794
|
+
.mb-picker-chip { appearance: none; border: 0; background: 0 0; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; height: 24px; padding: 0 7px; border-radius: var(--dsw-radius-sm); cursor: pointer; }
|
|
795
|
+
.mb-picker-chip:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
796
|
+
.mb-picker-chip[aria-pressed="true"] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
797
|
+
.mb-picker-error { border-radius: var(--dsw-radius-md); background: var(--dsw-alias-interactive-bg-hover-danger); color: var(--dsw-alias-state-error-primary); margin-bottom: 3px; padding: 6px 7px; font-size: 11px; line-height: 16px; }
|
|
798
|
+
.mb-picker-retry { align-self: flex-start; margin: 0 4px 2px; }
|
|
799
|
+
.mb-picker-status { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); padding: 4px 7px; font-size: 11px; line-height: 16px; }
|
|
800
|
+
.mb-picker-dot { width: 12px; height: 12px; border: 2px solid var(--dsw-alias-state-business-primary); border-right-color: transparent; border-radius: 50%; flex: none; animation: mb-picker-spin .7s linear infinite; }
|
|
801
|
+
@keyframes mb-picker-spin { to { transform: rotate(360deg); } }
|
|
802
|
+
.mb-picker-toast { position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 3; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-state-error-primary); color: #fff; padding: 7px 9px; font-size: 12px; line-height: 16px; box-shadow: var(--dsw-elevation-prominent); }
|
|
803
|
+
.mb-fatal { border: 1px solid var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); border-radius: 8px; padding: 12px; }
|
|
804
|
+
@media (max-width: 720px) {
|
|
805
|
+
.mb-price-grid { grid-template-columns: 1fr; }
|
|
806
|
+
.mb-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
807
|
+
.mb-head-actions { width: 100%; margin-left: 38px; justify-content: flex-start; }
|
|
808
|
+
}
|
|
809
|
+
`
|
|
810
|
+
|
|
811
|
+
const STYLE_MARKER = 'data-dsh-model-billing'
|
|
812
|
+
|
|
813
|
+
// The stylesheet belongs to the plugin, not to any one surface, so inject it
|
|
814
|
+
// once into <head> instead of rendering a copy per settings page, dock,
|
|
815
|
+
// panel and picker.
|
|
816
|
+
function ensureStylesheet() {
|
|
817
|
+
if (typeof document === 'undefined') return
|
|
818
|
+
if (document.head.querySelector(`style[${STYLE_MARKER}]`) !== null) return
|
|
819
|
+
const style = document.createElement('style')
|
|
820
|
+
style.setAttribute(STYLE_MARKER, '')
|
|
821
|
+
style.textContent = CSS
|
|
822
|
+
document.head.appendChild(style)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function Style() {
|
|
826
|
+
ensureStylesheet()
|
|
827
|
+
return null
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ---------------------------------------------------------------------
|
|
831
|
+
// Settings section
|
|
832
|
+
// ---------------------------------------------------------------------
|
|
833
|
+
|
|
834
|
+
const PRICE_FIELDS = ['cacheHit', 'cacheMiss', 'output']
|
|
835
|
+
const PRICE_LABEL_KEYS = {
|
|
836
|
+
cacheHit: 'cacheHitPrice',
|
|
837
|
+
cacheMiss: 'cacheMissPrice',
|
|
838
|
+
output: 'outputPrice',
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function toDraftPricing(pricing) {
|
|
842
|
+
if (pricing?.builtin === true) return { ...pricing, peak: { ...pricing.peak }, offPeak: { ...pricing.offPeak } }
|
|
843
|
+
const source = pricing ?? {}
|
|
844
|
+
return Object.fromEntries(PRICE_FIELDS.map((field) => [
|
|
845
|
+
field,
|
|
846
|
+
source[field] === null || source[field] === undefined ? '' : String(source[field]),
|
|
847
|
+
]))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function toDraft(models) {
|
|
851
|
+
return models.map((model) => ({
|
|
852
|
+
provider: model.provider,
|
|
853
|
+
providerName: model.providerName,
|
|
854
|
+
model: model.model,
|
|
855
|
+
name: model.name,
|
|
856
|
+
isDeepSeek: model.isDeepSeek,
|
|
857
|
+
pricing: toDraftPricing(model.pricing),
|
|
858
|
+
}))
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function toDraftArchive(entries) {
|
|
862
|
+
return (entries ?? []).map((entry) => ({
|
|
863
|
+
provider: entry.provider,
|
|
864
|
+
model: entry.model,
|
|
865
|
+
pricing: toDraftPricing(entry.pricing),
|
|
866
|
+
}))
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function parsePriceInput(value) {
|
|
870
|
+
if (typeof value === 'string' && value.trim() === '') return null
|
|
871
|
+
const number = Number(value)
|
|
872
|
+
if (!Number.isFinite(number) || number < 0 || number > 1e12) return undefined
|
|
873
|
+
return Math.round(number * 1e8) / 1e8
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function SettingsSection() {
|
|
877
|
+
const translate = useT()
|
|
878
|
+
const snap = useCatalog()
|
|
879
|
+
const [draft, setDraft] = useState(null)
|
|
880
|
+
const [archive, setArchive] = useState(null)
|
|
881
|
+
const [display, setDisplay] = useState(null)
|
|
882
|
+
const [dirty, setDirty] = useState(false)
|
|
883
|
+
const [saving, setSaving] = useState(false)
|
|
884
|
+
const [status, setStatus] = useState(null)
|
|
885
|
+
|
|
886
|
+
useEffect(() => {
|
|
887
|
+
if (snap.data === null) return
|
|
888
|
+
setDraft(toDraft(snap.data.models))
|
|
889
|
+
setArchive(toDraftArchive(snap.data.pricingArchive))
|
|
890
|
+
setDisplay(readDisplay(snap.data))
|
|
891
|
+
setDirty(false)
|
|
892
|
+
}, [snap.data])
|
|
893
|
+
|
|
894
|
+
const models = draft ?? []
|
|
895
|
+
const archiveRows = archive ?? []
|
|
896
|
+
const update = (index, change) => {
|
|
897
|
+
setDraft((current) => current === null ? current : current.map((entry, at) => at === index ? change(entry) : entry))
|
|
898
|
+
setDirty(true)
|
|
899
|
+
setStatus(null)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const updatePrice = (index, field, value) => {
|
|
903
|
+
update(index, (entry) => ({
|
|
904
|
+
...entry,
|
|
905
|
+
pricing: { ...entry.pricing, [field]: value },
|
|
906
|
+
}))
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// "Custom prices" starts from blank fields; the user opts in explicitly.
|
|
910
|
+
const resetPricing = (index) => {
|
|
911
|
+
update(index, (entry) => ({
|
|
912
|
+
...entry,
|
|
913
|
+
pricing: { cacheHit: '', cacheMiss: '', output: '' },
|
|
914
|
+
}))
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// "Restore official prices" puts the built-in peak/off-peak card back
|
|
918
|
+
// immediately, so the user sees the official table before saving.
|
|
919
|
+
const restoreOfficialPricing = (index) => {
|
|
920
|
+
const entry = models[index]
|
|
921
|
+
if (entry === undefined) return
|
|
922
|
+
const catalogRow = (snap.data?.models ?? []).find((row) =>
|
|
923
|
+
row.provider === entry.provider && row.model === entry.model)
|
|
924
|
+
const official = catalogRow?.pricing?.official
|
|
925
|
+
?? (catalogRow?.pricing?.builtin === true ? catalogRow.pricing : null)
|
|
926
|
+
const restored = official?.builtin === true
|
|
927
|
+
? toDraftPricing(official)
|
|
928
|
+
: { cacheHit: '', cacheMiss: '', output: '' }
|
|
929
|
+
update(index, (current) => ({ ...current, pricing: restored }))
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const move = (index, delta) => {
|
|
933
|
+
const target = index + delta
|
|
934
|
+
if (target < 0 || target >= models.length) return
|
|
935
|
+
setDraft((current) => {
|
|
936
|
+
if (current === null) return current
|
|
937
|
+
const next = [...current]
|
|
938
|
+
const [item] = next.splice(index, 1)
|
|
939
|
+
next.splice(target, 0, item)
|
|
940
|
+
return next
|
|
941
|
+
})
|
|
942
|
+
setDirty(true)
|
|
943
|
+
setStatus(null)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const updateArchive = (index, change) => {
|
|
947
|
+
setArchive((current) => current === null ? current : current.map((entry, at) => at === index ? change(entry) : entry))
|
|
948
|
+
setDirty(true)
|
|
949
|
+
setStatus(null)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const updateArchivePrice = (index, field, value) => {
|
|
953
|
+
updateArchive(index, (entry) => ({ ...entry, pricing: { ...entry.pricing, [field]: value } }))
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const addArchive = () => {
|
|
957
|
+
setArchive((current) => [...(current ?? []), { provider: '', model: '', pricing: { cacheHit: '', cacheMiss: '', output: '' } }])
|
|
958
|
+
setDirty(true)
|
|
959
|
+
setStatus(null)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const removeArchive = (index) => {
|
|
963
|
+
setArchive((current) => current === null ? current : current.filter((entry, at) => at !== index))
|
|
964
|
+
setDirty(true)
|
|
965
|
+
setStatus(null)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const reset = () => {
|
|
969
|
+
emitCatalog()
|
|
970
|
+
if (snap.data !== null) {
|
|
971
|
+
setDraft(toDraft(snap.data.models))
|
|
972
|
+
setArchive(toDraftArchive(snap.data.pricingArchive))
|
|
973
|
+
setDisplay(readDisplay(snap.data))
|
|
974
|
+
}
|
|
975
|
+
setDirty(false)
|
|
976
|
+
setStatus(null)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const displayValue = display ?? readDisplay(snap.data)
|
|
980
|
+
|
|
981
|
+
const updateDisplay = (change) => {
|
|
982
|
+
setDisplay((current) => ({ ...(current ?? readDisplay(snap.data)), ...change }))
|
|
983
|
+
setDirty(true)
|
|
984
|
+
setStatus(null)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const HERO_ICON = h('svg', {
|
|
988
|
+
viewBox: '0 0 64 64',
|
|
989
|
+
width: 42,
|
|
990
|
+
height: 42,
|
|
991
|
+
'aria-hidden': true,
|
|
992
|
+
focusable: 'false',
|
|
993
|
+
},
|
|
994
|
+
h('defs', null,
|
|
995
|
+
h('linearGradient', { id: 'mb-hero-coin', x1: 8, y1: 6, x2: 56, y2: 58, gradientUnits: 'userSpaceOnUse' },
|
|
996
|
+
h('stop', { offset: 0, stopColor: '#1f8a8a' }),
|
|
997
|
+
h('stop', { offset: 1, stopColor: '#185a9d' }))),
|
|
998
|
+
h('rect', { x: 4, y: 4, width: 56, height: 56, rx: 14, fill: 'url(#mb-hero-coin)' }),
|
|
999
|
+
h('circle', { cx: 32, cy: 32, r: 19, fill: 'none', stroke: '#ffffff', strokeOpacity: 0.9, strokeWidth: 2.4 }),
|
|
1000
|
+
h('path', {
|
|
1001
|
+
d: 'M25 23l7 9 7-9',
|
|
1002
|
+
fill: 'none',
|
|
1003
|
+
stroke: '#ffffff',
|
|
1004
|
+
strokeWidth: 2.8,
|
|
1005
|
+
strokeLinecap: 'round',
|
|
1006
|
+
strokeLinejoin: 'round',
|
|
1007
|
+
}),
|
|
1008
|
+
h('path', { d: 'M32 32v11M25.5 31.5h13M25.5 36.5h13', fill: 'none', stroke: '#ffffff', strokeWidth: 2.4, strokeLinecap: 'round' }),
|
|
1009
|
+
h('path', { d: 'M18 47.5h8M28 47.5h8M38 47.5h8', stroke: '#ffffff', strokeOpacity: 0.55, strokeWidth: 2, strokeLinecap: 'round' }))
|
|
1010
|
+
|
|
1011
|
+
const renderHero = () => h('header', { className: 'mb-hero' },
|
|
1012
|
+
h('span', { className: 'mb-mark' }, HERO_ICON),
|
|
1013
|
+
h('div', null,
|
|
1014
|
+
h('h2', null, translate('title')),
|
|
1015
|
+
h('p', null, translate('intro'))))
|
|
1016
|
+
|
|
1017
|
+
const switchControl = (checked, onChange, label, disabled = false) => h('button', {
|
|
1018
|
+
type: 'button',
|
|
1019
|
+
role: 'switch',
|
|
1020
|
+
className: 'mb-switch',
|
|
1021
|
+
'aria-checked': checked ? 'true' : 'false',
|
|
1022
|
+
'aria-label': label,
|
|
1023
|
+
title: label,
|
|
1024
|
+
disabled,
|
|
1025
|
+
onClick: () => onChange(!checked),
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
const row = (label, description, control, disabled = false, key) => h('div', {
|
|
1029
|
+
className: 'mb-row' + (disabled ? ' is-disabled' : ''),
|
|
1030
|
+
key,
|
|
1031
|
+
},
|
|
1032
|
+
h('div', { className: 'mb-row-copy' },
|
|
1033
|
+
h('div', { className: 'mb-row-label' }, label),
|
|
1034
|
+
description === undefined ? null : h('div', { className: 'mb-row-desc' }, description)),
|
|
1035
|
+
h('div', { className: 'mb-row-control' }, control))
|
|
1036
|
+
|
|
1037
|
+
const group = (title, description, ...children) => h('section', { className: 'mb-group' },
|
|
1038
|
+
h('header', { className: 'mb-group-head' },
|
|
1039
|
+
h('h3', null, title),
|
|
1040
|
+
description === undefined ? null : h('p', null, description)),
|
|
1041
|
+
h('div', { className: 'mb-group-body' }, children))
|
|
1042
|
+
|
|
1043
|
+
const toggleRow = (field, label, disabled = false) => row(
|
|
1044
|
+
translate(label),
|
|
1045
|
+
undefined,
|
|
1046
|
+
switchControl(displayValue[field] === true, (next) => updateDisplay({ [field]: next }), translate(label), disabled),
|
|
1047
|
+
disabled,
|
|
1048
|
+
field)
|
|
1049
|
+
|
|
1050
|
+
const renderDisplaySettings = () => h(Fragment, null,
|
|
1051
|
+
group(translate('pickerSection'), translate('takeoverHint'),
|
|
1052
|
+
toggleRow('takeoverPicker', 'takeoverPicker')),
|
|
1053
|
+
group(translate('dockSection'), translate('dockHint'),
|
|
1054
|
+
toggleRow('showDock', 'displayDock'),
|
|
1055
|
+
toggleRow('showUnpricedNotice', 'showUnpricedNotice', displayValue.showDock !== true)),
|
|
1056
|
+
group(translate('displaySection'), translate('displayHint'),
|
|
1057
|
+
toggleRow('showTurnTail', 'displayTurnTail'),
|
|
1058
|
+
h('div', { className: 'mb-subhead' }, translate('displayFields')),
|
|
1059
|
+
[
|
|
1060
|
+
['showProvider', 'displayProvider'],
|
|
1061
|
+
['showModels', 'displayModels'],
|
|
1062
|
+
['showCost', 'displayCost'],
|
|
1063
|
+
['showTokens', 'displayTokens'],
|
|
1064
|
+
['showCacheHit', 'displayCacheHit'],
|
|
1065
|
+
['showLabels', 'displayLabels'],
|
|
1066
|
+
].map(([field, label]) => {
|
|
1067
|
+
const off = displayValue.showTurnTail !== true ||
|
|
1068
|
+
(field === 'showProvider' && displayValue.showModels !== true)
|
|
1069
|
+
return toggleRow(field, label, off)
|
|
1070
|
+
}),
|
|
1071
|
+
h('div', { className: 'mb-subhead' }, translate('displayLook')),
|
|
1072
|
+
row(translate('displayFontSize'), undefined, h('select', {
|
|
1073
|
+
className: 'mb-select',
|
|
1074
|
+
value: displayValue.fontSize,
|
|
1075
|
+
onChange: (event) => updateDisplay({ fontSize: event.target.value }),
|
|
1076
|
+
},
|
|
1077
|
+
h('option', { value: 'small' }, translate('fontSizeSmall')),
|
|
1078
|
+
h('option', { value: 'normal' }, translate('fontSizeNormal')),
|
|
1079
|
+
h('option', { value: 'large' }, translate('fontSizeLarge')))),
|
|
1080
|
+
row(translate('displayDensity'), undefined, h('select', {
|
|
1081
|
+
className: 'mb-select',
|
|
1082
|
+
value: displayValue.density,
|
|
1083
|
+
onChange: (event) => updateDisplay({ density: event.target.value }),
|
|
1084
|
+
},
|
|
1085
|
+
h('option', { value: 'compact' }, translate('densityCompact')),
|
|
1086
|
+
h('option', { value: 'comfortable' }, translate('densityComfortable')))),
|
|
1087
|
+
row(translate('displayWeight'), undefined, h('select', {
|
|
1088
|
+
className: 'mb-select',
|
|
1089
|
+
value: displayValue.fontWeight,
|
|
1090
|
+
onChange: (event) => updateDisplay({ fontWeight: event.target.value }),
|
|
1091
|
+
},
|
|
1092
|
+
h('option', { value: 'normal' }, translate('weightNormal')),
|
|
1093
|
+
h('option', { value: 'bold' }, translate('weightBold'))))))
|
|
1094
|
+
|
|
1095
|
+
const renderArchive = () => group(translate('archiveSection'), translate('archiveHint'),
|
|
1096
|
+
archiveRows.length === 0
|
|
1097
|
+
? h('div', { className: 'mb-empty' }, translate('archiveEmpty'))
|
|
1098
|
+
: archiveRows.map((entry, index) => h('div', { className: 'mb-archive-row', key: index },
|
|
1099
|
+
h('div', { className: 'mb-archive-head' },
|
|
1100
|
+
h('input', {
|
|
1101
|
+
className: 'mb-input',
|
|
1102
|
+
placeholder: translate('archiveProvider'),
|
|
1103
|
+
'aria-label': translate('archiveProvider'),
|
|
1104
|
+
value: entry.provider,
|
|
1105
|
+
onChange: (event) => updateArchive(index, (current) => ({ ...current, provider: event.target.value })),
|
|
1106
|
+
}),
|
|
1107
|
+
h('input', {
|
|
1108
|
+
className: 'mb-input',
|
|
1109
|
+
placeholder: translate('archiveModel'),
|
|
1110
|
+
'aria-label': translate('archiveModel'),
|
|
1111
|
+
value: entry.model,
|
|
1112
|
+
onChange: (event) => updateArchive(index, (current) => ({ ...current, model: event.target.value })),
|
|
1113
|
+
}),
|
|
1114
|
+
h('button', {
|
|
1115
|
+
type: 'button',
|
|
1116
|
+
className: 'mb-btn',
|
|
1117
|
+
title: translate('remove'),
|
|
1118
|
+
'aria-label': translate('remove'),
|
|
1119
|
+
onClick: () => removeArchive(index),
|
|
1120
|
+
}, translate('remove'))),
|
|
1121
|
+
h('div', { className: 'mb-price-grid' },
|
|
1122
|
+
PRICE_FIELDS.map((field) => h('input', {
|
|
1123
|
+
key: field,
|
|
1124
|
+
className: 'mb-input',
|
|
1125
|
+
inputMode: 'decimal',
|
|
1126
|
+
placeholder: translate(PRICE_LABEL_KEYS[field]),
|
|
1127
|
+
'aria-label': translate(PRICE_LABEL_KEYS[field]),
|
|
1128
|
+
value: entry.pricing[field] ?? '',
|
|
1129
|
+
onChange: (event) => updateArchivePrice(index, field, event.target.value),
|
|
1130
|
+
}))))),
|
|
1131
|
+
h('div', { className: 'mb-archive-actions' },
|
|
1132
|
+
h('button', { type: 'button', className: 'mb-btn', onClick: addArchive }, translate('addArchive'))))
|
|
1133
|
+
|
|
1134
|
+
const save = async () => {
|
|
1135
|
+
if (draft === null) return
|
|
1136
|
+
const payload = []
|
|
1137
|
+
for (const entry of draft) {
|
|
1138
|
+
if (entry.pricing.builtin === true) {
|
|
1139
|
+
payload.push({ provider: entry.provider, model: entry.model, pricing: {} })
|
|
1140
|
+
continue
|
|
1141
|
+
}
|
|
1142
|
+
const pricing = {}
|
|
1143
|
+
const blank = PRICE_FIELDS.every((field) => String(entry.pricing[field] ?? '').trim() === '')
|
|
1144
|
+
if (blank) {
|
|
1145
|
+
// Three blank fields mean "drop the override", so the official table applies again.
|
|
1146
|
+
payload.push({ provider: entry.provider, model: entry.model, pricing: {} })
|
|
1147
|
+
continue
|
|
1148
|
+
}
|
|
1149
|
+
for (const field of PRICE_FIELDS) {
|
|
1150
|
+
const parsed = parsePriceInput(entry.pricing[field])
|
|
1151
|
+
if (parsed === undefined) {
|
|
1152
|
+
setStatus({ tone: 'error', text: translate('invalidPrice') })
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1155
|
+
pricing[field] = parsed
|
|
1156
|
+
}
|
|
1157
|
+
payload.push({ provider: entry.provider, model: entry.model, pricing })
|
|
1158
|
+
}
|
|
1159
|
+
const archivePayload = []
|
|
1160
|
+
const currentKeys = new Set(draft.map((entry) => `${entry.provider}\u0000${entry.model}`))
|
|
1161
|
+
for (const entry of archiveRows) {
|
|
1162
|
+
const provider = entry.provider.trim()
|
|
1163
|
+
const model = entry.model.trim()
|
|
1164
|
+
if (provider === '' && model === '') continue
|
|
1165
|
+
if (provider === '' || model === '') {
|
|
1166
|
+
setStatus({ tone: 'error', text: translate('invalidRoute') })
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
if (currentKeys.has(`${provider}\u0000${model}`)) {
|
|
1170
|
+
setStatus({ tone: 'error', text: translate('archiveConflict') })
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
const pricing = {}
|
|
1174
|
+
const blank = PRICE_FIELDS.every((field) => String(entry.pricing[field] ?? '').trim() === '')
|
|
1175
|
+
if (blank) {
|
|
1176
|
+
archivePayload.push({ provider, model, pricing: {} })
|
|
1177
|
+
continue
|
|
1178
|
+
}
|
|
1179
|
+
for (const field of PRICE_FIELDS) {
|
|
1180
|
+
const parsed = parsePriceInput(entry.pricing[field])
|
|
1181
|
+
if (parsed === undefined) {
|
|
1182
|
+
setStatus({ tone: 'error', text: translate('invalidPrice') })
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
pricing[field] = parsed
|
|
1186
|
+
}
|
|
1187
|
+
archivePayload.push({ provider, model, pricing })
|
|
1188
|
+
}
|
|
1189
|
+
setSaving(true)
|
|
1190
|
+
setStatus(null)
|
|
1191
|
+
try {
|
|
1192
|
+
await requestJson('/model-billing/save', {
|
|
1193
|
+
method: 'POST',
|
|
1194
|
+
headers: { 'content-type': 'application/json' },
|
|
1195
|
+
body: JSON.stringify({ models: payload, pricingArchive: archivePayload, display: displayValue }),
|
|
1196
|
+
})
|
|
1197
|
+
setDirty(false)
|
|
1198
|
+
setStatus({ tone: 'success', text: translate('saved') })
|
|
1199
|
+
await ensureCatalog(true)
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
setStatus({ tone: 'error', text: translate('saveFailed', { message: error instanceof Error ? error.message : String(error) }) })
|
|
1202
|
+
} finally {
|
|
1203
|
+
setSaving(false)
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (snap.data === null) {
|
|
1208
|
+
return h(Fragment, null,
|
|
1209
|
+
h(Style),
|
|
1210
|
+
h('section', { className: 'mb-page' },
|
|
1211
|
+
renderHero(),
|
|
1212
|
+
snap.status === 'error'
|
|
1213
|
+
? h('div', { className: 'mb-fatal' },
|
|
1214
|
+
h('div', null, translate('loadFailed')),
|
|
1215
|
+
h('p', { className: 'mb-note' }, snap.error),
|
|
1216
|
+
h('button', { type: 'button', className: 'mb-btn', onClick: () => void ensureCatalog(true) }, translate('retry')))
|
|
1217
|
+
: h('p', { className: 'mb-muted' }, translate('loading'))))
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (draft === null) {
|
|
1221
|
+
return h(Fragment, null,
|
|
1222
|
+
h(Style),
|
|
1223
|
+
h('section', { className: 'mb-page' },
|
|
1224
|
+
renderHero(),
|
|
1225
|
+
h('p', { className: 'mb-muted' }, translate('loading'))))
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const renderToolbar = () => h('div', { className: 'mb-bar' },
|
|
1229
|
+
h('span', { className: 'mb-bar-count' }, translate('modelsCount', { count: models.length })),
|
|
1230
|
+
h('span', { className: 'mb-spacer' }),
|
|
1231
|
+
dirty ? h('span', { className: 'mb-bar-count' }, translate('unsaved')) : null,
|
|
1232
|
+
h('button', { type: 'button', className: 'mb-btn', disabled: !dirty || saving, onClick: reset }, translate('undo')),
|
|
1233
|
+
h('button', { type: 'button', className: 'mb-btn mb-btn-primary', disabled: !dirty || saving, onClick: () => void save() }, saving ? translate('saving') : translate('save')))
|
|
1234
|
+
|
|
1235
|
+
const renderModels = () => group(translate('modelsSection'), translate('modelsHint'),
|
|
1236
|
+
models.length === 0
|
|
1237
|
+
? h('div', { className: 'mb-empty' }, translate('noModels'))
|
|
1238
|
+
: models.map((entry, index) => h(SettingsModelCard, {
|
|
1239
|
+
key: `${entry.provider}\u0000${entry.model}`,
|
|
1240
|
+
entry,
|
|
1241
|
+
index,
|
|
1242
|
+
count: models.length,
|
|
1243
|
+
translate,
|
|
1244
|
+
onMove: move,
|
|
1245
|
+
onUpdatePrice: updatePrice,
|
|
1246
|
+
onResetPricing: resetPricing,
|
|
1247
|
+
onRestoreOfficial: restoreOfficialPricing,
|
|
1248
|
+
})))
|
|
1249
|
+
|
|
1250
|
+
if (models.length === 0) {
|
|
1251
|
+
// The display controls stay editable with no models configured, so the
|
|
1252
|
+
// toolbar (and its save button) must still render here.
|
|
1253
|
+
return h(Fragment, null,
|
|
1254
|
+
h(Style),
|
|
1255
|
+
h('section', { className: 'mb-page' },
|
|
1256
|
+
renderHero(),
|
|
1257
|
+
renderToolbar(),
|
|
1258
|
+
status === null ? null : h('div', { className: 'mb-status', 'data-tone': status.tone, role: 'status' }, status.text),
|
|
1259
|
+
renderDisplaySettings(),
|
|
1260
|
+
renderModels(),
|
|
1261
|
+
renderArchive(),
|
|
1262
|
+
h('p', { className: 'mb-footer-note' }, translate('formula')),
|
|
1263
|
+
h('p', { className: 'mb-footer-note' }, translate('formulaNote'))))
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return h(Fragment, null,
|
|
1267
|
+
h(Style),
|
|
1268
|
+
h('section', { className: 'mb-page' },
|
|
1269
|
+
renderHero(),
|
|
1270
|
+
renderToolbar(),
|
|
1271
|
+
status === null ? null : h('div', { className: 'mb-status', 'data-tone': status.tone, role: 'status' }, status.text),
|
|
1272
|
+
renderDisplaySettings(),
|
|
1273
|
+
renderModels(),
|
|
1274
|
+
renderArchive(),
|
|
1275
|
+
h('p', { className: 'mb-footer-note' }, translate('formula')),
|
|
1276
|
+
h('p', { className: 'mb-footer-note' }, translate('formulaNote'))))
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function SettingsModelCard({ entry, index, count, translate, onMove, onUpdatePrice, onResetPricing, onRestoreOfficial }) {
|
|
1280
|
+
const id = useId()
|
|
1281
|
+
const pricing = entry.pricing
|
|
1282
|
+
const builtin = pricing.builtin === true
|
|
1283
|
+
const custom = entry.isDeepSeek && !builtin
|
|
1284
|
+
const fields = builtin ? null : PRICE_FIELDS.map((field) => {
|
|
1285
|
+
const inputId = `${id}-${field}`
|
|
1286
|
+
return h('label', { className: 'mb-field', htmlFor: inputId, key: field },
|
|
1287
|
+
h('span', { className: 'mb-field-label' }, translate(PRICE_LABEL_KEYS[field])),
|
|
1288
|
+
h('input', {
|
|
1289
|
+
id: inputId,
|
|
1290
|
+
className: 'mb-input',
|
|
1291
|
+
type: 'text',
|
|
1292
|
+
inputMode: 'decimal',
|
|
1293
|
+
autoComplete: 'off',
|
|
1294
|
+
placeholder: translate('pricePlaceholder'),
|
|
1295
|
+
value: pricing[field],
|
|
1296
|
+
onChange: (event) => onUpdatePrice(index, field, event.target.value),
|
|
1297
|
+
}))
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
return h('div', { className: 'mb-model' },
|
|
1301
|
+
h('div', { className: 'mb-model-head' },
|
|
1302
|
+
h('span', { className: 'mb-index' }, index + 1),
|
|
1303
|
+
h('div', { style: { minWidth: 0, flex: 1 } },
|
|
1304
|
+
h('div', { className: 'mb-name' },
|
|
1305
|
+
h('span', { className: 'mb-name-text' }, entry.name),
|
|
1306
|
+
entry.isDeepSeek ? h('span', { className: 'mb-tag' }, translate('deepseekTag')) : null),
|
|
1307
|
+
h('div', { className: 'mb-sub' }, `${entry.providerName} · ${entry.provider} / ${entry.model}`)),
|
|
1308
|
+
h('div', { className: 'mb-head-actions' },
|
|
1309
|
+
h('button', {
|
|
1310
|
+
type: 'button',
|
|
1311
|
+
className: 'mb-btn mb-btn-icon',
|
|
1312
|
+
disabled: index === 0,
|
|
1313
|
+
title: translate('up'),
|
|
1314
|
+
'aria-label': translate('up'),
|
|
1315
|
+
onClick: () => onMove(index, -1),
|
|
1316
|
+
}, '↑'),
|
|
1317
|
+
h('button', {
|
|
1318
|
+
type: 'button',
|
|
1319
|
+
className: 'mb-btn mb-btn-icon',
|
|
1320
|
+
disabled: index === count - 1,
|
|
1321
|
+
title: translate('down'),
|
|
1322
|
+
'aria-label': translate('down'),
|
|
1323
|
+
onClick: () => onMove(index, 1),
|
|
1324
|
+
}, '↓'))),
|
|
1325
|
+
h('div', { className: 'mb-model-body' },
|
|
1326
|
+
entry.isDeepSeek ? h('p', { className: 'mb-note', style: { margin: 0 } }, translate('deepseekNote')) : null,
|
|
1327
|
+
h('div', { className: 'mb-price-group' },
|
|
1328
|
+
h('div', { className: 'mb-price-group-title' },
|
|
1329
|
+
h('span', null, builtin ? translate('officialPrice') : translate('singlePrice')),
|
|
1330
|
+
custom ? h('span', { className: 'mb-tag' }, translate('priceSourceCustom')) : null,
|
|
1331
|
+
h('span', { className: 'mb-muted', style: { fontWeight: 400, fontSize: '11px' } }, translate('perMillion'))),
|
|
1332
|
+
builtin
|
|
1333
|
+
? h(Fragment, null,
|
|
1334
|
+
h(PriceLines, { pricing, translate, showSource: false }),
|
|
1335
|
+
h('p', { className: 'mb-note' }, translate('deepseekSchedule')))
|
|
1336
|
+
: h(Fragment, null,
|
|
1337
|
+
h('div', { className: 'mb-price-grid' }, fields),
|
|
1338
|
+
custom ? h('p', { className: 'mb-note' }, translate('customPriceNote')) : null),
|
|
1339
|
+
entry.isDeepSeek
|
|
1340
|
+
? h('div', { className: 'mb-card-actions' },
|
|
1341
|
+
h('button', {
|
|
1342
|
+
type: 'button',
|
|
1343
|
+
className: 'mb-btn',
|
|
1344
|
+
onClick: () => builtin ? onResetPricing(index) : onRestoreOfficial(index),
|
|
1345
|
+
}, builtin ? translate('customizePrice') : translate('useOfficialPrice')))
|
|
1346
|
+
: null)))
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// ---------------------------------------------------------------------
|
|
1350
|
+
// Composer dock cost summary
|
|
1351
|
+
// ---------------------------------------------------------------------
|
|
1352
|
+
|
|
1353
|
+
function CostDock({ sessionId, useProjection }) {
|
|
1354
|
+
const translate = useT()
|
|
1355
|
+
const tokenUsage = useProjection('tokenUsage')
|
|
1356
|
+
const revision = tokenUsageRevision(tokenUsage)
|
|
1357
|
+
const usage = useSessionUsage(sessionId, revision)
|
|
1358
|
+
const catalogSnap = useCatalog()
|
|
1359
|
+
const [open, setOpen] = useState(false)
|
|
1360
|
+
const [panelPos, setPanelPos] = useState(null)
|
|
1361
|
+
const [editingRoute, setEditingRoute] = useState(null)
|
|
1362
|
+
const [editPrices, setEditPrices] = useState({ cacheHit: '', cacheMiss: '', output: '' })
|
|
1363
|
+
const [savingPrice, setSavingPrice] = useState(false)
|
|
1364
|
+
const [priceStatus, setPriceStatus] = useState(null)
|
|
1365
|
+
const triggerRef = useRef(null)
|
|
1366
|
+
const panelRef = useRef(null)
|
|
1367
|
+
|
|
1368
|
+
useLayoutEffect(() => {
|
|
1369
|
+
if (!open) {
|
|
1370
|
+
setPanelPos(null)
|
|
1371
|
+
return undefined
|
|
1372
|
+
}
|
|
1373
|
+
const place = () => {
|
|
1374
|
+
const rect = triggerRef.current?.getBoundingClientRect()
|
|
1375
|
+
if (rect === undefined) return
|
|
1376
|
+
const width = Math.min(660, window.innerWidth - 24)
|
|
1377
|
+
const naturalLeft = Math.max(12, Math.min(rect.left, window.innerWidth - width - 12))
|
|
1378
|
+
// The dock sits near the right edge; shift the whole detail panel left so
|
|
1379
|
+
// the bottom-right whale widget cannot cover it.
|
|
1380
|
+
const left = Math.max(12, naturalLeft - 120)
|
|
1381
|
+
const spaceAbove = rect.top - 12
|
|
1382
|
+
const spaceBelow = window.innerHeight - rect.bottom - 12
|
|
1383
|
+
const above = spaceAbove > spaceBelow
|
|
1384
|
+
const maxHeight = Math.min(430, Math.max(180, above ? spaceAbove : spaceBelow))
|
|
1385
|
+
setPanelPos(above
|
|
1386
|
+
? { left, width, bottom: window.innerHeight - rect.top + 8, maxHeight }
|
|
1387
|
+
: { left, width, top: rect.bottom + 8, maxHeight })
|
|
1388
|
+
}
|
|
1389
|
+
place()
|
|
1390
|
+
window.addEventListener('resize', place)
|
|
1391
|
+
window.addEventListener('scroll', place, true)
|
|
1392
|
+
return () => {
|
|
1393
|
+
window.removeEventListener('resize', place)
|
|
1394
|
+
window.removeEventListener('scroll', place, true)
|
|
1395
|
+
}
|
|
1396
|
+
}, [open])
|
|
1397
|
+
|
|
1398
|
+
useEffect(() => {
|
|
1399
|
+
if (!open) return undefined
|
|
1400
|
+
const closeOutside = (event) => {
|
|
1401
|
+
if (triggerRef.current?.contains(event.target) === true) return
|
|
1402
|
+
if (panelRef.current?.contains(event.target) === true) return
|
|
1403
|
+
setOpen(false)
|
|
1404
|
+
}
|
|
1405
|
+
const closeOnEscape = (event) => {
|
|
1406
|
+
if (event.key !== 'Escape') return
|
|
1407
|
+
event.preventDefault()
|
|
1408
|
+
setOpen(false)
|
|
1409
|
+
triggerRef.current?.focus()
|
|
1410
|
+
}
|
|
1411
|
+
document.addEventListener('mousedown', closeOutside)
|
|
1412
|
+
document.addEventListener('keydown', closeOnEscape)
|
|
1413
|
+
// A dialog must take focus when it opens so keyboard users land inside it.
|
|
1414
|
+
panelRef.current?.focus()
|
|
1415
|
+
return () => {
|
|
1416
|
+
document.removeEventListener('mousedown', closeOutside)
|
|
1417
|
+
document.removeEventListener('keydown', closeOnEscape)
|
|
1418
|
+
}
|
|
1419
|
+
}, [open])
|
|
1420
|
+
|
|
1421
|
+
// Hooks must run on every render, so derive the catalog indexes before any
|
|
1422
|
+
// early return; React rejects a different hook count between renders.
|
|
1423
|
+
const orderIndex = useMemo(() =>
|
|
1424
|
+
new Map((catalogSnap.data?.models ?? []).map((model) => [`${model.provider}\u0000${model.model}`, model.order])),
|
|
1425
|
+
[catalogSnap.data])
|
|
1426
|
+
const catalogByKey = useMemo(() =>
|
|
1427
|
+
new Map((catalogSnap.data?.models ?? []).map((model) => [`${model.provider}\u0000${model.model}`, model])),
|
|
1428
|
+
[catalogSnap.data])
|
|
1429
|
+
|
|
1430
|
+
const display = readDisplay(catalogSnap.data)
|
|
1431
|
+
if (display.showDock === false) return null
|
|
1432
|
+
|
|
1433
|
+
const data = usage.data
|
|
1434
|
+
if (data === null || data === undefined || data.totals?.hasBillable !== true) {
|
|
1435
|
+
return null
|
|
1436
|
+
}
|
|
1437
|
+
const totals = data.totals
|
|
1438
|
+
const unpricedRoutes = data.unpricedRoutes ?? []
|
|
1439
|
+
const beginRouteEdit = (route) => {
|
|
1440
|
+
setEditingRoute(`${route.provider}\u0000${route.model}`)
|
|
1441
|
+
setEditPrices({ cacheHit: '', cacheMiss: '', output: '' })
|
|
1442
|
+
setPriceStatus(null)
|
|
1443
|
+
}
|
|
1444
|
+
const saveRoutePrice = async (route) => {
|
|
1445
|
+
const pricing = {}
|
|
1446
|
+
for (const field of PRICE_FIELDS) {
|
|
1447
|
+
const parsed = parsePriceInput(editPrices[field])
|
|
1448
|
+
if (parsed === undefined || parsed === null) {
|
|
1449
|
+
setPriceStatus({ tone: 'error', text: translate('invalidPrice') })
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
pricing[field] = parsed
|
|
1453
|
+
}
|
|
1454
|
+
setSavingPrice(true)
|
|
1455
|
+
setPriceStatus(null)
|
|
1456
|
+
try {
|
|
1457
|
+
await requestJson('/model-billing/save', {
|
|
1458
|
+
method: 'POST',
|
|
1459
|
+
headers: { 'content-type': 'application/json' },
|
|
1460
|
+
body: JSON.stringify({ pricingArchive: [{ provider: route.provider, model: route.model, pricing }] }),
|
|
1461
|
+
})
|
|
1462
|
+
await ensureCatalog(true)
|
|
1463
|
+
setEditingRoute(null)
|
|
1464
|
+
setPriceStatus({ tone: 'success', text: translate('priceSaved') })
|
|
1465
|
+
await ensureUsage(sessionId, revision, true)
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
setPriceStatus({ tone: 'error', text: translate('saveFailed', { message: error instanceof Error ? error.message : String(error) }) })
|
|
1468
|
+
} finally {
|
|
1469
|
+
setSavingPrice(false)
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const last = [...(data.turns ?? [])].reverse().find((entry) => entry.hasBillable) ?? null
|
|
1473
|
+
const totalText = totals.costKnown
|
|
1474
|
+
? `≈ ${formatMoney(totals.cost)}`
|
|
1475
|
+
: totals.pricedAttempts > 0 ? `≈ ${formatMoney(totals.cost)}` : translate('pricePlaceholder')
|
|
1476
|
+
const lastText = last === null || last === undefined || !last.hasBillable
|
|
1477
|
+
? null
|
|
1478
|
+
: last.costKnown ? `≈ ${formatMoney(last.cost)}` : last.pricedAttempts > 0 ? `≈ ${formatMoney(last.cost)}` : translate('pricePlaceholder')
|
|
1479
|
+
// Rebuilding the index and scanning the catalog on every render gets
|
|
1480
|
+
// expensive on a long session, so both indexes are memoized above.
|
|
1481
|
+
const byModel = [...(data.byModel ?? [])].sort((left, right) => {
|
|
1482
|
+
const leftKey = left.provider === null ? '' : `${left.provider}\u0000${left.model}`
|
|
1483
|
+
const rightKey = right.provider === null ? '' : `${right.provider}\u0000${right.model}`
|
|
1484
|
+
return (orderIndex.get(leftKey) ?? Number.MAX_SAFE_INTEGER) - (orderIndex.get(rightKey) ?? Number.MAX_SAFE_INTEGER)
|
|
1485
|
+
})
|
|
1486
|
+
|
|
1487
|
+
const modelNameOf = (model) => {
|
|
1488
|
+
const catalogRow = model?.provider === null || model?.provider === undefined
|
|
1489
|
+
? undefined
|
|
1490
|
+
: catalogByKey.get(`${model.provider}\u0000${model.model}`)
|
|
1491
|
+
return catalogRow?.name ?? model?.name ?? model?.model ?? '—'
|
|
1492
|
+
}
|
|
1493
|
+
const turnModelNames = (turn) => {
|
|
1494
|
+
const names = []
|
|
1495
|
+
for (const model of turn.models ?? []) {
|
|
1496
|
+
const name = modelNameOf(model)
|
|
1497
|
+
if (!names.includes(name)) names.push(name)
|
|
1498
|
+
}
|
|
1499
|
+
return names
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const header = h('thead', null,
|
|
1503
|
+
h('tr', null,
|
|
1504
|
+
h('th', null, translate('turn')),
|
|
1505
|
+
h('th', null, translate('content')),
|
|
1506
|
+
h('th', null, translate('time')),
|
|
1507
|
+
h('th', null, translate('model')),
|
|
1508
|
+
h('th', { className: 'num' }, translate('cost')),
|
|
1509
|
+
h('th', { className: 'num' }, translate('tokens')),
|
|
1510
|
+
h('th', { className: 'num' }, translate('cacheHit'))))
|
|
1511
|
+
|
|
1512
|
+
const turnRows = [...(data.turns ?? [])].reverse().slice(0, 20).map((turn) => {
|
|
1513
|
+
const costText = turn.costKnown
|
|
1514
|
+
? `≈ ${formatMoney(turn.cost)}`
|
|
1515
|
+
: turn.pricedAttempts > 0 ? `≈ ${formatMoney(turn.cost)}` : translate('pricePlaceholder')
|
|
1516
|
+
const modelNames = turnModelNames(turn)
|
|
1517
|
+
const modelText = modelNames.length === 0
|
|
1518
|
+
? '—'
|
|
1519
|
+
: modelNames.length === 1 ? modelNames[0] : `${modelNames[0]} +${modelNames.length - 1}`
|
|
1520
|
+
return h('tr', { key: turn.turn },
|
|
1521
|
+
h('td', null, `#${turn.turn}`),
|
|
1522
|
+
h('td', { className: 'mb-turn-preview', title: turn.preview }, turn.preview || '—'),
|
|
1523
|
+
h('td', null, `${timeText(turn.startTime)}–${timeText(turn.endTime)}`),
|
|
1524
|
+
h('td', { className: 'mb-turn-model', title: modelNames.join(' / ') }, modelText),
|
|
1525
|
+
h('td', { className: 'num' }, costText),
|
|
1526
|
+
h('td', { className: 'num' }, `${formatTokens(turn.promptTokens)} / ${formatTokens(turn.outputTokens)}`),
|
|
1527
|
+
h('td', { className: 'num' }, formatPercent(turn.cacheHit)))
|
|
1528
|
+
})
|
|
1529
|
+
|
|
1530
|
+
const turnTable = h('div', { style: { overflowX: 'auto' } },
|
|
1531
|
+
h('table', { className: 'mb-table mb-turn-table' }, header, h('tbody', null, turnRows)))
|
|
1532
|
+
|
|
1533
|
+
const modelRows = byModel.map((model) => {
|
|
1534
|
+
const catalogRow = model.provider === null
|
|
1535
|
+
? undefined
|
|
1536
|
+
: catalogByKey.get(`${model.provider}\u0000${model.model}`)
|
|
1537
|
+
const modelName = catalogRow === undefined ? model.name : `${catalogRow.providerName} / ${catalogRow.name}`
|
|
1538
|
+
return h('tr', { key: `${model.provider}\u0000${model.model}\u0000${model.name}` },
|
|
1539
|
+
h('td', null,
|
|
1540
|
+
modelName,
|
|
1541
|
+
model.isDeepSeek ? h('span', { className: 'mb-tag' }, translate('deepseekTag')) : null),
|
|
1542
|
+
h('td', { className: 'num' },
|
|
1543
|
+
model.pricedAttempts > 0 ? `≈ ${formatMoney(model.cost)}` : translate('pricePlaceholder')),
|
|
1544
|
+
h('td', { className: 'num', style: { color: 'var(--dsw-alias-label-secondary)' } },
|
|
1545
|
+
`${formatTokens(model.promptTokens)} / ${formatTokens(model.outputTokens)}`))
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
const modelTable = byModel.length === 0
|
|
1549
|
+
? null
|
|
1550
|
+
: h('div', { style: { marginTop: '10px' } },
|
|
1551
|
+
h('div', { className: 'mb-panel-title', style: { fontSize: '12px', marginBottom: '4px' } }, translate('byModel')),
|
|
1552
|
+
h('div', { style: { overflowX: 'auto' } },
|
|
1553
|
+
h('table', { className: 'mb-table' }, h('tbody', null, modelRows))))
|
|
1554
|
+
|
|
1555
|
+
const editingRouteRow = editingRoute === null
|
|
1556
|
+
? null
|
|
1557
|
+
: unpricedRoutes.find((route) => `${route.provider}\u0000${route.model}` === editingRoute) ?? null
|
|
1558
|
+
const routeEditor = editingRouteRow === null
|
|
1559
|
+
? null
|
|
1560
|
+
: h('div', { className: 'mb-route-editor' },
|
|
1561
|
+
h('span', { className: 'mb-muted' }, `${editingRouteRow.provider} / ${editingRouteRow.model}`),
|
|
1562
|
+
...PRICE_FIELDS.map((field) => h('input', {
|
|
1563
|
+
key: field,
|
|
1564
|
+
className: 'mb-input',
|
|
1565
|
+
inputMode: 'decimal',
|
|
1566
|
+
placeholder: translate(PRICE_LABEL_KEYS[field]),
|
|
1567
|
+
'aria-label': translate(PRICE_LABEL_KEYS[field]),
|
|
1568
|
+
value: editPrices[field],
|
|
1569
|
+
onChange: (event) => setEditPrices((current) => ({ ...current, [field]: event.target.value })),
|
|
1570
|
+
})),
|
|
1571
|
+
h('button', {
|
|
1572
|
+
type: 'button',
|
|
1573
|
+
className: 'mb-btn mb-btn-primary',
|
|
1574
|
+
disabled: savingPrice,
|
|
1575
|
+
onClick: () => void saveRoutePrice(editingRouteRow),
|
|
1576
|
+
}, savingPrice ? translate('saving') : translate('save')))
|
|
1577
|
+
|
|
1578
|
+
const unpricedTable = unpricedRoutes.length === 0 || display.showUnpricedNotice === false
|
|
1579
|
+
? null
|
|
1580
|
+
: h('div', { style: { marginTop: '10px' } },
|
|
1581
|
+
h('div', { className: 'mb-panel-title', style: { fontSize: '12px', marginBottom: '4px' } }, translate('unpricedRoutes')),
|
|
1582
|
+
h('div', { style: { overflowX: 'auto' } },
|
|
1583
|
+
h('table', { className: 'mb-table' },
|
|
1584
|
+
h('tbody', null, unpricedRoutes.map((route) => h('tr', { key: `${route.provider}\u0000${route.model}` },
|
|
1585
|
+
h('td', null, `${route.provider} / ${route.model}`),
|
|
1586
|
+
h('td', { className: 'num' }, String(route.attempts)),
|
|
1587
|
+
h('td', { className: 'num', style: { color: 'var(--dsw-alias-label-secondary)' } },
|
|
1588
|
+
`${formatTokens(route.promptTokens)} / ${formatTokens(route.outputTokens)}`),
|
|
1589
|
+
h('td', { className: 'num' },
|
|
1590
|
+
h('button', {
|
|
1591
|
+
type: 'button',
|
|
1592
|
+
className: 'mb-btn',
|
|
1593
|
+
onClick: () => beginRouteEdit(route),
|
|
1594
|
+
}, translate('addPrice')))))))),
|
|
1595
|
+
routeEditor,
|
|
1596
|
+
priceStatus === null ? null : h('div', { className: 'mb-status', 'data-tone': priceStatus.tone, role: 'status' }, priceStatus.text))
|
|
1597
|
+
|
|
1598
|
+
const panelStyle = panelPos === null
|
|
1599
|
+
? { left: 0, top: 0, width: 660, visibility: 'hidden' }
|
|
1600
|
+
: {
|
|
1601
|
+
left: panelPos.left,
|
|
1602
|
+
width: panelPos.width,
|
|
1603
|
+
maxHeight: panelPos.maxHeight,
|
|
1604
|
+
...panelPos.top === undefined ? { bottom: panelPos.bottom } : { top: panelPos.top },
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const panel = h('div', {
|
|
1608
|
+
ref: panelRef,
|
|
1609
|
+
className: 'mb-panel mb-panel-floating',
|
|
1610
|
+
style: panelStyle,
|
|
1611
|
+
role: 'dialog',
|
|
1612
|
+
'aria-modal': true,
|
|
1613
|
+
'aria-label': translate('costDetails'),
|
|
1614
|
+
tabIndex: -1,
|
|
1615
|
+
},
|
|
1616
|
+
h('div', { className: 'mb-panel-head' },
|
|
1617
|
+
h('span', { className: 'mb-panel-title' }, translate('costDetails')),
|
|
1618
|
+
h('span', { className: 'mb-muted' }, translate('sessionTotal')),
|
|
1619
|
+
usage.status === 'error'
|
|
1620
|
+
? h('button', {
|
|
1621
|
+
type: 'button',
|
|
1622
|
+
className: 'mb-btn',
|
|
1623
|
+
onClick: () => void ensureUsage(sessionId, revision, true),
|
|
1624
|
+
}, translate('retry'))
|
|
1625
|
+
: null,
|
|
1626
|
+
h('button', {
|
|
1627
|
+
type: 'button',
|
|
1628
|
+
className: 'mb-panel-close',
|
|
1629
|
+
'aria-label': translate('close'),
|
|
1630
|
+
title: translate('close'),
|
|
1631
|
+
onClick: () => {
|
|
1632
|
+
setOpen(false)
|
|
1633
|
+
triggerRef.current?.focus()
|
|
1634
|
+
},
|
|
1635
|
+
}, '×')),
|
|
1636
|
+
h('div', { className: 'mb-summary' },
|
|
1637
|
+
summaryItem(translate('sessionTotal'), totalText),
|
|
1638
|
+
summaryItem(translate('thisTurn'), lastText ?? '—'),
|
|
1639
|
+
summaryItem(translate('cacheHit'), formatPercent(totals.cacheHit)),
|
|
1640
|
+
summaryItem(translate('tokens'), `${formatTokens(totals.promptTokens)} / ${formatTokens(totals.outputTokens)}`)),
|
|
1641
|
+
turnTable,
|
|
1642
|
+
modelTable,
|
|
1643
|
+
unpricedTable,
|
|
1644
|
+
h('p', { className: 'mb-panel-note' }, translate('estimateNote')),
|
|
1645
|
+
totals.toolCost > 0
|
|
1646
|
+
? h('p', { className: 'mb-panel-note' }, translate('webToolNote', {
|
|
1647
|
+
amount: formatMoney(totals.toolCost),
|
|
1648
|
+
searchBilled: totals.webSearchBilled ?? 0,
|
|
1649
|
+
searchCalls: totals.webSearchCalls ?? 0,
|
|
1650
|
+
fetchBilled: totals.webFetchBilled ?? 0,
|
|
1651
|
+
fetchCalls: totals.webFetchCalls ?? 0,
|
|
1652
|
+
}))
|
|
1653
|
+
: null,
|
|
1654
|
+
totals.partial && display.showUnpricedNotice
|
|
1655
|
+
? h('p', { className: 'mb-warn' }, translate('partialNote'))
|
|
1656
|
+
: null)
|
|
1657
|
+
|
|
1658
|
+
return h('div', { className: 'mb-dock' },
|
|
1659
|
+
h(Style),
|
|
1660
|
+
h('button', {
|
|
1661
|
+
ref: triggerRef,
|
|
1662
|
+
type: 'button',
|
|
1663
|
+
className: 'mb-dock-trigger',
|
|
1664
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1665
|
+
'aria-haspopup': 'dialog',
|
|
1666
|
+
onClick: () => setOpen((value) => !value),
|
|
1667
|
+
title: translate('costEstimate'),
|
|
1668
|
+
},
|
|
1669
|
+
h('svg', {
|
|
1670
|
+
className: 'mb-dock-icon',
|
|
1671
|
+
viewBox: '0 0 16 16',
|
|
1672
|
+
fill: 'none',
|
|
1673
|
+
stroke: 'currentColor',
|
|
1674
|
+
strokeWidth: 1.2,
|
|
1675
|
+
'aria-hidden': true,
|
|
1676
|
+
},
|
|
1677
|
+
h('circle', { cx: 8, cy: 8, r: 5.5 }),
|
|
1678
|
+
h('path', { d: 'M8 5v6M6.2 6.8h3.6M6.2 9.2h3.6' })),
|
|
1679
|
+
h('span', null, translate('costEstimate')),
|
|
1680
|
+
h('span', { className: 'mb-dock-amount' }, totalText),
|
|
1681
|
+
lastText === null ? null : h(Fragment, null,
|
|
1682
|
+
h('span', { className: 'mb-dock-sep' }, '·'),
|
|
1683
|
+
h('span', null, `${translate('thisTurn')} ${lastText}`)),
|
|
1684
|
+
totals.partial && display.showUnpricedNotice ? h('span', { className: 'mb-warn', style: { margin: 0 } }, translate('partial')) : null),
|
|
1685
|
+
open ? ReactDOM.createPortal(panel, document.body) : null)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function summaryItem(label, value) {
|
|
1689
|
+
return h('div', { className: 'mb-summary-item', key: label },
|
|
1690
|
+
h('div', { className: 'mb-summary-key' }, label),
|
|
1691
|
+
h('div', { className: 'mb-summary-value' }, value))
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// ---------------------------------------------------------------------
|
|
1695
|
+
// Per-turn cost chip
|
|
1696
|
+
// ---------------------------------------------------------------------
|
|
1697
|
+
|
|
1698
|
+
function turnModelText(models, translate, showProvider, catalogModels) {
|
|
1699
|
+
const findModel = (entry) => {
|
|
1700
|
+
if (entry.provider === undefined || entry.model === undefined) return undefined
|
|
1701
|
+
return (catalogModels ?? []).find((candidate) => candidate.provider === entry.provider && candidate.model === entry.model)
|
|
1702
|
+
}
|
|
1703
|
+
const names = (models ?? []).map((entry) => {
|
|
1704
|
+
if (entry === undefined || entry === null) return null
|
|
1705
|
+
const matched = findModel(entry)
|
|
1706
|
+
const modelName = typeof matched?.name === 'string' && matched.name !== ''
|
|
1707
|
+
? matched.name
|
|
1708
|
+
: typeof entry.name === 'string' && entry.name !== ''
|
|
1709
|
+
? entry.name
|
|
1710
|
+
: typeof entry.model === 'string' && entry.model !== '' ? entry.model : null
|
|
1711
|
+
if (modelName === null) return null
|
|
1712
|
+
if (!showProvider) return modelName
|
|
1713
|
+
const providerName = typeof matched?.providerName === 'string' && matched.providerName !== ''
|
|
1714
|
+
? matched.providerName
|
|
1715
|
+
: typeof entry.providerName === 'string' && entry.providerName !== ''
|
|
1716
|
+
? entry.providerName
|
|
1717
|
+
: typeof entry.provider === 'string' && entry.provider !== '' ? entry.provider : null
|
|
1718
|
+
return providerName === null ? modelName : `${providerName} / ${modelName}`
|
|
1719
|
+
}).filter((name) => name !== null)
|
|
1720
|
+
if (names.length === 0) return null
|
|
1721
|
+
return names.join(translate('listSep'))
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function TurnCost(props) {
|
|
1725
|
+
const { turn } = props
|
|
1726
|
+
// The turnTail owner hands over the whole folded turn life cycle, not just its number.
|
|
1727
|
+
const turnNumber = turn === null || turn === undefined
|
|
1728
|
+
? undefined
|
|
1729
|
+
: typeof turn === 'number' ? turn : turn.turn
|
|
1730
|
+
const sessionId = props.sessionId
|
|
1731
|
+
const useProjection = props.useProjection
|
|
1732
|
+
const translate = useT()
|
|
1733
|
+
const catalogSnap = useCatalog()
|
|
1734
|
+
const display = readDisplay(catalogSnap.data)
|
|
1735
|
+
const tokenUsage = typeof useProjection === 'function' ? useProjection('tokenUsage') : undefined
|
|
1736
|
+
const revision = tokenUsageRevision(tokenUsage)
|
|
1737
|
+
const usage = useSessionUsage(sessionId, revision)
|
|
1738
|
+
// The master switch hides the whole chip while the per-field switches keep
|
|
1739
|
+
// their saved values, so turning it back on restores the previous layout.
|
|
1740
|
+
if (display.showTurnTail === false) return null
|
|
1741
|
+
const rows = usage.data?.turns
|
|
1742
|
+
const row = (usage.rowsByTurn instanceof Map ? usage.rowsByTurn.get(turnNumber) : undefined) ??
|
|
1743
|
+
rows?.find((entry) => entry.turn === turnNumber)
|
|
1744
|
+
if (row === undefined) return null
|
|
1745
|
+
const costText = row.costKnown
|
|
1746
|
+
? `≈ ${formatMoney(row.cost)}`
|
|
1747
|
+
: row.pricedAttempts > 0 ? `≈ ${formatMoney(row.cost)}` : translate('pricePlaceholder')
|
|
1748
|
+
const modelText = turnModelText(row.models, translate, display.showProvider === true, catalogSnap.data?.models)
|
|
1749
|
+
const label = (key) => display.showLabels ? `${translate(key)} ` : ''
|
|
1750
|
+
const parts = []
|
|
1751
|
+
if (display.showModels && modelText !== null) {
|
|
1752
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'model' },
|
|
1753
|
+
label('model'), h('strong', null, modelText)))
|
|
1754
|
+
}
|
|
1755
|
+
if (display.showCost) {
|
|
1756
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'cost' },
|
|
1757
|
+
label('turnCost'), h('strong', null, costText)))
|
|
1758
|
+
}
|
|
1759
|
+
const promptTokens = Number.isFinite(row.promptTokens) ? row.promptTokens : 0
|
|
1760
|
+
const outputTokens = Number.isFinite(row.outputTokens) ? row.outputTokens : 0
|
|
1761
|
+
if (display.showTokens && (promptTokens > 0 || outputTokens > 0)) {
|
|
1762
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'tokens' },
|
|
1763
|
+
label('turnTokens'), h('strong', null, `${formatTokens(promptTokens)} / ${formatTokens(outputTokens)}`)))
|
|
1764
|
+
}
|
|
1765
|
+
if (display.showCacheHit && row.cacheHit !== null && row.cacheHit !== undefined) {
|
|
1766
|
+
parts.push(h('span', { className: 'mb-turn-item', key: 'cache' },
|
|
1767
|
+
label('cacheHit'), h('strong', null, formatPercent(row.cacheHit))))
|
|
1768
|
+
}
|
|
1769
|
+
if (row.partial) {
|
|
1770
|
+
parts.push(h('span', { className: 'mb-turn-item mb-turn-partial', key: 'partial' }, translate('partialShort')))
|
|
1771
|
+
}
|
|
1772
|
+
if (parts.length === 0) return null
|
|
1773
|
+
return h('span', {
|
|
1774
|
+
className: 'mb-turn',
|
|
1775
|
+
'data-size': display.fontSize,
|
|
1776
|
+
'data-density': display.density,
|
|
1777
|
+
'data-weight': display.fontWeight,
|
|
1778
|
+
}, parts)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// ---------------------------------------------------------------------
|
|
1782
|
+
// Custom-ordered model picker in the composer seat
|
|
1783
|
+
// ---------------------------------------------------------------------
|
|
1784
|
+
|
|
1785
|
+
const MODEL_KEY_SEPARATOR = '\u0000'
|
|
1786
|
+
|
|
1787
|
+
function OrderedModelPicker({ locked, sessionId, useProjection }) {
|
|
1788
|
+
const translate = useT()
|
|
1789
|
+
const catalogSnap = useCatalog()
|
|
1790
|
+
const selectionProjection = useProjection('modelSelection')
|
|
1791
|
+
const directoryRevision = useModelDirectoriesRevision()
|
|
1792
|
+
const [directoryTick, setDirectoryTick] = useState(0)
|
|
1793
|
+
const directory = useMemo(() => {
|
|
1794
|
+
if (modelDirectories === undefined || sessionId === undefined) return null
|
|
1795
|
+
try {
|
|
1796
|
+
return modelDirectories.directoryFor(sessionId)
|
|
1797
|
+
} catch {
|
|
1798
|
+
return null
|
|
1799
|
+
}
|
|
1800
|
+
}, [sessionId, directoryRevision, directoryTick])
|
|
1801
|
+
|
|
1802
|
+
const [state, setState] = useState(null)
|
|
1803
|
+
const [open, setOpen] = useState(false)
|
|
1804
|
+
const [query, setQuery] = useState('')
|
|
1805
|
+
const [selecting, setSelecting] = useState(false)
|
|
1806
|
+
// The route being submitted, so its row can show a pending spinner, and a
|
|
1807
|
+
// transient rejection message that must not push the list around.
|
|
1808
|
+
const [pending, setPending] = useState(null)
|
|
1809
|
+
const [toast, setToast] = useState(null)
|
|
1810
|
+
const [menuPos, setMenuPos] = useState(null)
|
|
1811
|
+
// Roving cursor for arrow-key navigation inside the model list.
|
|
1812
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
1813
|
+
const rootRef = useRef(null)
|
|
1814
|
+
const triggerRef = useRef(null)
|
|
1815
|
+
const menuRef = useRef(null)
|
|
1816
|
+
const searchRef = useRef(null)
|
|
1817
|
+
const id = useId()
|
|
1818
|
+
|
|
1819
|
+
useEffect(() => {
|
|
1820
|
+
if (directory === null) {
|
|
1821
|
+
setState(null)
|
|
1822
|
+
return undefined
|
|
1823
|
+
}
|
|
1824
|
+
const sync = () => setState(directory.store.getSnapshot())
|
|
1825
|
+
sync()
|
|
1826
|
+
const unsubscribe = directory.store.subscribe(sync)
|
|
1827
|
+
directory.load().catch(() => {})
|
|
1828
|
+
return unsubscribe
|
|
1829
|
+
}, [directory])
|
|
1830
|
+
|
|
1831
|
+
useEffect(() => {
|
|
1832
|
+
if (directory !== null || directoryTick >= 20) return undefined
|
|
1833
|
+
const timer = setTimeout(() => setDirectoryTick((value) => value + 1), 300)
|
|
1834
|
+
return () => clearTimeout(timer)
|
|
1835
|
+
}, [directory, directoryTick])
|
|
1836
|
+
|
|
1837
|
+
useEffect(() => {
|
|
1838
|
+
if (!open) return undefined
|
|
1839
|
+
const closeOutside = (event) => {
|
|
1840
|
+
if (rootRef.current?.contains(event.target) === true) return
|
|
1841
|
+
if (menuRef.current?.contains(event.target) === true) return
|
|
1842
|
+
setOpen(false)
|
|
1843
|
+
}
|
|
1844
|
+
document.addEventListener('mousedown', closeOutside)
|
|
1845
|
+
return () => document.removeEventListener('mousedown', closeOutside)
|
|
1846
|
+
}, [open])
|
|
1847
|
+
|
|
1848
|
+
useEffect(() => {
|
|
1849
|
+
if (!open) {
|
|
1850
|
+
setQuery('')
|
|
1851
|
+
return undefined
|
|
1852
|
+
}
|
|
1853
|
+
const closeOnEscape = (event) => {
|
|
1854
|
+
if (event.key !== 'Escape') return
|
|
1855
|
+
event.preventDefault()
|
|
1856
|
+
setOpen(false)
|
|
1857
|
+
queueMicrotask(() => triggerRef.current?.focus())
|
|
1858
|
+
}
|
|
1859
|
+
document.addEventListener('keydown', closeOnEscape)
|
|
1860
|
+
return () => document.removeEventListener('keydown', closeOnEscape)
|
|
1861
|
+
}, [open])
|
|
1862
|
+
|
|
1863
|
+
useEffect(() => {
|
|
1864
|
+
if (locked && open) setOpen(false)
|
|
1865
|
+
}, [locked, open])
|
|
1866
|
+
|
|
1867
|
+
// A rejection toast clears itself; the menu is not blocked while it shows.
|
|
1868
|
+
useEffect(() => {
|
|
1869
|
+
if (toast === null) return undefined
|
|
1870
|
+
const timer = setTimeout(() => setToast(null), 4200)
|
|
1871
|
+
return () => clearTimeout(timer)
|
|
1872
|
+
}, [toast])
|
|
1873
|
+
|
|
1874
|
+
const rank = useMemo(() => {
|
|
1875
|
+
const map = new Map()
|
|
1876
|
+
for (const model of catalogSnap.data?.models ?? []) {
|
|
1877
|
+
map.set(`${model.provider}${MODEL_KEY_SEPARATOR}${model.model}`, model.order)
|
|
1878
|
+
}
|
|
1879
|
+
return map
|
|
1880
|
+
}, [catalogSnap.data])
|
|
1881
|
+
|
|
1882
|
+
const priceByKey = useMemo(() => {
|
|
1883
|
+
const map = new Map()
|
|
1884
|
+
for (const model of catalogSnap.data?.models ?? []) {
|
|
1885
|
+
map.set(`${model.provider}${MODEL_KEY_SEPARATOR}${model.model}`, model.pricing)
|
|
1886
|
+
}
|
|
1887
|
+
return map
|
|
1888
|
+
}, [catalogSnap.data])
|
|
1889
|
+
|
|
1890
|
+
const groups = state?.groups
|
|
1891
|
+
const catalogModels = catalogSnap.data?.models
|
|
1892
|
+
const entries = useMemo(() => {
|
|
1893
|
+
if (Array.isArray(groups) && groups.length > 0) {
|
|
1894
|
+
const list = []
|
|
1895
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
1896
|
+
for (const [modelIndex, model] of group.models.entries()) {
|
|
1897
|
+
list.push({ group, model, groupIndex, modelIndex })
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
list.sort((left, right) => {
|
|
1901
|
+
const leftRank = rank.get(`${left.group.id}${MODEL_KEY_SEPARATOR}${left.model.id}`) ?? Number.MAX_SAFE_INTEGER
|
|
1902
|
+
const rightRank = rank.get(`${right.group.id}${MODEL_KEY_SEPARATOR}${right.model.id}`) ?? Number.MAX_SAFE_INTEGER
|
|
1903
|
+
return leftRank - rightRank || left.groupIndex - right.groupIndex || left.modelIndex - right.modelIndex
|
|
1904
|
+
})
|
|
1905
|
+
return list
|
|
1906
|
+
}
|
|
1907
|
+
return (catalogModels ?? []).map((model, index) => ({
|
|
1908
|
+
group: { id: model.provider, name: model.providerName },
|
|
1909
|
+
model: {
|
|
1910
|
+
id: model.model,
|
|
1911
|
+
name: model.name,
|
|
1912
|
+
...(model.reasoning === undefined ? {} : { reasoning: model.reasoning }),
|
|
1913
|
+
},
|
|
1914
|
+
groupIndex: model.order ?? index,
|
|
1915
|
+
modelIndex: index,
|
|
1916
|
+
}))
|
|
1917
|
+
}, [groups, catalogModels, rank])
|
|
1918
|
+
|
|
1919
|
+
const projected = selectionProjection?.next ?? selectionProjection?.lastUsed ?? null
|
|
1920
|
+
const current = state?.current ?? projected
|
|
1921
|
+
const currentEntry = current === null
|
|
1922
|
+
? undefined
|
|
1923
|
+
: entries.find((entry) => entry.group.id === current.provider && entry.model.id === current.model)
|
|
1924
|
+
const currentCatalog = current === null
|
|
1925
|
+
? undefined
|
|
1926
|
+
: (catalogModels ?? []).find((model) => model.provider === current.provider && model.model === current.model)
|
|
1927
|
+
const currentReasoning = currentEntry?.model.reasoning
|
|
1928
|
+
const effectiveEffort = current?.reasoningEffort ?? currentReasoning?.defaultEffort
|
|
1929
|
+
const effortLabel = current === null
|
|
1930
|
+
? undefined
|
|
1931
|
+
: currentReasoning === undefined
|
|
1932
|
+
? state?.retainedEffort
|
|
1933
|
+
: effectiveEffort === undefined
|
|
1934
|
+
? translate('providerDefault')
|
|
1935
|
+
: currentReasoning.efforts.find((effort) => effort.id === effectiveEffort)?.name ?? effectiveEffort
|
|
1936
|
+
const effortChoices = currentReasoning === undefined
|
|
1937
|
+
? []
|
|
1938
|
+
: [
|
|
1939
|
+
...(currentReasoning.defaultEffort === undefined
|
|
1940
|
+
? [{ key: 'provider-default', effort: undefined, label: translate('providerDefault') }]
|
|
1941
|
+
: []),
|
|
1942
|
+
...currentReasoning.efforts.map((effort) => ({
|
|
1943
|
+
key: `effort:${effort.id}`,
|
|
1944
|
+
effort: effort.id,
|
|
1945
|
+
label: effort.name,
|
|
1946
|
+
})),
|
|
1947
|
+
]
|
|
1948
|
+
|
|
1949
|
+
const showSearch = entries.length > 4
|
|
1950
|
+
const term = query.trim().toLowerCase()
|
|
1951
|
+
const filtered = useMemo(() => {
|
|
1952
|
+
if (term === '') return entries
|
|
1953
|
+
return entries.filter((entry) => `${entry.model.name} ${entry.group.name}`.toLowerCase().includes(term))
|
|
1954
|
+
}, [entries, term])
|
|
1955
|
+
// Keep the roving cursor inside the visible list; a shrunk search result
|
|
1956
|
+
// must not leave the index pointing past the end.
|
|
1957
|
+
const active = filtered.length === 0 ? -1 : Math.min(Math.max(activeIndex, 0), filtered.length - 1)
|
|
1958
|
+
|
|
1959
|
+
useLayoutEffect(() => {
|
|
1960
|
+
if (!open) {
|
|
1961
|
+
setMenuPos(null)
|
|
1962
|
+
return undefined
|
|
1963
|
+
}
|
|
1964
|
+
const place = () => {
|
|
1965
|
+
const rect = triggerRef.current?.getBoundingClientRect()
|
|
1966
|
+
if (rect === undefined) return
|
|
1967
|
+
const width = Math.min(360, window.innerWidth - 24)
|
|
1968
|
+
const left = Math.max(12, Math.min(rect.left, window.innerWidth - width - 12))
|
|
1969
|
+
const spaceAbove = rect.top - 12
|
|
1970
|
+
const spaceBelow = window.innerHeight - rect.bottom - 12
|
|
1971
|
+
const openBelow = spaceBelow > spaceAbove
|
|
1972
|
+
const maxHeight = Math.min(480, Math.max(160, openBelow ? spaceBelow : spaceAbove))
|
|
1973
|
+
setMenuPos(openBelow
|
|
1974
|
+
? { left, top: rect.bottom + 8, width, maxHeight }
|
|
1975
|
+
: { left, bottom: window.innerHeight - rect.top + 8, width, maxHeight })
|
|
1976
|
+
}
|
|
1977
|
+
place()
|
|
1978
|
+
window.addEventListener('scroll', place, true)
|
|
1979
|
+
window.addEventListener('resize', place)
|
|
1980
|
+
return () => {
|
|
1981
|
+
window.removeEventListener('scroll', place, true)
|
|
1982
|
+
window.removeEventListener('resize', place)
|
|
1983
|
+
}
|
|
1984
|
+
}, [open, filtered.length, state?.status])
|
|
1985
|
+
|
|
1986
|
+
useEffect(() => {
|
|
1987
|
+
if (!open) return undefined
|
|
1988
|
+
const timer = setTimeout(() => {
|
|
1989
|
+
if (showSearch) {
|
|
1990
|
+
searchRef.current?.focus()
|
|
1991
|
+
return
|
|
1992
|
+
}
|
|
1993
|
+
const rows = menuRef.current?.querySelectorAll('.mb-picker-row')
|
|
1994
|
+
const target = rows?.[active]
|
|
1995
|
+
if (target instanceof HTMLElement) target.focus()
|
|
1996
|
+
}, 0)
|
|
1997
|
+
return () => clearTimeout(timer)
|
|
1998
|
+
// Focus follows the roving cursor so arrow keys keep focus on the active row.
|
|
1999
|
+
}, [open, showSearch, active])
|
|
2000
|
+
|
|
2001
|
+
const loading = catalogSnap.data === null && (state === null || state.status === 'loading')
|
|
2002
|
+
const busy = selecting || state?.status === 'selecting'
|
|
2003
|
+
const currentName = currentEntry?.model.name ?? currentCatalog?.name
|
|
2004
|
+
const triggerModel = currentName ?? (loading ? translate('loadingModels') : translate('chooseModel'))
|
|
2005
|
+
const triggerAria = effortLabel === undefined ? triggerModel : `${triggerModel} · ${effortLabel}`
|
|
2006
|
+
|
|
2007
|
+
const close = (restoreFocus) => {
|
|
2008
|
+
setOpen(false)
|
|
2009
|
+
setQuery('')
|
|
2010
|
+
setToast(null)
|
|
2011
|
+
if (restoreFocus) queueMicrotask(() => triggerRef.current?.focus())
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const submit = async (selection) => {
|
|
2015
|
+
if (busy) return
|
|
2016
|
+
setSelecting(true)
|
|
2017
|
+
setToast(null)
|
|
2018
|
+
setPending(selection)
|
|
2019
|
+
try {
|
|
2020
|
+
const result = directory !== null
|
|
2021
|
+
? await directory.select(selection)
|
|
2022
|
+
: await requestJson('/model-billing/select', {
|
|
2023
|
+
method: 'POST',
|
|
2024
|
+
headers: { 'content-type': 'application/json' },
|
|
2025
|
+
body: JSON.stringify({ sessionId, ...selection }),
|
|
2026
|
+
})
|
|
2027
|
+
if (result?.ok === false) {
|
|
2028
|
+
// A refusal is a toast: the list stays where the user left it.
|
|
2029
|
+
setToast(result.error?.message ?? translate('pickerEmpty'))
|
|
2030
|
+
return
|
|
2031
|
+
}
|
|
2032
|
+
close(true)
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
setToast(error instanceof Error ? error.message : String(error))
|
|
2035
|
+
} finally {
|
|
2036
|
+
setSelecting(false)
|
|
2037
|
+
setPending(null)
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const chooseModel = (entry) => {
|
|
2042
|
+
if (current?.provider === entry.group.id && current?.model === entry.model.id) {
|
|
2043
|
+
close(true)
|
|
2044
|
+
return
|
|
2045
|
+
}
|
|
2046
|
+
void submit({ provider: entry.group.id, model: entry.model.id })
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
const chooseEffort = (effort) => {
|
|
2050
|
+
if (current === null) return
|
|
2051
|
+
if (effectiveEffort === effort) {
|
|
2052
|
+
close(true)
|
|
2053
|
+
return
|
|
2054
|
+
}
|
|
2055
|
+
void submit({
|
|
2056
|
+
provider: current.provider,
|
|
2057
|
+
model: current.model,
|
|
2058
|
+
...effort === undefined ? {} : { reasoningEffort: effort },
|
|
2059
|
+
})
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
const toggle = () => {
|
|
2063
|
+
if (open) {
|
|
2064
|
+
close(true)
|
|
2065
|
+
return
|
|
2066
|
+
}
|
|
2067
|
+
setToast(null)
|
|
2068
|
+
// Open the cursor on the row the user is already using, when there is one.
|
|
2069
|
+
const selectedAt = filtered.findIndex((entry) => entry.group.id === current?.provider && entry.model.id === current?.model)
|
|
2070
|
+
setActiveIndex(selectedAt < 0 ? 0 : selectedAt)
|
|
2071
|
+
setOpen(true)
|
|
2072
|
+
if (directory !== null) directory.load().catch(() => {})
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const menuStyle = menuPos === null
|
|
2076
|
+
? { left: 0, top: 0, width: 360, visibility: 'hidden' }
|
|
2077
|
+
: {
|
|
2078
|
+
left: menuPos.left,
|
|
2079
|
+
width: menuPos.width,
|
|
2080
|
+
maxHeight: menuPos.maxHeight,
|
|
2081
|
+
...menuPos.top === undefined ? { bottom: menuPos.bottom } : { top: menuPos.top },
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Arrow keys move a roving cursor through the list. Enter and Space are
|
|
2085
|
+
// left to the focused row itself, so a <button> keeps its native
|
|
2086
|
+
// activation and the menu never fires the choice twice.
|
|
2087
|
+
const focusRow = (index) => {
|
|
2088
|
+
const rows = menuRef.current?.querySelectorAll('.mb-picker-row')
|
|
2089
|
+
const target = rows?.[index]
|
|
2090
|
+
if (target instanceof HTMLElement) target.focus()
|
|
2091
|
+
}
|
|
2092
|
+
const moveActive = (delta) => {
|
|
2093
|
+
if (filtered.length === 0) return
|
|
2094
|
+
const next = Math.min(Math.max(active + delta, 0), filtered.length - 1)
|
|
2095
|
+
setActiveIndex(next)
|
|
2096
|
+
focusRow(next)
|
|
2097
|
+
}
|
|
2098
|
+
const onMenuKeyDown = (event) => {
|
|
2099
|
+
if (event.key === 'ArrowDown') {
|
|
2100
|
+
event.preventDefault()
|
|
2101
|
+
moveActive(1)
|
|
2102
|
+
return
|
|
2103
|
+
}
|
|
2104
|
+
if (event.key === 'ArrowUp') {
|
|
2105
|
+
event.preventDefault()
|
|
2106
|
+
moveActive(-1)
|
|
2107
|
+
return
|
|
2108
|
+
}
|
|
2109
|
+
if (event.key === 'Home') {
|
|
2110
|
+
event.preventDefault()
|
|
2111
|
+
if (filtered.length === 0) return
|
|
2112
|
+
setActiveIndex(0)
|
|
2113
|
+
focusRow(0)
|
|
2114
|
+
return
|
|
2115
|
+
}
|
|
2116
|
+
if (event.key === 'End') {
|
|
2117
|
+
event.preventDefault()
|
|
2118
|
+
const last = filtered.length - 1
|
|
2119
|
+
if (last < 0) return
|
|
2120
|
+
setActiveIndex(last)
|
|
2121
|
+
focusRow(last)
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const menu = h('div', {
|
|
2126
|
+
ref: menuRef,
|
|
2127
|
+
id: `${id}-menu`,
|
|
2128
|
+
className: 'mb-picker-menu',
|
|
2129
|
+
style: menuStyle,
|
|
2130
|
+
role: 'dialog',
|
|
2131
|
+
'aria-label': translate('pickerAria'),
|
|
2132
|
+
onKeyDown: onMenuKeyDown,
|
|
2133
|
+
},
|
|
2134
|
+
showSearch ? h('input', {
|
|
2135
|
+
ref: searchRef,
|
|
2136
|
+
className: 'mb-picker-search',
|
|
2137
|
+
type: 'search',
|
|
2138
|
+
value: query,
|
|
2139
|
+
placeholder: translate('searchModel'),
|
|
2140
|
+
'aria-label': translate('searchModel'),
|
|
2141
|
+
onChange: (event) => {
|
|
2142
|
+
setQuery(event.target.value)
|
|
2143
|
+
// A shorter result list must not leave the cursor on a removed row.
|
|
2144
|
+
setActiveIndex(0)
|
|
2145
|
+
},
|
|
2146
|
+
onKeyDown: (event) => {
|
|
2147
|
+
// Enter picks the highlighted row; a downward arrow steps into the list.
|
|
2148
|
+
if (event.key === 'ArrowDown') {
|
|
2149
|
+
event.preventDefault()
|
|
2150
|
+
moveActive(1)
|
|
2151
|
+
return
|
|
2152
|
+
}
|
|
2153
|
+
if (event.key !== 'Enter' || filtered.length === 0 || busy) return
|
|
2154
|
+
event.preventDefault()
|
|
2155
|
+
chooseModel(filtered[active])
|
|
2156
|
+
},
|
|
2157
|
+
}) : null,
|
|
2158
|
+
h('div', { className: 'mb-picker-list', role: 'menu' },
|
|
2159
|
+
filtered.length === 0
|
|
2160
|
+
? h('div', { className: 'mb-picker-empty' }, term === '' ? translate('pickerEmpty') : translate('noMatch'))
|
|
2161
|
+
: filtered.map((entry, index) => {
|
|
2162
|
+
const selected = current?.provider === entry.group.id && current?.model === entry.model.id
|
|
2163
|
+
const previous = filtered[index - 1]
|
|
2164
|
+
const showGroup = previous === undefined || previous.group.id !== entry.group.id
|
|
2165
|
+
const price = priceByKey.get(`${entry.group.id}${MODEL_KEY_SEPARATOR}${entry.model.id}`)
|
|
2166
|
+
return h(Fragment, { key: `${entry.group.id}${MODEL_KEY_SEPARATOR}${entry.model.id}` },
|
|
2167
|
+
showGroup ? h('div', { className: 'mb-picker-group' }, entry.group.name) : null,
|
|
2168
|
+
h('button', {
|
|
2169
|
+
type: 'button',
|
|
2170
|
+
role: 'menuitemradio',
|
|
2171
|
+
className: 'mb-picker-row',
|
|
2172
|
+
'aria-checked': selected ? 'true' : 'false',
|
|
2173
|
+
title: entry.model.name,
|
|
2174
|
+
// Roving tabindex: only the cursor row is reachable by Tab.
|
|
2175
|
+
tabIndex: index === active ? 0 : -1,
|
|
2176
|
+
disabled: busy,
|
|
2177
|
+
onClick: () => chooseModel(entry),
|
|
2178
|
+
},
|
|
2179
|
+
h('span', { className: 'mb-picker-option-copy' },
|
|
2180
|
+
h('span', { className: 'mb-picker-model' }, entry.model.name),
|
|
2181
|
+
h(PriceLines, { pricing: price, translate })),
|
|
2182
|
+
h('span', { className: 'mb-picker-check' },
|
|
2183
|
+
pending !== null && pending.provider === entry.group.id && pending.model === entry.model.id
|
|
2184
|
+
? h('span', { className: 'mb-picker-dot', 'aria-hidden': true })
|
|
2185
|
+
: selected
|
|
2186
|
+
? h('svg', {
|
|
2187
|
+
viewBox: '0 0 16 16',
|
|
2188
|
+
width: 14,
|
|
2189
|
+
height: 14,
|
|
2190
|
+
fill: 'none',
|
|
2191
|
+
stroke: 'currentColor',
|
|
2192
|
+
strokeWidth: 1.7,
|
|
2193
|
+
'aria-hidden': true,
|
|
2194
|
+
},
|
|
2195
|
+
h('path', { d: 'M3.5 8.5l3 3 6-7' }))
|
|
2196
|
+
: null)))
|
|
2197
|
+
})),
|
|
2198
|
+
effortChoices.length === 0 ? null : h(Fragment, null,
|
|
2199
|
+
h('div', { className: 'mb-picker-divider' }),
|
|
2200
|
+
h('div', { className: 'mb-picker-efforts' },
|
|
2201
|
+
h('span', { className: 'mb-picker-efforts-label' }, translate('reasoningEffort')),
|
|
2202
|
+
effortChoices.map((choice) => h('button', {
|
|
2203
|
+
type: 'button',
|
|
2204
|
+
key: choice.key,
|
|
2205
|
+
className: 'mb-picker-chip',
|
|
2206
|
+
'aria-pressed': effectiveEffort === choice.effort ? 'true' : 'false',
|
|
2207
|
+
disabled: busy,
|
|
2208
|
+
onClick: () => chooseEffort(choice.effort),
|
|
2209
|
+
}, choice.label)))),
|
|
2210
|
+
busy ? h('div', { className: 'mb-picker-status' }, translate('switching')) : null,
|
|
2211
|
+
toast === null ? null : h('div', { className: 'mb-picker-toast', role: 'status', 'aria-live': 'polite' }, toast),
|
|
2212
|
+
state?.status === 'error'
|
|
2213
|
+
? h('div', { className: 'mb-picker-error' }, state.error)
|
|
2214
|
+
: null,
|
|
2215
|
+
state?.status === 'error'
|
|
2216
|
+
? h('button', {
|
|
2217
|
+
type: 'button',
|
|
2218
|
+
className: 'mb-btn mb-picker-retry',
|
|
2219
|
+
onClick: () => {
|
|
2220
|
+
directory.load().catch(() => {})
|
|
2221
|
+
},
|
|
2222
|
+
}, translate('pickerRetry'))
|
|
2223
|
+
: null)
|
|
2224
|
+
|
|
2225
|
+
return h('div', {
|
|
2226
|
+
className: 'mb-picker',
|
|
2227
|
+
ref: rootRef,
|
|
2228
|
+
},
|
|
2229
|
+
h(Style),
|
|
2230
|
+
h('button', {
|
|
2231
|
+
ref: triggerRef,
|
|
2232
|
+
type: 'button',
|
|
2233
|
+
className: 'mb-picker-trigger',
|
|
2234
|
+
disabled: locked,
|
|
2235
|
+
'aria-label': triggerAria,
|
|
2236
|
+
'aria-haspopup': 'menu',
|
|
2237
|
+
'aria-controls': open ? `${id}-menu` : undefined,
|
|
2238
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
2239
|
+
'aria-busy': busy ? 'true' : undefined,
|
|
2240
|
+
title: triggerAria,
|
|
2241
|
+
onClick: toggle,
|
|
2242
|
+
},
|
|
2243
|
+
h('svg', {
|
|
2244
|
+
viewBox: '0 0 16 16',
|
|
2245
|
+
width: 14,
|
|
2246
|
+
height: 14,
|
|
2247
|
+
fill: 'none',
|
|
2248
|
+
stroke: 'currentColor',
|
|
2249
|
+
strokeWidth: 1.3,
|
|
2250
|
+
'aria-hidden': true,
|
|
2251
|
+
className: 'mb-picker-icon',
|
|
2252
|
+
},
|
|
2253
|
+
h('rect', { x: 2, y: 3, width: 12, height: 10, rx: 2 }),
|
|
2254
|
+
h('path', { d: 'M2 6.5h12M6 6.5V13' })),
|
|
2255
|
+
h('span', { className: 'mb-picker-name' }, triggerModel),
|
|
2256
|
+
effortLabel === undefined ? null : h('span', { className: 'mb-picker-trigger-effort' }, effortLabel),
|
|
2257
|
+
h('svg', {
|
|
2258
|
+
className: open ? 'mb-picker-chevron mb-picker-chevron-open' : 'mb-picker-chevron',
|
|
2259
|
+
viewBox: '0 0 16 16',
|
|
2260
|
+
width: 14,
|
|
2261
|
+
height: 14,
|
|
2262
|
+
fill: 'none',
|
|
2263
|
+
stroke: 'currentColor',
|
|
2264
|
+
strokeWidth: 1.5,
|
|
2265
|
+
'aria-hidden': true,
|
|
2266
|
+
},
|
|
2267
|
+
h('path', { d: 'M4 6l4 4 4-4' }))),
|
|
2268
|
+
open ? ReactDOM.createPortal(menu, document.body) : null)
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function ModelPickerSeat(props) {
|
|
2272
|
+
const catalogSnap = useCatalog()
|
|
2273
|
+
// Take over only while we hold a usable catalog. On a fetch error or an
|
|
2274
|
+
// in-flight load, return null so the built-in picker keeps working
|
|
2275
|
+
// instead of leaving the user with no way to change models.
|
|
2276
|
+
if (catalogSnap.status !== 'ready') return null
|
|
2277
|
+
const display = readDisplay(catalogSnap.data)
|
|
2278
|
+
if (!display.takeoverPicker) return null
|
|
2279
|
+
return h(OrderedModelPicker, props)
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
return {
|
|
2283
|
+
inject: ['slots', 'locale'],
|
|
2284
|
+
apply(ctx) {
|
|
2285
|
+
localeService = ctx.get('locale')
|
|
2286
|
+
publishModelDirectories(ctx.get('modelDirectories'))
|
|
2287
|
+
ctx.inject(['modelDirectories'], (scope) => {
|
|
2288
|
+
publishModelDirectories(scope.modelDirectories)
|
|
2289
|
+
let seat = null
|
|
2290
|
+
const takeoverEnabled = () => catalog.snap.status === 'ready' && readDisplay(catalog.snap.data).takeoverPicker !== false
|
|
2291
|
+
const syncSeat = () => {
|
|
2292
|
+
if (takeoverEnabled()) {
|
|
2293
|
+
if (seat === null) {
|
|
2294
|
+
seat = scope.slots.register({
|
|
2295
|
+
name: 'conversation.input.model',
|
|
2296
|
+
priority: -1,
|
|
2297
|
+
}, ModelPickerSeat)
|
|
2298
|
+
}
|
|
2299
|
+
return
|
|
2300
|
+
}
|
|
2301
|
+
if (seat !== null) {
|
|
2302
|
+
seat()
|
|
2303
|
+
seat = null
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
scope.slots.inject('conversation.input.model', () => {
|
|
2307
|
+
const unsubscribe = subscribeCatalog(syncSeat)
|
|
2308
|
+
syncSeat()
|
|
2309
|
+
return () => {
|
|
2310
|
+
unsubscribe()
|
|
2311
|
+
if (seat !== null) {
|
|
2312
|
+
seat()
|
|
2313
|
+
seat = null
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
})
|
|
2317
|
+
})
|
|
2318
|
+
|
|
2319
|
+
void ensureCatalog()
|
|
2320
|
+
|
|
2321
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
2322
|
+
name: 'settings.section',
|
|
2323
|
+
id: 'model-billing',
|
|
2324
|
+
// Native sections run -10..20 (account, general, models, plugins,
|
|
2325
|
+
// agent presets); third-party pages start at 25 so they sit below
|
|
2326
|
+
// the native group instead of splitting it.
|
|
2327
|
+
order: 25,
|
|
2328
|
+
label: () => t('nav'),
|
|
2329
|
+
}, SettingsSection))
|
|
2330
|
+
|
|
2331
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
2332
|
+
name: 'conversation.composer.dock',
|
|
2333
|
+
id: 'model-billing-cost',
|
|
2334
|
+
order: 2,
|
|
2335
|
+
}, CostDock))
|
|
2336
|
+
|
|
2337
|
+
ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
|
2338
|
+
name: 'conversation.chat.turnTail',
|
|
2339
|
+
id: 'model-billing-turn',
|
|
2340
|
+
order: 30,
|
|
2341
|
+
}, TurnCost))
|
|
2342
|
+
},
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
})
|