@goodandready/dsh-clinebot 0.6.29 → 0.6.31
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 +14 -0
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +5 -5
- package/lib/config.js +283 -283
- package/lib/http.js +2 -2
- package/lib/index.js +427 -426
- package/lib/path-safety.js +57 -4
- package/lib/price-sync.js +2 -5
- package/lib/provider-sync.js +258 -275
- package/lib/stats-storage.js +2 -27
- package/lib/transport/cline-transport.js +342 -343
- package/lib/updater.js +4 -9
- package/package.json +117 -117
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.31] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
### Security & Bug Fixes
|
|
11
|
+
- **Profile Data Isolation (`DSH_HOME`)**: Stats, event logs, price cache, and models cache resolve strictly within the active `DSH_HOME` directory, preventing data leakage into `$HOME/.dsh` when isolated profiles are configured (#299).
|
|
12
|
+
- **Updater Hostname Hardening**: Origin hostname verification in `isTrustedUpdateRequest` now strictly uses `isLoopbackHostname`, rejecting non-loopback domains starting with `127.` such as `127.attacker.example` (#294).
|
|
13
|
+
|
|
14
|
+
## [0.6.30] - 2026-11-06
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Stream transport idle timer (#296)**: removed premature `timer.unref()` in `readChunkWithIdle()` that caused Node 22 event loops to exit early with pending promises during stream idle waits. Timer cleanup is reliably handled in `finally` via `clearTimeout`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Default on-screen indicators disabled (#297)**: `showSidebarQuota` and `showComposerCost` are now disabled (`false`) by default upon clean installs, ensuring an uncluttered UI out-of-the-box. Users can enable either indicator explicitly in Settings -> ClineBot -> Integrations.
|
|
21
|
+
|
|
8
22
|
## [0.6.29] - 2026-11-06
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.31</b> ensures strict profile data isolation (telemetry, stats, events, and price cache resolve strictly inside active <code>DSH_HOME</code> without leaking into <code>~/.dsh</code>, #299) and hardens updater loopback verification (origin hostname check with <code>isLoopbackHostname</code> rejects external domains like <code>127.attacker.example</code>, #294). Maintains full security boundary, default-disabled screen badges (#297), DSH <code>0.2.2-rc.1</code> compatibility, and native <code>ctx.llm</code> adapter.</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/README.ru.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.31</b> обеспечивает строгую изоляцию профилей (телеметрия, статистика, логи событий и кэш цен сохраняются строго внутри активного <code>DSH_HOME</code> без утечки в <code>~/.dsh</code>, #299) и усиливает безопасность updater'а (проверка origin hostname через <code>isLoopbackHostname</code> отклоняет домены вида <code>127.attacker.example</code>, #294). Сохраняются полная security-граница, экранные значки, отключенные по умолчанию (#297), поддержка DSH <code>0.2.2-rc.1</code> и нативный адаптер <code>ctx.llm</code>.</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
|
|
6
6
|
|
|
7
|
-
<p><b>0.6.
|
|
7
|
+
<p><b>0.6.31</b> 保证严格的配置隔离(统计信息、遥测、事件日志与价格缓存严格存储在激活的 <code>DSH_HOME</code> 目录中,防止泄露至 <code>~/.dsh</code>,#299),并加固更新器的回环验证(通过 <code>isLoopbackHostname</code> 拒绝形如 <code>127.attacker.example</code> 的域名,#294)。保留完整安全边界、默认禁用的屏幕指示器 (#297)、DSH <code>0.2.2-rc.1</code> 兼容性及原生 <code>ctx.llm</code> 适配器。</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
package/lib/client.js
CHANGED
|
@@ -403,7 +403,7 @@ function formatResetCountdown(isoString){if(!isoString)return"";try{const d=new
|
|
|
403
403
|
function SettingsGroup({title,description,divided=false,action,children}){return React.createElement("section",{className:divided?"cb-settings-group cb-settings-group-divided":"cb-settings-group","aria-label":title},React.createElement("div",{className:"cb-settings-group-head"},React.createElement("h3",{className:"cb-settings-group-title"},title),action?React.createElement("span",{className:"cb-settings-group-action"},action):null),description?React.createElement("div",{className:"cb-settings-group-desc"},description):null,React.createElement("div",{className:"cb-settings-group-body"},children))}function CollapsibleGroup({title,description,defaultOpen=false,divided=false,badge,children}){const[open,setOpen]=React.useState(Boolean(defaultOpen));return React.createElement("section",{className:divided?"cb-settings-group cb-settings-group-divided":"cb-settings-group","aria-label":title},React.createElement("button",{type:"button",className:"cb-settings-group-head cb-settings-disclosure","aria-expanded":open,onClick:()=>setOpen(value=>!value)},React.createElement("span",{className:"cb-settings-group-title"},title),badge!==void 0&&badge!==""?React.createElement("span",{className:"cb-badge"},badge):null,React.createElement("span",{className:"cb-settings-spacer"}),React.createElement("span",{className:open?"cb-settings-chevron cb-settings-chevron-open":"cb-settings-chevron","aria-hidden":"true"},"\u203A")),open&&description?React.createElement("div",{className:"cb-settings-group-desc"},description):null,open?React.createElement("div",{className:"cb-settings-group-body"},children):null)}function SettingRow({title,description,control,error,htmlFor}){return React.createElement("div",{className:"cb-settings-row"},React.createElement("div",{className:"cb-settings-row-text"},htmlFor?React.createElement("label",{className:"cb-settings-row-title",htmlFor},title):React.createElement("span",{className:"cb-settings-row-title"},title),error?React.createElement("div",{className:"cb-settings-row-error"},error):description?React.createElement("div",{className:"cb-settings-row-desc"},description):null),control?React.createElement("div",{className:"cb-settings-row-control"},control):null)}function SaveBar({pendingCount,saving,saveState,saveError,invalid,validationError,onSave,onDiscard,t}){const visible=pendingCount>0||saving||saveState==="saved"||saveState==="error";if(!visible)return null;const message=saveError||validationError||(saving?t("settings.saving"):saveState==="saved"?t("settings.saved"):t("settings.unsaved",{count:pendingCount}));return React.createElement("div",{className:"cb-savebar",role:"status"},React.createElement("div",{className:"cb-savebar-inner"},React.createElement("span",{className:"cb-savebar-text"},message),React.createElement("span",{className:"cb-settings-spacer"}),React.createElement("button",{type:"button",className:"cb-btn",disabled:saving||pendingCount===0,onClick:onDiscard},t("settings.discard")),React.createElement("button",{type:"button",className:"cb-btn cb-btn-primary",disabled:saving||pendingCount===0||invalid,onClick:onSave},saving?t("settings.saving"):t("settings.save"))))}
|
|
404
404
|
|
|
405
405
|
// --- src/client/components/integrations-section.js ---
|
|
406
|
-
function IntegrationToggle({id,label,hint,checked,onChange}){return React.createElement(SettingRow,{title:label,description:hint,htmlFor:id,control:React.createElement(BooleanControl,{checked:!!checked,label,onChange:next=>onChange(next)})})}function IntegrationsSection({draft,onStage,t}){const current=draft||{};return React.createElement(SettingsGroup,{title:t("settings.integrations"),description:t("settings.integrations_desc"),divided:true},React.createElement(IntegrationToggle,{id:"cb-web-search",label:t("settings.web_search"),hint:t("settings.web_search_hint"),checked:current.webSearch!==false,onChange:value=>onStage({webSearch:value})}),React.createElement(IntegrationToggle,{id:"cb-sidebar-quota",label:t("settings.sidebar_quota"),hint:t("settings.sidebar_quota_hint"),checked:current.showSidebarQuota
|
|
406
|
+
function IntegrationToggle({id,label,hint,checked,onChange}){return React.createElement(SettingRow,{title:label,description:hint,htmlFor:id,control:React.createElement(BooleanControl,{checked:!!checked,label,onChange:next=>onChange(next)})})}function IntegrationsSection({draft,onStage,t}){const current=draft||{};return React.createElement(SettingsGroup,{title:t("settings.integrations"),description:t("settings.integrations_desc"),divided:true},React.createElement(IntegrationToggle,{id:"cb-web-search",label:t("settings.web_search"),hint:t("settings.web_search_hint"),checked:current.webSearch!==false,onChange:value=>onStage({webSearch:value})}),React.createElement(IntegrationToggle,{id:"cb-sidebar-quota",label:t("settings.sidebar_quota"),hint:t("settings.sidebar_quota_hint"),checked:Boolean(current.showSidebarQuota),onChange:value=>onStage({showSidebarQuota:value})}),React.createElement(IntegrationToggle,{id:"cb-composer-cost",label:t("settings.composer_cost"),hint:t("settings.composer_cost_hint"),checked:Boolean(current.showComposerCost),onChange:value=>onStage({showComposerCost:value})}))}
|
|
407
407
|
|
|
408
408
|
// --- src/client/components/advanced-section.js ---
|
|
409
409
|
function AdvancedField({id,label,hint,value,type,onChange}){return React.createElement(SettingRow,{title:label,description:hint,htmlFor:id,control:React.createElement("input",{id,className:"cb-input cb-settings-input",type:type||"text",value:value===void 0||value===null?"":String(value),onChange:event=>onChange(event.target.value)})})}function AdvancedSection({draft,onStage,t}){const current=draft||{};const numeric=(key,minimum)=>raw=>{if(raw==="")return;const value=Number(raw);if(Number.isFinite(value)&&value>minimum)onStage({[key]:value})};return React.createElement(CollapsibleGroup,{title:t("settings.advanced"),description:t("settings.advanced_desc"),divided:true,defaultOpen:false},React.createElement(AdvancedField,{id:"cb-base-url",label:t("settings.base_url"),hint:t("settings.base_url_hint"),value:current.baseUrl,onChange:value=>onStage({baseUrl:value})}),React.createElement(AdvancedField,{id:"cb-timeout",label:t("settings.timeout"),hint:t("settings.timeout_hint"),value:current.timeoutMs,type:"number",onChange:numeric("timeoutMs",0)}),React.createElement(AdvancedField,{id:"cb-smoke-timeout",label:t("settings.smoke_timeout"),hint:t("settings.smoke_timeout_hint"),value:current.smokeTimeoutMs,type:"number",onChange:numeric("smokeTimeoutMs",0)}),React.createElement(AdvancedField,{id:"cb-stream-idle",label:t("settings.stream_idle"),hint:t("settings.stream_idle_hint"),value:current.streamIdleTimeoutMs,type:"number",onChange:numeric("streamIdleTimeoutMs",0)}),React.createElement(AdvancedField,{id:"cb-max-payload",label:t("settings.max_payload"),hint:t("settings.max_payload_hint"),value:current.maxPayloadMb,type:"number",onChange:numeric("maxPayloadMb",0)}),React.createElement(AdvancedField,{id:"cb-retention",label:t("settings.stats_retention"),hint:t("settings.stats_retention_hint"),value:current.statsRetentionDays,type:"number",onChange:numeric("statsRetentionDays",-1)}),React.createElement(AdvancedField,{id:"cb-daily-budget",label:t("settings.daily_budget"),hint:t("settings.daily_budget_hint"),value:current.dailyBudgetUsd,type:"number",onChange:numeric("dailyBudgetUsd",-1)}),React.createElement(AdvancedField,{id:"cb-monthly-budget",label:t("settings.monthly_budget"),hint:t("settings.monthly_budget_hint"),value:current.monthlyBudgetUsd,type:"number",onChange:numeric("monthlyBudgetUsd",-1)}),React.createElement(AdvancedField,{id:"cb-stats-path",label:t("settings.stats_path"),hint:t("settings.stats_path_hint"),value:current.statsPath,onChange:value=>onStage({statsPath:value})}),React.createElement(AdvancedField,{id:"cb-models-cache-path",label:t("settings.models_cache_path"),hint:t("settings.models_cache_path_hint"),value:current.modelsCachePath,onChange:value=>onStage({modelsCachePath:value})}))}
|
|
@@ -424,7 +424,7 @@ function AccountRoutes({accounts=[],modelRoutes={},onUpdateModelRoutes,busy,t}){
|
|
|
424
424
|
function AccountsSection({status,busy,handlePinAccount,handleAddAccount,handleDeleteAccount,handleRenameAccount,handleClearKey,accountMode,handleUpdateAccountMode,modelRoutes={},onUpdateModelRoutes,t}){const[showAdd,setShowAdd]=React.useState(false);const[newLabel,setNewLabel]=React.useState("");const[newEnv,setNewEnv]=React.useState("");const[newKey,setNewKey]=React.useState("");const[showKey,setShowKey]=React.useState(false);const[discoveredKeys,setDiscoveredKeys]=React.useState([]);const[usageByAccount,setUsageByAccount]=React.useState({});const[menuFor,setMenuFor]=React.useState("");const[renameFor,setRenameFor]=React.useState("");const[renameValue,setRenameValue]=React.useState("");const[confirmAction,setConfirmAction]=React.useState(null);const[historyOpen,setHistoryOpen]=React.useState(false);const[history,setHistory]=React.useState([]);const[historyLoading,setHistoryLoading]=React.useState(false);React.useEffect(()=>{let active=true;fetchWithTimeout(`${ROUTE_PREFIX}/keys/discovered`,{cache:"no-store"}).then(response=>response.json()).then(data=>{if(active&&data?.ok&&Array.isArray(data.discovered))setDiscoveredKeys(data.discovered)}).catch(()=>{});return()=>{active=false}},[]);const accountSignature=(status?.accounts||[]).map(account=>account.apiKeyEnv||account.id).join("|");React.useEffect(()=>{let active=true;fetchWithTimeout(`${ROUTE_PREFIX}/accounts/usage`,{cache:"no-store"}).then(response=>response.json()).then(data=>{if(!active||!data?.ok||!Array.isArray(data.accounts))return;const next={};for(const entry of data.accounts)next[entry.apiKeyEnv]=entry;setUsageByAccount(next)}).catch(()=>{});return()=>{active=false}},[accountSignature,status?.activeAccount,busy]);function toggleHistory(){if(historyOpen){setHistoryOpen(false);return}setHistoryOpen(true);setHistoryLoading(true);fetchWithTimeout(`${ROUTE_PREFIX}/accounts/history?limit=50`,{cache:"no-store"}).then(response=>response.json()).then(data=>{if(data?.ok&&Array.isArray(data.events))setHistory(data.events)}).catch(()=>{}).finally(()=>setHistoryLoading(false))}function nextEnvName(){let max=1;for(const account of status.accounts||[]){const match=String(account.apiKeyEnv||"").match(/^CLINEBOT_API_KEY_(\d+)$/);if(match)max=Math.max(max,Number(match[1])+1)}return`CLINEBOT_API_KEY_${max}`}function openAdd(){setNewEnv(nextEnvName());setShowAdd(true)}async function submitAdd(event){if(event?.preventDefault)event.preventDefault();if(!newKey.trim()||typeof handleAddAccount!=="function")return;await handleAddAccount({label:newLabel.trim()||newEnv.trim(),apiKeyEnv:newEnv.trim(),apiKey:newKey.trim()});setNewLabel("");setNewEnv("");setNewKey("");setShowAdd(false)}async function importDiscovered(item){if(typeof handleAddAccount!=="function")return;await handleAddAccount({label:item.source||nextEnvName(),apiKeyEnv:nextEnvName(),id:item.id});setDiscoveredKeys(previous=>previous.filter(entry=>entry.id!==item.id))}function openRename(account){setMenuFor("");setRenameFor(account.apiKeyEnv);setRenameValue(account.label||"")}async function submitRename(account){if(typeof handleRenameAccount==="function"){await handleRenameAccount(account.apiKeyEnv,renameValue.trim()||account.apiKeyEnv)}setRenameFor("");setRenameValue("")}async function runConfirm(){if(!confirmAction)return;if(confirmAction.type==="delete"&&typeof handleDeleteAccount==="function"){await handleDeleteAccount(confirmAction.env,true)}if(confirmAction.type==="clear"&&typeof handleClearKey==="function"){await handleClearKey(confirmAction.env)}setConfirmAction(null)}function dotClass(account,live){if(!account.present)return"cb-status-dot cb-status-dot-warn";if(live?.error)return"cb-status-dot cb-status-dot-bad";const percent=live?.usage?.windows?.fiveHour?.percentUsed;if(typeof percent==="number"&&percent>=95)return"cb-status-dot cb-status-dot-bad";if(account.isPinned||status.activeAccount===account.apiKeyEnv)return"cb-status-dot cb-status-dot-ok";return"cb-status-dot cb-status-dot-idle"}const accounts=status.accounts||[];return React.createElement("section",{className:"cb-section-card cb-accounts"},React.createElement("div",{className:"cb-section-head"},React.createElement("div",{className:"cb-section-title"},t("accounts.title")),React.createElement("button",{type:"button",className:"cb-btn",onClick:toggleHistory},historyOpen?t("accounts.history_hide"):t("accounts.history_show")),!showAdd?React.createElement("button",{type:"button",className:"cb-btn cb-btn-primary",disabled:!!busy,onClick:openAdd},React.createElement(CbIcon,{name:"plus",size:13}),` ${t("accounts.add_btn")}`):null),React.createElement("div",{className:"cb-section-desc"},t("accounts.desc")),discoveredKeys.length>0?React.createElement("div",{className:"cb-discovered"},React.createElement("div",{className:"cb-discovered-head"},React.createElement(CbIcon,{name:"key",size:14}),` ${t("discovery.title")}`),discoveredKeys.map(item=>React.createElement("div",{className:"cb-discovered-row",key:item.id},React.createElement("span",{className:"cb-mono"},item.maskedKey||item.source),React.createElement("span",{className:"cb-muted"},item.source||""),React.createElement("button",{type:"button",className:"cb-btn",disabled:!!busy,onClick:()=>importDiscovered(item)},React.createElement(CbIcon,{name:"download",size:12}),` ${t("discovery.import_btn",{source:item.source||""})}`)))):null,React.createElement("div",{className:"cb-account-mode"},React.createElement("span",{className:"cb-muted"},t("accounts.mode_label")),React.createElement("div",{className:"cb-seg"},React.createElement("button",{type:"button",className:(accountMode||"least_used")==="least_used"?"cb-seg-item cb-seg-item-active":"cb-seg-item",disabled:!!busy,onClick:()=>handleUpdateAccountMode&&handleUpdateAccountMode("least_used")},t("accounts.mode_least_used")),React.createElement("button",{type:"button",className:accountMode==="round_robin"?"cb-seg-item cb-seg-item-active":"cb-seg-item",disabled:!!busy,onClick:()=>handleUpdateAccountMode&&handleUpdateAccountMode("round_robin")},t("accounts.mode_round_robin")))),showAdd?React.createElement("form",{className:"cb-account-add",onSubmit:submitAdd},React.createElement("div",{className:"cb-account-add-row"},React.createElement("input",{type:"text",className:"cb-input",placeholder:t("accounts.label_placeholder"),value:newLabel,onChange:event=>setNewLabel(event.target.value)}),React.createElement("input",{type:"text",className:"cb-input",placeholder:t("accounts.env_placeholder"),value:newEnv,onChange:event=>setNewEnv(event.target.value)})),React.createElement("div",{className:"cb-account-add-row"},React.createElement("input",{type:showKey?"text":"password",className:"cb-input",placeholder:t("key.placeholder_empty"),value:newKey,onChange:event=>setNewKey(event.target.value)}),React.createElement("button",{type:"button",className:"cb-btn",onClick:()=>setShowKey(value=>!value)},showKey?t("key.hide"):t("key.show"))),React.createElement("div",{className:"cb-account-add-actions"},React.createElement("button",{type:"button",className:"cb-btn",onClick:()=>setShowAdd(false)},t("accounts.cancel_btn")),React.createElement("button",{type:"submit",className:"cb-btn cb-btn-primary",disabled:!newKey.trim()||!!busy},busy==="add-account"?t("key.saving"):t("accounts.save_btn")))):null,accounts.length===0?React.createElement("div",{className:"cb-empty"},t("accounts.no_accounts")):React.createElement("div",{className:"cb-account-list"},accounts.map(account=>{const envName=account.apiKeyEnv||account.id||"";const live=usageByAccount[envName];const isActive=status.activeAccount===envName||!status.activeAccount&&account.id==="default";const isPrimary=account.id==="default"||envName===status.config?.apiKeyEnv;const five=live?.usage?.windows?.fiveHour;const percent=typeof five?.percentUsed==="number"?five.percentUsed:typeof account.percentUsed==="number"?account.percentUsed:null;const plan=live?.usage?.plan||"";const email=live?.usage?.user?.email||"";return React.createElement("div",{className:"cb-account-item",key:envName||account.id},React.createElement("div",{className:"cb-account-row"},React.createElement("span",{className:dotClass(account,live),"aria-hidden":"true"}),React.createElement("div",{className:"cb-account-main"},React.createElement("div",{className:"cb-account-name"},account.label||envName,React.createElement("span",{className:"cb-account-env cb-mono"},envName)),React.createElement("div",{className:"cb-account-meta"},[plan,email].filter(Boolean).join(" \xB7 ")||t("accounts.source",{source:account.source||"\u2014"}))),React.createElement("div",{className:"cb-account-badges"},isActive?React.createElement("span",{className:"cb-badge cb-badge-ok"},t("accounts.active_badge")):null,account.isPinned?React.createElement("span",{className:"cb-badge"},t("accounts.pinned_badge")):null,account.present?React.createElement("span",{className:"cb-badge"},t("accounts.configured")):React.createElement("span",{className:"cb-badge cb-badge-warn"},t("accounts.missing_key"))),React.createElement("div",{className:"cb-account-quota"},percent===null?null:React.createElement(React.Fragment,null,React.createElement("span",{className:"cb-muted"},t("accounts.quota_5h",{pct:Math.round(percent)})),React.createElement("span",{className:"cb-meter"},React.createElement("span",{className:percent>=95?"cb-meter-fill cb-meter-fill-warn":"cb-meter-fill",style:{width:`${Math.max(0,Math.min(100,percent))}%`}})))),React.createElement("button",{type:"button",className:"cb-icon-btn","aria-label":t("accounts.actions"),"aria-expanded":menuFor===envName,disabled:!!busy,onClick:()=>setMenuFor(current=>current===envName?"":envName)},React.createElement(CbIcon,{name:"kebab",size:16}))),menuFor===envName?React.createElement("div",{className:"cb-account-menu"},isActive?React.createElement("button",{type:"button",className:"cb-menu-item",onClick:()=>{setMenuFor("");handlePinAccount("")}},t("accounts.unpin")):account.present?React.createElement("button",{type:"button",className:"cb-menu-item",onClick:()=>{setMenuFor("");handlePinAccount(envName)}},t("accounts.pin_btn")):null,!isPrimary?React.createElement("button",{type:"button",className:"cb-menu-item",onClick:()=>openRename(account)},t("accounts.rename")):null,account.present?React.createElement("button",{type:"button",className:"cb-menu-item",onClick:()=>{setMenuFor("");setConfirmAction({type:"clear",env:envName})}},t("accounts.clear_key")):null,!isPrimary?React.createElement("button",{type:"button",className:"cb-menu-item cb-menu-item-danger",onClick:()=>{setMenuFor("");setConfirmAction({type:"delete",env:envName})}},t("accounts.delete_btn")):null):null,renameFor===envName?React.createElement("div",{className:"cb-account-inline"},React.createElement("input",{type:"text",className:"cb-input",value:renameValue,onChange:event=>setRenameValue(event.target.value),placeholder:t("accounts.rename_placeholder")}),React.createElement("button",{type:"button",className:"cb-btn cb-btn-primary",disabled:!!busy,onClick:()=>submitRename(account)},t("accounts.rename_save")),React.createElement("button",{type:"button",className:"cb-btn",onClick:()=>setRenameFor("")},t("accounts.cancel"))):null,confirmAction&&confirmAction.env===envName?React.createElement("div",{className:"cb-account-inline cb-account-danger"},React.createElement("span",{className:"cb-account-danger-text"},confirmAction.type==="delete"?t("accounts.remove_confirm",{name:account.label||envName}):t("accounts.clear_confirm",{name:account.label||envName})),React.createElement("button",{type:"button",className:"cb-btn cb-btn-danger",disabled:!!busy,onClick:runConfirm},t("accounts.confirm")),React.createElement("button",{type:"button",className:"cb-btn",onClick:()=>setConfirmAction(null)},t("accounts.cancel"))):null)})),React.createElement(AccountRoutes,{accounts,modelRoutes,onUpdateModelRoutes,busy,t}),React.createElement(AccountHistory,{open:historyOpen,events:history,loading:historyLoading,accounts,t}))}
|
|
425
425
|
|
|
426
426
|
// --- src/client/components/quota-section.js ---
|
|
427
|
-
function QuotaSection({keyPresent,status,usage,busy,handleRefreshQuota,showSidebarQuota,onToggleSidebarQuota,t}){const isLowQuota=Boolean(usage?.windows?.weekly&&typeof usage.windows.weekly.remainingPercent==="number"&&usage.windows.weekly.remainingPercent<=10||usage?.windows?.monthly&&typeof usage.windows.monthly.remainingPercent==="number"&&usage.windows.monthly.remainingPercent<=10||usage?.windows?.fiveHour&&typeof usage.windows.fiveHour.remainingPercent==="number"&&usage.windows.fiveHour.remainingPercent<=10);return React.createElement(React.Fragment,null,status.quotaWarning?React.createElement("div",{className:status.quotaWarning.level==="exhausted"?"cb-banner-exhausted":"cb-banner-warning"},React.createElement("span",{className:"cb-banner-icon"},React.createElement(CbIcon,{name:status.quotaWarning.level==="exhausted"?"error":"warning",size:16})),React.createElement("div",{className:"cb-banner-body"},React.createElement("div",null,status.quotaWarning.code&&t(status.quotaWarning.code,status.quotaWarning.params)||status.quotaWarning.message),status.quotaWarning.resetsAt?React.createElement("div",{className:"cb-banner-sub"},t("quota.reset_at",{time:new Date(status.quotaWarning.resetsAt).toLocaleTimeString()})):null)):isLowQuota?React.createElement("div",{className:"cb-banner-warning"},React.createElement("span",{className:"cb-banner-icon"},React.createElement(CbIcon,{name:"warning",size:16})),React.createElement("div",{className:"cb-banner-body"},t("quota.warning_low"))):null,keyPresent?React.createElement("section",{className:"cb-section-card"},React.createElement("div",{className:"cb-section-head"},React.createElement("div",{className:"cb-section-title"},t("quota.title")),React.createElement("button",{type:"button",className:"cb-btn",disabled:!!busy,onClick:handleRefreshQuota},busy==="refresh-quota"?t("quota.refreshing"):t("quota.refresh"))),React.createElement("div",{className:"cb-section-desc"},usage?.user?.email?t("quota.account",{email:usage.user.email,plan:usage.plan||"ClinePass"}):t("quota.desc")),React.createElement("div",{className:"cb-quota-grid"},usage?.windows?.fiveHour?React.createElement(ProgressBar,{label:t("quota.window_5h"),percentUsed:usage.windows.fiveHour.percentUsed||0,remainingPercent:usage.windows.fiveHour.remainingPercent||100,resetsAt:usage.windows.fiveHour.resetsAt,windowMs:5*60*60*1e3,t}):null,React.createElement(ProgressBar,{label:t("quota.window_weekly"),percentUsed:usage?.windows?.weekly?.percentUsed||0,remainingPercent:usage?.windows?.weekly?.remainingPercent||100,resetsAt:usage?.windows?.weekly?.resetsAt,windowMs:7*24*60*60*1e3,t}),usage?.windows?.monthly?React.createElement(ProgressBar,{label:t("quota.window_monthly"),percentUsed:usage.windows.monthly.percentUsed||0,remainingPercent:usage.windows.monthly.remainingPercent||100,resetsAt:usage.windows.monthly.resetsAt,windowMs:30*24*60*60*1e3,t}):null),React.createElement(SettingRow,{title:t("sidebar.show_quota"),description:t("sidebar.show_quota_hint"),control:React.createElement(BooleanControl,{checked:showSidebarQuota
|
|
427
|
+
function QuotaSection({keyPresent,status,usage,busy,handleRefreshQuota,showSidebarQuota,onToggleSidebarQuota,t}){const isLowQuota=Boolean(usage?.windows?.weekly&&typeof usage.windows.weekly.remainingPercent==="number"&&usage.windows.weekly.remainingPercent<=10||usage?.windows?.monthly&&typeof usage.windows.monthly.remainingPercent==="number"&&usage.windows.monthly.remainingPercent<=10||usage?.windows?.fiveHour&&typeof usage.windows.fiveHour.remainingPercent==="number"&&usage.windows.fiveHour.remainingPercent<=10);return React.createElement(React.Fragment,null,status.quotaWarning?React.createElement("div",{className:status.quotaWarning.level==="exhausted"?"cb-banner-exhausted":"cb-banner-warning"},React.createElement("span",{className:"cb-banner-icon"},React.createElement(CbIcon,{name:status.quotaWarning.level==="exhausted"?"error":"warning",size:16})),React.createElement("div",{className:"cb-banner-body"},React.createElement("div",null,status.quotaWarning.code&&t(status.quotaWarning.code,status.quotaWarning.params)||status.quotaWarning.message),status.quotaWarning.resetsAt?React.createElement("div",{className:"cb-banner-sub"},t("quota.reset_at",{time:new Date(status.quotaWarning.resetsAt).toLocaleTimeString()})):null)):isLowQuota?React.createElement("div",{className:"cb-banner-warning"},React.createElement("span",{className:"cb-banner-icon"},React.createElement(CbIcon,{name:"warning",size:16})),React.createElement("div",{className:"cb-banner-body"},t("quota.warning_low"))):null,keyPresent?React.createElement("section",{className:"cb-section-card"},React.createElement("div",{className:"cb-section-head"},React.createElement("div",{className:"cb-section-title"},t("quota.title")),React.createElement("button",{type:"button",className:"cb-btn",disabled:!!busy,onClick:handleRefreshQuota},busy==="refresh-quota"?t("quota.refreshing"):t("quota.refresh"))),React.createElement("div",{className:"cb-section-desc"},usage?.user?.email?t("quota.account",{email:usage.user.email,plan:usage.plan||"ClinePass"}):t("quota.desc")),React.createElement("div",{className:"cb-quota-grid"},usage?.windows?.fiveHour?React.createElement(ProgressBar,{label:t("quota.window_5h"),percentUsed:usage.windows.fiveHour.percentUsed||0,remainingPercent:usage.windows.fiveHour.remainingPercent||100,resetsAt:usage.windows.fiveHour.resetsAt,windowMs:5*60*60*1e3,t}):null,React.createElement(ProgressBar,{label:t("quota.window_weekly"),percentUsed:usage?.windows?.weekly?.percentUsed||0,remainingPercent:usage?.windows?.weekly?.remainingPercent||100,resetsAt:usage?.windows?.weekly?.resetsAt,windowMs:7*24*60*60*1e3,t}),usage?.windows?.monthly?React.createElement(ProgressBar,{label:t("quota.window_monthly"),percentUsed:usage.windows.monthly.percentUsed||0,remainingPercent:usage.windows.monthly.remainingPercent||100,resetsAt:usage.windows.monthly.resetsAt,windowMs:30*24*60*60*1e3,t}):null),React.createElement(SettingRow,{title:t("sidebar.show_quota"),description:t("sidebar.show_quota_hint"),control:React.createElement(BooleanControl,{checked:Boolean(showSidebarQuota),label:t("sidebar.show_quota"),onChange:next=>onToggleSidebarQuota&&onToggleSidebarQuota(next)})})):null)}
|
|
428
428
|
|
|
429
429
|
// --- src/client/components/custom-models.js ---
|
|
430
430
|
function CustomModelsSub({customModels=[],onConfigUpdate,busy,t}){const[newId,setNewId]=React.useState("");const[newName,setNewName]=React.useState("");const[newCtx,setNewCtx]=React.useState("");const[newMax,setNewMax]=React.useState("");const[isExpanded,setIsExpanded]=React.useState(false);function handleAdd(){const trimmedId=newId.trim();const trimmedName=newName.trim()||trimmedId;if(!trimmedId)return;const parsedCtx=parseInt(newCtx,10);const parsedMax=parseInt(newMax,10);const nextList=[...customModels.filter(m=>m.id!==trimmedId),{id:trimmedId,name:trimmedName,contextLength:parsedCtx>0?parsedCtx:128e3,maxTokens:parsedMax>0?parsedMax:8192,category:"general",isCustom:true}];if(typeof onConfigUpdate==="function"){onConfigUpdate({customModels:nextList})}setNewId("");setNewName("");setNewCtx("");setNewMax("")}function handleDelete(id){const nextList=customModels.filter(m=>m.id!==id);if(typeof onConfigUpdate==="function"){onConfigUpdate({customModels:nextList})}}return React.createElement("div",{style:{marginTop:"16px",paddingTop:"14px",borderTop:"1px solid var(--dsw-alias-border-l2)"}},React.createElement("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",cursor:"pointer"},onClick:()=>setIsExpanded(!isExpanded)},React.createElement("span",{style:{fontWeight:500,fontSize:"13px",color:"var(--dsw-alias-label-primary)"}},React.createElement(CbIcon,{name:"plus",size:13}),` ${t("models.custom_add_title")} (${customModels.length})`),React.createElement("span",{style:{fontSize:"12px",color:"var(--dsw-alias-label-secondary)"}},React.createElement(CbIcon,{name:"chevron",size:13,className:isExpanded?"cb-icon cb-icon-chevron-open":"cb-icon"}))),isExpanded?React.createElement("div",{style:{marginTop:"12px",display:"flex",flexDirection:"column",gap:"10px"}},React.createElement("div",{style:{display:"flex",gap:"8px",flexWrap:"wrap"}},React.createElement("input",{type:"text",className:"cb-input",style:{flex:"2 1 140px"},placeholder:t("models.custom_id"),value:newId,disabled:!!busy,onChange:e=>setNewId(e.target.value)}),React.createElement("input",{type:"text",className:"cb-input",style:{flex:"2 1 140px"},placeholder:t("models.custom_name"),value:newName,disabled:!!busy,onChange:e=>setNewName(e.target.value)}),React.createElement("input",{type:"number",className:"cb-input",style:{flex:"1 1 90px"},placeholder:t("models.custom_ctx_label"),title:t("models.custom_ctx_label"),value:newCtx,disabled:!!busy,onChange:e=>setNewCtx(e.target.value)}),React.createElement("input",{type:"number",className:"cb-input",style:{flex:"1 1 80px"},placeholder:t("models.custom_max_label"),title:t("models.custom_max_label"),value:newMax,disabled:!!busy,onChange:e=>setNewMax(e.target.value)}),React.createElement("button",{type:"button",className:"cb-btn cb-btn-primary",disabled:!newId.trim()||!!busy,onClick:handleAdd},t("models.custom_btn_add"))),customModels.length>0?React.createElement("div",{style:{display:"flex",flexDirection:"column",gap:"6px",marginTop:"6px"}},customModels.map(cm=>React.createElement("div",{key:cm.id,style:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"6px 10px",background:"var(--dsw-alias-bg-layer-2)",border:"1px solid var(--dsw-alias-border-l2)",borderRadius:"6px",fontSize:"12px"}},React.createElement("div",null,React.createElement("span",{style:{fontWeight:500}},cm.name||cm.id),React.createElement("span",{style:{color:"var(--dsw-alias-label-secondary)",marginLeft:"8px"}},`(${cm.id})`),React.createElement("span",{style:{color:"var(--dsw-alias-label-secondary)",marginLeft:"8px",fontSize:"11px"}},`[${cm.contextLength||128e3} / ${cm.maxTokens||8192}]`)),React.createElement("button",{type:"button",className:"cb-btn",style:{padding:"2px 8px",fontSize:"11px"},disabled:!!busy,onClick:()=>handleDelete(cm.id)},t("models.custom_btn_del"))))):null):null)}
|
|
@@ -448,7 +448,7 @@ function StatsSection({status,onResetStats,busy,t}){const[confirmReset,setConfir
|
|
|
448
448
|
function DiagSection({keyPresent,busy,smokeResult,handleSmoke,t}){return React.createElement("section",{className:"cb-section-card"},React.createElement("div",{className:"cb-section-title"},t("diag.title")),React.createElement("div",{className:"cb-section-desc"},t("diag.desc")),React.createElement("div",{className:"cb-row cb-row-actions"},React.createElement("button",{type:"button",className:"cb-btn cb-btn-primary",disabled:!!busy||!keyPresent,onClick:handleSmoke},busy==="smoke"?t("diag.smoke_testing"):t("diag.smoke_btn"))),smokeResult?React.createElement("div",{className:"cb-preview"},t("diag.smoke_result",{latency:smokeResult.latencyMs,model:smokeResult.model,preview:smokeResult.preview||t("diag.empty")})):null)}
|
|
449
449
|
|
|
450
450
|
// --- src/client/components/sidebar-quota.js ---
|
|
451
|
-
function MiniRing({percent,size=18,warn=false}){const r=7;const c=2*Math.PI*r;const pct=Math.max(0,Math.min(100,Number(percent)||0));const offset=c-pct/100*c;const strokeColor=warn?"var(--dsw-alias-state-error-primary)":"var(--dsw-alias-brand-primary)";return React.createElement("svg",{className:"cb-glyph",width:size,height:size,viewBox:"0 0 20 20",fill:"none"},React.createElement("circle",{cx:10,cy:10,r,stroke:"var(--dsw-alias-bg-layer-2)",strokeWidth:2.5}),React.createElement("circle",{cx:10,cy:10,r,stroke:strokeColor,strokeWidth:2.5,strokeDasharray:c,strokeDashoffset:offset,strokeLinecap:"round",transform:"rotate(-90 10 10)",style:{transition:"stroke-dashoffset 0.3s ease"}}))}function SidebarQuota({ctx,wide,t:propT,open:openPanel}){const t=propT||makeT(en,en);const[data,setData]=React.useState(null);const[open,setOpen]=React.useState(false);const configSnapshot=useConfigFormsSnapshot(ctx);const show=configSnapshot?.value?.showSidebarQuota
|
|
451
|
+
function MiniRing({percent,size=18,warn=false}){const r=7;const c=2*Math.PI*r;const pct=Math.max(0,Math.min(100,Number(percent)||0));const offset=c-pct/100*c;const strokeColor=warn?"var(--dsw-alias-state-error-primary)":"var(--dsw-alias-brand-primary)";return React.createElement("svg",{className:"cb-glyph",width:size,height:size,viewBox:"0 0 20 20",fill:"none"},React.createElement("circle",{cx:10,cy:10,r,stroke:"var(--dsw-alias-bg-layer-2)",strokeWidth:2.5}),React.createElement("circle",{cx:10,cy:10,r,stroke:strokeColor,strokeWidth:2.5,strokeDasharray:c,strokeDashoffset:offset,strokeLinecap:"round",transform:"rotate(-90 10 10)",style:{transition:"stroke-dashoffset 0.3s ease"}}))}function SidebarQuota({ctx,wide,t:propT,open:openPanel}){const t=propT||makeT(en,en);const[data,setData]=React.useState(null);const[open,setOpen]=React.useState(false);const configSnapshot=useConfigFormsSnapshot(ctx);const show=Boolean(configSnapshot?.value?.showSidebarQuota);const loadQuota=React.useCallback(async()=>{try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/usage`,{cache:"no-store"});if(!res.ok)return;const json=await res.json().catch(()=>null);if(json&&json.ok)setData(json)}catch{}},[]);React.useEffect(()=>{if(!show)return void 0;let timer=null;const scheduleNext=()=>{timer=setTimeout(async()=>{await loadQuota();scheduleNext()},6e4)};loadQuota();scheduleNext();return()=>{if(timer)clearTimeout(timer)}},[loadQuota,show]);if(!show)return null;const windowEntries=[{key:"fiveHour",label:t("quota.window_5h"),value:data?.windows?.fiveHour},{key:"weekly",label:t("quota.window_weekly"),value:data?.windows?.weekly},{key:"monthly",label:t("quota.window_monthly"),value:data?.windows?.monthly}];const windowPct=window=>{const value=Number(window.value?.percentUsed);return Number.isFinite(value)?Math.max(0,Math.min(100,Math.round(value))):null};const measured=windowEntries.map(window=>({...window,pct:windowPct(window)})).filter(window=>window.pct!==null);const pct=measured.length?Math.max(...measured.map(window=>window.pct)):0;const isWarn=measured.some(window=>window.pct>=85);const planName=String(data?.plan||t("title")).replace(/\s*\(\$[^)]*\)\s*$/,"");const accountLabel=data?.account?` \xB7 ${data.account}`:"";const tooltip=`${planName}${accountLabel} \xB7 ${windowEntries.map(window=>{const value=windowPct(window);return`${window.label} ${value===null?"\u2014":`${value}% ${t("dashboard.used")}`}`}).join(" \xB7 ")}`;const handleOpen=()=>{if(typeof openPanel==="function"){try{if(openPanel()!==false)return}catch{}}setOpen(true)};const rows=windowEntries;const dialog=React.createElement(DashboardDialog,{t,open,onClose:()=>setOpen(false)});if(!wide){return React.createElement(React.Fragment,null,React.createElement("button",{type:"button",className:"cb-railButton","aria-label":tooltip,"aria-expanded":open,title:tooltip,onClick:handleOpen},React.createElement(MiniRing,{percent:pct,warn:isWarn,size:20})),dialog)}return React.createElement(React.Fragment,null,React.createElement("button",{type:"button",className:"cb-foot","aria-label":tooltip,"aria-expanded":open,title:tooltip,onClick:handleOpen},React.createElement("div",{className:"cb-footTop"},React.createElement(MiniRing,{percent:pct,warn:isWarn,size:16}),React.createElement("span",{className:"cb-footName"},planName),React.createElement("span",{className:isWarn?"cb-footPct cb-footPctWarn":"cb-footPct"},`${pct}%`)),rows.map(window=>{const value=windowPct(window);const hasData=value!==null;const warn=hasData&&value>=85;const reset=hasData&&typeof formatResetCountdown==="function"?formatResetCountdown(window.value?.resetsAt):"";const resetText=reset?` \xB7 ${reset==="resetting"?t("quota.resetting"):reset}`:"";return React.createElement("div",{className:hasData?"cb-footRow":"cb-footRow cb-footRowMissing",key:window.key},React.createElement("div",{className:"cb-footHead"},React.createElement("span",{className:"cb-footLabel"},window.label),React.createElement("span",{className:hasData?"cb-footAmount":"cb-footAmount cb-footAmountMissing"},hasData?`${100-value}% ${t("dashboard.remaining")}${resetText}`:"\u2014")),React.createElement("span",{className:"cb-footBar"},React.createElement("span",{className:warn?"cb-footFill cb-footFillWarn":"cb-footFill",style:{width:hasData?`${Math.min(100,Math.max(2,value))}%`:"0%"}})),hasData?React.createElement("span",{className:"cb-footForecast"},quotaForecast(window.value,window.key==="fiveHour"?5*60*60*1e3:window.key==="weekly"?7*24*60*60*1e3:30*24*60*60*1e3,t)):null)}),rows.some(window=>windowPct(window)!==null)?null:React.createElement("span",{className:"cb-footMessage"},data?.error||t("quota.no_windows"))),dialog)}
|
|
452
452
|
|
|
453
453
|
// --- src/client/components/price-section.js ---
|
|
454
454
|
function PriceSection({modelsList=[],priceOverrides={},priceAutoSync=true,onConfigUpdate,t}){const[expanded,setExpanded]=React.useState(false);const[syncState,setSyncState]=React.useState({busy:false,message:""});const models=Array.isArray(modelsList)?modelsList:[];const syncPrices=async()=>{setSyncState({busy:true,message:""});try{const response=await fetchWithTimeout(`${ROUTE_PREFIX}/prices/sync`,{method:"POST"});const data=await response.json().catch(()=>({}));if(!response.ok||!data.ok)throw new Error(data.error||`HTTP ${response.status}`);setSyncState({busy:false,message:t("prices.synced",{count:data.count})})}catch(err){setSyncState({busy:false,message:t("prices.sync_failed",{error:String(err?.message||err)})})}};const update=(modelId,field,raw)=>{const next={...priceOverrides||{}};const current={...next[modelId]||{}};const value=String(raw??"").trim();const numeric=Number(value);if(value===""||!Number.isFinite(numeric)||numeric<0)delete current[field];else current[field]=numeric;if(Object.keys(current).length===0)delete next[modelId];else next[modelId]=current;onConfigUpdate({priceOverrides:next})};return React.createElement("div",{className:"cb-price-section"},React.createElement("div",{className:"cb-price-head"},React.createElement("div",{className:"cb-price-copy"},React.createElement("div",{className:"cb-section-title"},t("prices.title")),React.createElement("div",{className:"cb-section-desc"},t("prices.desc")),React.createElement(BooleanControl,{checked:priceAutoSync!==false,label:t("prices.auto_sync"),onChange:next=>onConfigUpdate&&onConfigUpdate({priceAutoSync:next})}),React.createElement("div",{className:"cb-hint"},t("prices.auto_sync_hint"))),React.createElement("div",{className:"cb-row",style:{gap:"6px"}},React.createElement("button",{type:"button",className:"cb-btn",disabled:syncState.busy,onClick:syncPrices},syncState.busy?t("prices.syncing"):t("prices.sync")),React.createElement("button",{type:"button",className:"cb-btn",onClick:()=>setExpanded(value=>!value)},expanded?t("prices.hide"):t("prices.show")))),expanded?React.createElement("div",{className:"cb-price-table"},React.createElement("div",{className:"cb-price-head-row"},React.createElement("span",null,t("prices.model")),React.createElement("span",null,t("prices.input")),React.createElement("span",null,t("prices.output")),React.createElement("span",null,t("prices.cache_read")),React.createElement("span",null,t("prices.cache_write"))),models.length===0?React.createElement("div",{className:"cb-hint"},t("prices.empty")):models.map(model=>{const override=priceOverrides?.[model.id]||{};return React.createElement("div",{className:"cb-price-row",key:model.id},React.createElement("span",{className:"cb-mono",title:model.id},model.name||model.id),["input","output","cacheRead","cacheWrite"].map(field=>React.createElement("input",{key:field,className:"cb-input cb-price-input",type:"number",min:"0",step:"0.001",value:override[field]===void 0?"":String(override[field]),placeholder:t("prices.auto"),onChange:event=>update(model.id,field,event.target.value)})))}),React.createElement("div",{className:"cb-hint"},t("prices.public_hint")),React.createElement("div",{className:"cb-hint"},t("prices.hint")),syncState.message?React.createElement("div",{className:"cb-hint"},syncState.message):null):null)}
|
|
@@ -466,10 +466,10 @@ function AnalyticsControls({analyticsDays,setAnalyticsDays,groupBy,setGroupBy,mo
|
|
|
466
466
|
function DashboardDialog(props){const t=props?.t||makeT(en,en);const panel=props?.panel===true;const open=panel||props?.open!==false;const onClose=typeof props?.onClose==="function"?props.onClose:()=>{};const[data,setData]=React.useState(null);const[status,setStatus]=React.useState(null);const[analytics,setAnalytics]=React.useState(null);const[tab,setTab]=React.useState("quota");const[groupBy,setGroupBy]=React.useState("model");const[loading,setLoading]=React.useState(false);const[analyticsLoading,setAnalyticsLoading]=React.useState(false);const[updatedAt,setUpdatedAt]=React.useState("");const[err,setErr]=React.useState("");const[accountsUsage,setAccountsUsage]=React.useState(null);const[selectedAccount,setSelectedAccount]=React.useState("");const[analyticsDays,setAnalyticsDays]=React.useState(7);const[analyticsModel,setAnalyticsModel]=React.useState("");const[analyticsAccount,setAnalyticsAccount]=React.useState("");const[analyticsErrorsOnly,setAnalyticsErrorsOnly]=React.useState(false);const[analyticsFrom,setAnalyticsFrom]=React.useState("");const[analyticsTo,setAnalyticsTo]=React.useState("");const[heatmapModel,setHeatmapModel]=React.useState("");const[selectedHeat,setSelectedHeat]=React.useState(null);const loadData=React.useCallback(async()=>{setLoading(true);setErr("");try{const[uRes,sRes,aRes]=await Promise.all([fetchWithTimeout(`${ROUTE_PREFIX}/usage`,{cache:"no-store"}),fetchWithTimeout(`${ROUTE_PREFIX}/status`,{cache:"no-store"}),fetchWithTimeout(`${ROUTE_PREFIX}/accounts/usage`,{cache:"no-store"})]);const uJson=await uRes.json().catch(()=>({}));const sJson=await sRes.json().catch(()=>({}));const aJson=await aRes.json().catch(()=>({}));if(uJson?.ok)setData(uJson);if(sJson?.ok)setStatus(sJson);if(aJson?.ok){setAccountsUsage(aJson);setSelectedAccount(current=>{const accounts=Array.isArray(aJson.accounts)?aJson.accounts:[];if(accounts.some(entry=>entry.apiKeyEnv===current))return current;return aJson.activeAccount||accounts[0]?.apiKeyEnv||""})}setUpdatedAt(new Date().toLocaleTimeString())}catch(e){setErr(String(e?.message||e))}finally{setLoading(false)}},[]);const loadAnalytics=React.useCallback(async(group=groupBy,days=analyticsDays,model=heatmapModel)=>{setAnalyticsLoading(true);setErr("");try{const now=Date.now();const parsedFrom=analyticsFrom?Date.parse(analyticsFrom):NaN;const parsedTo=analyticsTo?Date.parse(analyticsTo):NaN;const from=Number.isFinite(parsedFrom)?parsedFrom:now-days*864e5;const to=Number.isFinite(parsedTo)?parsedTo:now;const query=[`groupBy=${encodeURIComponent(group)}`,"limit=20",`from=${from}`,`to=${to}`,`heatmapDays=${days}`,...model?[`heatmapModel=${encodeURIComponent(model)}`]:[],...analyticsModel?[`model=${encodeURIComponent(analyticsModel)}`]:[],...analyticsAccount?[`account=${encodeURIComponent(analyticsAccount)}`]:[],...analyticsErrorsOnly?["errorsOnly=1"]:[]].join("&");const res=await fetchWithTimeout(`${ROUTE_PREFIX}/analytics?${query}`,{cache:"no-store"});const json=await res.json().catch(()=>({}));if(!res.ok||!json.ok)throw new Error(json.error||`HTTP ${res.status}`);setAnalytics(json)}catch(e){setErr(String(e?.message||e))}finally{setAnalyticsLoading(false)}},[groupBy,analyticsDays,heatmapModel,analyticsModel,analyticsAccount,analyticsErrorsOnly,analyticsFrom,analyticsTo]);React.useEffect(()=>{if(open)loadData()},[open,loadData]);React.useEffect(()=>{if(open&&(panel||tab==="analytics"))loadAnalytics(groupBy,analyticsDays,heatmapModel)},[open,panel,tab,groupBy,analyticsDays,heatmapModel,loadAnalytics]);React.useEffect(()=>{if(!open)return void 0;let timer=null;const tick=()=>{loadData();if(panel||tab==="analytics")loadAnalytics(groupBy,analyticsDays,heatmapModel);timer=setTimeout(tick,12e4)};timer=setTimeout(tick,12e4);return()=>{if(timer)clearTimeout(timer)}},[open,panel,tab,groupBy,analyticsDays,heatmapModel,loadData,loadAnalytics]);if(!open)return null;function fmtTokens(value){const n=Number(value)||0;if(n>=1e6)return`${(n/1e6).toFixed(1)}M`;if(n>=1e3)return`${(n/1e3).toFixed(1)}k`;return String(n)}function fmtCost(value){const n=Number(value)||0;if(n===0)return"$0";if(n<1e-4)return"< $0.0001";return`$${n.toFixed(4)}`}function fmtMs(value){const n=Number(value)||0;return n>0?`${Math.round(n)} ms`:"\u2014"}const plan=data?.plan||status?.config?.defaultModel||"ClinePass";const email=data?.user?.email||"";const activeAcc=status?.config?.activeAccount||t("dashboard.auto_account");const enabledCount=status?.config?.enabledModels?.length||0;const routingMode=status?.config?.accountMode==="round_robin"?t("accounts.mode_round_robin"):t("accounts.mode_least_used");const accountEntries=Array.isArray(accountsUsage?.accounts)?accountsUsage.accounts:[];const selectedEntry=accountEntries.find(entry=>entry.apiKeyEnv===selectedAccount)||accountEntries.find(entry=>entry.active)||accountEntries[0];const selectedUsage=selectedEntry?.usage||data;const accountPlan=selectedUsage?.plan||plan;const accountEmail=selectedUsage?.user?.email||email;const accountName=selectedEntry?.label||selectedEntry?.apiKeyEnv||accountPlan;const accountBadges=[];if(selectedEntry){accountBadges.push(selectedEntry.configured===false?{className:"cb-badge cb-badge-warn",label:t("accounts.missing_key")}:{className:"cb-badge cb-badge-ok",label:t("accounts.configured")});if(selectedEntry.active)accountBadges.push({className:"cb-badge cb-badge-ok",label:t("accounts.active_badge")});if(selectedEntry.isPinned)accountBadges.push({className:"cb-badge",label:t("accounts.pinned_badge")})}const accountTabs=accountEntries.length>1?React.createElement("div",{className:"cb-account-tabs"},accountEntries.map(entry=>React.createElement("button",{type:"button",key:entry.apiKeyEnv||entry.id,className:entry.apiKeyEnv===selectedEntry?.apiKeyEnv?"cb-account-tab cb-account-tab-active":"cb-account-tab",onClick:()=>setSelectedAccount(entry.apiKeyEnv)},entry.label||entry.apiKeyEnv||entry.id))):null;const noticeItems=[];if(status?.key&&status.key.present===false){noticeItems.push(React.createElement("div",{className:"cb-notice",key:"no-key"},React.createElement("div",{className:"cb-notice-title"},t("analytics.no_key")),React.createElement("div",{className:"cb-notice-hint"},t("analytics.no_key_hint"))))}if(status?.health&&status.health.ok===false){noticeItems.push(React.createElement("div",{className:"cb-notice cb-notice-error",key:"health"},React.createElement("div",{className:"cb-notice-title"},t("analytics.health_error")),React.createElement("div",{className:"cb-notice-hint"},status.health.error||"")))}if(status&&status.isRegistered===false){noticeItems.push(React.createElement("div",{className:"cb-notice",key:"not-registered"},React.createElement("div",{className:"cb-notice-title"},t("analytics.not_registered")),React.createElement("div",{className:"cb-notice-hint"},t("analytics.not_registered_hint"))))}const notices=noticeItems.length?React.createElement(React.Fragment,null,...noticeItems):null;const errorNotice=err?React.createElement("div",{className:data?"cb-hint":"cb-alert-bad"},data?`${t("analytics.stale")} ${err}`:err):null;const totals=analytics?.totals;const secondaryTiles=totals?[[t("analytics.input"),fmtTokens(totals.promptTokens)],[t("analytics.output"),fmtTokens(totals.completionTokens)],[t("analytics.reasoning"),fmtTokens(totals.reasoningTokens)],[t("analytics.cache_read"),fmtTokens(totals.cacheReadTokens)],[t("analytics.cache_write"),fmtTokens(totals.cacheWriteTokens)],[t("analytics.cache_saved"),`\u2248 ${fmtCost(totals.cacheSavingsUsd)}`],[t("analytics.avg_ttft"),fmtMs(totals.avgTtftMs)],[t("analytics.avg_latency"),fmtMs(totals.avgLatencyMs)],[t("analytics.p50_latency"),fmtMs(totals.p50LatencyMs)],[t("analytics.p95_latency"),fmtMs(totals.p95LatencyMs)],[t("analytics.errors"),String(Math.round(Number(totals.failed)||0))]]:[];const tabs=React.createElement("div",{className:"cb-tabs"},React.createElement("button",{type:"button",className:tab==="quota"?"cb-tab cb-tab-active":"cb-tab",onClick:()=>setTab("quota")},t("dashboard.tab_quota")),React.createElement("button",{type:"button",className:tab==="analytics"?"cb-tab cb-tab-active":"cb-tab",onClick:()=>setTab("analytics")},t("dashboard.tab_analytics")));const quotaBody=React.createElement(QuotaPanelView,{accountTabs,selectedUsage,accountName,accountEmail,accountPlan,accountBadges,activeAcc,routingMode,enabledCount,t});const analyticsBody=React.createElement("div",{className:"cb-dialogCard"},React.createElement("div",{className:"cb-dialogRow"},React.createElement("div",{className:"cb-section-title"},t("analytics.title")),React.createElement(AnalyticsControls,{analyticsDays,setAnalyticsDays,groupBy,setGroupBy,model:analyticsModel,setModel:setAnalyticsModel,account:analyticsAccount,setAccount:setAnalyticsAccount,errorsOnly:analyticsErrorsOnly,setErrorsOnly:setAnalyticsErrorsOnly,from:analyticsFrom,setFrom:setAnalyticsFrom,to:analyticsTo,setTo:setAnalyticsTo,models:status?.availableModels||[],accounts:status?.accounts||[],t})),React.createElement("div",{className:"cb-row",style:{gap:"6px",marginBottom:"8px"}},React.createElement("a",{href:"/dsh-clinebot/stats/export?format=json",download:"clinebot-stats.json",className:"cb-btn cb-btn-link"},t("stats.export_json")),React.createElement("a",{href:"/dsh-clinebot/stats/export",download:"clinebot-stats.csv",className:"cb-btn cb-btn-link"},t("stats.export_csv"))),analytics?.totals?React.createElement("div",{className:"cb-dialogTiles"},React.createElement("div",{className:"cb-stat-box"},React.createElement("div",{className:"cb-stat-val"},String(analytics.totals.requests||0)),React.createElement("div",{className:"cb-stat-lbl"},t("analytics.requests"))),React.createElement("div",{className:"cb-stat-box"},React.createElement("div",{className:"cb-stat-val"},fmtTokens(analytics.totals.totalTokens)),React.createElement("div",{className:"cb-stat-lbl"},t("analytics.tokens"))),React.createElement("div",{className:"cb-stat-box"},React.createElement("div",{className:"cb-stat-val"},`\u2248 ${fmtCost(analytics.totals.costUsd)}`),React.createElement("div",{className:"cb-stat-lbl"},t("analytics.cost"))),React.createElement("div",{className:"cb-stat-box"},React.createElement("div",{className:"cb-stat-val"},`${Math.round(analytics.totals.successRate||0)}%`),React.createElement("div",{className:"cb-stat-lbl"},t("analytics.success")))):null,secondaryTiles.length?React.createElement("div",{className:"cb-dialogTiles"},secondaryTiles.map(([label,value])=>React.createElement("div",{className:"cb-stat-box",key:label},React.createElement("div",{className:"cb-stat-val"},value),React.createElement("div",{className:"cb-stat-lbl"},label)))):null,analyticsLoading?React.createElement("div",{className:"cb-hint"},t("analytics.loading")):analytics?.groups?.length?React.createElement("div",{className:"cb-analytics-list"},React.createElement("div",{className:"cb-analytics-head"},React.createElement("span",null,analytics.groupBy||groupBy),React.createElement("span",null,t("analytics.requests")),React.createElement("span",null,t("analytics.tokens")),React.createElement("span",null,t("analytics.cost")),React.createElement("span",null,t("analytics.ttft"))),analytics.groups.map(group=>React.createElement("div",{className:"cb-analytics-row",key:group.key},React.createElement("span",{className:"cb-mono"},group.label||group.key),React.createElement("span",null,String(group.requests||0)),React.createElement("span",null,fmtTokens(group.totalTokens)),React.createElement("span",null,`\u2248 ${fmtCost(group.costUsd)}`),React.createElement("span",null,group.avgTtftMs?`${Math.round(group.avgTtftMs)} ms`:"\u2014")))):React.createElement("div",{className:"cb-empty"},t("analytics.no_data")),analytics?.unpriced>0?React.createElement("div",{className:"cb-hint"},t("analytics.unpriced",{count:analytics.unpriced})):null);const heatmapCard=React.createElement("div",{className:"cb-dialogCard"},React.createElement("div",{className:"cb-section-title"},t("analytics.heatmap")),React.createElement("div",{className:"cb-section-desc"},t("analytics.heatmap_hint")),React.createElement(HeatmapView,{heatmap:analytics?.heatmap,model:heatmapModel,onModelChange:setHeatmapModel,selected:selectedHeat,onSelect:cell=>setSelectedHeat(current=>current&¤t.day===cell.day&¤t.hour===cell.hour?null:cell),t}),selectedHeat?React.createElement("div",{className:"cb-hint"},t("analytics.heatmap_selected",{day:selectedHeat.label,hour:String(selectedHeat.hour).padStart(2,"0"),count:selectedHeat.value})):null);const panelBody=React.createElement("div",{className:"cb-panel"},React.createElement("div",{className:"cb-panel-head"},React.createElement("div",{className:"cb-panel-identity"},React.createElement("div",{className:"cb-panel-title"},t("dashboard.title")),React.createElement("div",{className:"cb-panel-sub"},t("dashboard.subtitle"))),React.createElement("div",{className:"cb-panel-actions"},updatedAt?React.createElement("span",{className:"cb-panel-updated"},`${t("dashboard.updated_at")} ${updatedAt}`):null,React.createElement("button",{type:"button",className:"cb-btn",disabled:loading||analyticsLoading,onClick:()=>{loadData();loadAnalytics(groupBy)}},loading||analyticsLoading?t("dashboard.refreshing"):t("dashboard.refresh")),React.createElement("button",{type:"button",className:"cb-panel-close","aria-label":t("dashboard.close"),title:t("dashboard.close"),onClick:onClose},"\xD7"))),notices,errorNotice,quotaBody,analyticsBody,heatmapCard);const body=React.createElement("div",{className:"cb-dialog"},React.createElement("div",{className:"cb-dialogMeta"},updatedAt?React.createElement("span",null,`${t("dashboard.updated_at")} ${updatedAt}`):null,React.createElement("span",{className:"cb-settings-spacer"}),React.createElement("button",{type:"button",className:"cb-btn",disabled:loading||analyticsLoading,onClick:()=>tab==="quota"?loadData():loadAnalytics(groupBy)},loading||analyticsLoading?t("dashboard.refreshing"):t("dashboard.refresh"))),tabs,notices,errorNotice,tab==="quota"?quotaBody:React.createElement(React.Fragment,null,analyticsBody,heatmapCard));if(panel)return panelBody;if(Modal){return React.createElement(Modal,{open:true,onClose,title:t("dashboard.title"),description:t("dashboard.subtitle"),closeLabel:t("dashboard.close"),children:body})}return React.createElement("div",{className:"cb-dialogFallback",role:"dialog","aria-label":t("dashboard.title"),onClick:onClose},React.createElement("div",{className:"cb-dialogFallbackPanel",onClick:event=>event.stopPropagation()},React.createElement("div",{className:"cb-dialogMeta"},React.createElement("span",{className:"cb-row-title"},t("dashboard.title")),React.createElement("span",{className:"cb-settings-spacer"}),React.createElement("button",{type:"button",className:"cb-btn","aria-label":t("dashboard.close"),onClick:onClose},t("dashboard.close"))),body))}
|
|
467
467
|
|
|
468
468
|
// --- src/client/components/composer-cost.js ---
|
|
469
|
-
function formatTokens(count){const n=Number(count)||0;if(n>=1e6)return(n/1e6).toFixed(1)+"M";if(n>=1e3)return(n/1e3).toFixed(1)+"k";return String(n)}function findPrice(modelId,prices){if(!modelId||!Array.isArray(prices))return null;const exact=prices.find(entry=>entry&&entry.id===modelId);if(exact)return exact;return prices.find(entry=>entry&&entry.id&&(modelId===entry.id||modelId.endsWith(`/${entry.id}`)||modelId.includes(entry.id)))||null}function calculateCost(tokens,modelId,prices){const pTok=Number(tokens?.promptTokens||tokens?.inputTokens||0);const cTok=Number(tokens?.completionTokens||tokens?.outputTokens||0);const rTok=Number(tokens?.reasoningTokens||0);const cacheRead=Number(tokens?.cacheReadTokens||tokens?.cacheRead||0);const cacheWrite=Number(tokens?.cacheWriteTokens||tokens?.cacheWrite||0);const total=pTok+cTok+rTok;const price=findPrice(modelId,prices);if(!price)return{cost:0,priced:false,source:null,pTok,cTok,rTok,cacheRead,cacheWrite,total};const uncachedInput=Math.max(0,pTok-cacheRead-cacheWrite);const cost=(uncachedInput*Number(price.input||0)+cTok*Number(price.output||0)+cacheRead*Number(price.cacheRead||0)+cacheWrite*Number(price.cacheWrite||0))/1e6;return{cost,priced:true,source:price.source||"estimate",pTok,cTok,rTok,cacheRead,cacheWrite,total}}const COST_ANCHOR='[data-slot="conversation.composer.dock"]';const STATS_ROOT="[data-composer-stats]";const STATS_USAGE='[data-composer-stat="usage"]';const STATS_ANY="[data-composer-stat]";const DIALOG_TRIGGER='button[aria-haspopup="dialog"]';const MISSES_BEFORE_WARNING=3;function ComposerCostDock(props){if(typeof props?.useProjection!=="function")return null;return React.createElement(ComposerCostEntry,props)}function ComposerCostEntry(props){const t=props?.t||makeT(en,en);const configSnapshot=useConfigFormsSnapshot(props?.ctx);const[stats,setStats]=React.useState(null);const[prices,setPrices]=React.useState([]);const[sessionStats,setSessionStats]=React.useState(null);const markerRef=React.useRef(null);const projectedTokens=props.useProjection("tokenUsage");const selection=props.useProjection("modelSelection");const sessionId=String(props?.sessionId||"");const priceOverridesKey=JSON.stringify(configSnapshot?.value?.priceOverrides||{});React.useEffect(()=>{let active=true;fetchWithTimeout(`${ROUTE_PREFIX}/prices`,{cache:"no-store"}).then(response=>response.json()).then(data=>{if(active&&data?.ok&&Array.isArray(data.models))setPrices(data.models)}).catch(()=>{});return()=>{active=false}},[priceOverridesKey]);React.useEffect(()=>{if(!sessionId){setSessionStats(null);return void 0}let active=true;const loadSession=async()=>{try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats/session?sessionId=${encodeURIComponent(sessionId)}`,{cache:"no-store"});const data=await res.json().catch(()=>null);if(active&&data?.ok)setSessionStats(data.session||null)}catch{}};loadSession();let timer=null;const schedule=()=>{timer=setTimeout(async()=>{await loadSession();if(active)schedule()},1e4)};schedule();return()=>{active=false;if(timer)clearTimeout(timer)}},[sessionId,projectedTokens]);React.useEffect(()=>{if(projectedTokens||sessionId)return void 0;let active=true;const loadStats=async()=>{try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats`,{cache:"no-store"});if(!res.ok)return;const data=await res.json().catch(()=>null);const totals=data?.stats?.totals||data?.stats?.summary||null;if(active&&totals)setStats(totals)}catch{}};loadStats();const timer=setTimeout(loadStats,3e4);return()=>{active=false;clearTimeout(timer)}},[projectedTokens,sessionId]);const selected=selection?.lastUsed??selection?.next??(selection?.provider||selection?.model?selection:null);const provider=String(selected?.provider||"");const modelId=String(selected?.model||selected?.id||"");const isClineBot=(provider===""||provider==="clinebot"||provider==="llm-clinebot")&&(provider==="clinebot"||provider==="llm-clinebot"||modelId.startsWith("cline-pass/"));const session=sessionStats&&Number(sessionStats.requests||0)>0?sessionStats:null;const sessionTokens=session?{promptTokens:Number(session.promptTokens||0),completionTokens:Number(session.completionTokens||0),reasoningTokens:Number(session.reasoningTokens||0),cacheReadTokens:Number(session.cacheReadTokens||0),cacheWriteTokens:Number(session.cacheWriteTokens||0)}:null;const rawTokens=sessionTokens||(sessionId?null:projectedTokens||{promptTokens:stats?.promptTokens||0,completionTokens:stats?.completionTokens||0,reasoningTokens:stats?.reasoningTokens||0,cacheReadTokens:stats?.cacheReadTokens||0,cacheWriteTokens:stats?.cacheWriteTokens||0});const cacheRead=Number(rawTokens?.cacheReadTokens||0);const cacheWrite=Number(rawTokens?.cacheWriteTokens||0);const uncachedInput=Number(rawTokens?.uncachedInputTokens);const promptTokens=rawTokens?Number.isFinite(uncachedInput)?uncachedInput+cacheRead+cacheWrite:Number(rawTokens.promptTokens||rawTokens.inputTokens||0):0;const tokens={promptTokens,completionTokens:Number(rawTokens?.completionTokens||rawTokens?.outputTokens||0),reasoningTokens:Number(rawTokens?.reasoningTokens||0),cacheReadTokens:cacheRead,cacheWriteTokens:cacheWrite};const calculated=calculateCost(tokens,session?.model||modelId,prices);const cost=session?Number(session.costUsd||0):calculated.cost;const priced=session?Number(session.pricedRequests||0)>0:calculated.priced;const source=session?findPrice(session.model||modelId,prices)?.source||"estimate":calculated.source;const{pTok,cTok,rTok}=calculated;const total=session?Number(session.totalTokens||pTok+cTok):calculated.total;const last=session?.last||null;const lastCost=last?Number(last.costUsd||0):0;const lastPriced=Boolean(last?.priced);const costValue=priced?cost===0?t("composer.free"):cost<1e-4?"< $0.0001":`$${cost.toFixed(4)}`:t("composer.unpriced");const costDisplay=priced&&source!=="override"&&cost>0?`\u2248 ${costValue}`:costValue;const lastCostValue=last?lastPriced?lastCost===0?t("composer.free"):lastCost<1e-4?"< $0.0001":`$${lastCost.toFixed(4)}`:t("composer.unpriced"):"";const title=[`${t("composer.cost_title")} ${costDisplay}`,...last?[`${t("composer.last_request")} ${lastCostValue}`]:[],`${t("composer.prompt_tok")} ${formatTokens(pTok)}`,`${t("composer.completion_tok")} ${formatTokens(cTok)}`,...rTok>0?[`${t("composer.reasoning_tok")} ${formatTokens(rTok)}`]:[],...cacheRead>0?[`${t("composer.cache_read")} ${formatTokens(cacheRead)}`]:[],...cacheWrite>0?[`${t("composer.cache_write")} ${formatTokens(cacheWrite)}`]:[],...priced?[`${t("composer.price_source")} ${source==="override"?t("composer.price_override"):source==="public"?t("composer.price_public"):t("composer.price_estimate")}`]:[]].join(" \xB7 ");const inject=isClineBot&&priced&&total>0;React.useEffect(()=>{if(!inject||typeof document==="undefined")return void 0;const marker=markerRef.current;if(!marker)return void 0;const scope=marker.closest(COST_ANCHOR);if(!scope)return void 0;let host=null;let root=null;let valueNode=null;let misses=0;const apply=()=>{if(!root||root.parentNode===null){const usageAnchor=scope.querySelector(STATS_USAGE)||scope.querySelector(STATS_ANY);const statsRoot=usageAnchor||scope.querySelector(STATS_ROOT)||scope;const triggers=statsRoot.querySelectorAll(DIALOG_TRIGGER);const button=triggers[triggers.length-1]||usageAnchor;if(!button){misses+=1;if(misses===MISSES_BEFORE_WARNING){console.warn("[dsh-clinebot] no native token-usage pill found for the session cost")}return}misses=0;host=button;root=document.createElement("span");root.setAttribute("data-clinebot-session-cost","");root.style.display="inline-flex";root.style.alignItems="center";const separator=document.createElement("span");separator.textContent="\xB7";separator.setAttribute("aria-hidden","true");separator.style.color="var(--dsw-alias-separator-primary)";separator.style.margin="0 6px 0 0";valueNode=document.createElement("span");valueNode.style.fontWeight="500";root.appendChild(separator);root.appendChild(valueNode);button.appendChild(root)}if(root.parentNode!==host)host.appendChild(root);if(valueNode.textContent!==costDisplay)valueNode.textContent=costDisplay;if(root.title!==title)root.title=title};apply();const observer=typeof window!=="undefined"&&typeof window.MutationObserver==="function"?new window.MutationObserver(apply):null;observer?.observe(document.body,{childList:true});let timer=null;const tick=()=>{apply();timer=setTimeout(tick,2e3)};timer=setTimeout(tick,2e3);return()=>{observer?.disconnect();if(timer)clearTimeout(timer);if(root?.parentNode)root.parentNode.removeChild(root)}},[inject,costDisplay,title]);if(configSnapshot?.value?.showComposerCost
|
|
469
|
+
function formatTokens(count){const n=Number(count)||0;if(n>=1e6)return(n/1e6).toFixed(1)+"M";if(n>=1e3)return(n/1e3).toFixed(1)+"k";return String(n)}function findPrice(modelId,prices){if(!modelId||!Array.isArray(prices))return null;const exact=prices.find(entry=>entry&&entry.id===modelId);if(exact)return exact;return prices.find(entry=>entry&&entry.id&&(modelId===entry.id||modelId.endsWith(`/${entry.id}`)||modelId.includes(entry.id)))||null}function calculateCost(tokens,modelId,prices){const pTok=Number(tokens?.promptTokens||tokens?.inputTokens||0);const cTok=Number(tokens?.completionTokens||tokens?.outputTokens||0);const rTok=Number(tokens?.reasoningTokens||0);const cacheRead=Number(tokens?.cacheReadTokens||tokens?.cacheRead||0);const cacheWrite=Number(tokens?.cacheWriteTokens||tokens?.cacheWrite||0);const total=pTok+cTok+rTok;const price=findPrice(modelId,prices);if(!price)return{cost:0,priced:false,source:null,pTok,cTok,rTok,cacheRead,cacheWrite,total};const uncachedInput=Math.max(0,pTok-cacheRead-cacheWrite);const cost=(uncachedInput*Number(price.input||0)+cTok*Number(price.output||0)+cacheRead*Number(price.cacheRead||0)+cacheWrite*Number(price.cacheWrite||0))/1e6;return{cost,priced:true,source:price.source||"estimate",pTok,cTok,rTok,cacheRead,cacheWrite,total}}const COST_ANCHOR='[data-slot="conversation.composer.dock"]';const STATS_ROOT="[data-composer-stats]";const STATS_USAGE='[data-composer-stat="usage"]';const STATS_ANY="[data-composer-stat]";const DIALOG_TRIGGER='button[aria-haspopup="dialog"]';const MISSES_BEFORE_WARNING=3;function ComposerCostDock(props){if(typeof props?.useProjection!=="function")return null;return React.createElement(ComposerCostEntry,props)}function ComposerCostEntry(props){const t=props?.t||makeT(en,en);const configSnapshot=useConfigFormsSnapshot(props?.ctx);const[stats,setStats]=React.useState(null);const[prices,setPrices]=React.useState([]);const[sessionStats,setSessionStats]=React.useState(null);const markerRef=React.useRef(null);const projectedTokens=props.useProjection("tokenUsage");const selection=props.useProjection("modelSelection");const sessionId=String(props?.sessionId||"");const priceOverridesKey=JSON.stringify(configSnapshot?.value?.priceOverrides||{});React.useEffect(()=>{let active=true;fetchWithTimeout(`${ROUTE_PREFIX}/prices`,{cache:"no-store"}).then(response=>response.json()).then(data=>{if(active&&data?.ok&&Array.isArray(data.models))setPrices(data.models)}).catch(()=>{});return()=>{active=false}},[priceOverridesKey]);React.useEffect(()=>{if(!sessionId){setSessionStats(null);return void 0}let active=true;const loadSession=async()=>{try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats/session?sessionId=${encodeURIComponent(sessionId)}`,{cache:"no-store"});const data=await res.json().catch(()=>null);if(active&&data?.ok)setSessionStats(data.session||null)}catch{}};loadSession();let timer=null;const schedule=()=>{timer=setTimeout(async()=>{await loadSession();if(active)schedule()},1e4)};schedule();return()=>{active=false;if(timer)clearTimeout(timer)}},[sessionId,projectedTokens]);React.useEffect(()=>{if(projectedTokens||sessionId)return void 0;let active=true;const loadStats=async()=>{try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats`,{cache:"no-store"});if(!res.ok)return;const data=await res.json().catch(()=>null);const totals=data?.stats?.totals||data?.stats?.summary||null;if(active&&totals)setStats(totals)}catch{}};loadStats();const timer=setTimeout(loadStats,3e4);return()=>{active=false;clearTimeout(timer)}},[projectedTokens,sessionId]);const selected=selection?.lastUsed??selection?.next??(selection?.provider||selection?.model?selection:null);const provider=String(selected?.provider||"");const modelId=String(selected?.model||selected?.id||"");const isClineBot=(provider===""||provider==="clinebot"||provider==="llm-clinebot")&&(provider==="clinebot"||provider==="llm-clinebot"||modelId.startsWith("cline-pass/"));const session=sessionStats&&Number(sessionStats.requests||0)>0?sessionStats:null;const sessionTokens=session?{promptTokens:Number(session.promptTokens||0),completionTokens:Number(session.completionTokens||0),reasoningTokens:Number(session.reasoningTokens||0),cacheReadTokens:Number(session.cacheReadTokens||0),cacheWriteTokens:Number(session.cacheWriteTokens||0)}:null;const rawTokens=sessionTokens||(sessionId?null:projectedTokens||{promptTokens:stats?.promptTokens||0,completionTokens:stats?.completionTokens||0,reasoningTokens:stats?.reasoningTokens||0,cacheReadTokens:stats?.cacheReadTokens||0,cacheWriteTokens:stats?.cacheWriteTokens||0});const cacheRead=Number(rawTokens?.cacheReadTokens||0);const cacheWrite=Number(rawTokens?.cacheWriteTokens||0);const uncachedInput=Number(rawTokens?.uncachedInputTokens);const promptTokens=rawTokens?Number.isFinite(uncachedInput)?uncachedInput+cacheRead+cacheWrite:Number(rawTokens.promptTokens||rawTokens.inputTokens||0):0;const tokens={promptTokens,completionTokens:Number(rawTokens?.completionTokens||rawTokens?.outputTokens||0),reasoningTokens:Number(rawTokens?.reasoningTokens||0),cacheReadTokens:cacheRead,cacheWriteTokens:cacheWrite};const calculated=calculateCost(tokens,session?.model||modelId,prices);const cost=session?Number(session.costUsd||0):calculated.cost;const priced=session?Number(session.pricedRequests||0)>0:calculated.priced;const source=session?findPrice(session.model||modelId,prices)?.source||"estimate":calculated.source;const{pTok,cTok,rTok}=calculated;const total=session?Number(session.totalTokens||pTok+cTok):calculated.total;const last=session?.last||null;const lastCost=last?Number(last.costUsd||0):0;const lastPriced=Boolean(last?.priced);const costValue=priced?cost===0?t("composer.free"):cost<1e-4?"< $0.0001":`$${cost.toFixed(4)}`:t("composer.unpriced");const costDisplay=priced&&source!=="override"&&cost>0?`\u2248 ${costValue}`:costValue;const lastCostValue=last?lastPriced?lastCost===0?t("composer.free"):lastCost<1e-4?"< $0.0001":`$${lastCost.toFixed(4)}`:t("composer.unpriced"):"";const title=[`${t("composer.cost_title")} ${costDisplay}`,...last?[`${t("composer.last_request")} ${lastCostValue}`]:[],`${t("composer.prompt_tok")} ${formatTokens(pTok)}`,`${t("composer.completion_tok")} ${formatTokens(cTok)}`,...rTok>0?[`${t("composer.reasoning_tok")} ${formatTokens(rTok)}`]:[],...cacheRead>0?[`${t("composer.cache_read")} ${formatTokens(cacheRead)}`]:[],...cacheWrite>0?[`${t("composer.cache_write")} ${formatTokens(cacheWrite)}`]:[],...priced?[`${t("composer.price_source")} ${source==="override"?t("composer.price_override"):source==="public"?t("composer.price_public"):t("composer.price_estimate")}`]:[]].join(" \xB7 ");const inject=isClineBot&&priced&&total>0;React.useEffect(()=>{if(!inject||typeof document==="undefined")return void 0;const marker=markerRef.current;if(!marker)return void 0;const scope=marker.closest(COST_ANCHOR);if(!scope)return void 0;let host=null;let root=null;let valueNode=null;let misses=0;const apply=()=>{if(!root||root.parentNode===null){const usageAnchor=scope.querySelector(STATS_USAGE)||scope.querySelector(STATS_ANY);const statsRoot=usageAnchor||scope.querySelector(STATS_ROOT)||scope;const triggers=statsRoot.querySelectorAll(DIALOG_TRIGGER);const button=triggers[triggers.length-1]||usageAnchor;if(!button){misses+=1;if(misses===MISSES_BEFORE_WARNING){console.warn("[dsh-clinebot] no native token-usage pill found for the session cost")}return}misses=0;host=button;root=document.createElement("span");root.setAttribute("data-clinebot-session-cost","");root.style.display="inline-flex";root.style.alignItems="center";const separator=document.createElement("span");separator.textContent="\xB7";separator.setAttribute("aria-hidden","true");separator.style.color="var(--dsw-alias-separator-primary)";separator.style.margin="0 6px 0 0";valueNode=document.createElement("span");valueNode.style.fontWeight="500";root.appendChild(separator);root.appendChild(valueNode);button.appendChild(root)}if(root.parentNode!==host)host.appendChild(root);if(valueNode.textContent!==costDisplay)valueNode.textContent=costDisplay;if(root.title!==title)root.title=title};apply();const observer=typeof window!=="undefined"&&typeof window.MutationObserver==="function"?new window.MutationObserver(apply):null;observer?.observe(document.body,{childList:true});let timer=null;const tick=()=>{apply();timer=setTimeout(tick,2e3)};timer=setTimeout(tick,2e3);return()=>{observer?.disconnect();if(timer)clearTimeout(timer);if(root?.parentNode)root.parentNode.removeChild(root)}},[inject,costDisplay,title]);if(!configSnapshot?.value?.showComposerCost)return null;if(total===0&&!stats)return null;if(!inject)return null;return React.createElement("span",{ref:markerRef,style:{display:"none"},"data-clinebot-cost-anchor":""})}
|
|
470
470
|
|
|
471
471
|
// --- src/client/settings-page.js ---
|
|
472
|
-
function SettingsPage(props){const ctx=props?.ctx;const t=props?.t||makeT(en,en);const configSnapshot=useConfigFormsSnapshot(ctx);const[status,setStatus]=React.useState(null);const[draft,setDraft]=React.useState(null);const[busy,setBusy]=React.useState("");const[err,setErr]=React.useState(""),[msg,setMsg]=React.useState("");const[smokeResult,setSmokeResult]=React.useState(null);const[updateState,setUpdateState]=React.useState({status:"idle",checking:false,updating:false,currentVersion:"",latestVersion:"",updateAvailable:false,canAutoUpdate:true,latestCheckFailed:false,lastCheckedAt:null,error:"",notice:""});const[apiKeyInput,setApiKeyInput]=React.useState(""),[showKey,setShowKey]=React.useState(false);React.useEffect(()=>{ensureCss()},[]);const pendingRef=React.useRef({});const load=React.useCallback(async()=>{setErr("");const[res,statsRes]=await Promise.all([fetchWithTimeout(`${ROUTE_PREFIX}/status`,{cache:"no-store"}),fetchWithTimeout(`${ROUTE_PREFIX}/stats`,{cache:"no-store"}).catch(()=>null)]);const data=await res.json().catch(()=>({}));if(!res.ok)throw new Error(data.error||`HTTP ${res.status}`);const rawStats=statsRes&&statsRes.ok?await statsRes.json().catch(()=>null):null;const statsObj=rawStats&&typeof rawStats==="object"&&rawStats.stats?rawStats.stats:rawStats||data.statsSummary||{};setStatus({...data,statsSummary:statsObj});setDraft({...data.config||{},...pendingRef.current||{}})},[]);React.useEffect(()=>{load().catch(e=>setErr(String(e.message||e)))},[load]);const staged=useStagedConfig({status,draft,setDraft,load,t,pendingRef});const modelSelection=useModelSelection({status,draft,setDraft,load,setErr});const checkUpdate=React.useCallback(async()=>{setUpdateState(s=>({...s,checking:true,status:"checking",error:""}));try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/update`);if(!res.ok)throw new Error(`HTTP ${res.status}`);const data=await res.json().catch(()=>({}));if(data.latestCheckFailed){setUpdateState(s=>({...s,checking:false,status:"error",latestCheckFailed:true,currentVersion:data.currentVersion||s.currentVersion,error:t("update.check_failed")}));return}const isAvailable=Boolean(data.updateAvailable);setUpdateState(s=>({...s,checking:false,status:isAvailable?"available":"current",currentVersion:data.currentVersion||s.currentVersion,latestVersion:data.latestVersion||"",updateAvailable:isAvailable,latestCheckFailed:false,lastCheckedAt:Date.now(),canAutoUpdate:data.canAutoUpdate!==false,error:""}))}catch(err2){setUpdateState(s=>({...s,checking:false,status:"error",latestCheckFailed:true,error:String(err2?.message||err2||t("update.check_failed"))}))}},[t]);React.useEffect(()=>{checkUpdate()},[checkUpdate]);async function handleTriggerUpdate(){if(updateState.updating)return;setUpdateState(s=>({...s,updating:true,error:"",notice:""}));try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/update`,{method:"POST",headers:{"x-dsh-plugin-update":"1"}});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false||data.error)throw new Error(data.error||`HTTP ${res.status}`);const newVer=data.updatedVersion||updateState.latestVersion||updateState.currentVersion;setUpdateState(s=>({...s,updating:false,status:"current",updateAvailable:false,currentVersion:newVer,lastCheckedAt:Date.now(),notice:t("update.done",{version:newVer})}));setTimeout(()=>checkUpdate(),2e3)}catch(err2){setUpdateState(s=>({...s,updating:false,status:s.updateAvailable?"available":"error",error:t("update.failed",{error:String(err2?.message||err2)})}))}}async function performAction(busyKey,fn){setBusy(busyKey);setErr("");setMsg("");try{await fn()}catch(e){setErr(String(e.message||e))}finally{setBusy("")}}const{handlePinAccount,handleAddAccount,handleDeleteAccount,handleRenameAccount,handleClearKey}=createAccountActions({performAction,load,t,setMsg});async function handleSaveKey(){const keyVal=String(apiKeyInput||"").trim();if(!keyVal){setErr(t("key.empty_err"));return}await performAction("save-key",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/save-key`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:keyVal,apiKeyEnv:draft?.apiKeyEnv})});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setApiKeyInput("");setMsg(t("key.saved_msg",{status:data.validated?"OK":"Notice (check console)"}));await load()})}async function handleRefreshQuota(){await performAction("refresh-quota",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/usage`);const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setStatus(prev=>prev?{...prev,usage:data}:prev);setMsg(t("quota.refreshed_msg"))})}async function handleUpdateModelContext(payload){await performAction("update-context",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/context`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setMsg(t("models.context_updated_msg"));await load()})}async function handleSyncPlanModels(){await performAction("sync-models",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/sync`,{method:"POST"});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setMsg(t("models.synced_msg",{total:data.totalModelsCount,discovered:data.discoveredCount}));await load()})}async function handleApplyVisible(payload){await performAction("visible-models",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/visible`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload||{})});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false)throw new Error(data.error||`HTTP ${res.status}`);await load();setMsg(t("models.visible_applied"))})}async function handleSmoke(){setSmokeResult(null);await performAction("smoke",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/smoke`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:draft?.defaultModel})});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setSmokeResult(data)})}const[verifyStatus,setVerifyStatus]=React.useState({state:"idle",email:"",plan:"",error:""});const verifyTimerRef=typeof React.useRef==="function"?React.useRef(null):{current:null};React.useEffect(()=>{const raw=String(apiKeyInput||"").trim();if(!raw||raw.length<10){setVerifyStatus({state:"idle",email:"",plan:"",error:""});return}if(verifyTimerRef.current)clearTimeout(verifyTimerRef.current);verifyTimerRef.current=setTimeout(async()=>{setVerifyStatus({state:"verifying",email:"",plan:"",error:""});try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/key/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:raw})});const data=await res.json().catch(()=>({}));if(data.ok&&data.valid){setVerifyStatus({state:"valid",email:data.email,plan:data.plan,error:""})}else{setVerifyStatus({state:"invalid",email:"",plan:"",error:data.error||"Verification failed"})}}catch(err2){setVerifyStatus({state:"invalid",email:"",plan:"",error:String(err2?.message||err2)})}},500);return()=>{if(verifyTimerRef.current)clearTimeout(verifyTimerRef.current)}},[apiKeyInput]);function handleFastLogin(){window.open("https://app.cline.bot/settings/api-keys","_blank")}async function handleConfigPatch(patch){setDraft(d=>({...d||{},...patch||{}}));await performAction("patch-config",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/config`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:patch})});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false)throw new Error(data.error||`HTTP ${res.status}`);await load()})}async function handleResetStats(){await performAction("reset-stats",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats/reset`,{method:"POST"});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);await load()})}if(!status||!draft){if(err){return React.createElement("div",{className:"cb-page"},React.createElement("div",{className:"cb-alert cb-alert-err"},`${t("settings.loading")}: ${err}`),React.createElement("button",{type:"button",className:"cb-btn",style:{marginTop:"12px"},onClick:()=>{setErr("");load().catch(e=>setErr(String(e.message||e)))}},t("settings.retry")))}return React.createElement("div",{className:"cb-page"},t("settings.loading"))}const healthOk=!!status.health?.ok;const keyPresent=!!status.key?.present;const isRegistered=!!status.isRegistered;const modelsList=status.availableModels||[];const disabledSet=new Set(draft.disabledModels||[]);const enabledCount=modelsList.filter(m=>!disabledSet.has(m.id)).length;const usage=status.usage;return React.createElement("div",{className:"cb-page"},React.createElement("div",{className:"cb-header"},React.createElement("div",{className:"cb-page-title"},t("header.title"),React.createElement("span",{className:`cb-badge ${healthOk?"cb-badge-ok":"cb-badge-bad"}`},healthOk?t("badge.online",{latency:status.health?.latencyMs}):t("badge.offline")),React.createElement("span",{className:`cb-badge ${keyPresent?"cb-badge-ok":"cb-badge-warn"}`},keyPresent?t("badge.key_ok",{source:status.key?.source}):t("badge.key_missing")),React.createElement("span",{className:`cb-badge ${isRegistered?"cb-badge-ok":"cb-badge-warn"}`},isRegistered?t("badge.registered",{count:enabledCount}):t("badge.not_registered"))),React.createElement("div",{className:"cb-page-sub"},t("header.sub"))),React.createElement(UpdateBanner,{updateState,handleTriggerUpdate,handleCheckUpdate:checkUpdate,t}),configSnapshot?.status==="unavailable"?React.createElement("div",{className:"cb-alert-bad",style:{marginBottom:"12px"}},t("settings.unavailable")):null,err?React.createElement("div",{className:"cb-alert-bad"},err):null,msg?React.createElement("div",{className:"cb-alert-ok"},msg):null,React.createElement(KeySection,{keyPresent,apiKeyInput,setApiKeyInput,showKey,setShowKey,busy,draft,handleSaveKey,handleFastLogin,verifyStatus,t}),React.createElement(AccountsSection,{status,busy,handlePinAccount,handleAddAccount,handleDeleteAccount,handleRenameAccount,handleClearKey,accountMode:draft?.accountMode||status?.config?.accountMode||"least_used",handleUpdateAccountMode:mode=>handleConfigPatch({accountMode:mode}),modelRoutes:draft?.modelRoutes||status?.config?.modelRoutes||{},onUpdateModelRoutes:routes=>handleConfigPatch({modelRoutes:routes}),t}),React.createElement(QuotaSection,{keyPresent,status,usage,busy,handleRefreshQuota,showSidebarQuota:draft?.showSidebarQuota!==false,onToggleSidebarQuota:value=>staged.stage({showSidebarQuota:value}),t}),React.createElement(CollapsibleGroup,{title:t("settings.models_group"),description:t("settings.models_group_hint"),badge:`${enabledCount}/${modelsList.length}`,divided:true,defaultOpen:false},React.createElement(ModelsSection,{modelsList,disabledSet,enabledCount,keyPresent,busy,handleSyncPlanModels,handleUpdateModelContext,handleSetModelsFilter:modelSelection.handleSetModelsFilter,handleToggleModel:modelSelection.handleToggleModel,planSynced:!!(status?.config?.planSynced||status?.config?.dynamicModels&&status.config.dynamicModels.length>0),planSyncedAt:status?.config?.planSyncedAt||0,defaultModelWarning:status?.config?.defaultModelWarning||"",customModels:draft?.customModels||[],modelReasoningDefaults:draft?.modelReasoningDefaults||{},visibleModels:draft?.visibleModels||[],filterModelsByPlan:draft?.filterModelsByPlan!==false,onConfigUpdate:staged.stage,onApplyVisible:handleApplyVisible,onUpdateReasoningEffort:(modelId,effort)=>{const next={...draft?.modelReasoningDefaults||{}};if(!effort)delete next[modelId];else next[modelId]=effort;staged.stage({modelReasoningDefaults:next})},t}),React.createElement(PriceSection,{modelsList,priceOverrides:draft?.priceOverrides||{},priceAutoSync:draft?.priceAutoSync!==false,onConfigUpdate:staged.stage,t})),React.createElement(StatsSection,{status,onResetStats:handleResetStats,busy,t}),React.createElement(IntegrationsSection,{draft,onStage:staged.stage,t}),React.createElement(DiagSection,{keyPresent,busy,smokeResult,handleSmoke,t}),React.createElement(AdvancedSection,{draft,onStage:staged.stage,t}),React.createElement(SaveBar,{pendingCount:staged.pendingCount,saving:staged.saving,saveState:staged.saveState,saveError:staged.saveError,invalid:staged.invalid,validationError:staged.validationError,onSave:staged.save,onDiscard:staged.discard,t}))}
|
|
472
|
+
function SettingsPage(props){const ctx=props?.ctx;const t=props?.t||makeT(en,en);const configSnapshot=useConfigFormsSnapshot(ctx);const[status,setStatus]=React.useState(null);const[draft,setDraft]=React.useState(null);const[busy,setBusy]=React.useState("");const[err,setErr]=React.useState(""),[msg,setMsg]=React.useState("");const[smokeResult,setSmokeResult]=React.useState(null);const[updateState,setUpdateState]=React.useState({status:"idle",checking:false,updating:false,currentVersion:"",latestVersion:"",updateAvailable:false,canAutoUpdate:true,latestCheckFailed:false,lastCheckedAt:null,error:"",notice:""});const[apiKeyInput,setApiKeyInput]=React.useState(""),[showKey,setShowKey]=React.useState(false);React.useEffect(()=>{ensureCss()},[]);const pendingRef=React.useRef({});const load=React.useCallback(async()=>{setErr("");const[res,statsRes]=await Promise.all([fetchWithTimeout(`${ROUTE_PREFIX}/status`,{cache:"no-store"}),fetchWithTimeout(`${ROUTE_PREFIX}/stats`,{cache:"no-store"}).catch(()=>null)]);const data=await res.json().catch(()=>({}));if(!res.ok)throw new Error(data.error||`HTTP ${res.status}`);const rawStats=statsRes&&statsRes.ok?await statsRes.json().catch(()=>null):null;const statsObj=rawStats&&typeof rawStats==="object"&&rawStats.stats?rawStats.stats:rawStats||data.statsSummary||{};setStatus({...data,statsSummary:statsObj});setDraft({...data.config||{},...pendingRef.current||{}})},[]);React.useEffect(()=>{load().catch(e=>setErr(String(e.message||e)))},[load]);const staged=useStagedConfig({status,draft,setDraft,load,t,pendingRef});const modelSelection=useModelSelection({status,draft,setDraft,load,setErr});const checkUpdate=React.useCallback(async()=>{setUpdateState(s=>({...s,checking:true,status:"checking",error:""}));try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/update`);if(!res.ok)throw new Error(`HTTP ${res.status}`);const data=await res.json().catch(()=>({}));if(data.latestCheckFailed){setUpdateState(s=>({...s,checking:false,status:"error",latestCheckFailed:true,currentVersion:data.currentVersion||s.currentVersion,error:t("update.check_failed")}));return}const isAvailable=Boolean(data.updateAvailable);setUpdateState(s=>({...s,checking:false,status:isAvailable?"available":"current",currentVersion:data.currentVersion||s.currentVersion,latestVersion:data.latestVersion||"",updateAvailable:isAvailable,latestCheckFailed:false,lastCheckedAt:Date.now(),canAutoUpdate:data.canAutoUpdate!==false,error:""}))}catch(err2){setUpdateState(s=>({...s,checking:false,status:"error",latestCheckFailed:true,error:String(err2?.message||err2||t("update.check_failed"))}))}},[t]);React.useEffect(()=>{checkUpdate()},[checkUpdate]);async function handleTriggerUpdate(){if(updateState.updating)return;setUpdateState(s=>({...s,updating:true,error:"",notice:""}));try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/update`,{method:"POST",headers:{"x-dsh-plugin-update":"1"}});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false||data.error)throw new Error(data.error||`HTTP ${res.status}`);const newVer=data.updatedVersion||updateState.latestVersion||updateState.currentVersion;setUpdateState(s=>({...s,updating:false,status:"current",updateAvailable:false,currentVersion:newVer,lastCheckedAt:Date.now(),notice:t("update.done",{version:newVer})}));setTimeout(()=>checkUpdate(),2e3)}catch(err2){setUpdateState(s=>({...s,updating:false,status:s.updateAvailable?"available":"error",error:t("update.failed",{error:String(err2?.message||err2)})}))}}async function performAction(busyKey,fn){setBusy(busyKey);setErr("");setMsg("");try{await fn()}catch(e){setErr(String(e.message||e))}finally{setBusy("")}}const{handlePinAccount,handleAddAccount,handleDeleteAccount,handleRenameAccount,handleClearKey}=createAccountActions({performAction,load,t,setMsg});async function handleSaveKey(){const keyVal=String(apiKeyInput||"").trim();if(!keyVal){setErr(t("key.empty_err"));return}await performAction("save-key",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/save-key`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:keyVal,apiKeyEnv:draft?.apiKeyEnv})});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setApiKeyInput("");setMsg(t("key.saved_msg",{status:data.validated?"OK":"Notice (check console)"}));await load()})}async function handleRefreshQuota(){await performAction("refresh-quota",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/usage`);const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setStatus(prev=>prev?{...prev,usage:data}:prev);setMsg(t("quota.refreshed_msg"))})}async function handleUpdateModelContext(payload){await performAction("update-context",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/context`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setMsg(t("models.context_updated_msg"));await load()})}async function handleSyncPlanModels(){await performAction("sync-models",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/sync`,{method:"POST"});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setMsg(t("models.synced_msg",{total:data.totalModelsCount,discovered:data.discoveredCount}));await load()})}async function handleApplyVisible(payload){await performAction("visible-models",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/models/visible`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload||{})});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false)throw new Error(data.error||`HTTP ${res.status}`);await load();setMsg(t("models.visible_applied"))})}async function handleSmoke(){setSmokeResult(null);await performAction("smoke",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/smoke`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:draft?.defaultModel})});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);setSmokeResult(data)})}const[verifyStatus,setVerifyStatus]=React.useState({state:"idle",email:"",plan:"",error:""});const verifyTimerRef=typeof React.useRef==="function"?React.useRef(null):{current:null};React.useEffect(()=>{const raw=String(apiKeyInput||"").trim();if(!raw||raw.length<10){setVerifyStatus({state:"idle",email:"",plan:"",error:""});return}if(verifyTimerRef.current)clearTimeout(verifyTimerRef.current);verifyTimerRef.current=setTimeout(async()=>{setVerifyStatus({state:"verifying",email:"",plan:"",error:""});try{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/key/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:raw})});const data=await res.json().catch(()=>({}));if(data.ok&&data.valid){setVerifyStatus({state:"valid",email:data.email,plan:data.plan,error:""})}else{setVerifyStatus({state:"invalid",email:"",plan:"",error:data.error||"Verification failed"})}}catch(err2){setVerifyStatus({state:"invalid",email:"",plan:"",error:String(err2?.message||err2)})}},500);return()=>{if(verifyTimerRef.current)clearTimeout(verifyTimerRef.current)}},[apiKeyInput]);function handleFastLogin(){window.open("https://app.cline.bot/settings/api-keys","_blank")}async function handleConfigPatch(patch){setDraft(d=>({...d||{},...patch||{}}));await performAction("patch-config",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/config`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:patch})});const data=await res.json().catch(()=>({}));if(!res.ok||data.ok===false)throw new Error(data.error||`HTTP ${res.status}`);await load()})}async function handleResetStats(){await performAction("reset-stats",async()=>{const res=await fetchWithTimeout(`${ROUTE_PREFIX}/stats/reset`,{method:"POST"});const data=await res.json().catch(()=>({}));if(!res.ok||!data.ok)throw new Error(data.error||`HTTP ${res.status}`);await load()})}if(!status||!draft){if(err){return React.createElement("div",{className:"cb-page"},React.createElement("div",{className:"cb-alert cb-alert-err"},`${t("settings.loading")}: ${err}`),React.createElement("button",{type:"button",className:"cb-btn",style:{marginTop:"12px"},onClick:()=>{setErr("");load().catch(e=>setErr(String(e.message||e)))}},t("settings.retry")))}return React.createElement("div",{className:"cb-page"},t("settings.loading"))}const healthOk=!!status.health?.ok;const keyPresent=!!status.key?.present;const isRegistered=!!status.isRegistered;const modelsList=status.availableModels||[];const disabledSet=new Set(draft.disabledModels||[]);const enabledCount=modelsList.filter(m=>!disabledSet.has(m.id)).length;const usage=status.usage;return React.createElement("div",{className:"cb-page"},React.createElement("div",{className:"cb-header"},React.createElement("div",{className:"cb-page-title"},t("header.title"),React.createElement("span",{className:`cb-badge ${healthOk?"cb-badge-ok":"cb-badge-bad"}`},healthOk?t("badge.online",{latency:status.health?.latencyMs}):t("badge.offline")),React.createElement("span",{className:`cb-badge ${keyPresent?"cb-badge-ok":"cb-badge-warn"}`},keyPresent?t("badge.key_ok",{source:status.key?.source}):t("badge.key_missing")),React.createElement("span",{className:`cb-badge ${isRegistered?"cb-badge-ok":"cb-badge-warn"}`},isRegistered?t("badge.registered",{count:enabledCount}):t("badge.not_registered"))),React.createElement("div",{className:"cb-page-sub"},t("header.sub"))),React.createElement(UpdateBanner,{updateState,handleTriggerUpdate,handleCheckUpdate:checkUpdate,t}),configSnapshot?.status==="unavailable"?React.createElement("div",{className:"cb-alert-bad",style:{marginBottom:"12px"}},t("settings.unavailable")):null,err?React.createElement("div",{className:"cb-alert-bad"},err):null,msg?React.createElement("div",{className:"cb-alert-ok"},msg):null,React.createElement(KeySection,{keyPresent,apiKeyInput,setApiKeyInput,showKey,setShowKey,busy,draft,handleSaveKey,handleFastLogin,verifyStatus,t}),React.createElement(AccountsSection,{status,busy,handlePinAccount,handleAddAccount,handleDeleteAccount,handleRenameAccount,handleClearKey,accountMode:draft?.accountMode||status?.config?.accountMode||"least_used",handleUpdateAccountMode:mode=>handleConfigPatch({accountMode:mode}),modelRoutes:draft?.modelRoutes||status?.config?.modelRoutes||{},onUpdateModelRoutes:routes=>handleConfigPatch({modelRoutes:routes}),t}),React.createElement(QuotaSection,{keyPresent,status,usage,busy,handleRefreshQuota,showSidebarQuota:Boolean(draft?.showSidebarQuota),onToggleSidebarQuota:value=>staged.stage({showSidebarQuota:value}),t}),React.createElement(CollapsibleGroup,{title:t("settings.models_group"),description:t("settings.models_group_hint"),badge:`${enabledCount}/${modelsList.length}`,divided:true,defaultOpen:false},React.createElement(ModelsSection,{modelsList,disabledSet,enabledCount,keyPresent,busy,handleSyncPlanModels,handleUpdateModelContext,handleSetModelsFilter:modelSelection.handleSetModelsFilter,handleToggleModel:modelSelection.handleToggleModel,planSynced:!!(status?.config?.planSynced||status?.config?.dynamicModels&&status.config.dynamicModels.length>0),planSyncedAt:status?.config?.planSyncedAt||0,defaultModelWarning:status?.config?.defaultModelWarning||"",customModels:draft?.customModels||[],modelReasoningDefaults:draft?.modelReasoningDefaults||{},visibleModels:draft?.visibleModels||[],filterModelsByPlan:draft?.filterModelsByPlan!==false,onConfigUpdate:staged.stage,onApplyVisible:handleApplyVisible,onUpdateReasoningEffort:(modelId,effort)=>{const next={...draft?.modelReasoningDefaults||{}};if(!effort)delete next[modelId];else next[modelId]=effort;staged.stage({modelReasoningDefaults:next})},t}),React.createElement(PriceSection,{modelsList,priceOverrides:draft?.priceOverrides||{},priceAutoSync:draft?.priceAutoSync!==false,onConfigUpdate:staged.stage,t})),React.createElement(StatsSection,{status,onResetStats:handleResetStats,busy,t}),React.createElement(IntegrationsSection,{draft,onStage:staged.stage,t}),React.createElement(DiagSection,{keyPresent,busy,smokeResult,handleSmoke,t}),React.createElement(AdvancedSection,{draft,onStage:staged.stage,t}),React.createElement(SaveBar,{pendingCount:staged.pendingCount,saving:staged.saving,saveState:staged.saveState,saveError:staged.saveError,invalid:staged.invalid,validationError:staged.validationError,onSave:staged.save,onDiscard:staged.discard,t}))}
|
|
473
473
|
|
|
474
474
|
// --- src/client/entry.js ---
|
|
475
475
|
function ensureSettingsForm(ctx){const svc=readConfigForms(ctx);if(!svc||typeof svc.get!=="function")return;try{svc.get(NS)}catch(err){console.warn("[dsh-clinebot] configForms.get failed:",err)}}function apply(ctx){ensureCss();const addLocale=(locale,dictionary)=>{try{return ctx.locale.register(NS,locale,dictionary)}catch(err){console.warn(`[dsh-clinebot] Failed to register locale dictionary for ns "${NS}", locale "${locale}":`,err);return()=>{}}};if(ctx.locale&&ctx.locale.register){if(typeof ctx.effect==="function"){ctx.effect(()=>{const undo=[addLocale("en",en),addLocale("zh",zh)];return()=>{for(const off of undo)off()}},"dsh-clinebot: dictionaries")}else{addLocale("en",en);addLocale("zh",zh)}}if(typeof ctx.effect==="function"){ctx.effect(()=>{ensureSettingsForm(ctx)},"dsh-clinebot: open the settings form for this namespace")}function registerSlotWhenReady(slotName,registerFn){if(!ctx.slots)return;if(typeof ctx.slots.inject==="function"){try{ctx.slots.inject(slotName,()=>{try{return registerFn()}catch(err){console.warn("[dsh-clinebot] Error registering slot "+slotName+":",err)}});return}catch(err){console.warn("[dsh-clinebot] Failed to inject slot "+slotName+":",err)}}if(typeof ctx.slots.register==="function"){try{registerFn()}catch(err){console.warn("[dsh-clinebot] Failed direct registration for "+slotName+":",err)}}}function selectLayoutPanel(id){try{const layout=typeof ctx.get==="function"?ctx.get("layout"):ctx.layout;if(!layout||typeof layout.selectPanel!=="function")return false;layout.selectPanel(id);return true}catch(err){console.warn("[dsh-clinebot] layout.selectPanel failed:",err);return false}}const openPanel=()=>selectLayoutPanel("clinebot-panel");const closePanel=()=>{if(selectLayoutPanel(null))return;selectLayoutPanel("conversation")};registerSlotWhenReady("settings.section",()=>ctx.slots.register({name:"settings.section",id:NS,order:30,label:()=>{try{const t=ctx.locale&&typeof ctx.locale.bind==="function"?ctx.locale.bind(NS):null;const label=t?t("nav"):"";return label&&label!=="nav"?label:"ClineBot"}catch(_){return"ClineBot"}},locale:NS,inject:()=>({ctx})},props=>React.createElement(ErrorBoundary,{t:props&&props.t},React.createElement(SettingsPage,{...props,ctx:props&&props.ctx||ctx}))));registerSlotWhenReady("sidebar.footer.action",()=>ctx.slots.register({name:"sidebar.footer.action",id:"clinebot-quota",order:20,locale:NS,inject:()=>({ctx,open:openPanel})},props=>React.createElement(ErrorBoundary,{t:props&&props.t},React.createElement(SidebarQuota,{...props,ctx:props&&props.ctx||ctx}))));registerSlotWhenReady("main",()=>ctx.slots.register({name:"main",key:"clinebot-panel",locale:NS,inject:()=>({ctx,open:openPanel,close:closePanel})},props=>React.createElement(ErrorBoundary,{t:props&&props.t},React.createElement(DashboardDialog,{...props,panel:true,ctx:props&&props.ctx||ctx,onClose:props&&props.close||closePanel}))));registerSlotWhenReady("settings.models.provider-card",()=>ctx.slots.register({name:"settings.models.provider-card",key:NS,locale:NS,inject:()=>({ctx})},props=>React.createElement(ErrorBoundary,{t:props&&props.t},React.createElement(ProviderCard,{...props,ctx:props&&props.ctx||ctx}))));registerSlotWhenReady("conversation.composer.dock",()=>ctx.slots.register({name:"conversation.composer.dock",id:"clinebot-cost",order:10,locale:NS,inject:()=>({ctx})},props=>React.createElement(ErrorBoundary,{t:props&&props.t},React.createElement(ComposerCostDock,{...props,ctx:props&&props.ctx||ctx}))))}module.exports={apply,inject:["slots","locale","configForms"]};
|