@goodandready/dsh-clinebot 0.5.33 → 0.6.1
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 +32 -0
- package/README.md +63 -36
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/lib/adapter-messages.js +237 -0
- package/lib/adapter-stream.js +292 -0
- package/lib/adapter.js +616 -0
- package/lib/client.js +2634 -922
- package/lib/cline-client.js +15 -6
- package/lib/config.js +266 -233
- package/lib/credential-refs.js +104 -84
- package/lib/index.js +569 -502
- package/lib/model-prices.js +117 -0
- package/lib/provider-sync.js +404 -387
- package/lib/proxy-token.js +186 -186
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/settings.js +81 -2
- package/lib/services.js +55 -0
- package/lib/stats-storage.js +378 -165
- package/lib/transport/cline-transport.js +314 -0
- package/lib/tui-settings.js +158 -0
- package/package.json +113 -77
package/lib/client.js
CHANGED
|
@@ -51,7 +51,7 @@ const en = {
|
|
|
51
51
|
'header.sub': 'Connect models under your active ClinePass subscription. OpenAI API compatible, automated rolling limit tracking, and instant auto-registration in DSH.',
|
|
52
52
|
'badge.online': 'Host online ({latency} ms)',
|
|
53
53
|
'badge.offline': 'Host unreachable',
|
|
54
|
-
'badge.key_ok': 'Key
|
|
54
|
+
'badge.key_ok': 'Key set ({source})',
|
|
55
55
|
'badge.key_missing': 'Key missing',
|
|
56
56
|
'badge.registered': 'DSH Registered ({count} models)',
|
|
57
57
|
'badge.not_registered': 'Not registered',
|
|
@@ -65,8 +65,8 @@ const en = {
|
|
|
65
65
|
'key.saving': 'Saving…',
|
|
66
66
|
'key.login_fast': 'Open API Keys ↗',
|
|
67
67
|
'key.verifying': 'Validating key with Cline…',
|
|
68
|
-
'key.verified': '
|
|
69
|
-
'key.invalid': '
|
|
68
|
+
'key.verified': 'Valid key for {email} ({plan})',
|
|
69
|
+
'key.invalid': 'Key validation failed: {error}',
|
|
70
70
|
'key.env_label': 'Credential environment name: ',
|
|
71
71
|
'key.get_key': 'Get key in app.cline.bot console ↗',
|
|
72
72
|
'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
|
|
@@ -93,6 +93,11 @@ const en = {
|
|
|
93
93
|
'accounts.route_model_placeholder': 'Model ID (e.g. cline-pass/deepseek-v4-pro)',
|
|
94
94
|
'accounts.route_account_placeholder': 'Target Account',
|
|
95
95
|
'accounts.route_btn_add': 'Add Route',
|
|
96
|
+
'accounts.actions': 'Account actions', 'accounts.configured': 'Configured', 'accounts.missing_key': 'Missing key', 'accounts.source': 'Source: {source}',
|
|
97
|
+
'accounts.quota_5h': '5h {pct}%', 'accounts.rename': 'Rename', 'accounts.rename_save': 'Save name', 'accounts.rename_placeholder': 'Account label',
|
|
98
|
+
'accounts.clear_key': 'Clear stored key', 'accounts.clear_confirm': 'Clear the stored key for {name}? The account row stays.',
|
|
99
|
+
'accounts.remove_confirm': 'Remove {name} from the pool?', 'accounts.confirm': 'Confirm', 'accounts.cancel': 'Cancel', 'accounts.unpin': 'Unpin',
|
|
100
|
+
'accounts.no_accounts': 'No accounts configured yet.', 'accounts.route_remove': 'Remove route',
|
|
96
101
|
'quota.title': 'ClinePass Quota & Rate Limits',
|
|
97
102
|
'quota.desc': 'Official rolling window request limits from ClinePass',
|
|
98
103
|
'quota.account': 'Account: {email} · Plan: {plan}',
|
|
@@ -119,9 +124,9 @@ const en = {
|
|
|
119
124
|
'models.caps_vision': 'Vision',
|
|
120
125
|
'models.caps_general': 'General',
|
|
121
126
|
'models.caps_coding': 'Coding',
|
|
122
|
-
'diag.smoke_result': '
|
|
127
|
+
'diag.smoke_result': 'Latency: {latency} ms | Model: {model}\nResponse: {preview}',
|
|
123
128
|
'diag.empty': '(empty)',
|
|
124
|
-
'ui.error_title': '
|
|
129
|
+
'ui.error_title': 'ClineBot UI Error: ',
|
|
125
130
|
'ui.retry': 'Retry',
|
|
126
131
|
'update.check_failed': 'Failed to check for updates',
|
|
127
132
|
'update.retry': 'Retry check',
|
|
@@ -236,19 +241,94 @@ const en = {
|
|
|
236
241
|
'dashboard.account_pool': 'Active Routing Account',
|
|
237
242
|
'dashboard.credits': 'Credit Balance',
|
|
238
243
|
'dashboard.updated_at': 'Updated at',
|
|
239
|
-
'composer.cost_title': '
|
|
244
|
+
'composer.cost_title': 'ClineBot Session Cost',
|
|
245
|
+
'composer.last_request': 'Last request',
|
|
246
|
+
'composer.free': 'Free',
|
|
240
247
|
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
241
248
|
'composer.prompt_tok': 'Prompt Tokens',
|
|
242
249
|
'composer.completion_tok': 'Completion Tokens',
|
|
243
250
|
'composer.reasoning_tok': 'Reasoning Tokens',
|
|
244
251
|
'composer.est_cost': 'Estimated Cost',
|
|
252
|
+
'composer.unpriced': 'unpriced', 'composer.cache_read': 'Cache Read', 'composer.cache_write': 'Cache Write',
|
|
253
|
+
'composer.price_source': 'Price', 'composer.price_estimate': 'estimate', 'composer.price_override': 'override',
|
|
245
254
|
'discovery.title': 'Discovered Local Keys',
|
|
246
255
|
'discovery.import_btn': 'Import from {source}',
|
|
247
256
|
'discovery.imported': 'Imported to accounts pool',
|
|
248
257
|
'discovery.scanning': 'Scanning for local Cline keys…',
|
|
258
|
+
'settings.unsaved': '{count} unsaved change(s)', 'settings.saving': 'Saving…', 'settings.saved': 'Settings saved', 'settings.save_failed': 'Could not save settings', 'settings.save': 'Save', 'settings.discard': 'Discard',
|
|
259
|
+
'settings.save_invalid_base_url': 'Base URL must be https:// or a localhost address.', 'settings.save_invalid_number': 'Timeouts and payload limits must be positive numbers.',
|
|
260
|
+
'settings.models_group': 'Models & context', 'settings.models_group_hint': 'Plan filter, visible models, context windows, output limits and reasoning defaults.',
|
|
261
|
+
'settings.integrations': 'Integrations & display', 'settings.integrations_desc': 'Surfaces outside the chat request that reuse this provider.',
|
|
262
|
+
'settings.web_search': 'Serve web search with ClineBot', 'settings.web_search_hint': 'Use the ClinePass endpoint for the DSH web_search tool.',
|
|
263
|
+
'settings.sidebar_quota': 'Show quota meter in the sidebar', 'settings.sidebar_quota_hint': 'Display the compact plan and rolling-window meter above Settings.',
|
|
264
|
+
'settings.composer_cost': 'Show session cost in the composer', 'settings.composer_cost_hint': 'Display the estimated token cost beside the composer.',
|
|
265
|
+
'settings.proxy_mode': 'Transparent loopback proxy', 'settings.proxy_mode_hint': 'Legacy transport only: route requests through the local failover proxy.',
|
|
266
|
+
'settings.advanced': 'Advanced', 'settings.advanced_desc': 'Connection, timeouts, storage paths and the temporary transport switch.',
|
|
267
|
+
'settings.base_url': 'API base URL', 'settings.base_url_hint': 'ClinePass API root. Remote endpoints must use https://.',
|
|
268
|
+
'settings.timeout': 'Connect timeout (ms)', 'settings.timeout_hint': 'Maximum wait for the upstream response headers.', 'settings.smoke_timeout': 'Smoke-test timeout (ms)', 'settings.smoke_timeout_hint': 'Maximum wait for the diagnostic chat completion.',
|
|
269
|
+
'settings.stream_idle': 'Stream idle timeout (ms)', 'settings.stream_idle_hint': 'Maximum gap between SSE chunks before the stream is treated as dead.', 'settings.max_payload': 'Max multimodal payload (MB)', 'settings.max_payload_hint': 'Oldest images are evicted once the request exceeds this budget.',
|
|
270
|
+
'settings.stats_path': 'Stats file path', 'settings.stats_path_hint': 'Persistent JSON telemetry path (supports ~/).', 'settings.models_cache_path': 'Models cache path', 'settings.models_cache_path_hint': 'Offline model catalogue cache (supports ~/).',
|
|
271
|
+
'settings.transport': 'Transport', 'settings.transport_hint': 'Native ctx.llm adapter (recommended) or the legacy llm-pi-ai + loopback proxy.',
|
|
272
|
+
'providerCard.title': 'ClineBot (ClinePass)',
|
|
273
|
+
'providerCard.hint': 'Paste a ClinePass API key or manage the full account pool in Settings → ClineBot.',
|
|
274
|
+
'providerCard.keyPresent': 'Key configured',
|
|
275
|
+
'providerCard.keyMissing': 'Key missing',
|
|
276
|
+
'providerCard.routeActive': 'Route active',
|
|
277
|
+
'providerCard.placeholder': 'Paste API key',
|
|
278
|
+
'providerCard.replace': 'Replace API key',
|
|
279
|
+
'providerCard.save': 'Save key',
|
|
280
|
+
'providerCard.saving': 'Saving…',
|
|
281
|
+
'providerCard.saved': 'Key saved and validated',
|
|
282
|
+
'providerCard.failed': 'Could not save the key',
|
|
283
|
+
'providerCard.show': 'Show',
|
|
284
|
+
'providerCard.hide': 'Hide',
|
|
285
|
+
'providerCard.openSettings': 'Open Settings → ClineBot for accounts, models and quotas.',
|
|
286
|
+
'dashboard.tab_quota': 'Quota', 'dashboard.tab_analytics': 'Analytics', 'dashboard.auto_account': 'Automatic (least used)',
|
|
287
|
+
'analytics.title': 'Usage analytics', 'analytics.requests': 'Requests', 'analytics.tokens': 'Tokens', 'analytics.cost': 'Estimated cost', 'analytics.success': 'Success rate', 'analytics.ttft': 'Avg TTFT',
|
|
288
|
+
'analytics.loading': 'Loading analytics…', 'analytics.no_data': 'No analytics yet. Send a few messages first.',
|
|
289
|
+
'analytics.unpriced': '{count} request(s) have no price estimate and are excluded from cost totals.',
|
|
290
|
+
'analytics.group_model': 'Models', 'analytics.group_day': 'Days', 'analytics.group_session': 'Sessions',
|
|
291
|
+
'analytics.heatmap': 'Request heatmap', 'analytics.heatmap_hint': 'Requests per hour over the last 7 days.',
|
|
292
|
+
'analytics.heatmap_less': 'Less', 'analytics.heatmap_more': 'More',
|
|
293
|
+
'analytics.heatmap_empty': 'No hourly data yet. Send a message to start the heatmap.',
|
|
294
|
+
'analytics.input': 'Input', 'analytics.output': 'Output', 'analytics.reasoning': 'Reasoning',
|
|
295
|
+
'analytics.cache_read': 'Cache read', 'analytics.cache_write': 'Cache write',
|
|
296
|
+
'analytics.avg_ttft': 'Avg TTFT', 'analytics.avg_latency': 'Avg latency', 'analytics.errors': 'Errors',
|
|
297
|
+
'analytics.no_key': 'ClinePass key is not configured.',
|
|
298
|
+
'analytics.no_key_hint': 'Open Settings → ClineBot and paste a key, or import a discovered local key.',
|
|
299
|
+
'analytics.health_error': 'ClinePass endpoint is unreachable.',
|
|
300
|
+
'analytics.not_registered': 'ClineBot provider is not registered with DSH.',
|
|
301
|
+
'analytics.not_registered_hint': 'Open Settings → ClineBot and check Diagnostics.',
|
|
302
|
+
'analytics.stale': 'Showing the last successful snapshot.',
|
|
303
|
+
'analytics.range': 'Range', 'analytics.range_7': '7 days', 'analytics.range_30': '30 days', 'analytics.range_90': '90 days',
|
|
304
|
+
'analytics.heatmap_all_models': 'All models', 'analytics.heatmap_peak': 'Peak',
|
|
305
|
+
'analytics.heatmap_by_hour': 'By hour', 'analytics.heatmap_model': 'Heatmap model',
|
|
306
|
+
'quota.no_windows': 'No quota windows reported.',
|
|
307
|
+
'prices.title': 'Price overrides', 'prices.desc': 'USD per 1M tokens. Blank uses the local estimate.',
|
|
308
|
+
'prices.show': 'Show prices', 'prices.hide': 'Hide prices', 'prices.model': 'Model',
|
|
309
|
+
'prices.input': 'Input', 'prices.output': 'Output', 'prices.cache_read': 'Cache read', 'prices.cache_write': 'Cache write',
|
|
310
|
+
'prices.auto': 'auto', 'prices.empty': 'No models available.',
|
|
311
|
+
'prices.hint': 'These rates affect the composer and local analytics only; they are not an invoice.',
|
|
249
312
|
}
|
|
250
313
|
|
|
251
|
-
|
|
314
|
+
function makeT(dict, fallback) {
|
|
315
|
+
return function t(key, vars) {
|
|
316
|
+
if (key === 'quota.countdown' && vars?.time === 'resetting') {
|
|
317
|
+
return (dict && dict['quota.resetting']) || (fallback && fallback['quota.resetting']) || '正在重置'
|
|
318
|
+
}
|
|
319
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
320
|
+
if (vars && typeof val === 'string') {
|
|
321
|
+
for (const k of Object.keys(vars)) {
|
|
322
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return val
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- src/client/locales-zh.js ---
|
|
330
|
+
// Chinese locale dictionary.
|
|
331
|
+
const zh = {
|
|
252
332
|
nav: 'ClineBot',
|
|
253
333
|
title: 'ClineBot',
|
|
254
334
|
subtitle: 'ClinePass 订阅服务,包含动态套餐模型。',
|
|
@@ -274,8 +354,8 @@ const en = {
|
|
|
274
354
|
'key.saving': '正在保存…',
|
|
275
355
|
'key.login_fast': '打开 API 密钥页 ↗',
|
|
276
356
|
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
277
|
-
'key.verified': '
|
|
278
|
-
'key.invalid': '
|
|
357
|
+
'key.verified': '密钥有效:{email} ({plan})',
|
|
358
|
+
'key.invalid': '密钥验证未通过:{error}',
|
|
279
359
|
'key.env_label': '凭据环境变量名称:',
|
|
280
360
|
'key.get_key': '前往 app.cline.bot 控制台获取密钥 ↗',
|
|
281
361
|
'key.saved_msg': '密钥已成功保存至 DSH 凭据服务。验证状态:{status}',
|
|
@@ -302,6 +382,11 @@ const en = {
|
|
|
302
382
|
'accounts.route_model_placeholder': '模型 ID (例如 cline-pass/deepseek-v4-pro)',
|
|
303
383
|
'accounts.route_account_placeholder': '目标账号',
|
|
304
384
|
'accounts.route_btn_add': '添加路由',
|
|
385
|
+
'accounts.actions': '账户操作', 'accounts.configured': '已配置', 'accounts.missing_key': '缺少密钥', 'accounts.source': '来源:{source}',
|
|
386
|
+
'accounts.quota_5h': '5小时 {pct}%', 'accounts.rename': '重命名', 'accounts.rename_save': '保存名称', 'accounts.rename_placeholder': '账户名称',
|
|
387
|
+
'accounts.clear_key': '清除已存密钥', 'accounts.clear_confirm': '清除 {name} 的已存密钥?账户行会保留。',
|
|
388
|
+
'accounts.remove_confirm': '从池中移除 {name}?', 'accounts.confirm': '确认', 'accounts.cancel': '取消', 'accounts.unpin': '取消固定',
|
|
389
|
+
'accounts.no_accounts': '尚未配置账户。', 'accounts.route_remove': '移除路由',
|
|
305
390
|
'quota.title': 'ClinePass 用量配额与限额监控',
|
|
306
391
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
307
392
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
@@ -328,9 +413,9 @@ const en = {
|
|
|
328
413
|
'models.caps_vision': '视觉',
|
|
329
414
|
'models.caps_general': '通用',
|
|
330
415
|
'models.caps_coding': '编程',
|
|
331
|
-
'diag.smoke_result': '
|
|
416
|
+
'diag.smoke_result': '延迟:{latency} ms | 模型:{model}\n响应:{preview}',
|
|
332
417
|
'diag.empty': '(空)',
|
|
333
|
-
'ui.error_title': '
|
|
418
|
+
'ui.error_title': 'ClineBot 界面错误:',
|
|
334
419
|
'ui.retry': '重试',
|
|
335
420
|
'update.check_failed': '检查更新失败',
|
|
336
421
|
'update.retry': '重试检查',
|
|
@@ -445,31 +530,74 @@ const en = {
|
|
|
445
530
|
'dashboard.account_pool': '当前承载账户',
|
|
446
531
|
'dashboard.credits': '点数余额',
|
|
447
532
|
'dashboard.updated_at': '更新于',
|
|
448
|
-
'composer.cost_title': '
|
|
533
|
+
'composer.cost_title': 'ClineBot 会话成本',
|
|
534
|
+
'composer.last_request': '最近一次请求',
|
|
535
|
+
'composer.free': '免费',
|
|
449
536
|
'composer.cost_breakdown': 'Token 与费用明细',
|
|
450
537
|
'composer.prompt_tok': '提示词 Token',
|
|
451
538
|
'composer.completion_tok': '补全 Token',
|
|
452
539
|
'composer.reasoning_tok': '思考推理 Token',
|
|
453
540
|
'composer.est_cost': '预估花费',
|
|
541
|
+
'composer.unpriced': '无价格', 'composer.cache_read': '缓存读取', 'composer.cache_write': '缓存写入',
|
|
542
|
+
'composer.price_source': '价格来源', 'composer.price_estimate': '估算', 'composer.price_override': '覆盖',
|
|
454
543
|
'discovery.title': '发现本地密钥',
|
|
455
544
|
'discovery.import_btn': '从 {source} 导入',
|
|
456
545
|
'discovery.imported': '已成功导入账户池',
|
|
457
546
|
'discovery.scanning': '正在扫描本地 Cline 密钥…',
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
547
|
+
'settings.unsaved': '{count} 项未保存更改', 'settings.saving': '正在保存…', 'settings.saved': '设置已保存', 'settings.save_failed': '无法保存设置', 'settings.save': '保存', 'settings.discard': '放弃',
|
|
548
|
+
'settings.save_invalid_base_url': 'Base URL 必须使用 https:// 或 localhost。', 'settings.save_invalid_number': '超时和负载限制必须是正数。',
|
|
549
|
+
'settings.models_group': '模型与上下文', 'settings.models_group_hint': '套餐过滤、可见模型、上下文窗口、输出上限和推理默认值。',
|
|
550
|
+
'settings.integrations': '集成与显示', 'settings.integrations_desc': '聊天请求之外复用此服务商的界面入口。',
|
|
551
|
+
'settings.web_search': '使用 ClineBot 提供网页搜索', 'settings.web_search_hint': '为 DSH web_search 工具使用 ClinePass 端点。',
|
|
552
|
+
'settings.sidebar_quota': '在侧边栏显示配额', 'settings.sidebar_quota_hint': '在设置上方显示紧凑的套餐与滚动窗口配额。',
|
|
553
|
+
'settings.composer_cost': '在输入框显示会话成本', 'settings.composer_cost_hint': '在输入框旁显示预估 token 成本。',
|
|
554
|
+
'settings.proxy_mode': '透明回环代理', 'settings.proxy_mode_hint': '仅旧传输:通过本地故障转移代理发送请求。',
|
|
555
|
+
'settings.advanced': '高级', 'settings.advanced_desc': '连接、超时、存储路径和临时传输开关。',
|
|
556
|
+
'settings.base_url': 'API 基础地址', 'settings.base_url_hint': 'ClinePass API 根地址。远程端点必须使用 https://。',
|
|
557
|
+
'settings.timeout': '连接超时(毫秒)', 'settings.timeout_hint': '等待上游响应头的最大时间。', 'settings.smoke_timeout': '烟雾测试超时(毫秒)', 'settings.smoke_timeout_hint': '等待诊断聊天补全的最大时间。',
|
|
558
|
+
'settings.stream_idle': '流空闲超时(毫秒)', 'settings.stream_idle_hint': 'SSE 数据块之间的最大间隔,超过后视为断流。', 'settings.max_payload': '多模态负载上限(MB)', 'settings.max_payload_hint': '请求超过预算时会淘汰最旧的图片。',
|
|
559
|
+
'settings.stats_path': '统计文件路径', 'settings.stats_path_hint': '持久化 JSON 遥测路径(支持 ~/)。', 'settings.models_cache_path': '模型缓存路径', 'settings.models_cache_path_hint': '离线模型目录缓存(支持 ~/)。',
|
|
560
|
+
'settings.transport': '传输方式', 'settings.transport_hint': '原生 ctx.llm 适配器(推荐)或旧版 llm-pi-ai + 回环代理。',
|
|
561
|
+
'providerCard.title': 'ClineBot(ClinePass)',
|
|
562
|
+
'providerCard.hint': '粘贴 ClinePass API 密钥,或前往“设置 → ClineBot”管理完整账户池。',
|
|
563
|
+
'providerCard.keyPresent': '密钥已配置',
|
|
564
|
+
'providerCard.keyMissing': '缺少密钥',
|
|
565
|
+
'providerCard.routeActive': '路由已启用',
|
|
566
|
+
'providerCard.placeholder': '粘贴 API 密钥',
|
|
567
|
+
'providerCard.replace': '替换 API 密钥',
|
|
568
|
+
'providerCard.save': '保存密钥',
|
|
569
|
+
'providerCard.saving': '正在保存…',
|
|
570
|
+
'providerCard.saved': '密钥已保存并验证',
|
|
571
|
+
'providerCard.failed': '无法保存密钥',
|
|
572
|
+
'providerCard.show': '显示',
|
|
573
|
+
'providerCard.hide': '隐藏',
|
|
574
|
+
'providerCard.openSettings': '前往“设置 → ClineBot”管理账户、模型和配额。',
|
|
575
|
+
'dashboard.tab_quota': '配额', 'dashboard.tab_analytics': '分析', 'dashboard.auto_account': '自动(最少使用)',
|
|
576
|
+
'analytics.title': '用量分析', 'analytics.requests': '请求数', 'analytics.tokens': 'Token', 'analytics.cost': '预估成本', 'analytics.success': '成功率', 'analytics.ttft': '平均首字延迟',
|
|
577
|
+
'analytics.loading': '正在加载分析…', 'analytics.no_data': '暂无分析数据,请先发送几条消息。',
|
|
578
|
+
'analytics.unpriced': '{count} 个请求没有价格估算,未计入成本合计。',
|
|
579
|
+
'analytics.group_model': '模型', 'analytics.group_day': '日期', 'analytics.group_session': '会话',
|
|
580
|
+
'analytics.heatmap': '请求热力图', 'analytics.heatmap_hint': '最近 7 天每小时的请求数。',
|
|
581
|
+
'analytics.heatmap_less': '少', 'analytics.heatmap_more': '多',
|
|
582
|
+
'analytics.heatmap_empty': '暂无按小时数据。发送一条消息后开始记录。',
|
|
583
|
+
'analytics.input': '输入', 'analytics.output': '输出', 'analytics.reasoning': '推理',
|
|
584
|
+
'analytics.cache_read': '缓存读取', 'analytics.cache_write': '缓存写入',
|
|
585
|
+
'analytics.avg_ttft': '平均首字延迟', 'analytics.avg_latency': '平均延迟', 'analytics.errors': '错误',
|
|
586
|
+
'analytics.no_key': '尚未配置 ClinePass 密钥。',
|
|
587
|
+
'analytics.no_key_hint': '打开“设置 → ClineBot”粘贴密钥,或导入已发现的本地密钥。',
|
|
588
|
+
'analytics.health_error': 'ClinePass 端点不可达。',
|
|
589
|
+
'analytics.not_registered': 'ClineBot 提供商未在 DSH 中注册。',
|
|
590
|
+
'analytics.not_registered_hint': '打开“设置 → ClineBot”检查诊断。',
|
|
591
|
+
'analytics.stale': '正在显示上次成功获取的数据。',
|
|
592
|
+
'analytics.range': '范围', 'analytics.range_7': '7 天', 'analytics.range_30': '30 天', 'analytics.range_90': '90 天',
|
|
593
|
+
'analytics.heatmap_all_models': '所有模型', 'analytics.heatmap_peak': '峰值',
|
|
594
|
+
'analytics.heatmap_by_hour': '按小时', 'analytics.heatmap_model': '热力图模型',
|
|
595
|
+
'quota.no_windows': '未返回配额窗口。',
|
|
596
|
+
'prices.title': '价格覆盖', 'prices.desc': '每 100 万 token 的美元价格。留空使用本地估算。',
|
|
597
|
+
'prices.show': '显示价格', 'prices.hide': '隐藏价格', 'prices.model': '模型',
|
|
598
|
+
'prices.input': '输入', 'prices.output': '输出', 'prices.cache_read': '缓存读取', 'prices.cache_write': '缓存写入',
|
|
599
|
+
'prices.auto': '自动', 'prices.empty': '暂无模型。',
|
|
600
|
+
'prices.hint': '这些价格只影响输入框成本和本地分析,不是账单。',
|
|
473
601
|
}
|
|
474
602
|
|
|
475
603
|
// --- src/client/primitives.js ---
|
|
@@ -517,6 +645,121 @@ function BooleanControl(props) {
|
|
|
517
645
|
})
|
|
518
646
|
}
|
|
519
647
|
|
|
648
|
+
// --- src/client/icons.js ---
|
|
649
|
+
/**
|
|
650
|
+
* Calm 16px stroke icons. They inherit `currentColor` and never carry emoji or
|
|
651
|
+
* hardcoded palette values, so they follow the host theme in both modes.
|
|
652
|
+
*/
|
|
653
|
+
function CbIcon({ name, size = 16, className }) {
|
|
654
|
+
const common = {
|
|
655
|
+
width: size,
|
|
656
|
+
height: size,
|
|
657
|
+
viewBox: '0 0 16 16',
|
|
658
|
+
fill: 'none',
|
|
659
|
+
stroke: 'currentColor',
|
|
660
|
+
strokeWidth: 1.5,
|
|
661
|
+
strokeLinecap: 'round',
|
|
662
|
+
strokeLinejoin: 'round',
|
|
663
|
+
className: className || 'cb-icon',
|
|
664
|
+
'aria-hidden': 'true',
|
|
665
|
+
focusable: 'false',
|
|
666
|
+
}
|
|
667
|
+
const children = []
|
|
668
|
+
switch (name) {
|
|
669
|
+
case 'search':
|
|
670
|
+
children.push(
|
|
671
|
+
React.createElement('circle', { key: 'c', cx: 7, cy: 7, r: 4.5 }),
|
|
672
|
+
React.createElement('path', { key: 'l', d: 'M10.5 10.5 14 14' })
|
|
673
|
+
)
|
|
674
|
+
break
|
|
675
|
+
case 'plus':
|
|
676
|
+
children.push(
|
|
677
|
+
React.createElement('path', { key: 'v', d: 'M8 3v10' }),
|
|
678
|
+
React.createElement('path', { key: 'h', d: 'M3 8h10' })
|
|
679
|
+
)
|
|
680
|
+
break
|
|
681
|
+
case 'close':
|
|
682
|
+
children.push(React.createElement('path', { key: 'x', d: 'm4 4 8 8M12 4l-8 8' }))
|
|
683
|
+
break
|
|
684
|
+
case 'check':
|
|
685
|
+
children.push(React.createElement('path', { key: 'p', d: 'm3.5 8.5 3 3 6-7' }))
|
|
686
|
+
break
|
|
687
|
+
case 'warning':
|
|
688
|
+
children.push(
|
|
689
|
+
React.createElement('path', { key: 't', d: 'M8 2 14 13H2Z' }),
|
|
690
|
+
React.createElement('path', { key: 'l', d: 'M8 6v3' }),
|
|
691
|
+
React.createElement('circle', { key: 'd', cx: 8, cy: 11.2, r: 0.6, fill: 'currentColor', stroke: 'none' })
|
|
692
|
+
)
|
|
693
|
+
break
|
|
694
|
+
case 'error':
|
|
695
|
+
children.push(
|
|
696
|
+
React.createElement('circle', { key: 'c', cx: 8, cy: 8, r: 6 }),
|
|
697
|
+
React.createElement('path', { key: 'l', d: 'M8 4.5v4' }),
|
|
698
|
+
React.createElement('circle', { key: 'd', cx: 8, cy: 11.4, r: 0.6, fill: 'currentColor', stroke: 'none' })
|
|
699
|
+
)
|
|
700
|
+
break
|
|
701
|
+
case 'download':
|
|
702
|
+
children.push(
|
|
703
|
+
React.createElement('path', { key: 'a', d: 'M8 2v7' }),
|
|
704
|
+
React.createElement('path', { key: 'b', d: 'm5 6.5 3 3 3-3' }),
|
|
705
|
+
React.createElement('path', { key: 'c', d: 'M3 12.5h10' })
|
|
706
|
+
)
|
|
707
|
+
break
|
|
708
|
+
case 'chevron':
|
|
709
|
+
children.push(React.createElement('path', { key: 'p', d: 'm6 4 4 4-4 4' }))
|
|
710
|
+
break
|
|
711
|
+
case 'kebab':
|
|
712
|
+
children.push(
|
|
713
|
+
React.createElement('circle', { key: 'a', cx: 8, cy: 3.5, r: 1, fill: 'currentColor', stroke: 'none' }),
|
|
714
|
+
React.createElement('circle', { key: 'b', cx: 8, cy: 8, r: 1, fill: 'currentColor', stroke: 'none' }),
|
|
715
|
+
React.createElement('circle', { key: 'c', cx: 8, cy: 12.5, r: 1, fill: 'currentColor', stroke: 'none' })
|
|
716
|
+
)
|
|
717
|
+
break
|
|
718
|
+
case 'key':
|
|
719
|
+
children.push(
|
|
720
|
+
React.createElement('circle', { key: 'c', cx: 5, cy: 8, r: 2.5 }),
|
|
721
|
+
React.createElement('path', { key: 'a', d: 'M7.5 8H14' }),
|
|
722
|
+
React.createElement('path', { key: 'b', d: 'M11.5 8v2' }),
|
|
723
|
+
React.createElement('path', { key: 'd', d: 'M13.5 8v1.5' })
|
|
724
|
+
)
|
|
725
|
+
break
|
|
726
|
+
case 'edit':
|
|
727
|
+
children.push(
|
|
728
|
+
React.createElement('path', { key: 'a', d: 'M3 13l.5-3L11 2.5 13.5 5 6 12.5Z' }),
|
|
729
|
+
React.createElement('path', { key: 'b', d: 'm9.5 4 2.5 2.5' })
|
|
730
|
+
)
|
|
731
|
+
break
|
|
732
|
+
case 'trash':
|
|
733
|
+
children.push(
|
|
734
|
+
React.createElement('path', { key: 'a', d: 'M3 5h10' }),
|
|
735
|
+
React.createElement('path', { key: 'b', d: 'M6 5V3.5h4V5' }),
|
|
736
|
+
React.createElement('path', { key: 'c', d: 'M4.5 5 5 13h6l.5-8' })
|
|
737
|
+
)
|
|
738
|
+
break
|
|
739
|
+
case 'route':
|
|
740
|
+
children.push(
|
|
741
|
+
React.createElement('path', { key: 'a', d: 'M3 5h6a2 2 0 0 1 0 4H5a2 2 0 0 0 0 4h8' }),
|
|
742
|
+
React.createElement('path', { key: 'b', d: 'm11 10 2 3-2 3' })
|
|
743
|
+
)
|
|
744
|
+
break
|
|
745
|
+
case 'refresh':
|
|
746
|
+
children.push(
|
|
747
|
+
React.createElement('path', { key: 'a', d: 'M13 8a5 5 0 1 1-1.5-3.5' }),
|
|
748
|
+
React.createElement('path', { key: 'b', d: 'M13 2.5V6h-3.5' })
|
|
749
|
+
)
|
|
750
|
+
break
|
|
751
|
+
case 'account':
|
|
752
|
+
children.push(
|
|
753
|
+
React.createElement('circle', { key: 'a', cx: 8, cy: 5.5, r: 2.5 }),
|
|
754
|
+
React.createElement('path', { key: 'b', d: 'M3 13c.6-2.5 2.6-4 5-4s4.4 1.5 5 4' })
|
|
755
|
+
)
|
|
756
|
+
break
|
|
757
|
+
default:
|
|
758
|
+
children.push(React.createElement('circle', { key: 'c', cx: 8, cy: 8, r: 6 }))
|
|
759
|
+
}
|
|
760
|
+
return React.createElement('svg', common, children)
|
|
761
|
+
}
|
|
762
|
+
|
|
520
763
|
// --- src/client/styles.js ---
|
|
521
764
|
function ensureCss() {
|
|
522
765
|
if (typeof document === 'undefined') return
|
|
@@ -532,7 +775,7 @@ function ensureCss() {
|
|
|
532
775
|
|
|
533
776
|
.cb-section-card{display:flex;flex-direction:column;gap:12px;padding:12px 14px;border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-settings-card-fill)}
|
|
534
777
|
.cb-section-title{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 12px;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
|
|
535
|
-
.cb-section-desc{margin-top:-6px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
778
|
+
.cb-section-desc{margin-top:-6px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
536
779
|
|
|
537
780
|
.cb-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
538
781
|
.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
|
|
@@ -559,10 +802,13 @@ function ensureCss() {
|
|
|
559
802
|
.cb-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}
|
|
560
803
|
.cb-btn-disabled{opacity:.5;cursor:not-allowed}
|
|
561
804
|
|
|
562
|
-
.cb-progress-card, .cb-bar-container{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}
|
|
563
|
-
.cb-progress-
|
|
564
|
-
.cb-progress-label
|
|
565
|
-
.cb-progress-
|
|
805
|
+
.cb-progress-card, .cb-bar-container{display:flex;flex-direction:column;gap:8px;min-width:0;padding:12px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}
|
|
806
|
+
.cb-progress-card-bad{border-color:var(--dsw-alias-state-error-primary)}
|
|
807
|
+
.cb-progress-head, .cb-bar-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:4px 12px;width:100%;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
|
|
808
|
+
.cb-progress-label{flex:0 0 auto;font-weight:500;white-space:nowrap}
|
|
809
|
+
.cb-progress-meta, .cb-bar-meta{flex:1 1 140px;min-width:0;margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-align:right;white-space:normal;overflow-wrap:anywhere}
|
|
810
|
+
.cb-progress-meta-bad{color:var(--dsw-alias-state-error-primary)}
|
|
811
|
+
.cb-progress-low{display:inline-flex;align-items:center;gap:4px;margin-left:6px;font-size:11px;color:var(--dsw-alias-state-error-primary)}
|
|
566
812
|
.cb-progress-track, .cb-bar-track{width:100%;height:6px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
567
813
|
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
568
814
|
.cb-progress-bar-warn{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-state-warn-primary);transition:width .3s ease}
|
|
@@ -594,15 +840,19 @@ function ensureCss() {
|
|
|
594
840
|
.cb-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
|
|
595
841
|
.cb-foot:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
596
842
|
.cb-footTop{display:flex;align-items:center;gap:8px;min-width:0}
|
|
597
|
-
.cb-footName{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:20px;white-space:nowrap;text-overflow:ellipsis}
|
|
843
|
+
.cb-footName{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:20px;white-space:nowrap;text-overflow:ellipsis}
|
|
598
844
|
.cb-footRow{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
599
845
|
.cb-footHead{display:flex;align-items:baseline;gap:8px;min-width:0}
|
|
600
|
-
.cb-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
601
|
-
.cb-footAmount{flex:
|
|
846
|
+
.cb-footLabel{flex:1;min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px;white-space:nowrap;text-overflow:ellipsis}
|
|
847
|
+
.cb-footAmount{flex:0 1 auto;max-width:68%;overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap;text-overflow:ellipsis}
|
|
848
|
+
.cb-footRowMissing .cb-footLabel{opacity:.65}
|
|
849
|
+
.cb-footAmountMissing{color:var(--dsw-alias-label-tertiary)}
|
|
850
|
+
.cb-footMessage{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
602
851
|
.cb-footBar{display:block;height:4px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
603
852
|
.cb-footFill{display:block;height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
604
853
|
.cb-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
605
|
-
.cb-footPct{flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums}
|
|
854
|
+
.cb-footPct{margin-left:auto;flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px;font-weight:600;line-height:16px;font-variant-numeric:tabular-nums}
|
|
855
|
+
.cb-footPctWarn{color:var(--dsw-alias-state-error-primary)}
|
|
606
856
|
.cb-railButton{box-sizing:border-box;flex:none;display:inline-flex;justify-content:center;align-items:center;width:36px;height:36px;margin:0 0 4px;padding:0;border:.5px solid transparent;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
607
857
|
.cb-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
608
858
|
.cb-railButton:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
@@ -611,11 +861,261 @@ function ensureCss() {
|
|
|
611
861
|
/* The window chrome and its width belong to the shell's Modal, so the body only
|
|
612
862
|
fills whatever it is given — a fixed width here overflows the panel. */
|
|
613
863
|
.cb-dialog{display:flex;flex-direction:column;gap:12px;width:100%;min-width:0;max-height:min(72vh, 640px);overflow-y:auto;color:var(--dsw-alias-label-primary)}
|
|
864
|
+
.cb-panel{display:flex;flex-direction:column;gap:14px;width:100%;max-width:780px;margin:0 auto;padding:18px;height:100%;max-height:calc(100vh - 56px);min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;color:var(--dsw-alias-label-primary)}
|
|
865
|
+
.cb-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
866
|
+
.cb-panel-identity{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
867
|
+
.cb-panel-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:650;line-height:24px}
|
|
868
|
+
.cb-panel-sub{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
869
|
+
.cb-panel-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;flex:none}
|
|
870
|
+
.cb-panel-updated{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
|
|
871
|
+
.cb-panel-close{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:18px;line-height:1;cursor:pointer}
|
|
872
|
+
.cb-panel-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
873
|
+
.cb-notice{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2)}
|
|
874
|
+
.cb-notice-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px}
|
|
875
|
+
.cb-notice-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
876
|
+
.cb-notice-error{border-color:var(--dsw-alias-state-error-primary)}
|
|
877
|
+
.cb-notice-error .cb-notice-title{color:var(--dsw-alias-state-error-primary)}
|
|
878
|
+
@media(max-width:720px){.cb-panel{padding:12px;max-height:none}}
|
|
614
879
|
.cb-dialogMeta{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}
|
|
615
880
|
.cb-dialogCard{display:flex;flex-direction:column;gap:12px;padding:12px 14px;border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-settings-card-fill)}
|
|
616
881
|
.cb-dialogFallback{position:fixed;inset:0;z-index:900;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1)}
|
|
617
882
|
.cb-dialogFallbackPanel{display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 96px);overflow-y:auto;padding:18px;border-radius:var(--dsw-radius-panel);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent)}
|
|
618
883
|
|
|
884
|
+
/* Provider card inside Settings -> Models. */
|
|
885
|
+
.cb-provider-card{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}
|
|
886
|
+
.cb-provider-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
887
|
+
.cb-provider-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:20px}
|
|
888
|
+
.cb-provider-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
889
|
+
|
|
890
|
+
/* Grouped settings, disclosure rows and the staged-save bar. */
|
|
891
|
+
.cb-settings-group{display:flex;flex-direction:column;gap:10px;padding:4px 0}
|
|
892
|
+
.cb-settings-group-divided{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:16px;margin-top:4px}
|
|
893
|
+
.cb-settings-group-head{display:flex;align-items:center;gap:8px;margin:0;padding:0}
|
|
894
|
+
.cb-settings-disclosure{width:100%;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;padding:2px 0}
|
|
895
|
+
.cb-settings-group-title{margin:0;color:var(--dsw-alias-label-primary);font-size:14px;line-height:20px;font-weight:600}
|
|
896
|
+
.cb-settings-group-action{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
897
|
+
.cb-settings-group-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
898
|
+
.cb-settings-group-body{display:flex;flex-direction:column;gap:2px}
|
|
899
|
+
.cb-settings-spacer{flex:1 1 auto}
|
|
900
|
+
.cb-settings-chevron{display:inline-flex;justify-content:center;align-items:center;width:18px;height:18px;color:var(--dsw-alias-label-tertiary);font-size:18px;line-height:1;transition:transform .16s ease}
|
|
901
|
+
.cb-settings-chevron-open{transform:rotate(90deg)}
|
|
902
|
+
.cb-settings-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:12px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
|
|
903
|
+
.cb-settings-row:last-child{border-bottom:0}
|
|
904
|
+
.cb-settings-row-text{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
905
|
+
.cb-settings-row-title{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;font-weight:500}
|
|
906
|
+
.cb-settings-row-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
907
|
+
.cb-settings-row-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
|
|
908
|
+
.cb-settings-row-control{display:flex;align-items:center;gap:8px;flex:none}
|
|
909
|
+
.cb-settings-input{width:220px;max-width:46vw}
|
|
910
|
+
.cb-savebar{position:sticky;bottom:0;z-index:30;margin:18px -4px -4px;padding:10px 4px 4px;background:linear-gradient(to top,var(--dsw-alias-bg-layer-1) 70%,transparent)}
|
|
911
|
+
.cb-savebar-inner{display:flex;align-items:center;gap:10px;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent)}
|
|
912
|
+
.cb-savebar-text{min-width:0;overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap;text-overflow:ellipsis}
|
|
913
|
+
|
|
914
|
+
/* Calm icon and account-pool primitives. */
|
|
915
|
+
.cb-icon{flex:none;display:inline-block;vertical-align:-2px}
|
|
916
|
+
.cb-icon-chevron-open{transform:rotate(90deg)}
|
|
917
|
+
.cb-banner-icon{display:inline-flex;flex:none;align-items:center;justify-content:center;color:inherit}
|
|
918
|
+
.cb-inline-alert{display:flex;align-items:center;gap:6px}
|
|
919
|
+
.cb-mono{font-family:var(--ds-font-family-code,monospace);font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
920
|
+
.cb-muted{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
921
|
+
.cb-empty{padding:16px 0;color:var(--dsw-alias-label-secondary);font-size:12px;text-align:center}
|
|
922
|
+
.cb-section-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
923
|
+
.cb-accounts .cb-section-desc{margin-bottom:8px}
|
|
924
|
+
.cb-discovered{display:flex;flex-direction:column;gap:6px;margin:8px 0;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1)}
|
|
925
|
+
.cb-discovered-head{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500}
|
|
926
|
+
.cb-discovered-row{display:flex;align-items:center;gap:8px;font-size:12px}
|
|
927
|
+
.cb-discovered-row .cb-mono{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
928
|
+
.cb-account-mode{display:flex;align-items:center;gap:8px;margin:8px 0 4px;font-size:12px}
|
|
929
|
+
.cb-seg{display:inline-flex;overflow:hidden;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md)}
|
|
930
|
+
.cb-seg-item{padding:4px 10px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
|
|
931
|
+
.cb-seg-item+.cb-seg-item{border-left:.5px solid var(--dsw-alias-border-l2)}
|
|
932
|
+
.cb-seg-item-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}
|
|
933
|
+
.cb-account-add{display:flex;flex-direction:column;gap:8px;margin:8px 0 12px;padding:12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1)}
|
|
934
|
+
.cb-account-add-row{display:flex;gap:8px;flex-wrap:wrap}
|
|
935
|
+
.cb-account-add-row .cb-input{flex:1 1 180px}
|
|
936
|
+
.cb-account-add-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
937
|
+
.cb-account-list{display:flex;flex-direction:column;margin-top:4px}
|
|
938
|
+
.cb-account-item{position:relative;border-bottom:.5px solid var(--dsw-alias-border-l2)}
|
|
939
|
+
.cb-account-item:last-child{border-bottom:0}
|
|
940
|
+
.cb-account-row{display:flex;align-items:center;gap:10px;padding:10px 0}
|
|
941
|
+
.cb-status-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-border-l3)}
|
|
942
|
+
.cb-status-dot-ok{background:var(--dsw-alias-state-success-primary)}
|
|
943
|
+
.cb-status-dot-warn{background:var(--dsw-alias-state-warn-primary)}
|
|
944
|
+
.cb-status-dot-bad{background:var(--dsw-alias-state-error-primary)}
|
|
945
|
+
.cb-status-dot-idle{background:var(--dsw-alias-border-l3)}
|
|
946
|
+
.cb-account-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
947
|
+
.cb-account-name{display:flex;align-items:baseline;gap:8px;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:18px}
|
|
948
|
+
.cb-account-env{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
949
|
+
.cb-account-meta{min-width:0;overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;white-space:nowrap;text-overflow:ellipsis}
|
|
950
|
+
.cb-account-badges{display:flex;flex:none;flex-wrap:wrap;justify-content:flex-end;gap:4px;max-width:240px}
|
|
951
|
+
.cb-account-quota{display:flex;flex:none;align-items:center;justify-content:flex-end;gap:6px;min-width:112px}
|
|
952
|
+
.cb-meter{display:block;width:64px;height:4px;overflow:hidden;border-radius:2px;background:var(--dsw-alias-bg-layer-2)}
|
|
953
|
+
.cb-meter-fill{display:block;height:100%;background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
954
|
+
.cb-meter-fill-warn{background:var(--dsw-alias-state-error-primary)}
|
|
955
|
+
.cb-icon-btn{display:inline-flex;flex:none;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:.5px solid transparent;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
956
|
+
.cb-icon-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
957
|
+
.cb-icon-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
958
|
+
.cb-account-menu{display:flex;flex-direction:column;min-width:180px;margin:0 0 8px 18px;padding:4px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent)}
|
|
959
|
+
.cb-menu-item{padding:6px 8px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}
|
|
960
|
+
.cb-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
961
|
+
.cb-menu-item-danger{color:var(--dsw-alias-state-error-primary)}
|
|
962
|
+
.cb-account-inline{display:flex;align-items:center;gap:8px;padding:0 0 10px 18px}
|
|
963
|
+
.cb-account-inline .cb-input{flex:1;min-width:0}
|
|
964
|
+
.cb-account-danger{padding:8px 10px;margin:0 0 8px 18px;border-radius:var(--dsw-radius-md);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent)}
|
|
965
|
+
.cb-account-danger-text{flex:1;min-width:0;color:var(--dsw-alias-state-error-primary);font-size:12px}
|
|
966
|
+
.cb-account-routes{display:flex;flex-direction:column;gap:8px;margin-top:14px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
967
|
+
.cb-account-routes-head{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500}
|
|
968
|
+
.cb-account-route-add{display:flex;gap:8px;flex-wrap:wrap}
|
|
969
|
+
.cb-account-route-add .cb-input{flex:2 1 160px}
|
|
970
|
+
.cb-account-route-add .cb-select{flex:1 1 140px}
|
|
971
|
+
.cb-account-route-list{display:flex;flex-direction:column;gap:4px}
|
|
972
|
+
.cb-account-route-row{display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);font-size:12px}
|
|
973
|
+
.cb-account-route-row .cb-mono{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
974
|
+
@media(max-width:720px){
|
|
975
|
+
.cb-account-row{flex-wrap:wrap}
|
|
976
|
+
.cb-account-badges{justify-content:flex-start;max-width:none}
|
|
977
|
+
.cb-account-quota{min-width:0}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/* Calm section toolbars, stats and banners. */
|
|
981
|
+
.cb-section-actions{display:flex;align-items:center;gap:6px;flex:none}
|
|
982
|
+
.cb-btn-link{display:inline-flex;align-items:center;gap:4px;text-decoration:none}
|
|
983
|
+
.cb-quota-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:10px;margin-top:4px}
|
|
984
|
+
@media(max-width:640px){.cb-quota-grid{grid-template-columns:1fr}}
|
|
985
|
+
.cb-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin-top:4px}
|
|
986
|
+
.cb-model-list{display:flex;flex-direction:column;gap:4px;margin-top:12px}
|
|
987
|
+
.cb-model-list-title{margin-bottom:2px;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500}
|
|
988
|
+
.cb-model-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 8px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);font-size:12px}
|
|
989
|
+
.cb-model-row .cb-mono{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
990
|
+
.cb-banner-body{flex:1;min-width:0}
|
|
991
|
+
.cb-banner-sub{margin-top:2px;font-size:11px;opacity:.9}
|
|
992
|
+
.cb-state-ok{color:var(--dsw-alias-state-success-primary);font-size:12px}
|
|
993
|
+
.cb-hint{margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
994
|
+
.cb-key-status{margin-top:8px;font-size:12px}
|
|
995
|
+
.cb-key-meta{display:flex;align-items:center;gap:6px;margin-top:12px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
996
|
+
.cb-link{color:var(--dsw-alias-brand-primary);text-decoration:none}
|
|
997
|
+
.cb-link:hover{text-decoration:underline}
|
|
998
|
+
.cb-update-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2)}
|
|
999
|
+
.cb-update-info{display:flex;align-items:center;gap:8px;min-width:0;font-size:12px}
|
|
1000
|
+
.cb-update-version{color:var(--dsw-alias-label-secondary);font-weight:500}
|
|
1001
|
+
.cb-row-actions{display:flex;align-items:center;gap:8px;margin-top:4px}
|
|
1002
|
+
|
|
1003
|
+
/* Composer cost and analytics dialog. */
|
|
1004
|
+
.cb-composer-cost{position:relative;display:inline-flex;align-items:center}
|
|
1005
|
+
.cb-composer-cost-button{display:inline-flex;align-items:center;gap:4px;height:24px;padding:2px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer}
|
|
1006
|
+
.cb-composer-cost-button:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
|
|
1007
|
+
.cb-composer-cost-value{color:var(--dsw-alias-label-primary);font-weight:600}
|
|
1008
|
+
.cb-composer-popover{position:absolute;bottom:100%;left:0;z-index:100;display:flex;flex-direction:column;gap:6px;min-width:200px;margin-bottom:6px;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);font-size:12px}
|
|
1009
|
+
.cb-composer-title{padding-bottom:4px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
1010
|
+
.cb-composer-row,.cb-composer-total{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
1011
|
+
.cb-composer-total{padding-top:4px;border-top:.5px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
1012
|
+
.cb-composer-value{font-variant-numeric:tabular-nums}
|
|
1013
|
+
.cb-tabs{display:flex;gap:4px;border-bottom:.5px solid var(--dsw-alias-border-l2)}
|
|
1014
|
+
.cb-tab{padding:6px 10px;border:0;border-bottom:1.5px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
|
|
1015
|
+
.cb-tab-active{border-bottom-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);font-weight:500}
|
|
1016
|
+
.cb-dialogRow{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
1017
|
+
.cb-dialogBadges{display:flex;align-items:center;justify-content:flex-end;gap:4px;flex-wrap:wrap}
|
|
1018
|
+
.cb-analytics-controls{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
|
|
1019
|
+
.cb-account-tabs{display:flex;flex-wrap:wrap;gap:6px}
|
|
1020
|
+
.cb-account-tab{display:inline-flex;align-items:center;padding:2px 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}
|
|
1021
|
+
.cb-account-tab:hover:not(.cb-account-tab-active){color:var(--dsw-alias-label-primary)}
|
|
1022
|
+
.cb-account-tab-active{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
|
|
1023
|
+
.cb-price-section{display:flex;flex-direction:column;gap:8px;margin-top:14px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
1024
|
+
.cb-price-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
1025
|
+
.cb-price-copy{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1026
|
+
.cb-price-table{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-x:auto}
|
|
1027
|
+
.cb-price-head-row,.cb-price-row{display:grid;grid-template-columns:minmax(160px,1.6fr) repeat(4,minmax(72px,1fr));gap:6px;align-items:center;min-width:520px}
|
|
1028
|
+
.cb-price-head-row{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
1029
|
+
.cb-price-row .cb-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
1030
|
+
.cb-price-input{width:100%;min-width:0;padding:4px 6px;font-size:12px}
|
|
1031
|
+
.cb-dialogIdentity{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1032
|
+
.cb-dialogTiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px}
|
|
1033
|
+
.cb-dialogWindows{display:flex;flex-direction:column;gap:12px}
|
|
1034
|
+
.cb-analytics-list{display:flex;flex-direction:column;gap:2px;margin-top:8px}
|
|
1035
|
+
.cb-analytics-head,.cb-analytics-row{display:grid;grid-template-columns:minmax(120px,1fr) 70px 70px 80px 70px;gap:8px;align-items:center;padding:4px 0;font-size:11px}
|
|
1036
|
+
.cb-analytics-head{border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}
|
|
1037
|
+
.cb-analytics-row{border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
|
|
1038
|
+
.cb-analytics-row .cb-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1039
|
+
.cb-heat-wrap{display:flex;flex-direction:column;gap:8px;min-width:0}
|
|
1040
|
+
.cb-heat-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1041
|
+
.cb-heat-toolbar .cb-hint{margin:0}
|
|
1042
|
+
.cb-heat-select{max-width:260px;padding:3px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
|
|
1043
|
+
.cb-heat-scroll{overflow-x:auto;padding-bottom:4px}
|
|
1044
|
+
.cb-heat{display:grid;grid-template-columns:44px repeat(24,minmax(10px,1fr));gap:2px;align-items:center;min-width:620px}
|
|
1045
|
+
.cb-heat-hour{color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:12px;text-align:center}
|
|
1046
|
+
.cb-heat-day{color:var(--dsw-alias-label-secondary);font-size:10px;line-height:14px;white-space:nowrap}
|
|
1047
|
+
.cb-heat-cell,.cb-heat-swatch{display:block;border-radius:3px;background:var(--dsw-alias-bg-layer-2)}
|
|
1048
|
+
.cb-heat-cell{height:14px}
|
|
1049
|
+
.cb-heat-swatch{width:12px;height:12px;border-radius:2px}
|
|
1050
|
+
.cb-heat-l0{background:var(--dsw-alias-bg-layer-2)}
|
|
1051
|
+
.cb-heat-l1{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,var(--dsw-alias-bg-layer-2))}
|
|
1052
|
+
.cb-heat-l2{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 36%,var(--dsw-alias-bg-layer-2))}
|
|
1053
|
+
.cb-heat-l3{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 58%,var(--dsw-alias-bg-layer-2))}
|
|
1054
|
+
.cb-heat-l4{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 80%,var(--dsw-alias-bg-layer-2))}
|
|
1055
|
+
.cb-heat-legend{display:flex;align-items:center;gap:4px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:14px}
|
|
1056
|
+
.cb-hour-strip-head{margin:10px 0 0;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:14px}
|
|
1057
|
+
.cb-hour-strip{display:grid;grid-template-columns:44px repeat(24,minmax(10px,1fr));gap:2px;align-items:end;height:30px;min-width:620px}
|
|
1058
|
+
.cb-hour-col{display:flex;align-items:flex-end;height:100%}
|
|
1059
|
+
.cb-hour-bar{display:block;width:100%;min-height:2px;border-radius:2px 2px 0 0}
|
|
1060
|
+
.cb-hour-labels{display:grid;grid-template-columns:44px repeat(24,minmax(10px,1fr));gap:2px;color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:12px;text-align:center;min-width:620px}
|
|
1061
|
+
@media(max-width:720px){
|
|
1062
|
+
.cb-analytics-head,.cb-analytics-row{grid-template-columns:1fr 56px 64px}
|
|
1063
|
+
.cb-analytics-head span:nth-child(4),.cb-analytics-head span:nth-child(5),.cb-analytics-row span:nth-child(4),.cb-analytics-row span:nth-child(5){display:none}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* Opencode-inspired visual language: neutral rgba surfaces, 7-10px radii and
|
|
1067
|
+
the same green/amber/red accents. Text still follows the harness theme. */
|
|
1068
|
+
.cb-page{gap:14px;padding:4px 2px 24px}
|
|
1069
|
+
.cb-page-title{font-size:18px;font-weight:650;line-height:1.4}
|
|
1070
|
+
.cb-panel{max-width:760px;padding:18px}
|
|
1071
|
+
.cb-section-card,.cb-dialogCard,.cb-provider-card,.cb-progress-card,.cb-stat-box,.cb-account-add,.cb-discovered,.cb-update-banner,.cb-notice,.cb-savebar-inner,.cb-account-menu{border:1px solid rgba(127,127,127,.25);border-radius:10px;background:rgba(127,127,127,.06)}
|
|
1072
|
+
.cb-section-card,.cb-dialogCard{padding:12px 14px;gap:10px}
|
|
1073
|
+
.cb-section-title,.cb-settings-group-title{font-size:13px;font-weight:650;line-height:1.5}
|
|
1074
|
+
.cb-section-desc,.cb-settings-group-desc,.cb-settings-row-desc,.cb-hint{opacity:.72;line-height:1.5}
|
|
1075
|
+
.cb-input,.cb-select{height:auto;min-height:32px;padding:6px 8px;border:1px solid rgba(127,127,127,.35);border-radius:7px;background:rgba(127,127,127,.08);font-size:13px;line-height:1.4}
|
|
1076
|
+
.cb-input:focus,.cb-select:focus{border-color:rgba(80,140,255,.6);outline:2px solid rgba(80,140,255,.25);outline-offset:1px}
|
|
1077
|
+
.cb-btn{height:auto;min-height:30px;padding:6px 11px;border:1px solid rgba(127,127,127,.35);border-radius:7px;background:rgba(127,127,127,.1);font-size:13px;line-height:1.4}
|
|
1078
|
+
.cb-btn:hover:not(:disabled){background:rgba(127,127,127,.18)}
|
|
1079
|
+
.cb-btn-primary{border-color:rgba(80,140,255,.6);background:rgba(80,140,255,.18);color:inherit}
|
|
1080
|
+
.cb-btn-primary:hover:not(:disabled){background:rgba(80,140,255,.26)}
|
|
1081
|
+
.cb-btn-danger{border-color:rgba(220,80,80,.5);color:#e07a7a}
|
|
1082
|
+
.cb-badge{padding:2px 7px;border:1px solid rgba(127,127,127,.3);border-radius:999px;background:rgba(127,127,127,.1);font-size:11px;line-height:1.4}
|
|
1083
|
+
.cb-badge-ok{color:#4fbf86;border-color:rgba(60,180,110,.5);background:rgba(60,180,110,.10)}
|
|
1084
|
+
.cb-badge-warn{color:#d9a24a;border-color:rgba(220,160,60,.5);background:rgba(220,160,60,.10)}
|
|
1085
|
+
.cb-badge-bad{color:#e07a7a;border-color:rgba(220,80,80,.5);background:rgba(220,80,80,.10)}
|
|
1086
|
+
.cb-stat-box{padding:8px 10px;border-radius:8px}
|
|
1087
|
+
.cb-stat-val{font-size:15px;font-weight:600;line-height:1.4}
|
|
1088
|
+
.cb-stat-lbl{font-size:11px;line-height:1.5}
|
|
1089
|
+
.cb-table{font-size:12px}
|
|
1090
|
+
.cb-table th{padding:5px 8px;border-bottom:1px solid rgba(127,127,127,.18);font-size:11px;font-weight:600;opacity:.8}
|
|
1091
|
+
.cb-table td{padding:5px 8px;border-bottom:1px solid rgba(127,127,127,.18);font-size:12px}
|
|
1092
|
+
.cb-progress-track,.cb-bar-track{height:8px;border-radius:999px;background:rgba(127,127,127,.2)}
|
|
1093
|
+
.cb-progress-bar-fill,.cb-bar-fill,.cb-footFill,.cb-meter-fill{background:#4fbf86}
|
|
1094
|
+
.cb-progress-bar-warn{background:#d9a24a}
|
|
1095
|
+
.cb-progress-bar-bad,.cb-footFillWarn,.cb-meter-fill-warn{background:#e07a7a}
|
|
1096
|
+
.cb-progress-meta-bad,.cb-progress-low,.cb-footPctWarn{color:#e07a7a}
|
|
1097
|
+
.cb-footBar,.cb-meter{background:rgba(127,127,127,.2)}
|
|
1098
|
+
.cb-account-tab,.cb-seg{border:1px solid rgba(127,127,127,.3);border-radius:999px;background:rgba(127,127,127,.1)}
|
|
1099
|
+
.cb-seg{border-radius:7px;overflow:hidden}
|
|
1100
|
+
.cb-seg-item{border:0;border-left:1px solid rgba(127,127,127,.2);border-radius:0;background:transparent}
|
|
1101
|
+
.cb-seg-item:first-child{border-left:0}
|
|
1102
|
+
.cb-seg-item-active,.cb-account-tab-active{border-color:rgba(80,140,255,.6);background:rgba(80,140,255,.18);color:inherit}
|
|
1103
|
+
.cb-tab{border:0;border-radius:0;background:transparent;border-bottom:1.5px solid transparent}
|
|
1104
|
+
.cb-tab-active{border-bottom-color:#4fbf86;background:transparent}
|
|
1105
|
+
.cb-heat-l0{background:rgba(127,127,127,.10)}
|
|
1106
|
+
.cb-heat-l1{background:rgba(79,191,134,.234)}
|
|
1107
|
+
.cb-heat-l2{background:rgba(79,191,134,.382)}
|
|
1108
|
+
.cb-heat-l3{background:rgba(79,191,134,.530)}
|
|
1109
|
+
.cb-heat-l4{background:rgba(79,191,134,.826)}
|
|
1110
|
+
.cb-heat-cell,.cb-heat-swatch{border-radius:3px}
|
|
1111
|
+
.cb-analytics-head,.cb-analytics-row{padding:5px 0;border-bottom:1px solid rgba(127,127,127,.18)}
|
|
1112
|
+
.cb-settings-row,.cb-price-section{border-color:rgba(127,127,127,.18)}
|
|
1113
|
+
.cb-alert-ok{background:rgba(60,180,110,.10);color:#4fbf86}
|
|
1114
|
+
.cb-alert-bad,.cb-alert-err{background:rgba(220,80,80,.10);color:#e07a7a}
|
|
1115
|
+
.cb-banner-warning{background:rgba(220,160,60,.10);border-color:rgba(220,160,60,.5);color:#d9a24a}
|
|
1116
|
+
.cb-banner-exhausted{background:rgba(220,80,80,.10);border-color:rgba(220,80,80,.5);color:#e07a7a}
|
|
1117
|
+
.cb-account-route-row,.cb-model-row,.cb-preview,.cb-account-add{background:rgba(127,127,127,.06)}
|
|
1118
|
+
|
|
619
1119
|
`
|
|
620
1120
|
document.head.appendChild(style)
|
|
621
1121
|
}
|
|
@@ -643,6 +1143,257 @@ async function fetchWithTimeout(url, opts = {}, ms = 30000) {
|
|
|
643
1143
|
}
|
|
644
1144
|
}
|
|
645
1145
|
|
|
1146
|
+
// --- src/client/settings-draft.js ---
|
|
1147
|
+
/**
|
|
1148
|
+
* Staged configuration draft for the ClineBot settings page.
|
|
1149
|
+
*
|
|
1150
|
+
* Fields edited in the form are merged into `pending` and written with one
|
|
1151
|
+
* `PUT /config` when the user presses Save. Account operations remain
|
|
1152
|
+
* immediate and are handled by the page's existing handlers.
|
|
1153
|
+
*/
|
|
1154
|
+
function useStagedConfig({ status, draft, setDraft, load, t, pendingRef }) {
|
|
1155
|
+
const [pending, setPending] = React.useState({})
|
|
1156
|
+
const [saving, setSaving] = React.useState(false)
|
|
1157
|
+
const [saveState, setSaveState] = React.useState('idle')
|
|
1158
|
+
const [saveError, setSaveError] = React.useState('')
|
|
1159
|
+
const savedTimer = React.useRef(null)
|
|
1160
|
+
|
|
1161
|
+
React.useEffect(() => () => {
|
|
1162
|
+
if (savedTimer.current) clearTimeout(savedTimer.current)
|
|
1163
|
+
}, [])
|
|
1164
|
+
|
|
1165
|
+
const validationError = React.useMemo(() => {
|
|
1166
|
+
if (Object.hasOwn(pending, 'baseUrl')) {
|
|
1167
|
+
const raw = String(pending.baseUrl || '').trim()
|
|
1168
|
+
try {
|
|
1169
|
+
const url = new URL(raw)
|
|
1170
|
+
const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1'
|
|
1171
|
+
if (url.protocol !== 'https:' && !local) return t('settings.save_invalid_base_url')
|
|
1172
|
+
} catch {
|
|
1173
|
+
return t('settings.save_invalid_base_url')
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const key of ['timeoutMs', 'smokeTimeoutMs', 'streamIdleTimeoutMs', 'maxPayloadMb']) {
|
|
1177
|
+
if (!Object.hasOwn(pending, key)) continue
|
|
1178
|
+
const value = Number(pending[key])
|
|
1179
|
+
if (!Number.isFinite(value) || value <= 0) return t('settings.save_invalid_number')
|
|
1180
|
+
}
|
|
1181
|
+
return ''
|
|
1182
|
+
}, [pending, t])
|
|
1183
|
+
|
|
1184
|
+
const stage = React.useCallback((patch) => {
|
|
1185
|
+
if (!patch || typeof patch !== 'object') return
|
|
1186
|
+
const next = { ...(pendingRef.current || {}), ...patch }
|
|
1187
|
+
pendingRef.current = next
|
|
1188
|
+
setPending(next)
|
|
1189
|
+
setDraft((current) => ({ ...(current || {}), ...patch }))
|
|
1190
|
+
setSaveState('unsaved')
|
|
1191
|
+
setSaveError('')
|
|
1192
|
+
}, [pendingRef, setDraft])
|
|
1193
|
+
|
|
1194
|
+
const discard = React.useCallback(() => {
|
|
1195
|
+
pendingRef.current = {}
|
|
1196
|
+
setPending({})
|
|
1197
|
+
setDraft(status?.config || {})
|
|
1198
|
+
setSaveState('idle')
|
|
1199
|
+
setSaveError('')
|
|
1200
|
+
}, [pendingRef, setDraft, status])
|
|
1201
|
+
|
|
1202
|
+
const save = React.useCallback(async () => {
|
|
1203
|
+
const patch = pendingRef.current || {}
|
|
1204
|
+
if (Object.keys(patch).length === 0 || saving) return
|
|
1205
|
+
if (validationError) {
|
|
1206
|
+
setSaveState('error')
|
|
1207
|
+
setSaveError(validationError)
|
|
1208
|
+
return
|
|
1209
|
+
}
|
|
1210
|
+
setSaving(true)
|
|
1211
|
+
setSaveState('saving')
|
|
1212
|
+
setSaveError('')
|
|
1213
|
+
try {
|
|
1214
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/config`, {
|
|
1215
|
+
method: 'PUT',
|
|
1216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1217
|
+
body: JSON.stringify({ config: patch }),
|
|
1218
|
+
})
|
|
1219
|
+
const data = await res.json().catch(() => ({}))
|
|
1220
|
+
if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1221
|
+
pendingRef.current = {}
|
|
1222
|
+
setPending({})
|
|
1223
|
+
await load()
|
|
1224
|
+
setSaveState('saved')
|
|
1225
|
+
if (savedTimer.current) clearTimeout(savedTimer.current)
|
|
1226
|
+
savedTimer.current = setTimeout(() => {
|
|
1227
|
+
setSaveState((current) => (current === 'saved' ? 'idle' : current))
|
|
1228
|
+
}, 2500)
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
setSaveState('error')
|
|
1231
|
+
setSaveError(String(error?.message || error))
|
|
1232
|
+
} finally {
|
|
1233
|
+
setSaving(false)
|
|
1234
|
+
}
|
|
1235
|
+
}, [load, pendingRef, saving, validationError])
|
|
1236
|
+
|
|
1237
|
+
return {
|
|
1238
|
+
pending,
|
|
1239
|
+
pendingCount: Object.keys(pending).length,
|
|
1240
|
+
saving,
|
|
1241
|
+
saveState,
|
|
1242
|
+
saveError,
|
|
1243
|
+
validationError,
|
|
1244
|
+
invalid: Boolean(validationError),
|
|
1245
|
+
stage,
|
|
1246
|
+
discard,
|
|
1247
|
+
save,
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// --- src/client/settings-models.js ---
|
|
1252
|
+
/**
|
|
1253
|
+
* Model-picker selection state and debounced persistence.
|
|
1254
|
+
*
|
|
1255
|
+
* Model toggles are immediate by design: the picker is a direct inventory
|
|
1256
|
+
* control, not part of the staged settings form.
|
|
1257
|
+
*/
|
|
1258
|
+
function useModelSelection({ status, draft, setDraft, load, setErr }) {
|
|
1259
|
+
const debounceTimerRef = React.useRef(null)
|
|
1260
|
+
|
|
1261
|
+
React.useEffect(() => () => {
|
|
1262
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
1263
|
+
}, [])
|
|
1264
|
+
|
|
1265
|
+
function debounceSaveDisabledModels(nextDisabled, previousDisabled) {
|
|
1266
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
1267
|
+
debounceTimerRef.current = setTimeout(async () => {
|
|
1268
|
+
try {
|
|
1269
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/models/toggle`, {
|
|
1270
|
+
method: 'POST',
|
|
1271
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1272
|
+
body: JSON.stringify({ disabledModels: nextDisabled }),
|
|
1273
|
+
})
|
|
1274
|
+
const data = await res.json().catch(() => ({}))
|
|
1275
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1276
|
+
await load()
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
console.warn('[dsh-clinebot] Failed saving disabled models:', error)
|
|
1279
|
+
setErr(String(error?.message || error))
|
|
1280
|
+
setDraft((previous) => {
|
|
1281
|
+
if (!previous) return previous
|
|
1282
|
+
const all = status?.availableModels || []
|
|
1283
|
+
const previousEnabled = all.map((model) => model.id).filter((id) => !previousDisabled.includes(id))
|
|
1284
|
+
return { ...previous, disabledModels: previousDisabled, enabledModels: previousEnabled }
|
|
1285
|
+
})
|
|
1286
|
+
}
|
|
1287
|
+
}, 400)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function handleToggleModel(id) {
|
|
1291
|
+
const current = draft?.disabledModels || []
|
|
1292
|
+
const next = current.includes(id)
|
|
1293
|
+
? current.filter((entry) => entry !== id)
|
|
1294
|
+
: [...current, id]
|
|
1295
|
+
const all = status?.availableModels || []
|
|
1296
|
+
const enabled = all.map((model) => model.id).filter((modelId) => !next.includes(modelId))
|
|
1297
|
+
setDraft({ ...(draft || {}), disabledModels: next, enabledModels: enabled })
|
|
1298
|
+
debounceSaveDisabledModels(next, current)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function handleSetModelsFilter(type) {
|
|
1302
|
+
const all = status?.availableModels || []
|
|
1303
|
+
let allowed = new Set()
|
|
1304
|
+
if (type === 'all') allowed = new Set(all.map((model) => model.id))
|
|
1305
|
+
else if (type === 'vision') allowed = new Set(all.filter((model) => model.input?.includes('image') || model.input?.includes('vision')).map((model) => model.id))
|
|
1306
|
+
else if (type === 'coding') allowed = new Set(all.filter((model) => model.category === 'coding').map((model) => model.id))
|
|
1307
|
+
else if (type === 'recommended') allowed = new Set(all.filter((model) => model.recommended).map((model) => model.id))
|
|
1308
|
+
|
|
1309
|
+
const current = draft?.disabledModels || []
|
|
1310
|
+
const nextDisabled = all.map((model) => model.id).filter((id) => !allowed.has(id))
|
|
1311
|
+
const nextEnabled = Array.from(allowed)
|
|
1312
|
+
setDraft({ ...(draft || {}), disabledModels: nextDisabled, enabledModels: nextEnabled })
|
|
1313
|
+
debounceSaveDisabledModels(nextDisabled, current)
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
return { handleToggleModel, handleSetModelsFilter }
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// --- src/client/settings-accounts.js ---
|
|
1320
|
+
/**
|
|
1321
|
+
* Immediate account-pool operations. These are intentionally outside the
|
|
1322
|
+
* staged settings form: adding, pinning, renaming, clearing and deleting an
|
|
1323
|
+
* account commit on their own.
|
|
1324
|
+
*/
|
|
1325
|
+
function createAccountActions({ performAction, load, t, setMsg }) {
|
|
1326
|
+
async function handlePinAccount(accountEnv) {
|
|
1327
|
+
await performAction('pin-account', async () => {
|
|
1328
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/active`, {
|
|
1329
|
+
method: 'POST',
|
|
1330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1331
|
+
body: JSON.stringify({ account: accountEnv }),
|
|
1332
|
+
})
|
|
1333
|
+
const data = await res.json().catch(() => ({}))
|
|
1334
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1335
|
+
await load()
|
|
1336
|
+
})
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async function handleAddAccount({ label, apiKeyEnv, apiKey, id }) {
|
|
1340
|
+
await performAction('add-account', async () => {
|
|
1341
|
+
const endpoint = id ? `${ROUTE_PREFIX}/keys/discovered/import` : `${ROUTE_PREFIX}/accounts`
|
|
1342
|
+
const payload = id ? { id, label, apiKeyEnv } : { label, apiKeyEnv, apiKey }
|
|
1343
|
+
const res = await fetchWithTimeout(endpoint, {
|
|
1344
|
+
method: 'POST',
|
|
1345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1346
|
+
body: JSON.stringify(payload),
|
|
1347
|
+
})
|
|
1348
|
+
const data = await res.json().catch(() => ({}))
|
|
1349
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1350
|
+
setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : (data.validationError || 'Notice') }))
|
|
1351
|
+
await load()
|
|
1352
|
+
})
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
1356
|
+
await performAction('delete-account', async () => {
|
|
1357
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
1358
|
+
method: 'POST',
|
|
1359
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1360
|
+
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
1361
|
+
})
|
|
1362
|
+
const data = await res.json().catch(() => ({}))
|
|
1363
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1364
|
+
await load()
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function handleRenameAccount(accountEnv, label) {
|
|
1369
|
+
await performAction('rename-account', async () => {
|
|
1370
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/rename`, {
|
|
1371
|
+
method: 'POST',
|
|
1372
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1373
|
+
body: JSON.stringify({ apiKeyEnv: accountEnv, label }),
|
|
1374
|
+
})
|
|
1375
|
+
const data = await res.json().catch(() => ({}))
|
|
1376
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1377
|
+
await load()
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async function handleClearKey(accountEnv) {
|
|
1382
|
+
await performAction('clear-key', async () => {
|
|
1383
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/key/clear`, {
|
|
1384
|
+
method: 'POST',
|
|
1385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1386
|
+
body: JSON.stringify({ apiKeyEnv: accountEnv }),
|
|
1387
|
+
})
|
|
1388
|
+
const data = await res.json().catch(() => ({}))
|
|
1389
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1390
|
+
await load()
|
|
1391
|
+
})
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
return { handlePinAccount, handleAddAccount, handleDeleteAccount, handleRenameAccount, handleClearKey }
|
|
1395
|
+
}
|
|
1396
|
+
|
|
646
1397
|
// --- src/client/error-boundary.js ---
|
|
647
1398
|
function createErrorBoundary() {
|
|
648
1399
|
try {
|
|
@@ -663,7 +1414,7 @@ function createErrorBoundary() {
|
|
|
663
1414
|
render() {
|
|
664
1415
|
if (this.state.hasError) {
|
|
665
1416
|
const t = this.props && typeof this.props.t === 'function' ? this.props.t : null
|
|
666
|
-
const errorTitle = t ? t('ui.error_title') : '
|
|
1417
|
+
const errorTitle = t ? t('ui.error_title') : 'ClineBot UI Error: '
|
|
667
1418
|
const retryLabel = t ? t('ui.retry') : 'Retry'
|
|
668
1419
|
return React.createElement(
|
|
669
1420
|
'div',
|
|
@@ -736,128 +1487,343 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
736
1487
|
|
|
737
1488
|
return React.createElement(
|
|
738
1489
|
'div',
|
|
739
|
-
{
|
|
740
|
-
className: 'cb-progress-card cb-bar-container',
|
|
741
|
-
style: {
|
|
742
|
-
display: 'flex',
|
|
743
|
-
flexDirection: 'column',
|
|
744
|
-
gap: '8px',
|
|
745
|
-
padding: '12px 14px',
|
|
746
|
-
borderRadius: '8px',
|
|
747
|
-
border: isBad ? '1px solid var(--dsw-alias-state-error-primary)' : '1px solid var(--dsw-alias-border-l2)',
|
|
748
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
749
|
-
},
|
|
750
|
-
},
|
|
1490
|
+
{ className: isBad ? 'cb-progress-card cb-progress-card-bad' : 'cb-progress-card' },
|
|
751
1491
|
React.createElement(
|
|
752
1492
|
'div',
|
|
753
|
-
{
|
|
754
|
-
className: 'cb-progress-head cb-bar-head',
|
|
755
|
-
style: {
|
|
756
|
-
display: 'flex',
|
|
757
|
-
justifyContent: 'space-between',
|
|
758
|
-
alignItems: 'baseline',
|
|
759
|
-
gap: '16px',
|
|
760
|
-
width: '100%',
|
|
761
|
-
},
|
|
762
|
-
},
|
|
1493
|
+
{ className: 'cb-progress-head' },
|
|
763
1494
|
React.createElement(
|
|
764
1495
|
'span',
|
|
765
|
-
{ className: 'cb-progress-label'
|
|
1496
|
+
{ className: 'cb-progress-label' },
|
|
766
1497
|
label,
|
|
767
|
-
isBad
|
|
1498
|
+
isBad
|
|
1499
|
+
? React.createElement(
|
|
1500
|
+
'span',
|
|
1501
|
+
{ className: 'cb-progress-low' },
|
|
1502
|
+
React.createElement(CbIcon, { name: 'warning', size: 12 }),
|
|
1503
|
+
'<10%'
|
|
1504
|
+
)
|
|
1505
|
+
: null
|
|
768
1506
|
),
|
|
769
1507
|
React.createElement(
|
|
770
1508
|
'span',
|
|
771
|
-
{
|
|
772
|
-
className: 'cb-progress-meta cb-bar-meta',
|
|
773
|
-
style: {
|
|
774
|
-
fontSize: '12px',
|
|
775
|
-
color: isBad ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
776
|
-
whiteSpace: 'nowrap',
|
|
777
|
-
marginLeft: 'auto',
|
|
778
|
-
},
|
|
779
|
-
},
|
|
1509
|
+
{ className: isBad ? 'cb-progress-meta cb-progress-meta-bad' : 'cb-progress-meta' },
|
|
780
1510
|
t('quota.used', { pct }),
|
|
781
|
-
resetCountdown
|
|
1511
|
+
resetCountdown
|
|
1512
|
+
? ` · ${resetCountdown === 'resetting' ? (t('quota.resetting') || '正在重置') : t('quota.countdown', { time: resetCountdown })}`
|
|
1513
|
+
: (resetClock ? ` · ${t('quota.reset_at', { time: resetClock })}` : '')
|
|
782
1514
|
)
|
|
783
1515
|
),
|
|
784
1516
|
React.createElement(
|
|
785
1517
|
'div',
|
|
786
|
-
{
|
|
787
|
-
|
|
788
|
-
style: {
|
|
789
|
-
width: '100%',
|
|
790
|
-
height: '8px',
|
|
791
|
-
borderRadius: '999px',
|
|
792
|
-
background: 'var(--dsw-alias-bg-layer-1)',
|
|
793
|
-
overflow: 'hidden',
|
|
794
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
795
|
-
},
|
|
796
|
-
},
|
|
797
|
-
React.createElement('div', {
|
|
798
|
-
className: barClass,
|
|
799
|
-
style: { width: `${pct}%`, height: '100%', borderRadius: '999px' },
|
|
800
|
-
})
|
|
1518
|
+
{ className: 'cb-progress-track' },
|
|
1519
|
+
React.createElement('div', { className: barClass, style: { width: `${pct}%` } })
|
|
801
1520
|
)
|
|
802
1521
|
)
|
|
803
1522
|
}
|
|
804
1523
|
|
|
805
|
-
// --- src/client/components/
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1524
|
+
// --- src/client/components/settings-layout.js ---
|
|
1525
|
+
/**
|
|
1526
|
+
* Shared settings layout primitives: a titled group, a foldable group, a
|
|
1527
|
+
* title/description/control row, and the floating staged-save bar.
|
|
1528
|
+
*/
|
|
1529
|
+
function SettingsGroup({ title, description, divided = false, action, children }) {
|
|
1530
|
+
return React.createElement(
|
|
1531
|
+
'section',
|
|
1532
|
+
{ className: divided ? 'cb-settings-group cb-settings-group-divided' : 'cb-settings-group', 'aria-label': title },
|
|
1533
|
+
React.createElement(
|
|
1534
|
+
'div',
|
|
1535
|
+
{ className: 'cb-settings-group-head' },
|
|
1536
|
+
React.createElement('h3', { className: 'cb-settings-group-title' }, title),
|
|
1537
|
+
action ? React.createElement('span', { className: 'cb-settings-group-action' }, action) : null
|
|
1538
|
+
),
|
|
1539
|
+
description ? React.createElement('div', { className: 'cb-settings-group-desc' }, description) : null,
|
|
1540
|
+
React.createElement('div', { className: 'cb-settings-group-body' }, children)
|
|
815
1541
|
)
|
|
1542
|
+
}
|
|
816
1543
|
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
const isCurrent = status === 'current'
|
|
820
|
-
const isError = status === 'error' || updateState.latestCheckFailed
|
|
821
|
-
|
|
1544
|
+
function CollapsibleGroup({ title, description, defaultOpen = false, divided = false, badge, children }) {
|
|
1545
|
+
const [open, setOpen] = React.useState(Boolean(defaultOpen))
|
|
822
1546
|
return React.createElement(
|
|
823
|
-
|
|
824
|
-
|
|
1547
|
+
'section',
|
|
1548
|
+
{ className: divided ? 'cb-settings-group cb-settings-group-divided' : 'cb-settings-group', 'aria-label': title },
|
|
825
1549
|
React.createElement(
|
|
826
|
-
'
|
|
1550
|
+
'button',
|
|
827
1551
|
{
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
833
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
834
|
-
justifyContent: 'space-between',
|
|
835
|
-
}
|
|
1552
|
+
type: 'button',
|
|
1553
|
+
className: 'cb-settings-group-head cb-settings-disclosure',
|
|
1554
|
+
'aria-expanded': open,
|
|
1555
|
+
onClick: () => setOpen((value) => !value),
|
|
836
1556
|
},
|
|
1557
|
+
React.createElement('span', { className: 'cb-settings-group-title' }, title),
|
|
1558
|
+
badge !== undefined && badge !== '' ? React.createElement('span', { className: 'cb-badge' }, badge) : null,
|
|
1559
|
+
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
1560
|
+
React.createElement('span', { className: open ? 'cb-settings-chevron cb-settings-chevron-open' : 'cb-settings-chevron', 'aria-hidden': 'true' }, '›')
|
|
1561
|
+
),
|
|
1562
|
+
open && description ? React.createElement('div', { className: 'cb-settings-group-desc' }, description) : null,
|
|
1563
|
+
open ? React.createElement('div', { className: 'cb-settings-group-body' }, children) : null
|
|
1564
|
+
)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function SettingRow({ title, description, control, error, htmlFor }) {
|
|
1568
|
+
return React.createElement(
|
|
1569
|
+
'div',
|
|
1570
|
+
{ className: 'cb-settings-row' },
|
|
1571
|
+
React.createElement(
|
|
1572
|
+
'div',
|
|
1573
|
+
{ className: 'cb-settings-row-text' },
|
|
1574
|
+
htmlFor
|
|
1575
|
+
? React.createElement('label', { className: 'cb-settings-row-title', htmlFor }, title)
|
|
1576
|
+
: React.createElement('span', { className: 'cb-settings-row-title' }, title),
|
|
1577
|
+
error
|
|
1578
|
+
? React.createElement('div', { className: 'cb-settings-row-error' }, error)
|
|
1579
|
+
: (description ? React.createElement('div', { className: 'cb-settings-row-desc' }, description) : null)
|
|
1580
|
+
),
|
|
1581
|
+
control ? React.createElement('div', { className: 'cb-settings-row-control' }, control) : null
|
|
1582
|
+
)
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function SaveBar({ pendingCount, saving, saveState, saveError, invalid, validationError, onSave, onDiscard, t }) {
|
|
1586
|
+
const visible = pendingCount > 0 || saving || saveState === 'saved' || saveState === 'error'
|
|
1587
|
+
if (!visible) return null
|
|
1588
|
+
const message = saveError
|
|
1589
|
+
|| validationError
|
|
1590
|
+
|| (saving
|
|
1591
|
+
? t('settings.saving')
|
|
1592
|
+
: (saveState === 'saved' ? t('settings.saved') : t('settings.unsaved', { count: pendingCount })))
|
|
1593
|
+
return React.createElement(
|
|
1594
|
+
'div',
|
|
1595
|
+
{ className: 'cb-savebar', role: 'status' },
|
|
1596
|
+
React.createElement(
|
|
1597
|
+
'div',
|
|
1598
|
+
{ className: 'cb-savebar-inner' },
|
|
1599
|
+
React.createElement('span', { className: 'cb-savebar-text' }, message),
|
|
1600
|
+
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
837
1601
|
React.createElement(
|
|
838
|
-
'
|
|
839
|
-
{
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1602
|
+
'button',
|
|
1603
|
+
{
|
|
1604
|
+
type: 'button',
|
|
1605
|
+
className: 'cb-btn',
|
|
1606
|
+
disabled: saving || pendingCount === 0,
|
|
1607
|
+
onClick: onDiscard,
|
|
1608
|
+
},
|
|
1609
|
+
t('settings.discard')
|
|
1610
|
+
),
|
|
1611
|
+
React.createElement(
|
|
1612
|
+
'button',
|
|
1613
|
+
{
|
|
1614
|
+
type: 'button',
|
|
1615
|
+
className: 'cb-btn cb-btn-primary',
|
|
1616
|
+
disabled: saving || pendingCount === 0 || invalid,
|
|
1617
|
+
onClick: onSave,
|
|
1618
|
+
},
|
|
1619
|
+
saving ? t('settings.saving') : t('settings.save')
|
|
1620
|
+
)
|
|
1621
|
+
)
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// --- src/client/components/integrations-section.js ---
|
|
1626
|
+
/** Integrations & display settings: surfaces outside the chat request itself. */
|
|
1627
|
+
function IntegrationToggle({ id, label, hint, checked, onChange }) {
|
|
1628
|
+
return React.createElement(SettingRow, {
|
|
1629
|
+
title: label,
|
|
1630
|
+
description: hint,
|
|
1631
|
+
htmlFor: id,
|
|
1632
|
+
control: React.createElement(BooleanControl, {
|
|
1633
|
+
checked: !!checked,
|
|
1634
|
+
label,
|
|
1635
|
+
onChange: (next) => onChange(next),
|
|
1636
|
+
}),
|
|
1637
|
+
})
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function IntegrationsSection({ draft, onStage, transport, t }) {
|
|
1641
|
+
const current = draft || {}
|
|
1642
|
+
return React.createElement(
|
|
1643
|
+
SettingsGroup,
|
|
1644
|
+
{
|
|
1645
|
+
title: t('settings.integrations'),
|
|
1646
|
+
description: t('settings.integrations_desc'),
|
|
1647
|
+
divided: true,
|
|
1648
|
+
},
|
|
1649
|
+
React.createElement(IntegrationToggle, {
|
|
1650
|
+
id: 'cb-web-search',
|
|
1651
|
+
label: t('settings.web_search'),
|
|
1652
|
+
hint: t('settings.web_search_hint'),
|
|
1653
|
+
checked: current.webSearch !== false,
|
|
1654
|
+
onChange: (value) => onStage({ webSearch: value }),
|
|
1655
|
+
}),
|
|
1656
|
+
React.createElement(IntegrationToggle, {
|
|
1657
|
+
id: 'cb-sidebar-quota',
|
|
1658
|
+
label: t('settings.sidebar_quota'),
|
|
1659
|
+
hint: t('settings.sidebar_quota_hint'),
|
|
1660
|
+
checked: current.showSidebarQuota !== false,
|
|
1661
|
+
onChange: (value) => onStage({ showSidebarQuota: value }),
|
|
1662
|
+
}),
|
|
1663
|
+
React.createElement(IntegrationToggle, {
|
|
1664
|
+
id: 'cb-composer-cost',
|
|
1665
|
+
label: t('settings.composer_cost'),
|
|
1666
|
+
hint: t('settings.composer_cost_hint'),
|
|
1667
|
+
checked: current.showComposerCost !== false,
|
|
1668
|
+
onChange: (value) => onStage({ showComposerCost: value }),
|
|
1669
|
+
}),
|
|
1670
|
+
transport === 'pi-ai'
|
|
1671
|
+
? React.createElement(IntegrationToggle, {
|
|
1672
|
+
id: 'cb-proxy-mode',
|
|
1673
|
+
label: t('settings.proxy_mode'),
|
|
1674
|
+
hint: t('settings.proxy_mode_hint'),
|
|
1675
|
+
checked: current.proxyMode !== false,
|
|
1676
|
+
onChange: (value) => onStage({ proxyMode: value }),
|
|
1677
|
+
})
|
|
1678
|
+
: null
|
|
1679
|
+
)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// --- src/client/components/advanced-section.js ---
|
|
1683
|
+
/** Advanced connection, timeout and storage settings. */
|
|
1684
|
+
function AdvancedField({ id, label, hint, value, type, onChange }) {
|
|
1685
|
+
return React.createElement(SettingRow, {
|
|
1686
|
+
title: label,
|
|
1687
|
+
description: hint,
|
|
1688
|
+
htmlFor: id,
|
|
1689
|
+
control: React.createElement('input', {
|
|
1690
|
+
id,
|
|
1691
|
+
className: 'cb-input cb-settings-input',
|
|
1692
|
+
type: type || 'text',
|
|
1693
|
+
value: value === undefined || value === null ? '' : String(value),
|
|
1694
|
+
onChange: (event) => onChange(event.target.value),
|
|
1695
|
+
}),
|
|
1696
|
+
})
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function AdvancedSection({ draft, onStage, transport, t }) {
|
|
1700
|
+
const current = draft || {}
|
|
1701
|
+
const numeric = (key, minimum) => (raw) => {
|
|
1702
|
+
if (raw === '') return
|
|
1703
|
+
const value = Number(raw)
|
|
1704
|
+
if (Number.isFinite(value) && value > minimum) onStage({ [key]: value })
|
|
1705
|
+
}
|
|
1706
|
+
return React.createElement(
|
|
1707
|
+
CollapsibleGroup,
|
|
1708
|
+
{
|
|
1709
|
+
title: t('settings.advanced'),
|
|
1710
|
+
description: t('settings.advanced_desc'),
|
|
1711
|
+
divided: true,
|
|
1712
|
+
defaultOpen: false,
|
|
1713
|
+
},
|
|
1714
|
+
React.createElement(AdvancedField, {
|
|
1715
|
+
id: 'cb-base-url',
|
|
1716
|
+
label: t('settings.base_url'),
|
|
1717
|
+
hint: t('settings.base_url_hint'),
|
|
1718
|
+
value: current.baseUrl,
|
|
1719
|
+
onChange: (value) => onStage({ baseUrl: value }),
|
|
1720
|
+
}),
|
|
1721
|
+
React.createElement(AdvancedField, {
|
|
1722
|
+
id: 'cb-timeout',
|
|
1723
|
+
label: t('settings.timeout'),
|
|
1724
|
+
hint: t('settings.timeout_hint'),
|
|
1725
|
+
value: current.timeoutMs,
|
|
1726
|
+
type: 'number',
|
|
1727
|
+
onChange: numeric('timeoutMs', 0),
|
|
1728
|
+
}),
|
|
1729
|
+
React.createElement(AdvancedField, {
|
|
1730
|
+
id: 'cb-smoke-timeout',
|
|
1731
|
+
label: t('settings.smoke_timeout'),
|
|
1732
|
+
hint: t('settings.smoke_timeout_hint'),
|
|
1733
|
+
value: current.smokeTimeoutMs,
|
|
1734
|
+
type: 'number',
|
|
1735
|
+
onChange: numeric('smokeTimeoutMs', 0),
|
|
1736
|
+
}),
|
|
1737
|
+
React.createElement(AdvancedField, {
|
|
1738
|
+
id: 'cb-stream-idle',
|
|
1739
|
+
label: t('settings.stream_idle'),
|
|
1740
|
+
hint: t('settings.stream_idle_hint'),
|
|
1741
|
+
value: current.streamIdleTimeoutMs,
|
|
1742
|
+
type: 'number',
|
|
1743
|
+
onChange: numeric('streamIdleTimeoutMs', 0),
|
|
1744
|
+
}),
|
|
1745
|
+
React.createElement(AdvancedField, {
|
|
1746
|
+
id: 'cb-max-payload',
|
|
1747
|
+
label: t('settings.max_payload'),
|
|
1748
|
+
hint: t('settings.max_payload_hint'),
|
|
1749
|
+
value: current.maxPayloadMb,
|
|
1750
|
+
type: 'number',
|
|
1751
|
+
onChange: numeric('maxPayloadMb', 0),
|
|
1752
|
+
}),
|
|
1753
|
+
React.createElement(AdvancedField, {
|
|
1754
|
+
id: 'cb-stats-path',
|
|
1755
|
+
label: t('settings.stats_path'),
|
|
1756
|
+
hint: t('settings.stats_path_hint'),
|
|
1757
|
+
value: current.statsPath,
|
|
1758
|
+
onChange: (value) => onStage({ statsPath: value }),
|
|
1759
|
+
}),
|
|
1760
|
+
React.createElement(AdvancedField, {
|
|
1761
|
+
id: 'cb-models-cache-path',
|
|
1762
|
+
label: t('settings.models_cache_path'),
|
|
1763
|
+
hint: t('settings.models_cache_path_hint'),
|
|
1764
|
+
value: current.modelsCachePath,
|
|
1765
|
+
onChange: (value) => onStage({ modelsCachePath: value }),
|
|
1766
|
+
}),
|
|
1767
|
+
React.createElement(SettingRow, {
|
|
1768
|
+
title: t('settings.transport'),
|
|
1769
|
+
description: t('settings.transport_hint'),
|
|
1770
|
+
htmlFor: 'cb-transport',
|
|
1771
|
+
control: React.createElement(
|
|
1772
|
+
'select',
|
|
1773
|
+
{
|
|
1774
|
+
id: 'cb-transport',
|
|
1775
|
+
className: 'cb-select',
|
|
1776
|
+
value: current.transport === 'pi-ai' ? 'pi-ai' : 'adapter',
|
|
1777
|
+
onChange: (event) => onStage({ transport: event.target.value }),
|
|
1778
|
+
},
|
|
1779
|
+
React.createElement('option', { value: 'adapter' }, 'Native adapter'),
|
|
1780
|
+
React.createElement('option', { value: 'pi-ai' }, 'Legacy llm-pi-ai + proxy')
|
|
1781
|
+
),
|
|
1782
|
+
})
|
|
1783
|
+
)
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// --- src/client/components/update-banner.js ---
|
|
1787
|
+
function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }) {
|
|
1788
|
+
const status = updateState.status || (
|
|
1789
|
+
updateState.checking
|
|
1790
|
+
? 'checking'
|
|
1791
|
+
: (updateState.latestCheckFailed
|
|
1792
|
+
? 'error'
|
|
1793
|
+
: (updateState.updateAvailable
|
|
1794
|
+
? 'available'
|
|
1795
|
+
: (updateState.lastCheckedAt ? 'current' : 'idle')))
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
const isChecking = status === 'checking' || updateState.checking
|
|
1799
|
+
const isAvailable = status === 'available' || updateState.updateAvailable
|
|
1800
|
+
const isCurrent = status === 'current'
|
|
1801
|
+
const isError = status === 'error' || updateState.latestCheckFailed
|
|
1802
|
+
|
|
1803
|
+
return React.createElement(
|
|
1804
|
+
React.Fragment,
|
|
1805
|
+
null,
|
|
1806
|
+
React.createElement(
|
|
1807
|
+
'div',
|
|
1808
|
+
{ className: 'cb-update-banner' },
|
|
1809
|
+
React.createElement(
|
|
1810
|
+
'div',
|
|
1811
|
+
{ className: 'cb-update-info' },
|
|
1812
|
+
updateState.currentVersion
|
|
1813
|
+
? React.createElement('span', { className: 'cb-update-version' }, `v${updateState.currentVersion}`)
|
|
844
1814
|
: null,
|
|
845
1815
|
isChecking
|
|
846
|
-
? React.createElement('span', {
|
|
847
|
-
t('update.checking')
|
|
848
|
-
)
|
|
1816
|
+
? React.createElement('span', { className: 'cb-muted' }, t('update.checking'))
|
|
849
1817
|
: isAvailable
|
|
850
|
-
? React.createElement(
|
|
1818
|
+
? React.createElement(
|
|
1819
|
+
'span',
|
|
1820
|
+
{ className: 'cb-badge cb-badge-warn' },
|
|
851
1821
|
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
852
1822
|
)
|
|
853
1823
|
: isCurrent
|
|
854
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
855
|
-
'✓ ' + t('update.up_to_date')
|
|
856
|
-
)
|
|
1824
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('update.up_to_date'))
|
|
857
1825
|
: isError
|
|
858
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-bad' },
|
|
859
|
-
t('update.check_failed')
|
|
860
|
-
)
|
|
1826
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-bad' }, t('update.check_failed'))
|
|
861
1827
|
: null
|
|
862
1828
|
),
|
|
863
1829
|
isAvailable
|
|
@@ -868,7 +1834,6 @@ function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }
|
|
|
868
1834
|
className: 'cb-btn cb-btn-primary',
|
|
869
1835
|
disabled: updateState.updating,
|
|
870
1836
|
onClick: handleTriggerUpdate,
|
|
871
|
-
style: { padding: '5px 12px', fontSize: '12px' }
|
|
872
1837
|
},
|
|
873
1838
|
updateState.updating ? t('update.updating') : t('update.btn')
|
|
874
1839
|
)
|
|
@@ -880,13 +1845,12 @@ function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }
|
|
|
880
1845
|
className: 'cb-btn',
|
|
881
1846
|
disabled: isChecking,
|
|
882
1847
|
onClick: handleCheckUpdate,
|
|
883
|
-
style: { padding: '5px 12px', fontSize: '12px' }
|
|
884
1848
|
},
|
|
885
1849
|
t('update.retry')
|
|
886
1850
|
)
|
|
887
1851
|
: null
|
|
888
1852
|
),
|
|
889
|
-
updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' },
|
|
1853
|
+
updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, updateState.notice) : null,
|
|
890
1854
|
updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
|
|
891
1855
|
)
|
|
892
1856
|
}
|
|
@@ -894,7 +1858,7 @@ function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }
|
|
|
894
1858
|
// --- src/client/components/key-section.js ---
|
|
895
1859
|
function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, verifyStatus, t }) {
|
|
896
1860
|
return React.createElement(
|
|
897
|
-
'
|
|
1861
|
+
'section',
|
|
898
1862
|
{ className: 'cb-section-card' },
|
|
899
1863
|
React.createElement('div', { className: 'cb-section-title' }, t('key.title')),
|
|
900
1864
|
React.createElement('div', { className: 'cb-section-desc' }, t('key.desc')),
|
|
@@ -906,15 +1870,11 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
906
1870
|
type: showKey ? 'text' : 'password',
|
|
907
1871
|
placeholder: keyPresent ? t('key.placeholder_has') : t('key.placeholder_empty'),
|
|
908
1872
|
value: apiKeyInput,
|
|
909
|
-
onChange: (
|
|
1873
|
+
onChange: (event) => setApiKeyInput(event.target.value),
|
|
910
1874
|
}),
|
|
911
1875
|
React.createElement(
|
|
912
1876
|
'button',
|
|
913
|
-
{
|
|
914
|
-
type: 'button',
|
|
915
|
-
className: 'cb-btn',
|
|
916
|
-
onClick: () => setShowKey((v) => !v),
|
|
917
|
-
},
|
|
1877
|
+
{ type: 'button', className: 'cb-btn', onClick: () => setShowKey((value) => !value) },
|
|
918
1878
|
showKey ? t('key.hide') : t('key.show')
|
|
919
1879
|
),
|
|
920
1880
|
React.createElement(
|
|
@@ -929,37 +1889,32 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
929
1889
|
),
|
|
930
1890
|
React.createElement(
|
|
931
1891
|
'button',
|
|
932
|
-
{
|
|
933
|
-
type: 'button',
|
|
934
|
-
className: 'cb-btn',
|
|
935
|
-
disabled: !!busy,
|
|
936
|
-
onClick: handleFastLogin,
|
|
937
|
-
},
|
|
1892
|
+
{ type: 'button', className: 'cb-btn', disabled: !!busy, onClick: handleFastLogin },
|
|
938
1893
|
t('key.login_fast')
|
|
939
1894
|
)
|
|
940
1895
|
),
|
|
941
1896
|
verifyStatus && verifyStatus.state === 'verifying'
|
|
942
|
-
? React.createElement('div', {
|
|
1897
|
+
? React.createElement('div', { className: 'cb-hint' }, t('key.verifying'))
|
|
943
1898
|
: null,
|
|
944
1899
|
verifyStatus && verifyStatus.state === 'valid'
|
|
945
|
-
? React.createElement('div', { className: 'cb-alert-ok
|
|
1900
|
+
? React.createElement('div', { className: 'cb-alert-ok cb-key-status' }, t('key.verified', { email: verifyStatus.email, plan: verifyStatus.plan }))
|
|
946
1901
|
: null,
|
|
947
1902
|
verifyStatus && verifyStatus.state === 'invalid'
|
|
948
|
-
? React.createElement('div', { className: 'cb-alert-bad
|
|
1903
|
+
? React.createElement('div', { className: 'cb-alert-bad cb-key-status' }, t('key.invalid', { error: verifyStatus.error }))
|
|
949
1904
|
: null,
|
|
950
1905
|
React.createElement(
|
|
951
1906
|
'div',
|
|
952
|
-
{ className: 'cb-
|
|
1907
|
+
{ className: 'cb-key-meta' },
|
|
953
1908
|
React.createElement('span', null, t('key.env_label')),
|
|
954
|
-
React.createElement('code',
|
|
955
|
-
React.createElement('span',
|
|
1909
|
+
React.createElement('code', { className: 'cb-mono' }, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
|
|
1910
|
+
React.createElement('span', { className: 'cb-muted' }, '·'),
|
|
956
1911
|
React.createElement(
|
|
957
1912
|
'a',
|
|
958
1913
|
{
|
|
959
1914
|
href: 'https://app.cline.bot/settings/api-keys',
|
|
960
1915
|
target: '_blank',
|
|
961
1916
|
rel: 'noreferrer',
|
|
962
|
-
|
|
1917
|
+
className: 'cb-link',
|
|
963
1918
|
},
|
|
964
1919
|
t('key.get_key')
|
|
965
1920
|
)
|
|
@@ -968,188 +1923,201 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
968
1923
|
}
|
|
969
1924
|
|
|
970
1925
|
// --- src/client/components/accounts-section.js ---
|
|
1926
|
+
/**
|
|
1927
|
+
* Accounts pool: one calm row per account, with live quota, status dot and a
|
|
1928
|
+
* compact action menu. Account mutations commit immediately; the rest of the
|
|
1929
|
+
* settings page uses the staged save bar.
|
|
1930
|
+
*/
|
|
971
1931
|
function AccountsSection({
|
|
972
1932
|
status,
|
|
973
1933
|
busy,
|
|
974
1934
|
handlePinAccount,
|
|
975
1935
|
handleAddAccount,
|
|
976
1936
|
handleDeleteAccount,
|
|
1937
|
+
handleRenameAccount,
|
|
1938
|
+
handleClearKey,
|
|
977
1939
|
accountMode,
|
|
978
1940
|
handleUpdateAccountMode,
|
|
979
1941
|
modelRoutes = {},
|
|
980
1942
|
onUpdateModelRoutes,
|
|
981
1943
|
t,
|
|
982
1944
|
}) {
|
|
983
|
-
if (!status.accounts || !status.accounts.length) return null
|
|
984
|
-
|
|
985
1945
|
const [showAdd, setShowAdd] = React.useState(false)
|
|
986
1946
|
const [newLabel, setNewLabel] = React.useState('')
|
|
987
1947
|
const [newEnv, setNewEnv] = React.useState('')
|
|
988
1948
|
const [newKey, setNewKey] = React.useState('')
|
|
989
1949
|
const [showKey, setShowKey] = React.useState(false)
|
|
990
|
-
const [
|
|
1950
|
+
const [discoveredKeys, setDiscoveredKeys] = React.useState([])
|
|
1951
|
+
const [usageByAccount, setUsageByAccount] = React.useState({})
|
|
1952
|
+
const [menuFor, setMenuFor] = React.useState('')
|
|
1953
|
+
const [renameFor, setRenameFor] = React.useState('')
|
|
1954
|
+
const [renameValue, setRenameValue] = React.useState('')
|
|
1955
|
+
const [confirmAction, setConfirmAction] = React.useState(null)
|
|
991
1956
|
const [routeModel, setRouteModel] = React.useState('')
|
|
992
1957
|
const [routeAccount, setRouteAccount] = React.useState('')
|
|
993
|
-
const [discoveredKeys, setDiscoveredKeys] = React.useState([])
|
|
994
1958
|
|
|
995
1959
|
React.useEffect(() => {
|
|
996
1960
|
let active = true
|
|
997
1961
|
fetchWithTimeout(`${ROUTE_PREFIX}/keys/discovered`, { cache: 'no-store' })
|
|
998
|
-
.then((
|
|
1962
|
+
.then((response) => response.json())
|
|
999
1963
|
.then((data) => {
|
|
1000
|
-
if (active && data?.ok && Array.isArray(data.discovered))
|
|
1001
|
-
setDiscoveredKeys(data.discovered)
|
|
1002
|
-
}
|
|
1964
|
+
if (active && data?.ok && Array.isArray(data.discovered)) setDiscoveredKeys(data.discovered)
|
|
1003
1965
|
})
|
|
1004
1966
|
.catch(() => {})
|
|
1005
1967
|
return () => { active = false }
|
|
1006
1968
|
}, [])
|
|
1007
1969
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (typeof handleAddAccount === 'function') {
|
|
1019
|
-
await handleAddAccount({
|
|
1020
|
-
label: item.source || targetEnv,
|
|
1021
|
-
apiKeyEnv: targetEnv,
|
|
1022
|
-
id: item.id,
|
|
1970
|
+
const accountSignature = (status?.accounts || []).map((account) => account.apiKeyEnv || account.id).join('|')
|
|
1971
|
+
React.useEffect(() => {
|
|
1972
|
+
let active = true
|
|
1973
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/accounts/usage`, { cache: 'no-store' })
|
|
1974
|
+
.then((response) => response.json())
|
|
1975
|
+
.then((data) => {
|
|
1976
|
+
if (!active || !data?.ok || !Array.isArray(data.accounts)) return
|
|
1977
|
+
const next = {}
|
|
1978
|
+
for (const entry of data.accounts) next[entry.apiKeyEnv] = entry
|
|
1979
|
+
setUsageByAccount(next)
|
|
1023
1980
|
})
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1981
|
+
.catch(() => {})
|
|
1982
|
+
return () => { active = false }
|
|
1983
|
+
}, [accountSignature, status?.activeAccount, busy])
|
|
1027
1984
|
|
|
1028
|
-
function
|
|
1985
|
+
function nextEnvName() {
|
|
1029
1986
|
let max = 1
|
|
1030
|
-
for (const
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
const n = parseInt(m[1], 10)
|
|
1034
|
-
if (n >= max) max = n + 1
|
|
1035
|
-
}
|
|
1987
|
+
for (const account of status.accounts || []) {
|
|
1988
|
+
const match = String(account.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
1989
|
+
if (match) max = Math.max(max, Number(match[1]) + 1)
|
|
1036
1990
|
}
|
|
1037
|
-
|
|
1991
|
+
return `CLINEBOT_API_KEY_${max}`
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function openAdd() {
|
|
1995
|
+
setNewEnv(nextEnvName())
|
|
1038
1996
|
setShowAdd(true)
|
|
1039
1997
|
}
|
|
1040
1998
|
|
|
1041
|
-
async function
|
|
1042
|
-
if (
|
|
1043
|
-
if (!newKey.trim()) return
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
})
|
|
1050
|
-
}
|
|
1999
|
+
async function submitAdd(event) {
|
|
2000
|
+
if (event?.preventDefault) event.preventDefault()
|
|
2001
|
+
if (!newKey.trim() || typeof handleAddAccount !== 'function') return
|
|
2002
|
+
await handleAddAccount({
|
|
2003
|
+
label: newLabel.trim() || newEnv.trim(),
|
|
2004
|
+
apiKeyEnv: newEnv.trim(),
|
|
2005
|
+
apiKey: newKey.trim(),
|
|
2006
|
+
})
|
|
1051
2007
|
setNewLabel('')
|
|
1052
2008
|
setNewEnv('')
|
|
1053
2009
|
setNewKey('')
|
|
1054
2010
|
setShowAdd(false)
|
|
1055
2011
|
}
|
|
1056
2012
|
|
|
1057
|
-
async function
|
|
1058
|
-
if (typeof
|
|
1059
|
-
|
|
2013
|
+
async function importDiscovered(item) {
|
|
2014
|
+
if (typeof handleAddAccount !== 'function') return
|
|
2015
|
+
await handleAddAccount({
|
|
2016
|
+
label: item.source || nextEnvName(),
|
|
2017
|
+
apiKeyEnv: nextEnvName(),
|
|
2018
|
+
id: item.id,
|
|
2019
|
+
})
|
|
2020
|
+
setDiscoveredKeys((previous) => previous.filter((entry) => entry.id !== item.id))
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function openRename(account) {
|
|
2024
|
+
setMenuFor('')
|
|
2025
|
+
setRenameFor(account.apiKeyEnv)
|
|
2026
|
+
setRenameValue(account.label || '')
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
async function submitRename(account) {
|
|
2030
|
+
if (typeof handleRenameAccount === 'function') {
|
|
2031
|
+
await handleRenameAccount(account.apiKeyEnv, renameValue.trim() || account.apiKeyEnv)
|
|
1060
2032
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
2033
|
+
setRenameFor('')
|
|
2034
|
+
setRenameValue('')
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
async function runConfirm() {
|
|
2038
|
+
if (!confirmAction) return
|
|
2039
|
+
if (confirmAction.type === 'delete' && typeof handleDeleteAccount === 'function') {
|
|
2040
|
+
await handleDeleteAccount(confirmAction.env, true)
|
|
2041
|
+
}
|
|
2042
|
+
if (confirmAction.type === 'clear' && typeof handleClearKey === 'function') {
|
|
2043
|
+
await handleClearKey(confirmAction.env)
|
|
1063
2044
|
}
|
|
2045
|
+
setConfirmAction(null)
|
|
1064
2046
|
}
|
|
1065
2047
|
|
|
2048
|
+
function dotClass(account, live) {
|
|
2049
|
+
if (!account.present) return 'cb-status-dot cb-status-dot-warn'
|
|
2050
|
+
if (live?.error) return 'cb-status-dot cb-status-dot-bad'
|
|
2051
|
+
const percent = live?.usage?.windows?.fiveHour?.percentUsed
|
|
2052
|
+
if (typeof percent === 'number' && percent >= 95) return 'cb-status-dot cb-status-dot-bad'
|
|
2053
|
+
if (account.isPinned || status.activeAccount === account.apiKeyEnv) return 'cb-status-dot cb-status-dot-ok'
|
|
2054
|
+
return 'cb-status-dot cb-status-dot-idle'
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
const accounts = status.accounts || []
|
|
2058
|
+
|
|
1066
2059
|
return React.createElement(
|
|
1067
|
-
'
|
|
1068
|
-
{ className: 'cb-section-card' },
|
|
2060
|
+
'section',
|
|
2061
|
+
{ className: 'cb-section-card cb-accounts' },
|
|
1069
2062
|
React.createElement(
|
|
1070
2063
|
'div',
|
|
1071
|
-
{ className: 'cb-section-
|
|
1072
|
-
React.createElement('
|
|
2064
|
+
{ className: 'cb-section-head' },
|
|
2065
|
+
React.createElement('div', { className: 'cb-section-title' }, t('accounts.title')),
|
|
1073
2066
|
!showAdd
|
|
1074
2067
|
? React.createElement(
|
|
1075
2068
|
'button',
|
|
1076
|
-
{
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
style: { fontSize: '11px', padding: '4px 10px' },
|
|
1080
|
-
onClick: handleOpenAdd,
|
|
1081
|
-
disabled: !!busy,
|
|
1082
|
-
},
|
|
1083
|
-
t('accounts.add_btn')
|
|
2069
|
+
{ type: 'button', className: 'cb-btn cb-btn-primary', disabled: !!busy, onClick: openAdd },
|
|
2070
|
+
React.createElement(CbIcon, { name: 'plus', size: 13 }),
|
|
2071
|
+
` ${t('accounts.add_btn')}`
|
|
1084
2072
|
)
|
|
1085
2073
|
: null
|
|
1086
2074
|
),
|
|
1087
2075
|
React.createElement('div', { className: 'cb-section-desc' }, t('accounts.desc')),
|
|
2076
|
+
|
|
1088
2077
|
discoveredKeys.length > 0
|
|
1089
2078
|
? React.createElement(
|
|
1090
2079
|
'div',
|
|
1091
|
-
{ className: 'cb-
|
|
1092
|
-
React.createElement(
|
|
1093
|
-
|
|
2080
|
+
{ className: 'cb-discovered' },
|
|
2081
|
+
React.createElement(
|
|
2082
|
+
'div',
|
|
2083
|
+
{ className: 'cb-discovered-head' },
|
|
2084
|
+
React.createElement(CbIcon, { name: 'key', size: 14 }),
|
|
2085
|
+
` ${t('discovery.title')}`
|
|
2086
|
+
),
|
|
2087
|
+
discoveredKeys.map((item) => React.createElement(
|
|
2088
|
+
'div',
|
|
2089
|
+
{ className: 'cb-discovered-row', key: item.id },
|
|
2090
|
+
React.createElement('span', { className: 'cb-mono' }, item.maskedKey || item.source),
|
|
2091
|
+
React.createElement('span', { className: 'cb-muted' }, item.source || ''),
|
|
1094
2092
|
React.createElement(
|
|
1095
|
-
'
|
|
1096
|
-
{
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
disabled: !!busy,
|
|
1105
|
-
onClick: () => handleImportDiscovered(item),
|
|
1106
|
-
},
|
|
1107
|
-
t('discovery.import_btn', { source: item.source })
|
|
1108
|
-
)
|
|
2093
|
+
'button',
|
|
2094
|
+
{
|
|
2095
|
+
type: 'button',
|
|
2096
|
+
className: 'cb-btn',
|
|
2097
|
+
disabled: !!busy,
|
|
2098
|
+
onClick: () => importDiscovered(item),
|
|
2099
|
+
},
|
|
2100
|
+
React.createElement(CbIcon, { name: 'download', size: 12 }),
|
|
2101
|
+
` ${t('discovery.import_btn', { source: item.source || '' })}`
|
|
1109
2102
|
)
|
|
1110
|
-
)
|
|
2103
|
+
))
|
|
1111
2104
|
)
|
|
1112
2105
|
: null,
|
|
2106
|
+
|
|
1113
2107
|
React.createElement(
|
|
1114
2108
|
'div',
|
|
1115
|
-
{
|
|
1116
|
-
|
|
1117
|
-
display: 'flex',
|
|
1118
|
-
alignItems: 'center',
|
|
1119
|
-
gap: '8px',
|
|
1120
|
-
margin: '8px 0 12px 0',
|
|
1121
|
-
fontSize: '12px',
|
|
1122
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
1123
|
-
},
|
|
1124
|
-
},
|
|
1125
|
-
React.createElement('span', { style: { fontWeight: 500 } }, t('accounts.mode_label') + ':'),
|
|
2109
|
+
{ className: 'cb-account-mode' },
|
|
2110
|
+
React.createElement('span', { className: 'cb-muted' }, t('accounts.mode_label')),
|
|
1126
2111
|
React.createElement(
|
|
1127
2112
|
'div',
|
|
1128
|
-
{
|
|
1129
|
-
style: {
|
|
1130
|
-
display: 'inline-flex',
|
|
1131
|
-
borderRadius: '6px',
|
|
1132
|
-
border: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 12%, transparent))',
|
|
1133
|
-
overflow: 'hidden',
|
|
1134
|
-
},
|
|
1135
|
-
},
|
|
2113
|
+
{ className: 'cb-seg' },
|
|
1136
2114
|
React.createElement(
|
|
1137
2115
|
'button',
|
|
1138
2116
|
{
|
|
1139
2117
|
type: 'button',
|
|
1140
|
-
className: 'cb-
|
|
1141
|
-
style: {
|
|
1142
|
-
borderRadius: 0,
|
|
1143
|
-
fontSize: '11px',
|
|
1144
|
-
padding: '3px 9px',
|
|
1145
|
-
border: 'none',
|
|
1146
|
-
background: (accountMode || 'least_used') === 'least_used'
|
|
1147
|
-
? 'var(--dsw-alias-interactive-bg-active, color-mix(in srgb, currentColor 14%, transparent))'
|
|
1148
|
-
: 'transparent',
|
|
1149
|
-
fontWeight: (accountMode || 'least_used') === 'least_used' ? 600 : 400,
|
|
1150
|
-
},
|
|
1151
|
-
onClick: () => typeof handleUpdateAccountMode === 'function' && handleUpdateAccountMode('least_used'),
|
|
2118
|
+
className: (accountMode || 'least_used') === 'least_used' ? 'cb-seg-item cb-seg-item-active' : 'cb-seg-item',
|
|
1152
2119
|
disabled: !!busy,
|
|
2120
|
+
onClick: () => handleUpdateAccountMode && handleUpdateAccountMode('least_used'),
|
|
1153
2121
|
},
|
|
1154
2122
|
t('accounts.mode_least_used')
|
|
1155
2123
|
),
|
|
@@ -1157,20 +2125,9 @@ function AccountsSection({
|
|
|
1157
2125
|
'button',
|
|
1158
2126
|
{
|
|
1159
2127
|
type: 'button',
|
|
1160
|
-
className: 'cb-
|
|
1161
|
-
style: {
|
|
1162
|
-
borderRadius: 0,
|
|
1163
|
-
fontSize: '11px',
|
|
1164
|
-
padding: '3px 9px',
|
|
1165
|
-
border: 'none',
|
|
1166
|
-
borderLeft: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 12%, transparent))',
|
|
1167
|
-
background: accountMode === 'round_robin'
|
|
1168
|
-
? 'var(--dsw-alias-interactive-bg-active, color-mix(in srgb, currentColor 14%, transparent))'
|
|
1169
|
-
: 'transparent',
|
|
1170
|
-
fontWeight: accountMode === 'round_robin' ? 600 : 400,
|
|
1171
|
-
},
|
|
1172
|
-
onClick: () => typeof handleUpdateAccountMode === 'function' && handleUpdateAccountMode('round_robin'),
|
|
2128
|
+
className: accountMode === 'round_robin' ? 'cb-seg-item cb-seg-item-active' : 'cb-seg-item',
|
|
1173
2129
|
disabled: !!busy,
|
|
2130
|
+
onClick: () => handleUpdateAccountMode && handleUpdateAccountMode('round_robin'),
|
|
1174
2131
|
},
|
|
1175
2132
|
t('accounts.mode_round_robin')
|
|
1176
2133
|
)
|
|
@@ -1180,72 +2137,47 @@ function AccountsSection({
|
|
|
1180
2137
|
showAdd
|
|
1181
2138
|
? React.createElement(
|
|
1182
2139
|
'form',
|
|
1183
|
-
{
|
|
1184
|
-
onSubmit: onSubmitAdd,
|
|
1185
|
-
style: {
|
|
1186
|
-
background: 'var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
|
|
1187
|
-
border: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 12%, transparent))',
|
|
1188
|
-
borderRadius: '8px',
|
|
1189
|
-
padding: '12px',
|
|
1190
|
-
marginBottom: '14px',
|
|
1191
|
-
display: 'flex',
|
|
1192
|
-
flexDirection: 'column',
|
|
1193
|
-
gap: '8px',
|
|
1194
|
-
},
|
|
1195
|
-
},
|
|
2140
|
+
{ className: 'cb-account-add', onSubmit: submitAdd },
|
|
1196
2141
|
React.createElement(
|
|
1197
2142
|
'div',
|
|
1198
|
-
{
|
|
2143
|
+
{ className: 'cb-account-add-row' },
|
|
1199
2144
|
React.createElement('input', {
|
|
1200
2145
|
type: 'text',
|
|
1201
2146
|
className: 'cb-input',
|
|
1202
|
-
style: { flex: '1 1 180px' },
|
|
1203
2147
|
placeholder: t('accounts.label_placeholder'),
|
|
1204
2148
|
value: newLabel,
|
|
1205
|
-
onChange: (
|
|
2149
|
+
onChange: (event) => setNewLabel(event.target.value),
|
|
1206
2150
|
}),
|
|
1207
2151
|
React.createElement('input', {
|
|
1208
2152
|
type: 'text',
|
|
1209
2153
|
className: 'cb-input',
|
|
1210
|
-
style: { flex: '1 1 180px' },
|
|
1211
2154
|
placeholder: t('accounts.env_placeholder'),
|
|
1212
2155
|
value: newEnv,
|
|
1213
|
-
onChange: (
|
|
2156
|
+
onChange: (event) => setNewEnv(event.target.value),
|
|
1214
2157
|
})
|
|
1215
2158
|
),
|
|
1216
2159
|
React.createElement(
|
|
1217
2160
|
'div',
|
|
1218
|
-
{
|
|
2161
|
+
{ className: 'cb-account-add-row' },
|
|
1219
2162
|
React.createElement('input', {
|
|
1220
2163
|
type: showKey ? 'text' : 'password',
|
|
1221
2164
|
className: 'cb-input',
|
|
1222
|
-
style: { flex: '1' },
|
|
1223
2165
|
placeholder: t('key.placeholder_empty'),
|
|
1224
2166
|
value: newKey,
|
|
1225
|
-
onChange: (
|
|
2167
|
+
onChange: (event) => setNewKey(event.target.value),
|
|
1226
2168
|
}),
|
|
1227
2169
|
React.createElement(
|
|
1228
2170
|
'button',
|
|
1229
|
-
{
|
|
1230
|
-
type: 'button',
|
|
1231
|
-
className: 'cb-btn',
|
|
1232
|
-
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1233
|
-
onClick: () => setShowKey(!showKey),
|
|
1234
|
-
},
|
|
2171
|
+
{ type: 'button', className: 'cb-btn', onClick: () => setShowKey((value) => !value) },
|
|
1235
2172
|
showKey ? t('key.hide') : t('key.show')
|
|
1236
2173
|
)
|
|
1237
2174
|
),
|
|
1238
2175
|
React.createElement(
|
|
1239
2176
|
'div',
|
|
1240
|
-
{
|
|
2177
|
+
{ className: 'cb-account-add-actions' },
|
|
1241
2178
|
React.createElement(
|
|
1242
2179
|
'button',
|
|
1243
|
-
{
|
|
1244
|
-
type: 'button',
|
|
1245
|
-
className: 'cb-btn',
|
|
1246
|
-
style: { padding: '4px 10px', fontSize: '12px' },
|
|
1247
|
-
onClick: () => setShowAdd(false),
|
|
1248
|
-
},
|
|
2180
|
+
{ type: 'button', className: 'cb-btn', onClick: () => setShowAdd(false) },
|
|
1249
2181
|
t('accounts.cancel_btn')
|
|
1250
2182
|
),
|
|
1251
2183
|
React.createElement(
|
|
@@ -1253,7 +2185,6 @@ function AccountsSection({
|
|
|
1253
2185
|
{
|
|
1254
2186
|
type: 'submit',
|
|
1255
2187
|
className: 'cb-btn cb-btn-primary',
|
|
1256
|
-
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1257
2188
|
disabled: !newKey.trim() || !!busy,
|
|
1258
2189
|
},
|
|
1259
2190
|
busy === 'add-account' ? t('key.saving') : t('accounts.save_btn')
|
|
@@ -1262,147 +2193,186 @@ function AccountsSection({
|
|
|
1262
2193
|
)
|
|
1263
2194
|
: null,
|
|
1264
2195
|
|
|
1265
|
-
|
|
1266
|
-
'
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
|
|
2196
|
+
accounts.length === 0
|
|
2197
|
+
? React.createElement('div', { className: 'cb-empty' }, t('accounts.no_accounts'))
|
|
2198
|
+
: React.createElement(
|
|
2199
|
+
'div',
|
|
2200
|
+
{ className: 'cb-account-list' },
|
|
2201
|
+
accounts.map((account) => {
|
|
2202
|
+
const envName = account.apiKeyEnv || account.id || ''
|
|
2203
|
+
const live = usageByAccount[envName]
|
|
2204
|
+
const isActive = status.activeAccount === envName || (!status.activeAccount && account.id === 'default')
|
|
2205
|
+
const isPrimary = account.id === 'default' || envName === status.config?.apiKeyEnv
|
|
2206
|
+
const five = live?.usage?.windows?.fiveHour
|
|
2207
|
+
const percent = typeof five?.percentUsed === 'number'
|
|
2208
|
+
? five.percentUsed
|
|
2209
|
+
: (typeof account.percentUsed === 'number' ? account.percentUsed : null)
|
|
2210
|
+
const plan = live?.usage?.plan || ''
|
|
2211
|
+
const email = live?.usage?.user?.email || ''
|
|
2212
|
+
return React.createElement(
|
|
2213
|
+
'div',
|
|
2214
|
+
{ className: 'cb-account-item', key: envName || account.id },
|
|
2215
|
+
React.createElement(
|
|
2216
|
+
'div',
|
|
2217
|
+
{ className: 'cb-account-row' },
|
|
2218
|
+
React.createElement('span', { className: dotClass(account, live), 'aria-hidden': 'true' }),
|
|
2219
|
+
React.createElement(
|
|
2220
|
+
'div',
|
|
2221
|
+
{ className: 'cb-account-main' },
|
|
2222
|
+
React.createElement(
|
|
2223
|
+
'div',
|
|
2224
|
+
{ className: 'cb-account-name' },
|
|
2225
|
+
account.label || envName,
|
|
2226
|
+
React.createElement('span', { className: 'cb-account-env cb-mono' }, envName)
|
|
2227
|
+
),
|
|
2228
|
+
React.createElement(
|
|
2229
|
+
'div',
|
|
2230
|
+
{ className: 'cb-account-meta' },
|
|
2231
|
+
[plan, email].filter(Boolean).join(' · ') || t('accounts.source', { source: account.source || '—' })
|
|
2232
|
+
)
|
|
2233
|
+
),
|
|
2234
|
+
React.createElement(
|
|
2235
|
+
'div',
|
|
2236
|
+
{ className: 'cb-account-badges' },
|
|
2237
|
+
isActive ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('accounts.active_badge')) : null,
|
|
2238
|
+
account.isPinned ? React.createElement('span', { className: 'cb-badge' }, t('accounts.pinned_badge')) : null,
|
|
2239
|
+
account.present
|
|
2240
|
+
? React.createElement('span', { className: 'cb-badge' }, t('accounts.configured'))
|
|
2241
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn' }, t('accounts.missing_key'))
|
|
2242
|
+
),
|
|
2243
|
+
React.createElement(
|
|
2244
|
+
'div',
|
|
2245
|
+
{ className: 'cb-account-quota' },
|
|
2246
|
+
percent === null
|
|
2247
|
+
? null
|
|
2248
|
+
: React.createElement(
|
|
2249
|
+
React.Fragment,
|
|
2250
|
+
null,
|
|
2251
|
+
React.createElement('span', { className: 'cb-muted' }, t('accounts.quota_5h', { pct: Math.round(percent) })),
|
|
2252
|
+
React.createElement(
|
|
2253
|
+
'span',
|
|
2254
|
+
{ className: 'cb-meter' },
|
|
2255
|
+
React.createElement('span', {
|
|
2256
|
+
className: percent >= 95 ? 'cb-meter-fill cb-meter-fill-warn' : 'cb-meter-fill',
|
|
2257
|
+
style: { width: `${Math.max(0, Math.min(100, percent))}%` },
|
|
2258
|
+
})
|
|
2259
|
+
)
|
|
2260
|
+
)
|
|
2261
|
+
),
|
|
2262
|
+
React.createElement(
|
|
2263
|
+
'button',
|
|
2264
|
+
{
|
|
2265
|
+
type: 'button',
|
|
2266
|
+
className: 'cb-icon-btn',
|
|
2267
|
+
'aria-label': t('accounts.actions'),
|
|
2268
|
+
'aria-expanded': menuFor === envName,
|
|
2269
|
+
disabled: !!busy,
|
|
2270
|
+
onClick: () => setMenuFor((current) => (current === envName ? '' : envName)),
|
|
2271
|
+
},
|
|
2272
|
+
React.createElement(CbIcon, { name: 'kebab', size: 16 })
|
|
2273
|
+
)
|
|
2274
|
+
),
|
|
2275
|
+
|
|
2276
|
+
menuFor === envName
|
|
2277
|
+
? React.createElement(
|
|
2278
|
+
'div',
|
|
2279
|
+
{ className: 'cb-account-menu' },
|
|
2280
|
+
isActive
|
|
2281
|
+
? React.createElement('button', { type: 'button', className: 'cb-menu-item', onClick: () => { setMenuFor(''); handlePinAccount('') } }, t('accounts.unpin'))
|
|
2282
|
+
: account.present
|
|
2283
|
+
? React.createElement('button', { type: 'button', className: 'cb-menu-item', onClick: () => { setMenuFor(''); handlePinAccount(envName) } }, t('accounts.pin_btn'))
|
|
2284
|
+
: null,
|
|
2285
|
+
!isPrimary
|
|
2286
|
+
? React.createElement('button', { type: 'button', className: 'cb-menu-item', onClick: () => openRename(account) }, t('accounts.rename'))
|
|
2287
|
+
: null,
|
|
2288
|
+
account.present
|
|
2289
|
+
? React.createElement('button', { type: 'button', className: 'cb-menu-item', onClick: () => { setMenuFor(''); setConfirmAction({ type: 'clear', env: envName }) } }, t('accounts.clear_key'))
|
|
2290
|
+
: null,
|
|
2291
|
+
!isPrimary
|
|
2292
|
+
? React.createElement('button', { type: 'button', className: 'cb-menu-item cb-menu-item-danger', onClick: () => { setMenuFor(''); setConfirmAction({ type: 'delete', env: envName }) } }, t('accounts.delete_btn'))
|
|
2293
|
+
: null
|
|
2294
|
+
)
|
|
1304
2295
|
: null,
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
: null
|
|
1308
|
-
),
|
|
1309
|
-
React.createElement(
|
|
1310
|
-
'td',
|
|
1311
|
-
{ style: { textAlign: 'right' } },
|
|
1312
|
-
!isActive && acc.present
|
|
2296
|
+
|
|
2297
|
+
renameFor === envName
|
|
1313
2298
|
? React.createElement(
|
|
1314
|
-
'
|
|
1315
|
-
{
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
2299
|
+
'div',
|
|
2300
|
+
{ className: 'cb-account-inline' },
|
|
2301
|
+
React.createElement('input', {
|
|
2302
|
+
type: 'text',
|
|
2303
|
+
className: 'cb-input',
|
|
2304
|
+
value: renameValue,
|
|
2305
|
+
onChange: (event) => setRenameValue(event.target.value),
|
|
2306
|
+
placeholder: t('accounts.rename_placeholder'),
|
|
2307
|
+
}),
|
|
2308
|
+
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !!busy, onClick: () => submitRename(account) }, t('accounts.rename_save')),
|
|
2309
|
+
React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => setRenameFor('') }, t('accounts.cancel'))
|
|
1323
2310
|
)
|
|
1324
2311
|
: null,
|
|
1325
|
-
|
|
2312
|
+
|
|
2313
|
+
confirmAction && confirmAction.env === envName
|
|
1326
2314
|
? React.createElement(
|
|
1327
|
-
'
|
|
1328
|
-
{
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2315
|
+
'div',
|
|
2316
|
+
{ className: 'cb-account-inline cb-account-danger' },
|
|
2317
|
+
React.createElement(
|
|
2318
|
+
'span',
|
|
2319
|
+
{ className: 'cb-account-danger-text' },
|
|
2320
|
+
confirmAction.type === 'delete'
|
|
2321
|
+
? t('accounts.remove_confirm', { name: account.label || envName })
|
|
2322
|
+
: t('accounts.clear_confirm', { name: account.label || envName })
|
|
2323
|
+
),
|
|
2324
|
+
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-danger', disabled: !!busy, onClick: runConfirm }, t('accounts.confirm')),
|
|
2325
|
+
React.createElement('button', { type: 'button', className: 'cb-btn', onClick: () => setConfirmAction(null) }, t('accounts.cancel'))
|
|
1336
2326
|
)
|
|
1337
2327
|
: null
|
|
1338
2328
|
)
|
|
1339
|
-
)
|
|
1340
|
-
})
|
|
1341
|
-
)
|
|
1342
|
-
),
|
|
1343
|
-
status.lastRotation
|
|
1344
|
-
? React.createElement(
|
|
1345
|
-
'div',
|
|
1346
|
-
{ className: 'cb-rotation-info', style: { marginTop: '10px', fontSize: '12px', opacity: 0.85 } },
|
|
1347
|
-
t('accounts.last_failover', {
|
|
1348
|
-
from: status.lastRotation.from,
|
|
1349
|
-
to: status.lastRotation.to,
|
|
1350
|
-
reason: status.lastRotation.reason,
|
|
1351
2329
|
})
|
|
1352
|
-
)
|
|
1353
|
-
|
|
2330
|
+
),
|
|
2331
|
+
|
|
1354
2332
|
React.createElement(
|
|
1355
2333
|
'div',
|
|
1356
|
-
{
|
|
2334
|
+
{ className: 'cb-account-routes' },
|
|
1357
2335
|
React.createElement(
|
|
1358
2336
|
'div',
|
|
1359
|
-
{
|
|
1360
|
-
|
|
2337
|
+
{ className: 'cb-account-routes-head' },
|
|
2338
|
+
React.createElement(CbIcon, { name: 'route', size: 14 }),
|
|
2339
|
+
` ${t('accounts.routes_title')}`
|
|
1361
2340
|
),
|
|
2341
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('accounts.routes_desc')),
|
|
1362
2342
|
React.createElement(
|
|
1363
2343
|
'div',
|
|
1364
|
-
{
|
|
1365
|
-
t('accounts.routes_desc')
|
|
1366
|
-
),
|
|
1367
|
-
React.createElement(
|
|
1368
|
-
'div',
|
|
1369
|
-
{ style: { display: 'flex', gap: '6px', flexWrap: 'wrap', alignItems: 'center' } },
|
|
2344
|
+
{ className: 'cb-account-route-add' },
|
|
1370
2345
|
React.createElement('input', {
|
|
1371
2346
|
type: 'text',
|
|
1372
2347
|
className: 'cb-input',
|
|
1373
|
-
style: { flex: '2 1 140px', height: '26px', fontSize: '11px', padding: '0 6px' },
|
|
1374
2348
|
placeholder: t('accounts.route_model_placeholder'),
|
|
1375
2349
|
value: routeModel,
|
|
1376
|
-
onChange: (
|
|
2350
|
+
onChange: (event) => setRouteModel(event.target.value),
|
|
1377
2351
|
}),
|
|
1378
2352
|
React.createElement(
|
|
1379
2353
|
'select',
|
|
1380
2354
|
{
|
|
1381
2355
|
className: 'cb-select',
|
|
1382
|
-
style: { flex: '1 1 120px', height: '26px', fontSize: '11px', padding: '0 4px' },
|
|
1383
2356
|
value: routeAccount,
|
|
1384
|
-
onChange: (
|
|
2357
|
+
onChange: (event) => setRouteAccount(event.target.value),
|
|
1385
2358
|
},
|
|
1386
2359
|
React.createElement('option', { value: '' }, t('accounts.route_account_placeholder')),
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
return React.createElement('option', { key
|
|
2360
|
+
accounts.map((account) => {
|
|
2361
|
+
const key = account.apiKeyEnv || account.id
|
|
2362
|
+
return React.createElement('option', { key, value: key }, `${account.label || key} (${key})`)
|
|
1390
2363
|
})
|
|
1391
2364
|
),
|
|
1392
2365
|
React.createElement(
|
|
1393
2366
|
'button',
|
|
1394
2367
|
{
|
|
1395
2368
|
type: 'button',
|
|
1396
|
-
className: 'cb-btn
|
|
1397
|
-
style: { padding: '3px 8px', fontSize: '11px' },
|
|
2369
|
+
className: 'cb-btn',
|
|
1398
2370
|
disabled: !routeModel.trim() || !routeAccount.trim() || !!busy,
|
|
1399
2371
|
onClick: () => {
|
|
1400
|
-
if (typeof onUpdateModelRoutes
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
setRouteAccount('')
|
|
1405
|
-
}
|
|
2372
|
+
if (typeof onUpdateModelRoutes !== 'function') return
|
|
2373
|
+
onUpdateModelRoutes({ ...(modelRoutes || {}), [routeModel.trim()]: routeAccount.trim() })
|
|
2374
|
+
setRouteModel('')
|
|
2375
|
+
setRouteAccount('')
|
|
1406
2376
|
},
|
|
1407
2377
|
},
|
|
1408
2378
|
t('accounts.route_btn_add')
|
|
@@ -1411,48 +2381,30 @@ function AccountsSection({
|
|
|
1411
2381
|
Object.keys(modelRoutes || {}).length > 0
|
|
1412
2382
|
? React.createElement(
|
|
1413
2383
|
'div',
|
|
1414
|
-
{
|
|
1415
|
-
Object.entries(modelRoutes || {}).map(([
|
|
2384
|
+
{ className: 'cb-account-route-list' },
|
|
2385
|
+
Object.entries(modelRoutes || {}).map(([modelId, accountKey]) => React.createElement(
|
|
2386
|
+
'div',
|
|
2387
|
+
{ className: 'cb-account-route-row', key: modelId },
|
|
2388
|
+
React.createElement('span', { className: 'cb-mono' }, modelId),
|
|
2389
|
+
React.createElement(CbIcon, { name: 'chevron', size: 12 }),
|
|
2390
|
+
React.createElement('strong', null, accountKey),
|
|
1416
2391
|
React.createElement(
|
|
1417
|
-
'
|
|
2392
|
+
'button',
|
|
1418
2393
|
{
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2394
|
+
type: 'button',
|
|
2395
|
+
className: 'cb-icon-btn',
|
|
2396
|
+
'aria-label': t('accounts.route_remove'),
|
|
2397
|
+
disabled: !!busy,
|
|
2398
|
+
onClick: () => {
|
|
2399
|
+
if (typeof onUpdateModelRoutes !== 'function') return
|
|
2400
|
+
const next = { ...(modelRoutes || {}) }
|
|
2401
|
+
delete next[modelId]
|
|
2402
|
+
onUpdateModelRoutes(next)
|
|
1428
2403
|
},
|
|
1429
2404
|
},
|
|
1430
|
-
React.createElement(
|
|
1431
|
-
'span',
|
|
1432
|
-
null,
|
|
1433
|
-
React.createElement('code', null, mId),
|
|
1434
|
-
' ➔ ',
|
|
1435
|
-
React.createElement('strong', null, accKey)
|
|
1436
|
-
),
|
|
1437
|
-
React.createElement(
|
|
1438
|
-
'button',
|
|
1439
|
-
{
|
|
1440
|
-
type: 'button',
|
|
1441
|
-
className: 'cb-btn cb-btn-danger',
|
|
1442
|
-
style: { padding: '1px 6px', fontSize: '10px' },
|
|
1443
|
-
disabled: !!busy,
|
|
1444
|
-
onClick: () => {
|
|
1445
|
-
if (typeof onUpdateModelRoutes === 'function') {
|
|
1446
|
-
const next = { ...(modelRoutes || {}) }
|
|
1447
|
-
delete next[mId]
|
|
1448
|
-
onUpdateModelRoutes(next)
|
|
1449
|
-
}
|
|
1450
|
-
},
|
|
1451
|
-
},
|
|
1452
|
-
'✕'
|
|
1453
|
-
)
|
|
2405
|
+
React.createElement(CbIcon, { name: 'close', size: 13 })
|
|
1454
2406
|
)
|
|
1455
|
-
)
|
|
2407
|
+
))
|
|
1456
2408
|
)
|
|
1457
2409
|
: null
|
|
1458
2410
|
)
|
|
@@ -1474,15 +2426,26 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1474
2426
|
? React.createElement(
|
|
1475
2427
|
'div',
|
|
1476
2428
|
{ className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
|
|
1477
|
-
React.createElement(
|
|
2429
|
+
React.createElement(
|
|
2430
|
+
'span',
|
|
2431
|
+
{ className: 'cb-banner-icon' },
|
|
2432
|
+
React.createElement(CbIcon, {
|
|
2433
|
+
name: status.quotaWarning.level === 'exhausted' ? 'error' : 'warning',
|
|
2434
|
+
size: 16,
|
|
2435
|
+
})
|
|
2436
|
+
),
|
|
1478
2437
|
React.createElement(
|
|
1479
2438
|
'div',
|
|
1480
|
-
{
|
|
1481
|
-
React.createElement(
|
|
2439
|
+
{ className: 'cb-banner-body' },
|
|
2440
|
+
React.createElement(
|
|
2441
|
+
'div',
|
|
2442
|
+
null,
|
|
2443
|
+
(status.quotaWarning.code && t(status.quotaWarning.code, status.quotaWarning.params)) || status.quotaWarning.message
|
|
2444
|
+
),
|
|
1482
2445
|
status.quotaWarning.resetsAt
|
|
1483
2446
|
? React.createElement(
|
|
1484
2447
|
'div',
|
|
1485
|
-
{
|
|
2448
|
+
{ className: 'cb-banner-sub' },
|
|
1486
2449
|
t('quota.reset_at', { time: new Date(status.quotaWarning.resetsAt).toLocaleTimeString() })
|
|
1487
2450
|
)
|
|
1488
2451
|
: null
|
|
@@ -1492,26 +2455,25 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1492
2455
|
? React.createElement(
|
|
1493
2456
|
'div',
|
|
1494
2457
|
{ className: 'cb-banner-warning' },
|
|
1495
|
-
React.createElement(
|
|
1496
|
-
|
|
2458
|
+
React.createElement(
|
|
2459
|
+
'span',
|
|
2460
|
+
{ className: 'cb-banner-icon' },
|
|
2461
|
+
React.createElement(CbIcon, { name: 'warning', size: 16 })
|
|
2462
|
+
),
|
|
2463
|
+
React.createElement('div', { className: 'cb-banner-body' }, t('quota.warning_low'))
|
|
1497
2464
|
)
|
|
1498
2465
|
: null),
|
|
1499
2466
|
keyPresent
|
|
1500
2467
|
? React.createElement(
|
|
1501
|
-
'
|
|
2468
|
+
'section',
|
|
1502
2469
|
{ className: 'cb-section-card' },
|
|
1503
2470
|
React.createElement(
|
|
1504
2471
|
'div',
|
|
1505
|
-
{ className: 'cb-section-
|
|
1506
|
-
t('quota.title'),
|
|
2472
|
+
{ className: 'cb-section-head' },
|
|
2473
|
+
React.createElement('div', { className: 'cb-section-title' }, t('quota.title')),
|
|
1507
2474
|
React.createElement(
|
|
1508
2475
|
'button',
|
|
1509
|
-
{
|
|
1510
|
-
type: 'button',
|
|
1511
|
-
className: 'cb-btn',
|
|
1512
|
-
disabled: !!busy,
|
|
1513
|
-
onClick: handleRefreshQuota,
|
|
1514
|
-
},
|
|
2476
|
+
{ type: 'button', className: 'cb-btn', disabled: !!busy, onClick: handleRefreshQuota },
|
|
1515
2477
|
busy === 'refresh-quota' ? t('quota.refreshing') : t('quota.refresh')
|
|
1516
2478
|
)
|
|
1517
2479
|
),
|
|
@@ -1524,7 +2486,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1524
2486
|
),
|
|
1525
2487
|
React.createElement(
|
|
1526
2488
|
'div',
|
|
1527
|
-
{ className: 'cb-
|
|
2489
|
+
{ className: 'cb-quota-grid' },
|
|
1528
2490
|
usage?.windows?.fiveHour
|
|
1529
2491
|
? React.createElement(ProgressBar, {
|
|
1530
2492
|
label: t('quota.window_5h'),
|
|
@@ -1551,21 +2513,15 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1551
2513
|
})
|
|
1552
2514
|
: null
|
|
1553
2515
|
),
|
|
1554
|
-
React.createElement(
|
|
1555
|
-
'
|
|
1556
|
-
|
|
1557
|
-
React.createElement(
|
|
1558
|
-
'div',
|
|
1559
|
-
{ style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
|
|
1560
|
-
React.createElement('span', { style: { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary)' } }, t('sidebar.show_quota')),
|
|
1561
|
-
React.createElement('span', { style: { marginTop: '4px', fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' } }, t('sidebar.show_quota_hint'))
|
|
1562
|
-
),
|
|
1563
|
-
React.createElement(BooleanControl, {
|
|
2516
|
+
React.createElement(SettingRow, {
|
|
2517
|
+
title: t('sidebar.show_quota'),
|
|
2518
|
+
description: t('sidebar.show_quota_hint'),
|
|
2519
|
+
control: React.createElement(BooleanControl, {
|
|
1564
2520
|
checked: showSidebarQuota !== false,
|
|
1565
2521
|
label: t('sidebar.show_quota'),
|
|
1566
2522
|
onChange: (next) => onToggleSidebarQuota && onToggleSidebarQuota(next),
|
|
1567
|
-
})
|
|
1568
|
-
)
|
|
2523
|
+
}),
|
|
2524
|
+
})
|
|
1569
2525
|
)
|
|
1570
2526
|
: null
|
|
1571
2527
|
)
|
|
@@ -1632,12 +2588,17 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1632
2588
|
React.createElement(
|
|
1633
2589
|
'span',
|
|
1634
2590
|
{ style: { fontWeight: 500, fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } },
|
|
1635
|
-
|
|
2591
|
+
React.createElement(CbIcon, { name: 'plus', size: 13 }),
|
|
2592
|
+
` ${t('models.custom_add_title')} (${customModels.length})`
|
|
1636
2593
|
),
|
|
1637
2594
|
React.createElement(
|
|
1638
2595
|
'span',
|
|
1639
2596
|
{ style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
1640
|
-
|
|
2597
|
+
React.createElement(CbIcon, {
|
|
2598
|
+
name: 'chevron',
|
|
2599
|
+
size: 13,
|
|
2600
|
+
className: isExpanded ? 'cb-icon cb-icon-chevron-open' : 'cb-icon',
|
|
2601
|
+
})
|
|
1641
2602
|
)
|
|
1642
2603
|
),
|
|
1643
2604
|
isExpanded
|
|
@@ -1897,7 +2858,12 @@ function VisibleModelsSub({
|
|
|
1897
2858
|
style: { padding: '4px 10px', fontSize: '12px' },
|
|
1898
2859
|
onClick: () => setOpen(!open),
|
|
1899
2860
|
},
|
|
1900
|
-
|
|
2861
|
+
React.createElement(CbIcon, {
|
|
2862
|
+
name: 'chevron',
|
|
2863
|
+
size: 12,
|
|
2864
|
+
className: open ? 'cb-icon cb-icon-chevron-open' : 'cb-icon',
|
|
2865
|
+
}),
|
|
2866
|
+
` ${t('models.models_selected', { selected: count, total: modelsList.length })}`
|
|
1901
2867
|
)
|
|
1902
2868
|
)
|
|
1903
2869
|
),
|
|
@@ -1984,7 +2950,7 @@ function VisibleModelsSub({
|
|
|
1984
2950
|
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 6px' } }, t('models.in_plan'))
|
|
1985
2951
|
: React.createElement('span', { className: 'cb-badge', style: { fontSize: '10px', padding: '1px 6px', color: 'var(--dsw-alias-label-secondary)' } }, t('models.out_of_plan')),
|
|
1986
2952
|
!m.isContextOverridden
|
|
1987
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 5px' } }, '
|
|
2953
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 5px' } }, t('models.ctx_tag_original'))
|
|
1988
2954
|
: null
|
|
1989
2955
|
)
|
|
1990
2956
|
)
|
|
@@ -1995,6 +2961,23 @@ function VisibleModelsSub({
|
|
|
1995
2961
|
)
|
|
1996
2962
|
}
|
|
1997
2963
|
|
|
2964
|
+
// --- src/client/components/provider-card.js ---
|
|
2965
|
+
/**
|
|
2966
|
+
* Provider card inside DSH Settings -> Models.
|
|
2967
|
+
*
|
|
2968
|
+
* The host owns the row chrome: provider name, status dot and the Edit action.
|
|
2969
|
+
* Accounts, the API key, models and quotas live on the full Settings -> ClineBot
|
|
2970
|
+
* page, so this slot intentionally renders nothing instead of duplicating that
|
|
2971
|
+
* form inside the Models page.
|
|
2972
|
+
*/
|
|
2973
|
+
function ProviderCard(props) {
|
|
2974
|
+
const row = props?.provider || {}
|
|
2975
|
+
const providerId = typeof row === 'string' ? row : (row.provider || props?.providerId || '')
|
|
2976
|
+
const settingsNs = row && typeof row === 'object' ? (row.settingsNs || '') : ''
|
|
2977
|
+
if (providerId !== 'clinebot' && settingsNs !== NS) return null
|
|
2978
|
+
return null
|
|
2979
|
+
}
|
|
2980
|
+
|
|
1998
2981
|
// --- src/client/components/models-section.js ---
|
|
1999
2982
|
function ModelsSection({
|
|
2000
2983
|
modelsList,
|
|
@@ -2061,9 +3044,7 @@ function ModelsSection({
|
|
|
2061
3044
|
return React.createElement(
|
|
2062
3045
|
'div',
|
|
2063
3046
|
{ className: 'cb-section-card' },
|
|
2064
|
-
defaultModelWarning
|
|
2065
|
-
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, `⚠️ ${defaultModelWarning}`)
|
|
2066
|
-
: null,
|
|
3047
|
+
defaultModelWarning ? React.createElement('div', { className: 'cb-alert-bad cb-inline-alert', style: { marginBottom: '12px' } }, React.createElement(CbIcon, { name: 'warning', size: 14 }), ` ${defaultModelWarning}`) : null,
|
|
2067
3048
|
React.createElement(VisibleModelsSub, {
|
|
2068
3049
|
modelsList,
|
|
2069
3050
|
visibleModels,
|
|
@@ -2424,7 +3405,7 @@ function ModelsSection({
|
|
|
2424
3405
|
setEditingModelId(m.id)
|
|
2425
3406
|
},
|
|
2426
3407
|
},
|
|
2427
|
-
'
|
|
3408
|
+
React.createElement(CbIcon, { name: 'edit', size: 13 })
|
|
2428
3409
|
)
|
|
2429
3410
|
)
|
|
2430
3411
|
),
|
|
@@ -2503,6 +3484,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2503
3484
|
const requests = typeof totals.requests === 'number' ? totals.requests : (typeof s.requests === 'number' ? s.requests : 0)
|
|
2504
3485
|
const totalTokens = typeof totals.totalTokens === 'number' ? totals.totalTokens : (typeof s.totalTokens === 'number' ? s.totalTokens : 0)
|
|
2505
3486
|
const errors429 = typeof totals.rateLimited429 === 'number' ? totals.rateLimited429 : (typeof s.errors429 === 'number' ? s.errors429 : (typeof totals.failed === 'number' ? totals.failed : 0))
|
|
3487
|
+
const reasoning = Number(totals.reasoningTokens || sess.reasoningTokensEst || 0)
|
|
2506
3488
|
|
|
2507
3489
|
const topModels = Object.entries(s.byModel || {})
|
|
2508
3490
|
.sort((a, b) => {
|
|
@@ -2513,24 +3495,24 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2513
3495
|
.slice(0, 5)
|
|
2514
3496
|
|
|
2515
3497
|
return React.createElement(
|
|
2516
|
-
'
|
|
3498
|
+
'section',
|
|
2517
3499
|
{ className: 'cb-section-card' },
|
|
2518
3500
|
React.createElement(
|
|
2519
3501
|
'div',
|
|
2520
|
-
{
|
|
3502
|
+
{ className: 'cb-section-head' },
|
|
2521
3503
|
React.createElement('div', { className: 'cb-section-title' }, t('stats.title')),
|
|
2522
3504
|
React.createElement(
|
|
2523
3505
|
'div',
|
|
2524
|
-
{
|
|
3506
|
+
{ className: 'cb-section-actions' },
|
|
2525
3507
|
React.createElement(
|
|
2526
3508
|
'a',
|
|
2527
3509
|
{
|
|
2528
3510
|
href: '/dsh-clinebot/stats/export',
|
|
2529
3511
|
download: 'clinebot-stats.csv',
|
|
2530
|
-
className: 'cb-btn',
|
|
2531
|
-
style: { padding: '2px 8px', fontSize: '11px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
3512
|
+
className: 'cb-btn cb-btn-link',
|
|
2532
3513
|
},
|
|
2533
|
-
|
|
3514
|
+
React.createElement(CbIcon, { name: 'download', size: 13 }),
|
|
3515
|
+
` ${t('stats.export_csv')}`
|
|
2534
3516
|
),
|
|
2535
3517
|
confirmReset
|
|
2536
3518
|
? React.createElement(
|
|
@@ -2541,7 +3523,6 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2541
3523
|
{
|
|
2542
3524
|
type: 'button',
|
|
2543
3525
|
className: 'cb-btn cb-btn-warn',
|
|
2544
|
-
style: { padding: '2px 8px', fontSize: '11px', marginRight: '6px' },
|
|
2545
3526
|
disabled: !!busy,
|
|
2546
3527
|
onClick: () => {
|
|
2547
3528
|
setConfirmReset(false)
|
|
@@ -2552,12 +3533,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2552
3533
|
),
|
|
2553
3534
|
React.createElement(
|
|
2554
3535
|
'button',
|
|
2555
|
-
{
|
|
2556
|
-
type: 'button',
|
|
2557
|
-
className: 'cb-btn',
|
|
2558
|
-
style: { padding: '2px 8px', fontSize: '11px' },
|
|
2559
|
-
onClick: () => setConfirmReset(false),
|
|
2560
|
-
},
|
|
3536
|
+
{ type: 'button', className: 'cb-btn', onClick: () => setConfirmReset(false) },
|
|
2561
3537
|
t('stats.cancel_reset')
|
|
2562
3538
|
)
|
|
2563
3539
|
)
|
|
@@ -2566,7 +3542,6 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2566
3542
|
{
|
|
2567
3543
|
type: 'button',
|
|
2568
3544
|
className: 'cb-btn',
|
|
2569
|
-
style: { padding: '2px 8px', fontSize: '11px' },
|
|
2570
3545
|
disabled: !!busy,
|
|
2571
3546
|
onClick: () => setConfirmReset(true),
|
|
2572
3547
|
},
|
|
@@ -2577,7 +3552,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2577
3552
|
React.createElement('div', { className: 'cb-section-desc' }, t('stats.desc')),
|
|
2578
3553
|
React.createElement(
|
|
2579
3554
|
'div',
|
|
2580
|
-
{ className: 'cb-grid
|
|
3555
|
+
{ className: 'cb-stats-grid' },
|
|
2581
3556
|
React.createElement(
|
|
2582
3557
|
'div',
|
|
2583
3558
|
{ className: 'cb-stat-box' },
|
|
@@ -2587,16 +3562,12 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2587
3562
|
React.createElement(
|
|
2588
3563
|
'div',
|
|
2589
3564
|
{ className: 'cb-stat-box' },
|
|
2590
|
-
React.createElement(
|
|
2591
|
-
'div',
|
|
2592
|
-
{ className: 'cb-stat-val' },
|
|
2593
|
-
`${Number(totalTokens).toLocaleString()}`
|
|
2594
|
-
),
|
|
3565
|
+
React.createElement('div', { className: 'cb-stat-val' }, Number(totalTokens).toLocaleString()),
|
|
2595
3566
|
React.createElement(
|
|
2596
3567
|
'div',
|
|
2597
3568
|
{ className: 'cb-stat-lbl' },
|
|
2598
|
-
|
|
2599
|
-
? `${t('stats.tokens')} (${
|
|
3569
|
+
reasoning > 0
|
|
3570
|
+
? `${t('stats.tokens')} (${reasoning.toLocaleString()} reasoning)`
|
|
2600
3571
|
: t('stats.tokens')
|
|
2601
3572
|
)
|
|
2602
3573
|
),
|
|
@@ -2616,30 +3587,18 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
2616
3587
|
topModels.length > 0
|
|
2617
3588
|
? React.createElement(
|
|
2618
3589
|
'div',
|
|
2619
|
-
{
|
|
2620
|
-
React.createElement('div', {
|
|
2621
|
-
React.createElement(
|
|
3590
|
+
{ className: 'cb-model-list' },
|
|
3591
|
+
React.createElement('div', { className: 'cb-model-list-title' }, t('stats.top_models')),
|
|
3592
|
+
topModels.map(([modelId, data]) => React.createElement(
|
|
2622
3593
|
'div',
|
|
2623
|
-
{
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
style: {
|
|
2630
|
-
display: 'flex',
|
|
2631
|
-
justifyContent: 'space-between',
|
|
2632
|
-
fontSize: '11px',
|
|
2633
|
-
padding: '4px 8px',
|
|
2634
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
2635
|
-
borderRadius: '4px',
|
|
2636
|
-
},
|
|
2637
|
-
},
|
|
2638
|
-
React.createElement('span', { style: { fontFamily: 'monospace' } }, modId),
|
|
2639
|
-
React.createElement('span', null, `${data.requests || 0} reqs · ${Number(data.totalTokens ?? data.tokens ?? 0).toLocaleString()} tok${data.reasoningTokens ? ` (${Number(data.reasoningTokens).toLocaleString()} thk)` : ''}`)
|
|
2640
|
-
)
|
|
3594
|
+
{ className: 'cb-model-row', key: modelId },
|
|
3595
|
+
React.createElement('span', { className: 'cb-mono' }, modelId),
|
|
3596
|
+
React.createElement(
|
|
3597
|
+
'span',
|
|
3598
|
+
{ className: 'cb-muted' },
|
|
3599
|
+
`${data.requests || 0} reqs · ${Number(data.totalTokens ?? data.tokens ?? 0).toLocaleString()} tok${data.reasoningTokens ? ` (${Number(data.reasoningTokens).toLocaleString()} reasoning)` : ''}`
|
|
2641
3600
|
)
|
|
2642
|
-
)
|
|
3601
|
+
))
|
|
2643
3602
|
)
|
|
2644
3603
|
: null
|
|
2645
3604
|
)
|
|
@@ -2659,49 +3618,19 @@ function DiagSection({
|
|
|
2659
3618
|
t,
|
|
2660
3619
|
}) {
|
|
2661
3620
|
return React.createElement(
|
|
2662
|
-
'
|
|
3621
|
+
'section',
|
|
2663
3622
|
{ className: 'cb-section-card' },
|
|
2664
3623
|
React.createElement('div', { className: 'cb-section-title' }, t('diag.title')),
|
|
2665
3624
|
React.createElement('div', { className: 'cb-section-desc' }, t('diag.desc')),
|
|
2666
|
-
React.createElement(
|
|
2667
|
-
'
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
justifyContent: 'space-between',
|
|
2673
|
-
padding: '10px 14px',
|
|
2674
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
2675
|
-
borderRadius: '8px',
|
|
2676
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
2677
|
-
},
|
|
2678
|
-
},
|
|
2679
|
-
React.createElement(
|
|
2680
|
-
'div',
|
|
2681
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
|
|
2682
|
-
React.createElement(
|
|
2683
|
-
'span',
|
|
2684
|
-
{ style: { fontSize: '13px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
|
|
2685
|
-
t('diag.proxy_title')
|
|
2686
|
-
),
|
|
2687
|
-
React.createElement(
|
|
2688
|
-
'span',
|
|
2689
|
-
{ style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
2690
|
-
t('diag.proxy_desc')
|
|
2691
|
-
)
|
|
2692
|
-
),
|
|
2693
|
-
React.createElement(
|
|
2694
|
-
'div',
|
|
2695
|
-
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px' } },
|
|
3625
|
+
React.createElement(SettingRow, {
|
|
3626
|
+
title: t('diag.proxy_title'),
|
|
3627
|
+
description: t('diag.proxy_desc'),
|
|
3628
|
+
control: React.createElement(
|
|
3629
|
+
React.Fragment,
|
|
3630
|
+
null,
|
|
2696
3631
|
React.createElement(
|
|
2697
3632
|
'span',
|
|
2698
|
-
{
|
|
2699
|
-
style: {
|
|
2700
|
-
fontSize: '14px',
|
|
2701
|
-
lineHeight: '20px',
|
|
2702
|
-
color: proxyMode ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
2703
|
-
},
|
|
2704
|
-
},
|
|
3633
|
+
{ className: proxyMode ? 'cb-state-ok' : 'cb-muted' },
|
|
2705
3634
|
proxyMode ? t('diag.proxy_enabled') : t('diag.proxy_disabled')
|
|
2706
3635
|
),
|
|
2707
3636
|
React.createElement(BooleanControl, {
|
|
@@ -2710,11 +3639,11 @@ function DiagSection({
|
|
|
2710
3639
|
label: t('diag.proxy_title'),
|
|
2711
3640
|
onChange: (next) => onToggleProxy && onToggleProxy(next),
|
|
2712
3641
|
})
|
|
2713
|
-
)
|
|
2714
|
-
),
|
|
3642
|
+
),
|
|
3643
|
+
}),
|
|
2715
3644
|
React.createElement(
|
|
2716
3645
|
'div',
|
|
2717
|
-
{ className: 'cb-row' },
|
|
3646
|
+
{ className: 'cb-row cb-row-actions' },
|
|
2718
3647
|
React.createElement(
|
|
2719
3648
|
'button',
|
|
2720
3649
|
{
|
|
@@ -2737,12 +3666,12 @@ function DiagSection({
|
|
|
2737
3666
|
busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
|
|
2738
3667
|
)
|
|
2739
3668
|
: null,
|
|
3669
|
+
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
2740
3670
|
React.createElement(
|
|
2741
3671
|
'button',
|
|
2742
3672
|
{
|
|
2743
3673
|
type: 'button',
|
|
2744
3674
|
className: 'cb-btn cb-btn-primary',
|
|
2745
|
-
style: { marginLeft: 'auto' },
|
|
2746
3675
|
disabled: !!busy,
|
|
2747
3676
|
onClick: handleRegister,
|
|
2748
3677
|
},
|
|
@@ -2802,7 +3731,7 @@ function MiniRing({ percent, size = 18, warn = false }) {
|
|
|
2802
3731
|
)
|
|
2803
3732
|
}
|
|
2804
3733
|
|
|
2805
|
-
function SidebarQuota({ ctx, wide, t: propT }) {
|
|
3734
|
+
function SidebarQuota({ ctx, wide, t: propT, open: openPanel }) {
|
|
2806
3735
|
const t = propT || makeT(en, en)
|
|
2807
3736
|
const [data, setData] = React.useState(null)
|
|
2808
3737
|
const [open, setOpen] = React.useState(false)
|
|
@@ -2839,18 +3768,48 @@ function SidebarQuota({ ctx, wide, t: propT }) {
|
|
|
2839
3768
|
|
|
2840
3769
|
if (!show) return null
|
|
2841
3770
|
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3771
|
+
const windowEntries = [
|
|
3772
|
+
{ key: 'fiveHour', label: t('quota.window_5h'), value: data?.windows?.fiveHour },
|
|
3773
|
+
{ key: 'weekly', label: t('quota.window_weekly'), value: data?.windows?.weekly },
|
|
3774
|
+
{ key: 'monthly', label: t('quota.window_monthly'), value: data?.windows?.monthly },
|
|
3775
|
+
]
|
|
3776
|
+
|
|
3777
|
+
const windowPct = (window) => {
|
|
3778
|
+
const value = Number(window.value?.percentUsed)
|
|
3779
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(100, Math.round(value))) : null
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
const measured = windowEntries
|
|
3783
|
+
.map((window) => ({ ...window, pct: windowPct(window) }))
|
|
3784
|
+
.filter((window) => window.pct !== null)
|
|
3785
|
+
|
|
3786
|
+
const pct = measured.length ? Math.max(...measured.map((window) => window.pct)) : 0
|
|
3787
|
+
const isWarn = measured.some((window) => window.pct >= 85)
|
|
3788
|
+
const planName = String(data?.plan || t('title')).replace(/\s*\(\$[^)]*\)\s*$/, '')
|
|
3789
|
+
const accountLabel = data?.account ? ` · ${data.account}` : ''
|
|
3790
|
+
const tooltip = `${planName}${accountLabel} · ${windowEntries.map((window) => {
|
|
3791
|
+
const value = windowPct(window)
|
|
3792
|
+
return `${window.label} ${value === null ? '—' : `${value}% ${t('dashboard.used')}`}`
|
|
3793
|
+
}).join(' · ')}`
|
|
2848
3794
|
|
|
2849
3795
|
// The window is local to the meter: nothing is selected or navigated, so the
|
|
2850
3796
|
// conversation stays where it is and closing simply drops the window.
|
|
2851
|
-
const handleOpen = () =>
|
|
3797
|
+
const handleOpen = () => {
|
|
3798
|
+
if (typeof openPanel === 'function') {
|
|
3799
|
+
try {
|
|
3800
|
+
if (openPanel() !== false) return
|
|
3801
|
+
} catch {
|
|
3802
|
+
/* fall through to the local window */
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
setOpen(true)
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
// Keep all three rows visible even when the endpoint omits a window for the
|
|
3809
|
+
// current plan; the missing row shows an explicit dash instead of silently
|
|
3810
|
+
// relabelling another window.
|
|
3811
|
+
const rows = windowEntries
|
|
2852
3812
|
|
|
2853
|
-
const tooltip = `${t('sidebar.quota_title')}: ${pct}% ${t('dashboard.used')} (${planName})`
|
|
2854
3813
|
const dialog = React.createElement(DashboardDialog, { t, open, onClose: () => setOpen(false) })
|
|
2855
3814
|
|
|
2856
3815
|
if (!wide) {
|
|
@@ -2891,68 +3850,403 @@ function SidebarQuota({ ctx, wide, t: propT }) {
|
|
|
2891
3850
|
{ className: 'cb-footTop' },
|
|
2892
3851
|
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 16 }),
|
|
2893
3852
|
React.createElement('span', { className: 'cb-footName' }, planName),
|
|
2894
|
-
React.createElement('span', { className:
|
|
3853
|
+
React.createElement('span', { className: isWarn ? 'cb-footPct cb-footPctWarn' : 'cb-footPct' }, `${pct}%`)
|
|
2895
3854
|
),
|
|
3855
|
+
rows.map((window) => {
|
|
3856
|
+
const value = windowPct(window)
|
|
3857
|
+
const hasData = value !== null
|
|
3858
|
+
const warn = hasData && value >= 85
|
|
3859
|
+
const reset = hasData && typeof formatResetCountdown === 'function'
|
|
3860
|
+
? formatResetCountdown(window.value?.resetsAt)
|
|
3861
|
+
: ''
|
|
3862
|
+
const resetText = reset
|
|
3863
|
+
? ` · ${reset === 'resetting' ? t('quota.resetting') : reset}`
|
|
3864
|
+
: ''
|
|
3865
|
+
return React.createElement(
|
|
3866
|
+
'div',
|
|
3867
|
+
{ className: hasData ? 'cb-footRow' : 'cb-footRow cb-footRowMissing', key: window.key },
|
|
3868
|
+
React.createElement(
|
|
3869
|
+
'div',
|
|
3870
|
+
{ className: 'cb-footHead' },
|
|
3871
|
+
React.createElement('span', { className: 'cb-footLabel' }, window.label),
|
|
3872
|
+
React.createElement(
|
|
3873
|
+
'span',
|
|
3874
|
+
{ className: hasData ? 'cb-footAmount' : 'cb-footAmount cb-footAmountMissing' },
|
|
3875
|
+
hasData ? `${100 - value}% ${t('dashboard.remaining')}${resetText}` : '—'
|
|
3876
|
+
)
|
|
3877
|
+
),
|
|
3878
|
+
React.createElement(
|
|
3879
|
+
'span',
|
|
3880
|
+
{ className: 'cb-footBar' },
|
|
3881
|
+
React.createElement('span', {
|
|
3882
|
+
className: warn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
|
|
3883
|
+
style: { width: hasData ? `${Math.min(100, Math.max(2, value))}%` : '0%' },
|
|
3884
|
+
})
|
|
3885
|
+
)
|
|
3886
|
+
)
|
|
3887
|
+
}),
|
|
3888
|
+
rows.some((window) => windowPct(window) !== null)
|
|
3889
|
+
? null
|
|
3890
|
+
: React.createElement('span', { className: 'cb-footMessage' }, data?.error || t('quota.no_windows'))
|
|
3891
|
+
),
|
|
3892
|
+
dialog
|
|
3893
|
+
)
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
// --- src/client/components/price-section.js ---
|
|
3897
|
+
/**
|
|
3898
|
+
* Per-model price overrides. A blank field falls back to the built-in local
|
|
3899
|
+
* estimate; the values are USD per 1M tokens.
|
|
3900
|
+
*/
|
|
3901
|
+
function PriceSection({ modelsList = [], priceOverrides = {}, onConfigUpdate, t }) {
|
|
3902
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
3903
|
+
const models = Array.isArray(modelsList) ? modelsList : []
|
|
3904
|
+
|
|
3905
|
+
const update = (modelId, field, raw) => {
|
|
3906
|
+
const next = { ...(priceOverrides || {}) }
|
|
3907
|
+
const current = { ...(next[modelId] || {}) }
|
|
3908
|
+
const value = String(raw ?? '').trim()
|
|
3909
|
+
const numeric = Number(value)
|
|
3910
|
+
if (value === '' || !Number.isFinite(numeric) || numeric < 0) delete current[field]
|
|
3911
|
+
else current[field] = numeric
|
|
3912
|
+
if (Object.keys(current).length === 0) delete next[modelId]
|
|
3913
|
+
else next[modelId] = current
|
|
3914
|
+
onConfigUpdate({ priceOverrides: next })
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
return React.createElement(
|
|
3918
|
+
'div',
|
|
3919
|
+
{ className: 'cb-price-section' },
|
|
3920
|
+
React.createElement(
|
|
3921
|
+
'div',
|
|
3922
|
+
{ className: 'cb-price-head' },
|
|
2896
3923
|
React.createElement(
|
|
2897
3924
|
'div',
|
|
2898
|
-
{ className: 'cb-
|
|
2899
|
-
React.createElement(
|
|
3925
|
+
{ className: 'cb-price-copy' },
|
|
3926
|
+
React.createElement('div', { className: 'cb-section-title' }, t('prices.title')),
|
|
3927
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('prices.desc'))
|
|
3928
|
+
),
|
|
3929
|
+
React.createElement(
|
|
3930
|
+
'button',
|
|
3931
|
+
{ type: 'button', className: 'cb-btn', onClick: () => setExpanded((value) => !value) },
|
|
3932
|
+
expanded ? t('prices.hide') : t('prices.show')
|
|
3933
|
+
)
|
|
3934
|
+
),
|
|
3935
|
+
expanded
|
|
3936
|
+
? React.createElement(
|
|
2900
3937
|
'div',
|
|
2901
|
-
{ className: 'cb-
|
|
2902
|
-
React.createElement(
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3938
|
+
{ className: 'cb-price-table' },
|
|
3939
|
+
React.createElement(
|
|
3940
|
+
'div',
|
|
3941
|
+
{ className: 'cb-price-head-row' },
|
|
3942
|
+
React.createElement('span', null, t('prices.model')),
|
|
3943
|
+
React.createElement('span', null, t('prices.input')),
|
|
3944
|
+
React.createElement('span', null, t('prices.output')),
|
|
3945
|
+
React.createElement('span', null, t('prices.cache_read')),
|
|
3946
|
+
React.createElement('span', null, t('prices.cache_write'))
|
|
3947
|
+
),
|
|
3948
|
+
models.length === 0
|
|
3949
|
+
? React.createElement('div', { className: 'cb-hint' }, t('prices.empty'))
|
|
3950
|
+
: models.map((model) => {
|
|
3951
|
+
const override = priceOverrides?.[model.id] || {}
|
|
3952
|
+
return React.createElement(
|
|
3953
|
+
'div',
|
|
3954
|
+
{ className: 'cb-price-row', key: model.id },
|
|
3955
|
+
React.createElement('span', { className: 'cb-mono', title: model.id }, model.name || model.id),
|
|
3956
|
+
['input', 'output', 'cacheRead', 'cacheWrite'].map((field) => React.createElement('input', {
|
|
3957
|
+
key: field,
|
|
3958
|
+
className: 'cb-input cb-price-input',
|
|
3959
|
+
type: 'number',
|
|
3960
|
+
min: '0',
|
|
3961
|
+
step: '0.001',
|
|
3962
|
+
value: override[field] === undefined ? '' : String(override[field]),
|
|
3963
|
+
placeholder: t('prices.auto'),
|
|
3964
|
+
onChange: (event) => update(model.id, field, event.target.value),
|
|
3965
|
+
}))
|
|
3966
|
+
)
|
|
3967
|
+
}),
|
|
3968
|
+
React.createElement('div', { className: 'cb-hint' }, t('prices.hint'))
|
|
3969
|
+
)
|
|
3970
|
+
: null
|
|
3971
|
+
)
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
// --- src/client/components/analytics-heatmap.js ---
|
|
3975
|
+
/**
|
|
3976
|
+
* Weekday × hour request heatmap, matching the reference providers: seven rows
|
|
3977
|
+
* (Mon–Sun), 24 columns, a peak-hour line and a compact by-hour strip. The
|
|
3978
|
+
* model selector re-fetches the same range for one model.
|
|
3979
|
+
*/
|
|
3980
|
+
function heatWeekdayLabels() {
|
|
3981
|
+
return Array.from({ length: 7 }, (_, index) => {
|
|
3982
|
+
const date = new Date(2024, 0, 1 + index)
|
|
3983
|
+
return date.toLocaleDateString(undefined, { weekday: 'short' })
|
|
3984
|
+
})
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
function heatHourLabel(hour) {
|
|
3988
|
+
return hour % 3 === 0 ? String(hour).padStart(2, '0') : ''
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
function HeatmapView({ heatmap, model, onModelChange, t }) {
|
|
3992
|
+
if (!heatmap || !Array.isArray(heatmap.grid)) {
|
|
3993
|
+
return React.createElement('div', { className: 'cb-hint' }, t('analytics.heatmap_empty'))
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
const grid = heatmap.grid
|
|
3997
|
+
const hours = Array.isArray(heatmap.hours) ? heatmap.hours : Array(24).fill(0)
|
|
3998
|
+
const total = Number(heatmap.total) || 0
|
|
3999
|
+
const models = Array.isArray(heatmap.models) ? heatmap.models : []
|
|
4000
|
+
const max = Math.max(1, ...grid.flat())
|
|
4001
|
+
const maxHours = Math.max(1, ...hours)
|
|
4002
|
+
const peakHour = hours.reduce((best, value, hour) => (value > (hours[best] || 0) ? hour : best), 0)
|
|
4003
|
+
const peak = hours[peakHour] || 0
|
|
4004
|
+
const level = (value) => {
|
|
4005
|
+
const requests = Number(value) || 0
|
|
4006
|
+
if (requests <= 0) return 0
|
|
4007
|
+
return Math.min(4, Math.max(1, Math.ceil((requests / max) * 4)))
|
|
4008
|
+
}
|
|
4009
|
+
const weekdays = heatWeekdayLabels()
|
|
4010
|
+
|
|
4011
|
+
return React.createElement(
|
|
4012
|
+
'div',
|
|
4013
|
+
{ className: 'cb-heat-wrap' },
|
|
4014
|
+
React.createElement(
|
|
4015
|
+
'div',
|
|
4016
|
+
{ className: 'cb-heat-toolbar' },
|
|
4017
|
+
React.createElement('span', { className: 'cb-badge' }, `${total} ${t('analytics.requests')}`),
|
|
4018
|
+
models.length > 1
|
|
4019
|
+
? React.createElement(
|
|
4020
|
+
'select',
|
|
4021
|
+
{
|
|
4022
|
+
className: 'cb-heat-select',
|
|
4023
|
+
value: model || '',
|
|
4024
|
+
title: t('analytics.heatmap_model'),
|
|
4025
|
+
onChange: (event) => onModelChange && onModelChange(event.target.value),
|
|
4026
|
+
},
|
|
4027
|
+
React.createElement('option', { value: '' }, `${t('analytics.heatmap_all_models')} (${total})`),
|
|
4028
|
+
models.map((entry) => React.createElement(
|
|
4029
|
+
'option',
|
|
4030
|
+
{ key: entry.model, value: entry.model },
|
|
4031
|
+
`${entry.model} (${entry.total})`
|
|
4032
|
+
))
|
|
4033
|
+
)
|
|
4034
|
+
: null,
|
|
4035
|
+
peak > 0
|
|
4036
|
+
? React.createElement('span', { className: 'cb-hint' }, `${t('analytics.heatmap_peak')}: ${String(peakHour).padStart(2, '0')}:00 · ${peak}`)
|
|
4037
|
+
: null
|
|
4038
|
+
),
|
|
4039
|
+
React.createElement(
|
|
4040
|
+
'div',
|
|
4041
|
+
{ className: 'cb-heat-scroll' },
|
|
4042
|
+
React.createElement(
|
|
4043
|
+
'div',
|
|
4044
|
+
{ className: 'cb-heat' },
|
|
4045
|
+
React.createElement('span', { className: 'cb-heat-corner' }),
|
|
4046
|
+
hours.map((_, hour) => React.createElement(
|
|
4047
|
+
'span',
|
|
4048
|
+
{ className: 'cb-heat-hour', key: `hour-${hour}` },
|
|
4049
|
+
heatHourLabel(hour)
|
|
4050
|
+
)),
|
|
4051
|
+
weekdays.map((label, day) => React.createElement(
|
|
4052
|
+
React.Fragment,
|
|
4053
|
+
{ key: `day-${day}` },
|
|
4054
|
+
React.createElement('span', { className: 'cb-heat-day' }, label),
|
|
4055
|
+
(grid[day] || []).map((value, hour) => React.createElement('span', {
|
|
4056
|
+
key: `cell-${day}-${hour}`,
|
|
4057
|
+
className: `cb-heat-cell cb-heat-l${level(value)}`,
|
|
4058
|
+
title: `${label} ${String(hour).padStart(2, '0')}:00 — ${value} ${t('analytics.requests')}`,
|
|
4059
|
+
}))
|
|
4060
|
+
))
|
|
4061
|
+
),
|
|
4062
|
+
React.createElement('p', { className: 'cb-hour-strip-head' }, t('analytics.heatmap_by_hour')),
|
|
4063
|
+
React.createElement(
|
|
4064
|
+
'div',
|
|
4065
|
+
{ className: 'cb-hour-strip' },
|
|
4066
|
+
React.createElement('span'),
|
|
4067
|
+
hours.map((value, hour) => React.createElement(
|
|
2906
4068
|
'span',
|
|
2907
|
-
{ className: 'cb-
|
|
4069
|
+
{ className: 'cb-hour-col', key: `strip-${hour}`, title: `${String(hour).padStart(2, '0')}:00 — ${value}` },
|
|
2908
4070
|
React.createElement('span', {
|
|
2909
|
-
className:
|
|
2910
|
-
style: {
|
|
4071
|
+
className: `cb-hour-bar cb-heat-l${level(value)}`,
|
|
4072
|
+
style: { height: `${Math.max(2, Math.round(28 * (Number(value) || 0) / maxHours))}px` },
|
|
2911
4073
|
})
|
|
4074
|
+
))
|
|
4075
|
+
),
|
|
4076
|
+
React.createElement(
|
|
4077
|
+
'div',
|
|
4078
|
+
{ className: 'cb-hour-labels' },
|
|
4079
|
+
React.createElement('span'),
|
|
4080
|
+
hours.map((_, hour) => React.createElement('span', { key: `label-${hour}` }, heatHourLabel(hour)))
|
|
4081
|
+
)
|
|
4082
|
+
),
|
|
4083
|
+
React.createElement(
|
|
4084
|
+
'div',
|
|
4085
|
+
{ className: 'cb-heat-legend' },
|
|
4086
|
+
React.createElement('span', null, t('analytics.heatmap_less')),
|
|
4087
|
+
[0, 1, 2, 3, 4].map((bucket) => React.createElement('span', {
|
|
4088
|
+
key: bucket,
|
|
4089
|
+
className: `cb-heat-swatch cb-heat-l${bucket}`,
|
|
4090
|
+
})),
|
|
4091
|
+
React.createElement('span', null, t('analytics.heatmap_more'))
|
|
4092
|
+
),
|
|
4093
|
+
total === 0
|
|
4094
|
+
? React.createElement('div', { className: 'cb-hint' }, t('analytics.heatmap_empty'))
|
|
4095
|
+
: null
|
|
4096
|
+
)
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// --- src/client/components/quota-panel-view.js ---
|
|
4100
|
+
/**
|
|
4101
|
+
* The account/quota cards shared by the center-column panel and the modal
|
|
4102
|
+
* fallback. Account tabs and the selected account's report are passed in by
|
|
4103
|
+
* DashboardDialog; this component only renders the selected snapshot.
|
|
4104
|
+
*/
|
|
4105
|
+
function QuotaPanelView({
|
|
4106
|
+
accountTabs,
|
|
4107
|
+
selectedUsage,
|
|
4108
|
+
accountName,
|
|
4109
|
+
accountEmail,
|
|
4110
|
+
accountPlan,
|
|
4111
|
+
accountBadges = [],
|
|
4112
|
+
activeAcc,
|
|
4113
|
+
routingMode,
|
|
4114
|
+
enabledCount,
|
|
4115
|
+
t,
|
|
4116
|
+
}) {
|
|
4117
|
+
return React.createElement(
|
|
4118
|
+
React.Fragment,
|
|
4119
|
+
null,
|
|
4120
|
+
accountTabs,
|
|
4121
|
+
selectedUsage?.ok === false
|
|
4122
|
+
? React.createElement('div', { className: 'cb-alert-bad' }, selectedUsage.error || t('analytics.no_data'))
|
|
4123
|
+
: null,
|
|
4124
|
+
React.createElement(
|
|
4125
|
+
'div',
|
|
4126
|
+
{ className: 'cb-dialogCard' },
|
|
4127
|
+
React.createElement(
|
|
4128
|
+
'div',
|
|
4129
|
+
{ className: 'cb-dialogRow' },
|
|
4130
|
+
React.createElement(
|
|
4131
|
+
'div',
|
|
4132
|
+
{ className: 'cb-dialogIdentity' },
|
|
4133
|
+
React.createElement('div', { className: 'cb-row-title' }, accountName),
|
|
4134
|
+
React.createElement('div', { className: 'cb-row-desc' }, [accountEmail, accountPlan].filter(Boolean).join(' · '))
|
|
4135
|
+
),
|
|
4136
|
+
React.createElement(
|
|
4137
|
+
'div',
|
|
4138
|
+
{ className: 'cb-dialogBadges' },
|
|
4139
|
+
accountBadges.map((badge, index) => React.createElement('span', { className: badge.className, key: `account-badge-${index}` }, badge.label)),
|
|
4140
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('badge.registered', { count: enabledCount }))
|
|
4141
|
+
)
|
|
4142
|
+
),
|
|
4143
|
+
React.createElement(
|
|
4144
|
+
'div',
|
|
4145
|
+
{ className: 'cb-dialogTiles' },
|
|
4146
|
+
React.createElement(
|
|
4147
|
+
'div',
|
|
4148
|
+
{ className: 'cb-stat-box' },
|
|
4149
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('dashboard.account_pool')),
|
|
4150
|
+
React.createElement('div', { className: 'cb-stat-val' }, activeAcc)
|
|
4151
|
+
),
|
|
4152
|
+
React.createElement(
|
|
4153
|
+
'div',
|
|
4154
|
+
{ className: 'cb-stat-box' },
|
|
4155
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('accounts.mode_label')),
|
|
4156
|
+
React.createElement('div', { className: 'cb-stat-val' }, routingMode)
|
|
2912
4157
|
)
|
|
2913
4158
|
)
|
|
2914
4159
|
),
|
|
2915
|
-
|
|
4160
|
+
React.createElement(
|
|
4161
|
+
'div',
|
|
4162
|
+
{ className: 'cb-dialogCard' },
|
|
4163
|
+
React.createElement('div', { className: 'cb-section-title' }, t('quota.title')),
|
|
4164
|
+
React.createElement(
|
|
4165
|
+
'div',
|
|
4166
|
+
{ className: 'cb-dialogWindows' },
|
|
4167
|
+
selectedUsage?.windows?.fiveHour
|
|
4168
|
+
? React.createElement(ProgressBar, {
|
|
4169
|
+
label: t('quota.window_5h'),
|
|
4170
|
+
percentUsed: selectedUsage.windows.fiveHour.percentUsed || 0,
|
|
4171
|
+
remainingPercent: selectedUsage.windows.fiveHour.remainingPercent || 100,
|
|
4172
|
+
resetsAt: selectedUsage.windows.fiveHour.resetsAt,
|
|
4173
|
+
t,
|
|
4174
|
+
})
|
|
4175
|
+
: null,
|
|
4176
|
+
selectedUsage?.windows?.weekly
|
|
4177
|
+
? React.createElement(ProgressBar, {
|
|
4178
|
+
label: t('quota.window_weekly'),
|
|
4179
|
+
percentUsed: selectedUsage.windows.weekly.percentUsed || 0,
|
|
4180
|
+
remainingPercent: selectedUsage.windows.weekly.remainingPercent || 100,
|
|
4181
|
+
resetsAt: selectedUsage.windows.weekly.resetsAt,
|
|
4182
|
+
t,
|
|
4183
|
+
})
|
|
4184
|
+
: null,
|
|
4185
|
+
selectedUsage?.windows?.monthly
|
|
4186
|
+
? React.createElement(ProgressBar, {
|
|
4187
|
+
label: t('quota.window_monthly'),
|
|
4188
|
+
percentUsed: selectedUsage.windows.monthly.percentUsed || 0,
|
|
4189
|
+
remainingPercent: selectedUsage.windows.monthly.remainingPercent || 100,
|
|
4190
|
+
resetsAt: selectedUsage.windows.monthly.resetsAt,
|
|
4191
|
+
t,
|
|
4192
|
+
})
|
|
4193
|
+
: null,
|
|
4194
|
+
!selectedUsage?.windows || (!selectedUsage.windows.fiveHour && !selectedUsage.windows.weekly && !selectedUsage.windows.monthly)
|
|
4195
|
+
? React.createElement('div', { className: 'cb-hint' }, t('quota.no_windows'))
|
|
4196
|
+
: null
|
|
4197
|
+
)
|
|
4198
|
+
)
|
|
2916
4199
|
)
|
|
2917
4200
|
}
|
|
2918
4201
|
|
|
2919
4202
|
// --- src/client/components/dashboard-dialog.js ---
|
|
2920
4203
|
/**
|
|
2921
|
-
* The
|
|
2922
|
-
*
|
|
2923
|
-
*
|
|
2924
|
-
* `layout.selectPanel('clinebot-dashboard')`, which swapped the conversation out
|
|
2925
|
-
* of the centre column for a full-bleed dashboard. A status read-out has no
|
|
2926
|
-
* business replacing the conversation, so it is a window now — the shell's own
|
|
2927
|
-
* `Modal`, which brings the backdrop, the close affordance, Escape and focus
|
|
2928
|
-
* handling with it.
|
|
2929
|
-
*
|
|
2930
|
-
* `open` is owned by the caller (the meter), and the data is fetched on open
|
|
2931
|
-
* rather than on mount, so a closed window costs nothing.
|
|
4204
|
+
* Quota and analytics surfaces. The sidebar card opens this as the layout's
|
|
4205
|
+
* center-column panel (the Command Code / OpenCode shape); when the layout
|
|
4206
|
+
* service is unavailable the same content falls back to the shell's Modal.
|
|
2932
4207
|
*/
|
|
2933
4208
|
function DashboardDialog(props) {
|
|
2934
4209
|
const t = props?.t || makeT(en, en)
|
|
2935
|
-
const
|
|
4210
|
+
const panel = props?.panel === true
|
|
4211
|
+
const open = panel || props?.open !== false
|
|
2936
4212
|
const onClose = typeof props?.onClose === 'function' ? props.onClose : () => {}
|
|
2937
4213
|
|
|
2938
4214
|
const [data, setData] = React.useState(null)
|
|
2939
4215
|
const [status, setStatus] = React.useState(null)
|
|
4216
|
+
const [analytics, setAnalytics] = React.useState(null)
|
|
4217
|
+
const [tab, setTab] = React.useState('quota')
|
|
4218
|
+
const [groupBy, setGroupBy] = React.useState('model')
|
|
2940
4219
|
const [loading, setLoading] = React.useState(false)
|
|
4220
|
+
const [analyticsLoading, setAnalyticsLoading] = React.useState(false)
|
|
2941
4221
|
const [updatedAt, setUpdatedAt] = React.useState('')
|
|
2942
4222
|
const [err, setErr] = React.useState('')
|
|
4223
|
+
const [accountsUsage, setAccountsUsage] = React.useState(null)
|
|
4224
|
+
const [selectedAccount, setSelectedAccount] = React.useState('')
|
|
4225
|
+
const [analyticsDays, setAnalyticsDays] = React.useState(7)
|
|
4226
|
+
const [heatmapModel, setHeatmapModel] = React.useState('')
|
|
2943
4227
|
|
|
2944
4228
|
const loadData = React.useCallback(async () => {
|
|
2945
4229
|
setLoading(true)
|
|
2946
4230
|
setErr('')
|
|
2947
4231
|
try {
|
|
2948
|
-
const [uRes, sRes] = await Promise.all([
|
|
4232
|
+
const [uRes, sRes, aRes] = await Promise.all([
|
|
2949
4233
|
fetchWithTimeout(`${ROUTE_PREFIX}/usage`, { cache: 'no-store' }),
|
|
2950
4234
|
fetchWithTimeout(`${ROUTE_PREFIX}/status`, { cache: 'no-store' }),
|
|
4235
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/accounts/usage`, { cache: 'no-store' }),
|
|
2951
4236
|
])
|
|
2952
4237
|
const uJson = await uRes.json().catch(() => ({}))
|
|
2953
4238
|
const sJson = await sRes.json().catch(() => ({}))
|
|
4239
|
+
const aJson = await aRes.json().catch(() => ({}))
|
|
2954
4240
|
if (uJson?.ok) setData(uJson)
|
|
2955
4241
|
if (sJson?.ok) setStatus(sJson)
|
|
4242
|
+
if (aJson?.ok) {
|
|
4243
|
+
setAccountsUsage(aJson)
|
|
4244
|
+
setSelectedAccount((current) => {
|
|
4245
|
+
const accounts = Array.isArray(aJson.accounts) ? aJson.accounts : []
|
|
4246
|
+
if (accounts.some((entry) => entry.apiKeyEnv === current)) return current
|
|
4247
|
+
return aJson.activeAccount || accounts[0]?.apiKeyEnv || ''
|
|
4248
|
+
})
|
|
4249
|
+
}
|
|
2956
4250
|
setUpdatedAt(new Date().toLocaleTimeString())
|
|
2957
4251
|
} catch (e) {
|
|
2958
4252
|
setErr(String(e?.message || e))
|
|
@@ -2961,105 +4255,375 @@ function DashboardDialog(props) {
|
|
|
2961
4255
|
}
|
|
2962
4256
|
}, [])
|
|
2963
4257
|
|
|
4258
|
+
const loadAnalytics = React.useCallback(async (group = groupBy, days = analyticsDays, model = heatmapModel) => {
|
|
4259
|
+
setAnalyticsLoading(true)
|
|
4260
|
+
setErr('')
|
|
4261
|
+
try {
|
|
4262
|
+
const from = Date.now() - days * 86_400_000
|
|
4263
|
+
const query = [
|
|
4264
|
+
`groupBy=${encodeURIComponent(group)}`,
|
|
4265
|
+
'limit=20',
|
|
4266
|
+
`from=${from}`,
|
|
4267
|
+
`heatmapDays=${days}`,
|
|
4268
|
+
...(model ? [`heatmapModel=${encodeURIComponent(model)}`] : []),
|
|
4269
|
+
].join('&')
|
|
4270
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/analytics?${query}`, { cache: 'no-store' })
|
|
4271
|
+
const json = await res.json().catch(() => ({}))
|
|
4272
|
+
if (!res.ok || !json.ok) throw new Error(json.error || `HTTP ${res.status}`)
|
|
4273
|
+
setAnalytics(json)
|
|
4274
|
+
} catch (e) {
|
|
4275
|
+
setErr(String(e?.message || e))
|
|
4276
|
+
} finally {
|
|
4277
|
+
setAnalyticsLoading(false)
|
|
4278
|
+
}
|
|
4279
|
+
}, [groupBy, analyticsDays, heatmapModel])
|
|
4280
|
+
|
|
2964
4281
|
React.useEffect(() => {
|
|
2965
4282
|
if (open) loadData()
|
|
2966
4283
|
}, [open, loadData])
|
|
2967
4284
|
|
|
4285
|
+
React.useEffect(() => {
|
|
4286
|
+
if (open && (panel || tab === 'analytics')) loadAnalytics(groupBy, analyticsDays, heatmapModel)
|
|
4287
|
+
}, [open, panel, tab, groupBy, analyticsDays, heatmapModel, loadAnalytics])
|
|
4288
|
+
|
|
4289
|
+
React.useEffect(() => {
|
|
4290
|
+
if (!open) return undefined
|
|
4291
|
+
let timer = null
|
|
4292
|
+
const tick = () => {
|
|
4293
|
+
loadData()
|
|
4294
|
+
if (panel || tab === 'analytics') loadAnalytics(groupBy, analyticsDays, heatmapModel)
|
|
4295
|
+
timer = setTimeout(tick, 120000)
|
|
4296
|
+
}
|
|
4297
|
+
timer = setTimeout(tick, 120000)
|
|
4298
|
+
return () => { if (timer) clearTimeout(timer) }
|
|
4299
|
+
}, [open, panel, tab, groupBy, analyticsDays, heatmapModel, loadData, loadAnalytics])
|
|
4300
|
+
|
|
2968
4301
|
if (!open) return null
|
|
2969
4302
|
|
|
4303
|
+
function fmtTokens(value) {
|
|
4304
|
+
const n = Number(value) || 0
|
|
4305
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
4306
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
|
|
4307
|
+
return String(n)
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
function fmtCost(value) {
|
|
4311
|
+
const n = Number(value) || 0
|
|
4312
|
+
if (n === 0) return '$0'
|
|
4313
|
+
if (n < 0.0001) return '< $0.0001'
|
|
4314
|
+
return `$${n.toFixed(4)}`
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
function fmtMs(value) {
|
|
4318
|
+
const n = Number(value) || 0
|
|
4319
|
+
return n > 0 ? `${Math.round(n)} ms` : '—'
|
|
4320
|
+
}
|
|
4321
|
+
|
|
2970
4322
|
const plan = data?.plan || status?.config?.defaultModel || 'ClinePass'
|
|
2971
4323
|
const email = data?.user?.email || ''
|
|
2972
|
-
const activeAcc = status?.config?.activeAccount || '
|
|
4324
|
+
const activeAcc = status?.config?.activeAccount || t('dashboard.auto_account')
|
|
2973
4325
|
const enabledCount = status?.config?.enabledModels?.length || 0
|
|
2974
4326
|
const routingMode = status?.config?.accountMode === 'round_robin' ? t('accounts.mode_round_robin') : t('accounts.mode_least_used')
|
|
4327
|
+
const accountEntries = Array.isArray(accountsUsage?.accounts) ? accountsUsage.accounts : []
|
|
4328
|
+
const selectedEntry = accountEntries.find((entry) => entry.apiKeyEnv === selectedAccount)
|
|
4329
|
+
|| accountEntries.find((entry) => entry.active)
|
|
4330
|
+
|| accountEntries[0]
|
|
4331
|
+
const selectedUsage = selectedEntry?.usage || data
|
|
4332
|
+
const accountPlan = selectedUsage?.plan || plan
|
|
4333
|
+
const accountEmail = selectedUsage?.user?.email || email
|
|
4334
|
+
const accountName = selectedEntry?.label || selectedEntry?.apiKeyEnv || accountPlan
|
|
4335
|
+
const accountBadges = []
|
|
4336
|
+
if (selectedEntry) {
|
|
4337
|
+
accountBadges.push(selectedEntry.configured === false
|
|
4338
|
+
? { className: 'cb-badge cb-badge-warn', label: t('accounts.missing_key') }
|
|
4339
|
+
: { className: 'cb-badge cb-badge-ok', label: t('accounts.configured') })
|
|
4340
|
+
if (selectedEntry.active) accountBadges.push({ className: 'cb-badge cb-badge-ok', label: t('accounts.active_badge') })
|
|
4341
|
+
if (selectedEntry.isPinned) accountBadges.push({ className: 'cb-badge', label: t('accounts.pinned_badge') })
|
|
4342
|
+
}
|
|
4343
|
+
const accountTabs = accountEntries.length > 1
|
|
4344
|
+
? React.createElement(
|
|
4345
|
+
'div',
|
|
4346
|
+
{ className: 'cb-account-tabs' },
|
|
4347
|
+
accountEntries.map((entry) => React.createElement(
|
|
4348
|
+
'button',
|
|
4349
|
+
{
|
|
4350
|
+
type: 'button',
|
|
4351
|
+
key: entry.apiKeyEnv || entry.id,
|
|
4352
|
+
className: entry.apiKeyEnv === selectedEntry?.apiKeyEnv ? 'cb-account-tab cb-account-tab-active' : 'cb-account-tab',
|
|
4353
|
+
onClick: () => setSelectedAccount(entry.apiKeyEnv),
|
|
4354
|
+
},
|
|
4355
|
+
entry.label || entry.apiKeyEnv || entry.id
|
|
4356
|
+
))
|
|
4357
|
+
)
|
|
4358
|
+
: null
|
|
2975
4359
|
|
|
2976
|
-
const
|
|
4360
|
+
const noticeItems = []
|
|
4361
|
+
if (status?.key && status.key.present === false) {
|
|
4362
|
+
noticeItems.push(React.createElement(
|
|
4363
|
+
'div',
|
|
4364
|
+
{ className: 'cb-notice', key: 'no-key' },
|
|
4365
|
+
React.createElement('div', { className: 'cb-notice-title' }, t('analytics.no_key')),
|
|
4366
|
+
React.createElement('div', { className: 'cb-notice-hint' }, t('analytics.no_key_hint'))
|
|
4367
|
+
))
|
|
4368
|
+
}
|
|
4369
|
+
if (status?.health && status.health.ok === false) {
|
|
4370
|
+
noticeItems.push(React.createElement(
|
|
4371
|
+
'div',
|
|
4372
|
+
{ className: 'cb-notice cb-notice-error', key: 'health' },
|
|
4373
|
+
React.createElement('div', { className: 'cb-notice-title' }, t('analytics.health_error')),
|
|
4374
|
+
React.createElement('div', { className: 'cb-notice-hint' }, status.health.error || '')
|
|
4375
|
+
))
|
|
4376
|
+
}
|
|
4377
|
+
if (status && status.isRegistered === false) {
|
|
4378
|
+
noticeItems.push(React.createElement(
|
|
4379
|
+
'div',
|
|
4380
|
+
{ className: 'cb-notice', key: 'not-registered' },
|
|
4381
|
+
React.createElement('div', { className: 'cb-notice-title' }, t('analytics.not_registered')),
|
|
4382
|
+
React.createElement('div', { className: 'cb-notice-hint' }, t('analytics.not_registered_hint'))
|
|
4383
|
+
))
|
|
4384
|
+
}
|
|
4385
|
+
const notices = noticeItems.length
|
|
4386
|
+
? React.createElement(React.Fragment, null, ...noticeItems)
|
|
4387
|
+
: null
|
|
4388
|
+
const errorNotice = err
|
|
4389
|
+
? React.createElement('div', { className: data ? 'cb-hint' : 'cb-alert-bad' }, data ? `${t('analytics.stale')} ${err}` : err)
|
|
4390
|
+
: null
|
|
4391
|
+
const totals = analytics?.totals
|
|
4392
|
+
const secondaryTiles = totals
|
|
4393
|
+
? [
|
|
4394
|
+
[t('analytics.input'), fmtTokens(totals.promptTokens)],
|
|
4395
|
+
[t('analytics.output'), fmtTokens(totals.completionTokens)],
|
|
4396
|
+
[t('analytics.reasoning'), fmtTokens(totals.reasoningTokens)],
|
|
4397
|
+
[t('analytics.cache_read'), fmtTokens(totals.cacheReadTokens)],
|
|
4398
|
+
[t('analytics.cache_write'), fmtTokens(totals.cacheWriteTokens)],
|
|
4399
|
+
[t('analytics.avg_ttft'), fmtMs(totals.avgTtftMs)],
|
|
4400
|
+
[t('analytics.avg_latency'), fmtMs(totals.avgLatencyMs)],
|
|
4401
|
+
[t('analytics.errors'), String(Math.round(Number(totals.failed) || 0))],
|
|
4402
|
+
]
|
|
4403
|
+
: []
|
|
4404
|
+
|
|
4405
|
+
const tabs = React.createElement(
|
|
2977
4406
|
'div',
|
|
2978
|
-
{ className: 'cb-
|
|
4407
|
+
{ className: 'cb-tabs' },
|
|
4408
|
+
React.createElement(
|
|
4409
|
+
'button',
|
|
4410
|
+
{ type: 'button', className: tab === 'quota' ? 'cb-tab cb-tab-active' : 'cb-tab', onClick: () => setTab('quota') },
|
|
4411
|
+
t('dashboard.tab_quota')
|
|
4412
|
+
),
|
|
4413
|
+
React.createElement(
|
|
4414
|
+
'button',
|
|
4415
|
+
{ type: 'button', className: tab === 'analytics' ? 'cb-tab cb-tab-active' : 'cb-tab', onClick: () => setTab('analytics') },
|
|
4416
|
+
t('dashboard.tab_analytics')
|
|
4417
|
+
)
|
|
4418
|
+
)
|
|
4419
|
+
|
|
4420
|
+
const quotaBody = React.createElement(QuotaPanelView, {
|
|
4421
|
+
accountTabs,
|
|
4422
|
+
selectedUsage,
|
|
4423
|
+
accountName,
|
|
4424
|
+
accountEmail,
|
|
4425
|
+
accountPlan,
|
|
4426
|
+
accountBadges,
|
|
4427
|
+
activeAcc,
|
|
4428
|
+
routingMode,
|
|
4429
|
+
enabledCount,
|
|
4430
|
+
t,
|
|
4431
|
+
})
|
|
4432
|
+
|
|
4433
|
+
const analyticsBody = React.createElement(
|
|
4434
|
+
'div',
|
|
4435
|
+
{ className: 'cb-dialogCard' },
|
|
2979
4436
|
React.createElement(
|
|
2980
4437
|
'div',
|
|
2981
|
-
{ className: 'cb-
|
|
2982
|
-
|
|
2983
|
-
React.createElement('span', { style: { flex: 1 } }),
|
|
4438
|
+
{ className: 'cb-dialogRow' },
|
|
4439
|
+
React.createElement('div', { className: 'cb-section-title' }, t('analytics.title')),
|
|
2984
4440
|
React.createElement(
|
|
2985
|
-
'
|
|
2986
|
-
{
|
|
2987
|
-
|
|
4441
|
+
'div',
|
|
4442
|
+
{ className: 'cb-analytics-controls' },
|
|
4443
|
+
React.createElement(
|
|
4444
|
+
'select',
|
|
4445
|
+
{
|
|
4446
|
+
className: 'cb-heat-select',
|
|
4447
|
+
value: String(analyticsDays),
|
|
4448
|
+
title: t('analytics.range'),
|
|
4449
|
+
onChange: (event) => setAnalyticsDays(Number(event.target.value)),
|
|
4450
|
+
},
|
|
4451
|
+
React.createElement('option', { value: '7' }, t('analytics.range_7')),
|
|
4452
|
+
React.createElement('option', { value: '30' }, t('analytics.range_30')),
|
|
4453
|
+
React.createElement('option', { value: '90' }, t('analytics.range_90'))
|
|
4454
|
+
),
|
|
4455
|
+
React.createElement(
|
|
4456
|
+
'div',
|
|
4457
|
+
{ className: 'cb-seg' },
|
|
4458
|
+
['model', 'day', 'session'].map((group) => React.createElement(
|
|
4459
|
+
'button',
|
|
4460
|
+
{
|
|
4461
|
+
key: group,
|
|
4462
|
+
type: 'button',
|
|
4463
|
+
className: groupBy === group ? 'cb-seg-item cb-seg-item-active' : 'cb-seg-item',
|
|
4464
|
+
onClick: () => setGroupBy(group),
|
|
4465
|
+
},
|
|
4466
|
+
t(`analytics.group_${group}`)
|
|
4467
|
+
))
|
|
4468
|
+
)
|
|
2988
4469
|
)
|
|
2989
4470
|
),
|
|
4471
|
+
analytics?.totals
|
|
4472
|
+
? React.createElement(
|
|
4473
|
+
'div',
|
|
4474
|
+
{ className: 'cb-dialogTiles' },
|
|
4475
|
+
React.createElement(
|
|
4476
|
+
'div',
|
|
4477
|
+
{ className: 'cb-stat-box' },
|
|
4478
|
+
React.createElement('div', { className: 'cb-stat-val' }, String(analytics.totals.requests || 0)),
|
|
4479
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('analytics.requests'))
|
|
4480
|
+
),
|
|
4481
|
+
React.createElement(
|
|
4482
|
+
'div',
|
|
4483
|
+
{ className: 'cb-stat-box' },
|
|
4484
|
+
React.createElement('div', { className: 'cb-stat-val' }, fmtTokens(analytics.totals.totalTokens)),
|
|
4485
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('analytics.tokens'))
|
|
4486
|
+
),
|
|
4487
|
+
React.createElement(
|
|
4488
|
+
'div',
|
|
4489
|
+
{ className: 'cb-stat-box' },
|
|
4490
|
+
React.createElement('div', { className: 'cb-stat-val' }, `≈ ${fmtCost(analytics.totals.costUsd)}`),
|
|
4491
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('analytics.cost'))
|
|
4492
|
+
),
|
|
4493
|
+
React.createElement(
|
|
4494
|
+
'div',
|
|
4495
|
+
{ className: 'cb-stat-box' },
|
|
4496
|
+
React.createElement('div', { className: 'cb-stat-val' }, `${Math.round(analytics.totals.successRate || 0)}%`),
|
|
4497
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('analytics.success'))
|
|
4498
|
+
)
|
|
4499
|
+
)
|
|
4500
|
+
: null,
|
|
4501
|
+
secondaryTiles.length
|
|
4502
|
+
? React.createElement(
|
|
4503
|
+
'div',
|
|
4504
|
+
{ className: 'cb-dialogTiles' },
|
|
4505
|
+
secondaryTiles.map(([label, value]) => React.createElement(
|
|
4506
|
+
'div',
|
|
4507
|
+
{ className: 'cb-stat-box', key: label },
|
|
4508
|
+
React.createElement('div', { className: 'cb-stat-val' }, value),
|
|
4509
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, label)
|
|
4510
|
+
))
|
|
4511
|
+
)
|
|
4512
|
+
: null,
|
|
4513
|
+
analyticsLoading
|
|
4514
|
+
? React.createElement('div', { className: 'cb-hint' }, t('analytics.loading'))
|
|
4515
|
+
: analytics?.groups?.length
|
|
4516
|
+
? React.createElement(
|
|
4517
|
+
'div',
|
|
4518
|
+
{ className: 'cb-analytics-list' },
|
|
4519
|
+
React.createElement(
|
|
4520
|
+
'div',
|
|
4521
|
+
{ className: 'cb-analytics-head' },
|
|
4522
|
+
React.createElement('span', null, analytics.groupBy || groupBy),
|
|
4523
|
+
React.createElement('span', null, t('analytics.requests')),
|
|
4524
|
+
React.createElement('span', null, t('analytics.tokens')),
|
|
4525
|
+
React.createElement('span', null, t('analytics.cost')),
|
|
4526
|
+
React.createElement('span', null, t('analytics.ttft'))
|
|
4527
|
+
),
|
|
4528
|
+
analytics.groups.map((group) => React.createElement(
|
|
4529
|
+
'div',
|
|
4530
|
+
{ className: 'cb-analytics-row', key: group.key },
|
|
4531
|
+
React.createElement('span', { className: 'cb-mono' }, group.label || group.key),
|
|
4532
|
+
React.createElement('span', null, String(group.requests || 0)),
|
|
4533
|
+
React.createElement('span', null, fmtTokens(group.totalTokens)),
|
|
4534
|
+
React.createElement('span', null, `≈ ${fmtCost(group.costUsd)}`),
|
|
4535
|
+
React.createElement('span', null, group.avgTtftMs ? `${Math.round(group.avgTtftMs)} ms` : '—')
|
|
4536
|
+
))
|
|
4537
|
+
)
|
|
4538
|
+
: React.createElement('div', { className: 'cb-empty' }, t('analytics.no_data')),
|
|
4539
|
+
analytics?.unpriced > 0
|
|
4540
|
+
? React.createElement('div', { className: 'cb-hint' }, t('analytics.unpriced', { count: analytics.unpriced }))
|
|
4541
|
+
: null
|
|
4542
|
+
)
|
|
2990
4543
|
|
|
2991
|
-
|
|
4544
|
+
const heatmapCard = React.createElement(
|
|
4545
|
+
'div',
|
|
4546
|
+
{ className: 'cb-dialogCard' },
|
|
4547
|
+
React.createElement('div', { className: 'cb-section-title' }, t('analytics.heatmap')),
|
|
4548
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('analytics.heatmap_hint')),
|
|
4549
|
+
React.createElement(HeatmapView, {
|
|
4550
|
+
heatmap: analytics?.heatmap,
|
|
4551
|
+
model: heatmapModel,
|
|
4552
|
+
onModelChange: setHeatmapModel,
|
|
4553
|
+
t,
|
|
4554
|
+
})
|
|
4555
|
+
)
|
|
2992
4556
|
|
|
2993
|
-
|
|
4557
|
+
const panelBody = React.createElement(
|
|
4558
|
+
'div',
|
|
4559
|
+
{ className: 'cb-panel' },
|
|
2994
4560
|
React.createElement(
|
|
2995
4561
|
'div',
|
|
2996
|
-
{ className: 'cb-
|
|
4562
|
+
{ className: 'cb-panel-head' },
|
|
2997
4563
|
React.createElement(
|
|
2998
4564
|
'div',
|
|
2999
|
-
{
|
|
3000
|
-
React.createElement(
|
|
3001
|
-
|
|
3002
|
-
{ style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
|
|
3003
|
-
React.createElement('div', { className: 'cb-row-title' }, plan),
|
|
3004
|
-
email ? React.createElement('div', { className: 'cb-row-desc' }, email) : null
|
|
3005
|
-
),
|
|
3006
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('badge.registered', { count: enabledCount }))
|
|
4565
|
+
{ className: 'cb-panel-identity' },
|
|
4566
|
+
React.createElement('div', { className: 'cb-panel-title' }, t('dashboard.title')),
|
|
4567
|
+
React.createElement('div', { className: 'cb-panel-sub' }, t('dashboard.subtitle'))
|
|
3007
4568
|
),
|
|
3008
4569
|
React.createElement(
|
|
3009
4570
|
'div',
|
|
3010
|
-
{ className: 'cb-
|
|
4571
|
+
{ className: 'cb-panel-actions' },
|
|
4572
|
+
updatedAt ? React.createElement('span', { className: 'cb-panel-updated' }, `${t('dashboard.updated_at')} ${updatedAt}`) : null,
|
|
3011
4573
|
React.createElement(
|
|
3012
|
-
'
|
|
3013
|
-
{
|
|
3014
|
-
|
|
3015
|
-
|
|
4574
|
+
'button',
|
|
4575
|
+
{
|
|
4576
|
+
type: 'button',
|
|
4577
|
+
className: 'cb-btn',
|
|
4578
|
+
disabled: loading || analyticsLoading,
|
|
4579
|
+
onClick: () => { loadData(); loadAnalytics(groupBy) },
|
|
4580
|
+
},
|
|
4581
|
+
(loading || analyticsLoading) ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
3016
4582
|
),
|
|
3017
4583
|
React.createElement(
|
|
3018
|
-
'
|
|
3019
|
-
{
|
|
3020
|
-
|
|
3021
|
-
|
|
4584
|
+
'button',
|
|
4585
|
+
{
|
|
4586
|
+
type: 'button',
|
|
4587
|
+
className: 'cb-panel-close',
|
|
4588
|
+
'aria-label': t('dashboard.close'),
|
|
4589
|
+
title: t('dashboard.close'),
|
|
4590
|
+
onClick: onClose,
|
|
4591
|
+
},
|
|
4592
|
+
'×'
|
|
3022
4593
|
)
|
|
3023
4594
|
)
|
|
3024
4595
|
),
|
|
4596
|
+
notices,
|
|
4597
|
+
errorNotice,
|
|
4598
|
+
quotaBody,
|
|
4599
|
+
analyticsBody,
|
|
4600
|
+
heatmapCard
|
|
4601
|
+
)
|
|
3025
4602
|
|
|
3026
|
-
|
|
4603
|
+
const body = React.createElement(
|
|
4604
|
+
'div',
|
|
4605
|
+
{ className: 'cb-dialog' },
|
|
3027
4606
|
React.createElement(
|
|
3028
4607
|
'div',
|
|
3029
|
-
{ className: 'cb-
|
|
3030
|
-
React.createElement('
|
|
4608
|
+
{ className: 'cb-dialogMeta' },
|
|
4609
|
+
updatedAt ? React.createElement('span', null, `${t('dashboard.updated_at')} ${updatedAt}`) : null,
|
|
4610
|
+
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
3031
4611
|
React.createElement(
|
|
3032
|
-
'
|
|
3033
|
-
{
|
|
3034
|
-
|
|
3035
|
-
? React.createElement(ProgressBar, {
|
|
3036
|
-
label: t('quota.window_5h'),
|
|
3037
|
-
percentUsed: data.windows.fiveHour.percentUsed || 0,
|
|
3038
|
-
remainingPercent: data.windows.fiveHour.remainingPercent || 100,
|
|
3039
|
-
resetsAt: data.windows.fiveHour.resetsAt,
|
|
3040
|
-
t,
|
|
3041
|
-
})
|
|
3042
|
-
: null,
|
|
3043
|
-
React.createElement(ProgressBar, {
|
|
3044
|
-
label: t('quota.window_weekly'),
|
|
3045
|
-
percentUsed: data?.windows?.weekly?.percentUsed || 0,
|
|
3046
|
-
remainingPercent: data?.windows?.weekly?.remainingPercent || 100,
|
|
3047
|
-
resetsAt: data?.windows?.weekly?.resetsAt,
|
|
3048
|
-
t,
|
|
3049
|
-
}),
|
|
3050
|
-
data?.windows?.monthly
|
|
3051
|
-
? React.createElement(ProgressBar, {
|
|
3052
|
-
label: t('quota.window_monthly'),
|
|
3053
|
-
percentUsed: data.windows.monthly.percentUsed || 0,
|
|
3054
|
-
remainingPercent: data.windows.monthly.remainingPercent || 100,
|
|
3055
|
-
resetsAt: data.windows.monthly.resetsAt,
|
|
3056
|
-
t,
|
|
3057
|
-
})
|
|
3058
|
-
: null
|
|
4612
|
+
'button',
|
|
4613
|
+
{ type: 'button', className: 'cb-btn', disabled: loading || analyticsLoading, onClick: () => (tab === 'quota' ? loadData() : loadAnalytics(groupBy)) },
|
|
4614
|
+
(loading || analyticsLoading) ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
3059
4615
|
)
|
|
3060
|
-
)
|
|
4616
|
+
),
|
|
4617
|
+
tabs,
|
|
4618
|
+
notices,
|
|
4619
|
+
errorNotice,
|
|
4620
|
+
tab === 'quota'
|
|
4621
|
+
? quotaBody
|
|
4622
|
+
: React.createElement(React.Fragment, null, analyticsBody, heatmapCard)
|
|
3061
4623
|
)
|
|
3062
4624
|
|
|
4625
|
+
if (panel) return panelBody
|
|
4626
|
+
|
|
3063
4627
|
if (Modal) {
|
|
3064
4628
|
return React.createElement(Modal, {
|
|
3065
4629
|
open: true,
|
|
@@ -3071,18 +4635,17 @@ function DashboardDialog(props) {
|
|
|
3071
4635
|
})
|
|
3072
4636
|
}
|
|
3073
4637
|
|
|
3074
|
-
// Older shells without the primitive still get a window, just a plainer one.
|
|
3075
4638
|
return React.createElement(
|
|
3076
4639
|
'div',
|
|
3077
4640
|
{ className: 'cb-dialogFallback', role: 'dialog', 'aria-label': t('dashboard.title'), onClick: onClose },
|
|
3078
4641
|
React.createElement(
|
|
3079
4642
|
'div',
|
|
3080
|
-
{ className: 'cb-dialogFallbackPanel', onClick: (
|
|
4643
|
+
{ className: 'cb-dialogFallbackPanel', onClick: (event) => event.stopPropagation() },
|
|
3081
4644
|
React.createElement(
|
|
3082
4645
|
'div',
|
|
3083
4646
|
{ className: 'cb-dialogMeta' },
|
|
3084
4647
|
React.createElement('span', { className: 'cb-row-title' }, t('dashboard.title')),
|
|
3085
|
-
React.createElement('span', {
|
|
4648
|
+
React.createElement('span', { className: 'cb-settings-spacer' }),
|
|
3086
4649
|
React.createElement(
|
|
3087
4650
|
'button',
|
|
3088
4651
|
{ type: 'button', className: 'cb-btn', 'aria-label': t('dashboard.close'), onClick: onClose },
|
|
@@ -3102,155 +4665,275 @@ function formatTokens(count) {
|
|
|
3102
4665
|
return String(n)
|
|
3103
4666
|
}
|
|
3104
4667
|
|
|
3105
|
-
function
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
4668
|
+
function findPrice(modelId, prices) {
|
|
4669
|
+
if (!modelId || !Array.isArray(prices)) return null
|
|
4670
|
+
const exact = prices.find((entry) => entry && entry.id === modelId)
|
|
4671
|
+
if (exact) return exact
|
|
4672
|
+
return prices.find((entry) => entry && entry.id && (modelId === entry.id || modelId.endsWith(`/${entry.id}`) || modelId.includes(entry.id))) || null
|
|
4673
|
+
}
|
|
3111
4674
|
|
|
4675
|
+
function calculateCost(tokens, modelId, prices) {
|
|
3112
4676
|
const pTok = Number(tokens?.promptTokens || tokens?.inputTokens || 0)
|
|
3113
4677
|
const cTok = Number(tokens?.completionTokens || tokens?.outputTokens || 0)
|
|
3114
4678
|
const rTok = Number(tokens?.reasoningTokens || 0)
|
|
3115
|
-
|
|
3116
|
-
const
|
|
3117
|
-
|
|
4679
|
+
const cacheRead = Number(tokens?.cacheReadTokens || tokens?.cacheRead || 0)
|
|
4680
|
+
const cacheWrite = Number(tokens?.cacheWriteTokens || tokens?.cacheWrite || 0)
|
|
4681
|
+
const total = pTok + cTok + rTok
|
|
4682
|
+
const price = findPrice(modelId, prices)
|
|
4683
|
+
if (!price) return { cost: 0, priced: false, source: null, pTok, cTok, rTok, cacheRead, cacheWrite, total }
|
|
4684
|
+
const uncachedInput = Math.max(0, pTok - cacheRead - cacheWrite)
|
|
4685
|
+
const cost = (
|
|
4686
|
+
uncachedInput * Number(price.input || 0) +
|
|
4687
|
+
cTok * Number(price.output || 0) +
|
|
4688
|
+
cacheRead * Number(price.cacheRead || 0) +
|
|
4689
|
+
cacheWrite * Number(price.cacheWrite || 0)
|
|
4690
|
+
) / 1000000
|
|
4691
|
+
return { cost, priced: true, source: price.source || 'estimate', pTok, cTok, rTok, cacheRead, cacheWrite, total }
|
|
3118
4692
|
}
|
|
3119
4693
|
|
|
4694
|
+
const COST_ANCHOR = '[data-slot="conversation.composer.dock"]'
|
|
4695
|
+
const STATS_ROOT = '[data-composer-stats]'
|
|
4696
|
+
const STATS_USAGE = '[data-composer-stat="usage"]'
|
|
4697
|
+
const STATS_ANY = '[data-composer-stat]'
|
|
4698
|
+
const DIALOG_TRIGGER = 'button[aria-haspopup="dialog"]'
|
|
4699
|
+
const MISSES_BEFORE_WARNING = 3
|
|
4700
|
+
|
|
4701
|
+
/**
|
|
4702
|
+
* Inject the session cost into the native token-usage pill instead of drawing a
|
|
4703
|
+
* second pill. The composer's `conversation.composer.dock` entry is still the
|
|
4704
|
+
* only way to read the session projection; the rendered node is a hidden anchor
|
|
4705
|
+
* the injection locates its own composer from.
|
|
4706
|
+
*/
|
|
3120
4707
|
function ComposerCostDock(props) {
|
|
4708
|
+
if (typeof props?.useProjection !== 'function') return null
|
|
4709
|
+
return React.createElement(ComposerCostEntry, props)
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
function ComposerCostEntry(props) {
|
|
3121
4713
|
const t = props?.t || makeT(en, en)
|
|
4714
|
+
const configSnapshot = useConfigFormsSnapshot(props?.ctx)
|
|
3122
4715
|
const [stats, setStats] = React.useState(null)
|
|
3123
|
-
const [
|
|
4716
|
+
const [prices, setPrices] = React.useState([])
|
|
4717
|
+
const [sessionStats, setSessionStats] = React.useState(null)
|
|
4718
|
+
const markerRef = React.useRef(null)
|
|
4719
|
+
|
|
4720
|
+
const projectedTokens = props.useProjection('tokenUsage')
|
|
4721
|
+
const selection = props.useProjection('modelSelection')
|
|
4722
|
+
const sessionId = String(props?.sessionId || '')
|
|
4723
|
+
const priceOverridesKey = JSON.stringify(configSnapshot?.value?.priceOverrides || {})
|
|
4724
|
+
|
|
4725
|
+
React.useEffect(() => {
|
|
4726
|
+
let active = true
|
|
4727
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/prices`, { cache: 'no-store' })
|
|
4728
|
+
.then((response) => response.json())
|
|
4729
|
+
.then((data) => {
|
|
4730
|
+
if (active && data?.ok && Array.isArray(data.models)) setPrices(data.models)
|
|
4731
|
+
})
|
|
4732
|
+
.catch(() => {})
|
|
4733
|
+
return () => { active = false }
|
|
4734
|
+
}, [priceOverridesKey])
|
|
3124
4735
|
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
4736
|
+
React.useEffect(() => {
|
|
4737
|
+
if (!sessionId) {
|
|
4738
|
+
setSessionStats(null)
|
|
4739
|
+
return undefined
|
|
4740
|
+
}
|
|
4741
|
+
let active = true
|
|
4742
|
+
const loadSession = async () => {
|
|
4743
|
+
try {
|
|
4744
|
+
const res = await fetchWithTimeout(
|
|
4745
|
+
`${ROUTE_PREFIX}/stats/session?sessionId=${encodeURIComponent(sessionId)}`,
|
|
4746
|
+
{ cache: 'no-store' },
|
|
4747
|
+
)
|
|
4748
|
+
const data = await res.json().catch(() => null)
|
|
4749
|
+
if (active && data?.ok) setSessionStats(data.session || null)
|
|
4750
|
+
} catch {
|
|
4751
|
+
/* quiet */
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
loadSession()
|
|
4755
|
+
let timer = null
|
|
4756
|
+
const schedule = () => {
|
|
4757
|
+
timer = setTimeout(async () => {
|
|
4758
|
+
await loadSession()
|
|
4759
|
+
if (active) schedule()
|
|
4760
|
+
}, 10000)
|
|
4761
|
+
}
|
|
4762
|
+
schedule()
|
|
4763
|
+
return () => {
|
|
4764
|
+
active = false
|
|
4765
|
+
if (timer) clearTimeout(timer)
|
|
4766
|
+
}
|
|
4767
|
+
}, [sessionId, projectedTokens])
|
|
3128
4768
|
|
|
3129
4769
|
React.useEffect(() => {
|
|
3130
|
-
if (projectedTokens) return
|
|
4770
|
+
if (projectedTokens || sessionId) return undefined
|
|
3131
4771
|
let active = true
|
|
3132
4772
|
const loadStats = async () => {
|
|
3133
4773
|
try {
|
|
3134
4774
|
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/stats`, { cache: 'no-store' })
|
|
3135
4775
|
if (!res.ok) return
|
|
3136
4776
|
const data = await res.json().catch(() => null)
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
}
|
|
4777
|
+
const totals = data?.stats?.totals || data?.stats?.summary || null
|
|
4778
|
+
if (active && totals) setStats(totals)
|
|
3140
4779
|
} catch {
|
|
3141
|
-
|
|
4780
|
+
/* quiet */
|
|
3142
4781
|
}
|
|
3143
4782
|
}
|
|
3144
4783
|
loadStats()
|
|
3145
4784
|
const timer = setTimeout(loadStats, 30000)
|
|
3146
4785
|
return () => { active = false; clearTimeout(timer) }
|
|
3147
|
-
}, [projectedTokens])
|
|
3148
|
-
|
|
3149
|
-
const
|
|
4786
|
+
}, [projectedTokens, sessionId])
|
|
4787
|
+
|
|
4788
|
+
const selected = selection?.lastUsed
|
|
4789
|
+
?? selection?.next
|
|
4790
|
+
?? (selection?.provider || selection?.model ? selection : null)
|
|
4791
|
+
const provider = String(selected?.provider || '')
|
|
4792
|
+
const modelId = String(selected?.model || selected?.id || '')
|
|
4793
|
+
const isClineBot = (provider === '' || provider === 'clinebot' || provider === 'llm-clinebot')
|
|
4794
|
+
&& (provider === 'clinebot' || provider === 'llm-clinebot' || modelId.startsWith('cline-pass/'))
|
|
4795
|
+
|
|
4796
|
+
// ClineBot requests are recorded in the plugin's own per-session bucket. That
|
|
4797
|
+
// bucket is provider-scoped, unlike the host's session-wide tokenUsage, so a
|
|
4798
|
+
// session that also used OpenCode cannot inflate the ClineBot read-out.
|
|
4799
|
+
const session = sessionStats && Number(sessionStats.requests || 0) > 0 ? sessionStats : null
|
|
4800
|
+
const sessionTokens = session
|
|
4801
|
+
? {
|
|
4802
|
+
promptTokens: Number(session.promptTokens || 0),
|
|
4803
|
+
completionTokens: Number(session.completionTokens || 0),
|
|
4804
|
+
reasoningTokens: Number(session.reasoningTokens || 0),
|
|
4805
|
+
cacheReadTokens: Number(session.cacheReadTokens || 0),
|
|
4806
|
+
cacheWriteTokens: Number(session.cacheWriteTokens || 0),
|
|
4807
|
+
}
|
|
4808
|
+
: null
|
|
4809
|
+
const rawTokens = sessionTokens || (sessionId ? null : (projectedTokens || {
|
|
3150
4810
|
promptTokens: stats?.promptTokens || 0,
|
|
3151
4811
|
completionTokens: stats?.completionTokens || 0,
|
|
3152
4812
|
reasoningTokens: stats?.reasoningTokens || 0,
|
|
4813
|
+
cacheReadTokens: stats?.cacheReadTokens || 0,
|
|
4814
|
+
cacheWriteTokens: stats?.cacheWriteTokens || 0,
|
|
4815
|
+
}))
|
|
4816
|
+
const cacheRead = Number(rawTokens?.cacheReadTokens || 0)
|
|
4817
|
+
const cacheWrite = Number(rawTokens?.cacheWriteTokens || 0)
|
|
4818
|
+
const uncachedInput = Number(rawTokens?.uncachedInputTokens)
|
|
4819
|
+
const promptTokens = rawTokens
|
|
4820
|
+
? (Number.isFinite(uncachedInput)
|
|
4821
|
+
? uncachedInput + cacheRead + cacheWrite
|
|
4822
|
+
: Number(rawTokens.promptTokens || rawTokens.inputTokens || 0))
|
|
4823
|
+
: 0
|
|
4824
|
+
const tokens = {
|
|
4825
|
+
promptTokens,
|
|
4826
|
+
completionTokens: Number(rawTokens?.completionTokens || rawTokens?.outputTokens || 0),
|
|
4827
|
+
reasoningTokens: Number(rawTokens?.reasoningTokens || 0),
|
|
4828
|
+
cacheReadTokens: cacheRead,
|
|
4829
|
+
cacheWriteTokens: cacheWrite,
|
|
3153
4830
|
}
|
|
4831
|
+
const calculated = calculateCost(tokens, session?.model || modelId, prices)
|
|
4832
|
+
const cost = session ? Number(session.costUsd || 0) : calculated.cost
|
|
4833
|
+
const priced = session ? Number(session.pricedRequests || 0) > 0 : calculated.priced
|
|
4834
|
+
const source = session
|
|
4835
|
+
? (findPrice(session.model || modelId, prices)?.source || 'estimate')
|
|
4836
|
+
: calculated.source
|
|
4837
|
+
const { pTok, cTok, rTok } = calculated
|
|
4838
|
+
const total = session ? Number(session.totalTokens || pTok + cTok) : calculated.total
|
|
4839
|
+
const last = session?.last || null
|
|
4840
|
+
const lastCost = last ? Number(last.costUsd || 0) : 0
|
|
4841
|
+
const lastPriced = Boolean(last?.priced)
|
|
4842
|
+
|
|
4843
|
+
const costValue = priced
|
|
4844
|
+
? (cost === 0 ? t('composer.free') : (cost < 0.0001 ? '< $0.0001' : `$${cost.toFixed(4)}`))
|
|
4845
|
+
: t('composer.unpriced')
|
|
4846
|
+
const costDisplay = priced && source === 'estimate' && cost > 0 ? `≈ ${costValue}` : costValue
|
|
4847
|
+
const lastCostValue = last
|
|
4848
|
+
? (lastPriced
|
|
4849
|
+
? (lastCost === 0 ? t('composer.free') : (lastCost < 0.0001 ? '< $0.0001' : `$${lastCost.toFixed(4)}`))
|
|
4850
|
+
: t('composer.unpriced'))
|
|
4851
|
+
: ''
|
|
4852
|
+
const title = [
|
|
4853
|
+
`${t('composer.cost_title')} ${costDisplay}`,
|
|
4854
|
+
...(last ? [`${t('composer.last_request')} ${lastCostValue}`] : []),
|
|
4855
|
+
`${t('composer.prompt_tok')} ${formatTokens(pTok)}`,
|
|
4856
|
+
`${t('composer.completion_tok')} ${formatTokens(cTok)}`,
|
|
4857
|
+
...(rTok > 0 ? [`${t('composer.reasoning_tok')} ${formatTokens(rTok)}`] : []),
|
|
4858
|
+
...(cacheRead > 0 ? [`${t('composer.cache_read')} ${formatTokens(cacheRead)}`] : []),
|
|
4859
|
+
...(cacheWrite > 0 ? [`${t('composer.cache_write')} ${formatTokens(cacheWrite)}`] : []),
|
|
4860
|
+
...(priced ? [`${t('composer.price_source')} ${source === 'override' ? t('composer.price_override') : t('composer.price_estimate')}`] : []),
|
|
4861
|
+
].join(' · ')
|
|
4862
|
+
const inject = isClineBot && priced && total > 0
|
|
3154
4863
|
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
4864
|
+
React.useEffect(() => {
|
|
4865
|
+
if (!inject || typeof document === 'undefined') return undefined
|
|
4866
|
+
const marker = markerRef.current
|
|
4867
|
+
if (!marker) return undefined
|
|
4868
|
+
const scope = marker.closest(COST_ANCHOR)
|
|
4869
|
+
if (!scope) return undefined
|
|
4870
|
+
|
|
4871
|
+
let host = null
|
|
4872
|
+
let root = null
|
|
4873
|
+
let valueNode = null
|
|
4874
|
+
let misses = 0
|
|
4875
|
+
|
|
4876
|
+
const apply = () => {
|
|
4877
|
+
if (!root || root.parentNode === null) {
|
|
4878
|
+
const usageAnchor = scope.querySelector(STATS_USAGE) || scope.querySelector(STATS_ANY)
|
|
4879
|
+
const statsRoot = usageAnchor || scope.querySelector(STATS_ROOT) || scope
|
|
4880
|
+
const triggers = statsRoot.querySelectorAll(DIALOG_TRIGGER)
|
|
4881
|
+
const button = triggers[triggers.length - 1] || usageAnchor
|
|
4882
|
+
if (!button) {
|
|
4883
|
+
misses += 1
|
|
4884
|
+
if (misses === MISSES_BEFORE_WARNING) {
|
|
4885
|
+
console.warn('[dsh-clinebot] no native token-usage pill found for the session cost')
|
|
4886
|
+
}
|
|
4887
|
+
return
|
|
4888
|
+
}
|
|
4889
|
+
misses = 0
|
|
4890
|
+
host = button
|
|
4891
|
+
root = document.createElement('span')
|
|
4892
|
+
root.setAttribute('data-clinebot-session-cost', '')
|
|
4893
|
+
root.style.display = 'inline-flex'
|
|
4894
|
+
root.style.alignItems = 'center'
|
|
4895
|
+
const separator = document.createElement('span')
|
|
4896
|
+
separator.textContent = '·'
|
|
4897
|
+
separator.setAttribute('aria-hidden', 'true')
|
|
4898
|
+
separator.style.color = 'var(--dsw-alias-separator-primary)'
|
|
4899
|
+
separator.style.margin = '0 6px 0 0'
|
|
4900
|
+
valueNode = document.createElement('span')
|
|
4901
|
+
valueNode.style.fontWeight = '500'
|
|
4902
|
+
root.appendChild(separator)
|
|
4903
|
+
root.appendChild(valueNode)
|
|
4904
|
+
button.appendChild(root)
|
|
4905
|
+
}
|
|
4906
|
+
if (root.parentNode !== host) host.appendChild(root)
|
|
4907
|
+
if (valueNode.textContent !== costDisplay) valueNode.textContent = costDisplay
|
|
4908
|
+
if (root.title !== title) root.title = title
|
|
4909
|
+
}
|
|
3163
4910
|
|
|
3164
|
-
|
|
3165
|
-
'
|
|
3166
|
-
|
|
3167
|
-
className: 'cb-composer-cost-dock',
|
|
3168
|
-
'data-composer-session-cost': 'true',
|
|
3169
|
-
style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
|
|
3170
|
-
},
|
|
3171
|
-
React.createElement(
|
|
3172
|
-
'button',
|
|
3173
|
-
{
|
|
3174
|
-
type: 'button',
|
|
3175
|
-
className: 'cb-btn',
|
|
3176
|
-
style: {
|
|
3177
|
-
padding: '2px 8px',
|
|
3178
|
-
fontSize: '11px',
|
|
3179
|
-
height: '24px',
|
|
3180
|
-
borderRadius: '999px',
|
|
3181
|
-
display: 'inline-flex',
|
|
3182
|
-
alignItems: 'center',
|
|
3183
|
-
gap: '4px',
|
|
3184
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
3185
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
3186
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3187
|
-
cursor: 'pointer',
|
|
3188
|
-
},
|
|
3189
|
-
title: `${t('composer.cost_title')}: ${costDisplay} (${formatTokens(total)} tokens)`,
|
|
3190
|
-
onClick: () => setShowPopover(!showPopover),
|
|
3191
|
-
},
|
|
3192
|
-
React.createElement('span', { style: { fontSize: '12px' } }, '🪙'),
|
|
3193
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `≈ ${costDisplay}`),
|
|
3194
|
-
React.createElement('span', { style: { opacity: 0.7 } }, `· ${formatTokens(total)} tok`)
|
|
3195
|
-
),
|
|
3196
|
-
showPopover
|
|
3197
|
-
? React.createElement(
|
|
3198
|
-
'div',
|
|
3199
|
-
{
|
|
3200
|
-
className: 'cb-popover',
|
|
3201
|
-
style: {
|
|
3202
|
-
position: 'absolute',
|
|
3203
|
-
bottom: '100%',
|
|
3204
|
-
left: 0,
|
|
3205
|
-
marginBottom: '6px',
|
|
3206
|
-
minWidth: '200px',
|
|
3207
|
-
padding: '10px 12px',
|
|
3208
|
-
background: 'var(--dsw-alias-bg-layer-3)',
|
|
3209
|
-
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3210
|
-
borderRadius: '8px',
|
|
3211
|
-
boxShadow: 'var(--dsw-elevation-prominent)',
|
|
3212
|
-
zIndex: 100,
|
|
3213
|
-
fontSize: '12px',
|
|
3214
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
3215
|
-
display: 'flex',
|
|
3216
|
-
flexDirection: 'column',
|
|
3217
|
-
gap: '6px',
|
|
3218
|
-
},
|
|
3219
|
-
},
|
|
3220
|
-
React.createElement(
|
|
3221
|
-
'div',
|
|
3222
|
-
{ style: { fontWeight: 600, borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '4px' } },
|
|
3223
|
-
t('composer.cost_breakdown')
|
|
3224
|
-
),
|
|
3225
|
-
React.createElement(
|
|
3226
|
-
'div',
|
|
3227
|
-
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3228
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.prompt_tok')),
|
|
3229
|
-
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(pTok))
|
|
3230
|
-
),
|
|
3231
|
-
React.createElement(
|
|
3232
|
-
'div',
|
|
3233
|
-
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3234
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.completion_tok')),
|
|
3235
|
-
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(cTok))
|
|
3236
|
-
),
|
|
3237
|
-
rTok > 0
|
|
3238
|
-
? React.createElement(
|
|
3239
|
-
'div',
|
|
3240
|
-
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3241
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.reasoning_tok')),
|
|
3242
|
-
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(rTok))
|
|
3243
|
-
)
|
|
3244
|
-
: null,
|
|
3245
|
-
React.createElement(
|
|
3246
|
-
'div',
|
|
3247
|
-
{ style: { display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '4px', fontWeight: 600 } },
|
|
3248
|
-
React.createElement('span', null, t('composer.est_cost')),
|
|
3249
|
-
React.createElement('span', { style: { color: 'var(--dsw-alias-brand-primary)' } }, `≈ ${costDisplay}`)
|
|
3250
|
-
)
|
|
3251
|
-
)
|
|
4911
|
+
apply()
|
|
4912
|
+
const observer = typeof window !== 'undefined' && typeof window.MutationObserver === 'function'
|
|
4913
|
+
? new window.MutationObserver(apply)
|
|
3252
4914
|
: null
|
|
3253
|
-
|
|
4915
|
+
observer?.observe(document.body, { childList: true })
|
|
4916
|
+
let timer = null
|
|
4917
|
+
const tick = () => {
|
|
4918
|
+
apply()
|
|
4919
|
+
timer = setTimeout(tick, 2000)
|
|
4920
|
+
}
|
|
4921
|
+
timer = setTimeout(tick, 2000)
|
|
4922
|
+
return () => {
|
|
4923
|
+
observer?.disconnect()
|
|
4924
|
+
if (timer) clearTimeout(timer)
|
|
4925
|
+
if (root?.parentNode) root.parentNode.removeChild(root)
|
|
4926
|
+
}
|
|
4927
|
+
}, [inject, costDisplay, title])
|
|
4928
|
+
|
|
4929
|
+
if (configSnapshot?.value?.showComposerCost === false) return null
|
|
4930
|
+
if (total === 0 && !stats) return null
|
|
4931
|
+
if (!inject) return null
|
|
4932
|
+
return React.createElement('span', {
|
|
4933
|
+
ref: markerRef,
|
|
4934
|
+
style: { display: 'none' },
|
|
4935
|
+
'data-clinebot-cost-anchor': '',
|
|
4936
|
+
})
|
|
3254
4937
|
}
|
|
3255
4938
|
|
|
3256
4939
|
// --- src/client/settings-page.js ---
|
|
@@ -3275,6 +4958,7 @@ function SettingsPage(props) {
|
|
|
3275
4958
|
|
|
3276
4959
|
React.useEffect(() => { ensureCss() }, [])
|
|
3277
4960
|
|
|
4961
|
+
const pendingRef = React.useRef({})
|
|
3278
4962
|
const load = React.useCallback(async () => {
|
|
3279
4963
|
setErr('')
|
|
3280
4964
|
const [res, statsRes] = await Promise.all([
|
|
@@ -3286,11 +4970,15 @@ function SettingsPage(props) {
|
|
|
3286
4970
|
const rawStats = statsRes && statsRes.ok ? await statsRes.json().catch(() => null) : null
|
|
3287
4971
|
const statsObj = (rawStats && typeof rawStats === 'object' && rawStats.stats) ? rawStats.stats : (rawStats || data.statsSummary || {})
|
|
3288
4972
|
setStatus({ ...data, statsSummary: statsObj })
|
|
3289
|
-
|
|
4973
|
+
// Preserve unsaved staged fields across an account/model refresh.
|
|
4974
|
+
setDraft({ ...(data.config || {}), ...(pendingRef.current || {}) })
|
|
3290
4975
|
}, [])
|
|
3291
4976
|
|
|
3292
4977
|
React.useEffect(() => { load().catch((e) => setErr(String(e.message || e))) }, [load])
|
|
3293
4978
|
|
|
4979
|
+
const staged = useStagedConfig({ status, draft, setDraft, load, t, pendingRef })
|
|
4980
|
+
const modelSelection = useModelSelection({ status, draft, setDraft, load, setErr })
|
|
4981
|
+
|
|
3294
4982
|
const checkUpdate = React.useCallback(async () => {
|
|
3295
4983
|
setUpdateState((s) => ({ ...s, checking: true, status: 'checking', error: '' }))
|
|
3296
4984
|
try {
|
|
@@ -3378,6 +5066,14 @@ function SettingsPage(props) {
|
|
|
3378
5066
|
}
|
|
3379
5067
|
}
|
|
3380
5068
|
|
|
5069
|
+
const {
|
|
5070
|
+
handlePinAccount,
|
|
5071
|
+
handleAddAccount,
|
|
5072
|
+
handleDeleteAccount,
|
|
5073
|
+
handleRenameAccount,
|
|
5074
|
+
handleClearKey,
|
|
5075
|
+
} = createAccountActions({ performAction, load, t, setMsg })
|
|
5076
|
+
|
|
3381
5077
|
async function handleSaveKey() {
|
|
3382
5078
|
const keyVal = String(apiKeyInput || '').trim()
|
|
3383
5079
|
if (!keyVal) { setErr(t('key.empty_err')); return }
|
|
@@ -3498,45 +5194,6 @@ function SettingsPage(props) {
|
|
|
3498
5194
|
window.open('https://app.cline.bot/settings/api-keys', '_blank')
|
|
3499
5195
|
}
|
|
3500
5196
|
|
|
3501
|
-
async function handlePinAccount(accountEnv) {
|
|
3502
|
-
await performAction('pin-account', async () => {
|
|
3503
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/active`, {
|
|
3504
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3505
|
-
body: JSON.stringify({ account: accountEnv }),
|
|
3506
|
-
})
|
|
3507
|
-
const data = await res.json().catch(() => ({}))
|
|
3508
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3509
|
-
await load()
|
|
3510
|
-
})
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
async function handleAddAccount({ label, apiKeyEnv, apiKey, id }) {
|
|
3514
|
-
await performAction('add-account', async () => {
|
|
3515
|
-
const endpoint = id ? `${ROUTE_PREFIX}/keys/discovered/import` : `${ROUTE_PREFIX}/accounts`
|
|
3516
|
-
const payload = id ? { id, label, apiKeyEnv } : { label, apiKeyEnv, apiKey }
|
|
3517
|
-
const res = await fetchWithTimeout(endpoint, {
|
|
3518
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3519
|
-
body: JSON.stringify(payload),
|
|
3520
|
-
})
|
|
3521
|
-
const data = await res.json().catch(() => ({}))
|
|
3522
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3523
|
-
setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : (data.validationError || 'Notice') }))
|
|
3524
|
-
await load()
|
|
3525
|
-
})
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
3529
|
-
await performAction('delete-account', async () => {
|
|
3530
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
3531
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3532
|
-
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
3533
|
-
})
|
|
3534
|
-
const data = await res.json().catch(() => ({}))
|
|
3535
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3536
|
-
await load()
|
|
3537
|
-
})
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
5197
|
async function handleConfigPatch(patch) {
|
|
3541
5198
|
setDraft((d) => ({ ...(d || {}), ...(patch || {}) }))
|
|
3542
5199
|
await performAction('patch-config', async () => {
|
|
@@ -3559,60 +5216,6 @@ function SettingsPage(props) {
|
|
|
3559
5216
|
})
|
|
3560
5217
|
}
|
|
3561
5218
|
|
|
3562
|
-
const debounceTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
|
|
3563
|
-
|
|
3564
|
-
React.useEffect(() => () => { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) }, [])
|
|
3565
|
-
|
|
3566
|
-
function debounceSaveDisabledModels(nextDisabled, previousDisabled) {
|
|
3567
|
-
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
3568
|
-
debounceTimerRef.current = setTimeout(async () => {
|
|
3569
|
-
try {
|
|
3570
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/models/toggle`, {
|
|
3571
|
-
method: 'POST',
|
|
3572
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3573
|
-
body: JSON.stringify({ disabledModels: nextDisabled }),
|
|
3574
|
-
})
|
|
3575
|
-
const data = await res.json().catch(() => ({}))
|
|
3576
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3577
|
-
await load()
|
|
3578
|
-
} catch (err) {
|
|
3579
|
-
console.warn('[dsh-clinebot] Failed saving disabled models:', err)
|
|
3580
|
-
setErr(String(err?.message || err))
|
|
3581
|
-
setDraft((prev) => {
|
|
3582
|
-
if (!prev) return prev
|
|
3583
|
-
const all = status?.availableModels || []
|
|
3584
|
-
const prevEnabled = all.map((m) => m.id).filter((mId) => !previousDisabled.includes(mId))
|
|
3585
|
-
return { ...prev, disabledModels: previousDisabled, enabledModels: prevEnabled }
|
|
3586
|
-
})
|
|
3587
|
-
}
|
|
3588
|
-
}, 400)
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
function handleToggleModel(id) {
|
|
3592
|
-
const curr = draft?.disabledModels || []
|
|
3593
|
-
const isCurrentlyDisabled = curr.includes(id)
|
|
3594
|
-
const next = isCurrentlyDisabled ? curr.filter((x) => x !== id) : [...curr, id]
|
|
3595
|
-
const all = status?.availableModels || []
|
|
3596
|
-
const enabledIds = all.map((m) => m.id).filter((mId) => !next.includes(mId))
|
|
3597
|
-
setDraft({ ...draft, disabledModels: next, enabledModels: enabledIds })
|
|
3598
|
-
debounceSaveDisabledModels(next, curr)
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
function handleSetModelsFilter(type) {
|
|
3602
|
-
const all = status?.availableModels || []
|
|
3603
|
-
let allowed = new Set()
|
|
3604
|
-
if (type === 'all') allowed = new Set(all.map((m) => m.id))
|
|
3605
|
-
else if (type === 'vision') allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
|
|
3606
|
-
else if (type === 'coding') allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
3607
|
-
else if (type === 'recommended') allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
3608
|
-
|
|
3609
|
-
const curr = draft?.disabledModels || []
|
|
3610
|
-
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
3611
|
-
const nextEnabled = Array.from(allowed)
|
|
3612
|
-
setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
|
|
3613
|
-
debounceSaveDisabledModels(nextDisabled, curr)
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
5219
|
if (!status || !draft) {
|
|
3617
5220
|
if (err) {
|
|
3618
5221
|
return React.createElement(
|
|
@@ -3693,7 +5296,13 @@ function SettingsPage(props) {
|
|
|
3693
5296
|
|
|
3694
5297
|
// Accounts Pool Card
|
|
3695
5298
|
React.createElement(AccountsSection, {
|
|
3696
|
-
status,
|
|
5299
|
+
status,
|
|
5300
|
+
busy,
|
|
5301
|
+
handlePinAccount,
|
|
5302
|
+
handleAddAccount,
|
|
5303
|
+
handleDeleteAccount,
|
|
5304
|
+
handleRenameAccount,
|
|
5305
|
+
handleClearKey,
|
|
3697
5306
|
accountMode: draft?.accountMode || status?.config?.accountMode || 'least_used',
|
|
3698
5307
|
handleUpdateAccountMode: (mode) => handleConfigPatch({ accountMode: mode }),
|
|
3699
5308
|
modelRoutes: draft?.modelRoutes || status?.config?.modelRoutes || {},
|
|
@@ -3702,39 +5311,72 @@ function SettingsPage(props) {
|
|
|
3702
5311
|
}),
|
|
3703
5312
|
|
|
3704
5313
|
// Quota Warning & Dashboard Card
|
|
3705
|
-
React.createElement(QuotaSection, {
|
|
3706
|
-
|
|
3707
|
-
// Card 3: Model Picker Management
|
|
3708
|
-
React.createElement(ModelsSection, {
|
|
3709
|
-
modelsList,
|
|
3710
|
-
disabledSet,
|
|
3711
|
-
enabledCount,
|
|
5314
|
+
React.createElement(QuotaSection, {
|
|
3712
5315
|
keyPresent,
|
|
5316
|
+
status,
|
|
5317
|
+
usage,
|
|
3713
5318
|
busy,
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
handleToggleModel,
|
|
3718
|
-
planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
|
|
3719
|
-
planSyncedAt: status?.config?.planSyncedAt || 0,
|
|
3720
|
-
defaultModelWarning: status?.config?.defaultModelWarning || '',
|
|
3721
|
-
customModels: draft?.customModels || [],
|
|
3722
|
-
modelReasoningDefaults: draft?.modelReasoningDefaults || {},
|
|
3723
|
-
visibleModels: draft?.visibleModels || [],
|
|
3724
|
-
filterModelsByPlan: draft?.filterModelsByPlan !== false,
|
|
3725
|
-
onConfigUpdate: handleConfigPatch,
|
|
3726
|
-
onUpdateReasoningEffort: (modelId, effort) => {
|
|
3727
|
-
const next = { ...(draft?.modelReasoningDefaults || {}) }
|
|
3728
|
-
if (!effort) delete next[modelId]; else next[modelId] = effort
|
|
3729
|
-
handleConfigPatch({ modelReasoningDefaults: next })
|
|
3730
|
-
},
|
|
5319
|
+
handleRefreshQuota,
|
|
5320
|
+
showSidebarQuota: draft?.showSidebarQuota !== false,
|
|
5321
|
+
onToggleSidebarQuota: (value) => staged.stage({ showSidebarQuota: value }),
|
|
3731
5322
|
t,
|
|
3732
5323
|
}),
|
|
3733
5324
|
|
|
3734
|
-
//
|
|
5325
|
+
// Models & context: a foldable group keeps the long model table out of the
|
|
5326
|
+
// initial page while preserving the picker controls.
|
|
5327
|
+
React.createElement(
|
|
5328
|
+
CollapsibleGroup,
|
|
5329
|
+
{
|
|
5330
|
+
title: t('settings.models_group'),
|
|
5331
|
+
description: t('settings.models_group_hint'),
|
|
5332
|
+
badge: `${enabledCount}/${modelsList.length}`,
|
|
5333
|
+
divided: true,
|
|
5334
|
+
defaultOpen: false,
|
|
5335
|
+
},
|
|
5336
|
+
React.createElement(ModelsSection, {
|
|
5337
|
+
modelsList,
|
|
5338
|
+
disabledSet,
|
|
5339
|
+
enabledCount,
|
|
5340
|
+
keyPresent,
|
|
5341
|
+
busy,
|
|
5342
|
+
handleSyncPlanModels,
|
|
5343
|
+
handleUpdateModelContext,
|
|
5344
|
+
handleSetModelsFilter: modelSelection.handleSetModelsFilter,
|
|
5345
|
+
handleToggleModel: modelSelection.handleToggleModel,
|
|
5346
|
+
planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
|
|
5347
|
+
planSyncedAt: status?.config?.planSyncedAt || 0,
|
|
5348
|
+
defaultModelWarning: status?.config?.defaultModelWarning || '',
|
|
5349
|
+
customModels: draft?.customModels || [],
|
|
5350
|
+
modelReasoningDefaults: draft?.modelReasoningDefaults || {},
|
|
5351
|
+
visibleModels: draft?.visibleModels || [],
|
|
5352
|
+
filterModelsByPlan: draft?.filterModelsByPlan !== false,
|
|
5353
|
+
onConfigUpdate: staged.stage,
|
|
5354
|
+
onUpdateReasoningEffort: (modelId, effort) => {
|
|
5355
|
+
const next = { ...(draft?.modelReasoningDefaults || {}) }
|
|
5356
|
+
if (!effort) delete next[modelId]; else next[modelId] = effort
|
|
5357
|
+
staged.stage({ modelReasoningDefaults: next })
|
|
5358
|
+
},
|
|
5359
|
+
t,
|
|
5360
|
+
}),
|
|
5361
|
+
React.createElement(PriceSection, {
|
|
5362
|
+
modelsList,
|
|
5363
|
+
priceOverrides: draft?.priceOverrides || {},
|
|
5364
|
+
onConfigUpdate: staged.stage,
|
|
5365
|
+
t,
|
|
5366
|
+
})
|
|
5367
|
+
),
|
|
5368
|
+
|
|
5369
|
+
// Session Metrics & Usage Tracking
|
|
3735
5370
|
React.createElement(StatsSection, { status, onResetStats: handleResetStats, busy, t }),
|
|
3736
5371
|
|
|
3737
|
-
|
|
5372
|
+
React.createElement(IntegrationsSection, {
|
|
5373
|
+
draft,
|
|
5374
|
+
onStage: staged.stage,
|
|
5375
|
+
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5376
|
+
t,
|
|
5377
|
+
}),
|
|
5378
|
+
|
|
5379
|
+
// Diagnostics & Sync with DSH
|
|
3738
5380
|
React.createElement(DiagSection, {
|
|
3739
5381
|
keyPresent,
|
|
3740
5382
|
isRegistered,
|
|
@@ -3744,7 +5386,26 @@ function SettingsPage(props) {
|
|
|
3744
5386
|
handleUnregister,
|
|
3745
5387
|
handleRegister,
|
|
3746
5388
|
proxyMode: draft?.proxyMode !== false,
|
|
3747
|
-
onToggleProxy: (
|
|
5389
|
+
onToggleProxy: (value) => staged.stage({ proxyMode: value }),
|
|
5390
|
+
t,
|
|
5391
|
+
}),
|
|
5392
|
+
|
|
5393
|
+
React.createElement(AdvancedSection, {
|
|
5394
|
+
draft,
|
|
5395
|
+
onStage: staged.stage,
|
|
5396
|
+
transport: draft?.transport || status?.config?.transport || 'adapter',
|
|
5397
|
+
t,
|
|
5398
|
+
}),
|
|
5399
|
+
|
|
5400
|
+
React.createElement(SaveBar, {
|
|
5401
|
+
pendingCount: staged.pendingCount,
|
|
5402
|
+
saving: staged.saving,
|
|
5403
|
+
saveState: staged.saveState,
|
|
5404
|
+
saveError: staged.saveError,
|
|
5405
|
+
invalid: staged.invalid,
|
|
5406
|
+
validationError: staged.validationError,
|
|
5407
|
+
onSave: staged.save,
|
|
5408
|
+
onDiscard: staged.discard,
|
|
3748
5409
|
t,
|
|
3749
5410
|
})
|
|
3750
5411
|
)
|
|
@@ -3820,6 +5481,24 @@ function apply(ctx) {
|
|
|
3820
5481
|
}
|
|
3821
5482
|
}
|
|
3822
5483
|
|
|
5484
|
+
function selectLayoutPanel(id) {
|
|
5485
|
+
try {
|
|
5486
|
+
const layout = typeof ctx.get === 'function' ? ctx.get('layout') : ctx.layout
|
|
5487
|
+
if (!layout || typeof layout.selectPanel !== 'function') return false
|
|
5488
|
+
layout.selectPanel(id)
|
|
5489
|
+
return true
|
|
5490
|
+
} catch (err) {
|
|
5491
|
+
console.warn('[dsh-clinebot] layout.selectPanel failed:', err)
|
|
5492
|
+
return false
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
const openPanel = () => selectLayoutPanel('clinebot-panel')
|
|
5497
|
+
const closePanel = () => {
|
|
5498
|
+
if (selectLayoutPanel(null)) return
|
|
5499
|
+
selectLayoutPanel('conversation')
|
|
5500
|
+
}
|
|
5501
|
+
|
|
3823
5502
|
// The plugin's configuration is a section of DSH Settings (`settings.section`),
|
|
3824
5503
|
// a peer of General/Models/Plugins — not a row in the Plugins list and not a tab
|
|
3825
5504
|
// inside the Plugins settings section. The shell owns the modal, the section nav
|
|
@@ -3856,24 +5535,57 @@ function apply(ctx) {
|
|
|
3856
5535
|
id: 'clinebot-quota',
|
|
3857
5536
|
order: 20,
|
|
3858
5537
|
locale: NS,
|
|
3859
|
-
inject: () => ({ ctx }),
|
|
5538
|
+
inject: () => ({ ctx, open: openPanel }),
|
|
3860
5539
|
},
|
|
3861
5540
|
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(SidebarQuota, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3862
5541
|
)
|
|
3863
5542
|
)
|
|
3864
5543
|
|
|
3865
|
-
// The quota &
|
|
3866
|
-
//
|
|
3867
|
-
//
|
|
3868
|
-
|
|
3869
|
-
//
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
5544
|
+
// The quota & usage dashboard opens in the layout's center column, matching
|
|
5545
|
+
// the Command Code / OpenCode provider surfaces. The sidebar card selects it
|
|
5546
|
+
// through `layout.selectPanel`; the panel's Close button selects the
|
|
5547
|
+
// conversation again. When no layout service is available the card falls back
|
|
5548
|
+
// to its local Modal.
|
|
5549
|
+
registerSlotWhenReady('main', () =>
|
|
5550
|
+
ctx.slots.register(
|
|
5551
|
+
{
|
|
5552
|
+
name: 'main',
|
|
5553
|
+
key: 'clinebot-panel',
|
|
5554
|
+
locale: NS,
|
|
5555
|
+
inject: () => ({ ctx, open: openPanel, close: closePanel }),
|
|
5556
|
+
},
|
|
5557
|
+
(props) => React.createElement(
|
|
5558
|
+
ErrorBoundary,
|
|
5559
|
+
{ t: props && props.t },
|
|
5560
|
+
React.createElement(DashboardDialog, {
|
|
5561
|
+
...props,
|
|
5562
|
+
panel: true,
|
|
5563
|
+
ctx: (props && props.ctx) || ctx,
|
|
5564
|
+
onClose: (props && props.close) || closePanel,
|
|
5565
|
+
})
|
|
5566
|
+
)
|
|
5567
|
+
)
|
|
5568
|
+
)
|
|
5569
|
+
|
|
5570
|
+
// Models-page provider card. The seat is keyed by the provider's settings
|
|
5571
|
+
// namespace, which the native adapter registration declares as `dsh-clinebot`
|
|
5572
|
+
// (`settingsNs`). The component returns null for every other provider row, so
|
|
5573
|
+
// it cannot leak onto a sibling's card.
|
|
5574
|
+
registerSlotWhenReady('settings.models.provider-card', () =>
|
|
5575
|
+
ctx.slots.register(
|
|
5576
|
+
{
|
|
5577
|
+
name: 'settings.models.provider-card',
|
|
5578
|
+
key: NS,
|
|
5579
|
+
locale: NS,
|
|
5580
|
+
inject: () => ({ ctx }),
|
|
5581
|
+
},
|
|
5582
|
+
(props) => React.createElement(
|
|
5583
|
+
ErrorBoundary,
|
|
5584
|
+
{ t: props && props.t },
|
|
5585
|
+
React.createElement(ProviderCard, { ...props, ctx: (props && props.ctx) || ctx })
|
|
5586
|
+
)
|
|
5587
|
+
)
|
|
5588
|
+
)
|
|
3877
5589
|
|
|
3878
5590
|
// Composer Session Cost Dock (conversation.composer.dock slot)
|
|
3879
5591
|
registerSlotWhenReady('conversation.composer.dock', () =>
|