dsh-llm-verifier 0.1.13 → 0.1.16

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.
Files changed (90) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +328 -301
  3. package/cordis.patch.yml +4 -4
  4. package/lib/caller-BWwSVNZf.js.map +1 -0
  5. package/lib/client.js +176 -14
  6. package/lib/client.js.map +1 -0
  7. package/lib/core.js.map +1 -0
  8. package/lib/index.js +400 -51
  9. package/lib/index.js.map +1 -0
  10. package/lib/types/auto.d.ts.map +1 -0
  11. package/lib/types/auto.js +8 -2
  12. package/lib/types/auto.js.map +1 -0
  13. package/lib/types/cache.d.ts.map +1 -0
  14. package/lib/types/cache.js.map +1 -0
  15. package/lib/types/caller.d.ts.map +1 -0
  16. package/lib/types/caller.js.map +1 -0
  17. package/lib/types/client-i18n.d.ts +27 -0
  18. package/lib/types/client-i18n.d.ts.map +1 -0
  19. package/lib/types/client-i18n.js +18 -0
  20. package/lib/types/client-i18n.js.map +1 -0
  21. package/lib/types/client.d.ts +15 -2
  22. package/lib/types/client.d.ts.map +1 -0
  23. package/lib/types/client.js +137 -10
  24. package/lib/types/client.js.map +1 -0
  25. package/lib/types/config.d.ts +4 -0
  26. package/lib/types/config.d.ts.map +1 -0
  27. package/lib/types/config.js +5 -3
  28. package/lib/types/config.js.map +1 -0
  29. package/lib/types/core.d.ts.map +1 -0
  30. package/lib/types/core.js.map +1 -0
  31. package/lib/types/engine.d.ts.map +1 -0
  32. package/lib/types/engine.js.map +1 -0
  33. package/lib/types/images.d.ts.map +1 -0
  34. package/lib/types/images.js.map +1 -0
  35. package/lib/types/index.d.ts +2 -0
  36. package/lib/types/index.d.ts.map +1 -0
  37. package/lib/types/index.js +162 -9
  38. package/lib/types/index.js.map +1 -0
  39. package/lib/types/plan-gate.d.ts +27 -0
  40. package/lib/types/plan-gate.d.ts.map +1 -0
  41. package/lib/types/plan-gate.js +60 -0
  42. package/lib/types/plan-gate.js.map +1 -0
  43. package/lib/types/router.d.ts +28 -32
  44. package/lib/types/router.d.ts.map +1 -0
  45. package/lib/types/router.js +41 -9
  46. package/lib/types/router.js.map +1 -0
  47. package/lib/types/session.d.ts +11 -0
  48. package/lib/types/session.d.ts.map +1 -0
  49. package/lib/types/session.js +41 -11
  50. package/lib/types/session.js.map +1 -0
  51. package/lib/types/statistics.d.ts.map +1 -0
  52. package/lib/types/statistics.js.map +1 -0
  53. package/lib/types/team-gate.d.ts +20 -0
  54. package/lib/types/team-gate.d.ts.map +1 -0
  55. package/lib/types/team-gate.js +59 -0
  56. package/lib/types/team-gate.js.map +1 -0
  57. package/lib/types/top-logprobs.d.ts.map +1 -0
  58. package/lib/types/top-logprobs.js.map +1 -0
  59. package/lib/types/topic-storage.d.ts.map +1 -0
  60. package/lib/types/topic-storage.js.map +1 -0
  61. package/package.json +114 -112
  62. package/src/auto.test.ts +95 -86
  63. package/src/auto.ts +140 -133
  64. package/src/cache.ts +115 -115
  65. package/src/caller.test.ts +34 -34
  66. package/src/caller.ts +151 -151
  67. package/src/client-i18n.ts +18 -0
  68. package/src/client.tsx +383 -213
  69. package/src/config.ts +189 -183
  70. package/src/core.test.ts +71 -71
  71. package/src/core.ts +205 -205
  72. package/src/engine.test.ts +136 -136
  73. package/src/engine.ts +140 -140
  74. package/src/images.ts +33 -33
  75. package/src/index.ts +394 -247
  76. package/src/parity.test.ts +85 -85
  77. package/src/plan-gate.test.ts +56 -0
  78. package/src/plan-gate.ts +66 -0
  79. package/src/router.test.ts +107 -89
  80. package/src/router.ts +361 -309
  81. package/src/session.test.ts +71 -36
  82. package/src/session.ts +132 -103
  83. package/src/statistics.test.ts +80 -80
  84. package/src/statistics.ts +328 -328
  85. package/src/team-gate.test.ts +92 -0
  86. package/src/team-gate.ts +80 -0
  87. package/src/top-logprobs.test.ts +84 -84
  88. package/src/top-logprobs.ts +153 -153
  89. package/src/topic-storage.test.ts +31 -31
  90. package/src/topic-storage.ts +42 -42
package/src/client.tsx CHANGED
@@ -1,213 +1,383 @@
1
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
- import type { ModelProviderGroup, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
3
- import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
4
- import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
5
- import type {} from '@deepseek-ai/dsh-client-ui-slots'
6
- import { Button, IconDataOutline16, IconRefreshOutline16, Input } from '@deepseek-ai/dsh-client-ui-primitives'
7
- import { useEffect, useMemo, useState } from 'react'
8
- import {
9
- zh, en, dictionaries, toolLabels, tFormat, useLanguage, detectLanguage,
10
- compact, money, duration, dateTime, type I18nDict,
11
- } from './client-i18n.ts'
12
-
13
- export { zh, en, dictionaries, toolLabels, tFormat, useLanguage, detectLanguage, compact, money, duration, dateTime, type I18nDict }
14
-
15
- const NS = 'llm-verifier'
16
- interface Values { enabled: boolean; autoVerifyMode: 'manual'|'smart'|'strict'; autoVerifyThreshold: number; autoVerifyRepeats: number; autoVerifyMinToolCalls: number; autoVerifyMaxChars: number; autoVerifyMaxPerTask: number; autoVerifyMaxPerSession: number; autoRouteSemantic: boolean; autoRouteMinConfidence: number; autoRouteMaxCandidates: number; autoRouteMaxPerTask: number; autoRouteMaxPerSession: number; autoTrackCompletionThreshold: number; autoRouteMaxItemChars: number; autoRouteMaxInputChars: number; autoMaxModelCallsPerTask: number; autoMaxModelCallsPerSession: number; provider: string; model: string; reasoningEffort?: string; maxTokens: number; maxConcurrency: number; maxRetries: number; timeoutMs: number; cacheMaxEntries: number; estimatedInputUsdPerMillion: number; estimatedOutputUsdPerMillion: number }
17
- interface Loaded { groups: readonly ModelProviderGroup[]; settings: SettingsNamespaceView; writable: boolean; failures: string[] }
18
- interface RunStats { calls: number; attempts: number; retries: number; inputTokens: number; cachedInputTokens: number; outputTokens: number; reasoningTokens: number; cacheHits: number; cacheMisses: number; estimatedCostUsd: number; topLogprobScores: number; explicitTagScores: number }
19
- interface InvocationRecord { id: string; toolName: string; sessionId?: string; startedAt: number; finishedAt: number; durationMs: number; success: boolean; errorName?: string; errorMessage?: string; provider: string; model: string; stats: RunStats }
20
- interface DailyStatistics { date: string; invocations: number; successes: number; failures: number; calls: number; tokens: number; estimatedCostUsd: number; byTool: Record<string, number> }
21
- interface ToolStatistics { toolName: string; invocations: number; successes: number; failures: number; successRate: number; averageDurationMs: number; calls: number; tokens: number; cacheHits: number; cacheMisses: number; estimatedCostUsd: number }
22
- interface ModelStatistics { provider: string; model: string; invocations: number; calls: number; tokens: number; estimatedCostUsd: number }
23
- interface Totals extends RunStats { invocations: number; successes: number; failures: number; successRate: number; averageDurationMs: number; tokens: number; cacheHitRate: number }
24
- interface StatisticsOverview { generatedAt: number; fromMs: number; toMs: number; sessionId?: string; totals: Totals; daily: DailyStatistics[]; tools: ToolStatistics[]; models: ModelStatistics[]; recent: InvocationRecord[] }
25
- interface VerifierRemote {
26
- session: {
27
- modelCatalog(): Promise<{
28
- ok: boolean
29
- value: { groups: readonly ModelProviderGroup[]; failures: readonly { id?: string; provider?: string; name?: string; message: string }[] }
30
- error: { message: string }
31
- }>
32
- }
33
- settings: {
34
- describe(): Promise<{
35
- ok: boolean
36
- value: { writable: boolean; namespaces: readonly SettingsNamespaceView[] }
37
- error: { message: string }
38
- }>
39
- update(
40
- ns: string,
41
- patch: Record<string, unknown>,
42
- expectedRevision: number | undefined,
43
- ): Promise<{
44
- ok: boolean
45
- value: SettingsNamespaceView
46
- error: { message: string }
47
- }>
48
- }
49
- }
50
-
51
- interface VerifierSettingsProps { remote: VerifierRemote }
52
- interface StatisticsPageProps {
53
- sessionId: string
54
- rpc: {
55
- call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<{ ok: boolean; value?: unknown; error?: { message: string } }>
56
- }
57
- }
58
-
59
- const shell: React.CSSProperties = { width: '100%', maxWidth: 720, display: 'flex', flexDirection: 'column', gap: 12, padding: '0 0 32px', color: 'var(--dsw-alias-label-primary)' }
60
- const settingsHeading: React.CSSProperties = { margin: 0, fontSize: 16, fontWeight: 500, lineHeight: '24px', color: 'var(--dsw-alias-label-primary)' }
61
- const settingsIntro: React.CSSProperties = { margin: 0, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-tertiary)' }
62
- const group: React.CSSProperties = { width: '100%', display: 'flex', flexDirection: 'column', borderTop: '1px solid var(--dsw-alias-border-l2)' }
63
- const groupTitle: React.CSSProperties = { margin: 0, padding: '18px 0 8px', fontSize: 14, fontWeight: 500, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' }
64
- const row: React.CSSProperties = { minHeight: 64, display: 'grid', gridTemplateColumns: 'minmax(180px, 1fr) minmax(230px, 288px)', gap: 24, alignItems: 'center', padding: '12px 0', borderBottom: '1px solid var(--dsw-alias-border-l2)' }
65
- const selectStyle: React.CSSProperties = { boxSizing: 'border-box', width: '100%', height: 36, padding: '0 34px 0 12px', borderRadius: 8, color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-input)', border: '1px solid var(--dsw-alias-border-l2)', font: 'inherit', fontSize: 14, lineHeight: '22px', outline: 'none' }
66
- const toggleStyle = (enabled: boolean): React.CSSProperties => ({ position: 'relative', justifySelf: 'end', width: 40, height: 22, padding: 0, border: 0, borderRadius: 999, cursor: 'pointer', transition: 'background .15s ease', background: enabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-input)' })
67
- const toggleThumbStyle = (enabled: boolean): React.CSSProperties => ({ position: 'absolute', top: 3, left: enabled ? 21 : 3, width: 16, height: 16, borderRadius: '50%', background: 'var(--dsw-static-neutral-00, #fff)', boxShadow: '0 1px 3px rgba(0,0,0,.28)', transition: 'left .15s ease' })
68
- const dashboardCard: React.CSSProperties = { border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.13))', background: 'color-mix(in srgb, var(--dsw-alias-bg-module, #171925) 88%, transparent)', borderRadius: 16, boxShadow: '0 12px 36px rgba(0,0,0,.12)' }
69
- const muted: React.CSSProperties = { color: 'var(--dsw-text-secondary)', fontSize: 12 }
70
- const toolColors: Record<string, string> = { verifier_route_classify: '#d97706', verifier_compare: '#4f8cff', verifier_select: '#8b6df6', verifier_track: '#2fc5c9', verifier_current_session: '#f5a524' }
71
-
72
- function record(value: unknown): Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {} }
73
- function values(view: SettingsNamespaceView): Values { const v=record(view.value); const mode=v.autoVerifyMode==='manual'||v.autoVerifyMode==='strict'?v.autoVerifyMode:'smart'; return { enabled:v.enabled!==false,autoVerifyMode:mode,autoVerifyThreshold:Number(v.autoVerifyThreshold??.65),autoVerifyRepeats:Number(v.autoVerifyRepeats??1),autoVerifyMinToolCalls:Number(v.autoVerifyMinToolCalls??3),autoVerifyMaxChars:Number(v.autoVerifyMaxChars??80000),autoVerifyMaxPerTask:Number(v.autoVerifyMaxPerTask??2),autoVerifyMaxPerSession:Number(v.autoVerifyMaxPerSession??8),autoRouteSemantic:v.autoRouteSemantic!==false,autoRouteMinConfidence:Number(v.autoRouteMinConfidence??.9),autoRouteMaxCandidates:Number(v.autoRouteMaxCandidates??8),autoRouteMaxPerTask:Number(v.autoRouteMaxPerTask??2),autoRouteMaxPerSession:Number(v.autoRouteMaxPerSession??8),autoTrackCompletionThreshold:Number(v.autoTrackCompletionThreshold??.8),autoRouteMaxItemChars:Number(v.autoRouteMaxItemChars??20000),autoRouteMaxInputChars:Number(v.autoRouteMaxInputChars??60000),autoMaxModelCallsPerTask:Number(v.autoMaxModelCallsPerTask??48),autoMaxModelCallsPerSession:Number(v.autoMaxModelCallsPerSession??160),provider:String(v.provider??''),model:String(v.model??''),...(typeof v.reasoningEffort==='string'?{reasoningEffort:v.reasoningEffort}:{}),maxTokens:Number(v.maxTokens??32768),maxConcurrency:Number(v.maxConcurrency??8),maxRetries:Number(v.maxRetries??3),timeoutMs:Number(v.timeoutMs??300000),cacheMaxEntries:Number(v.cacheMaxEntries??10000),estimatedInputUsdPerMillion:Number(v.estimatedInputUsdPerMillion??0),estimatedOutputUsdPerMillion:Number(v.estimatedOutputUsdPerMillion??0) } }
74
- function message(error: unknown): string { return error instanceof Error ? error.message : String(error) }
75
- function Label({title,help}:{title:string;help:string}) { return <div style={{minWidth:0}}><div style={{fontSize:14,fontWeight:400,lineHeight:'22px',color:'var(--dsw-alias-label-primary)'}}>{title}</div><div style={{fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-label-tertiary)',marginTop:2}}>{help}</div></div> }
76
- function GroupTitle({children}:{children:React.ReactNode}) { return <h3 style={groupTitle}>{children}</h3> }
77
- function startOfRange(days: number): number { const date = new Date(); date.setHours(0,0,0,0); date.setDate(date.getDate() - days + 1); return date.getTime() }
78
- function endOfToday(): number { const date = new Date(); date.setHours(0,0,0,0); date.setDate(date.getDate() + 1); return date.getTime() }
79
-
80
- export function VerifierSettings({ remote }: VerifierSettingsProps) {
81
- const lang = useLanguage()
82
- const t = dictionaries[lang]
83
- const [loaded,setLoaded]=useState<Loaded|null>(null); const [draft,setDraft]=useState<Values|null>(null); const [busy,setBusy]=useState(false); const [error,setError]=useState<string|null>(null); const [saved,setSaved]=useState(false)
84
- const load=async()=>{setError(null);try{const [m,s]=await Promise.all([remote.session.modelCatalog(),remote.settings.describe()]);if(!m.ok)throw new Error(m.error.message);if(!s.ok)throw new Error(s.error.message);const view=s.value.namespaces.find((x:SettingsNamespaceView)=>x.ns===NS);if(!view)throw new Error(t['settings.nsUnregistered']);const next={groups:m.value.groups,settings:view,writable:s.value.writable,failures:m.value.failures.map((f: { id?: string; provider?: string; name?: string; message: string })=>(f.id??f.provider??f.name??'unknown')+': '+f.message)};setLoaded(next);setDraft(values(view))}catch(e){setError(message(e))}}
85
- useEffect(()=>{void load()},[])
86
- const models=useMemo(()=>loaded?.groups.find(g=>g.id===draft?.provider)?.models??[],[loaded,draft?.provider])
87
- const selected=models.find(m=>m.id===draft?.model); const efforts=selected?.reasoning?.efforts??[]
88
- const patch=<K extends keyof Values>(key:K,value:Values[K])=>setDraft(v=>v?{...v,[key]:value}:v)
89
- const save=async()=>{if(!loaded||!draft)return;setBusy(true);setSaved(false);setError(null);try{const section={...record(loaded.settings.user),...draft};if(!draft.reasoningEffort)delete section.reasoningEffort;const res=await remote.settings.update(NS,section as never,loaded.settings.revision);if(!res.ok)throw new Error(res.error.message);setLoaded(v=>v?{...v,settings:res.value}:v);setDraft(values(res.value));setSaved(true)}catch(e){setError(message(e))}finally{setBusy(false)}}
90
- if(!loaded||!draft)return <div style={shell}><h2 style={settingsHeading}>{t['settings.title']}</h2><p style={settingsIntro}>{error??t['settings.loading']}</p>{error&&<div><Button variant="outline" onClick={()=>void load()}>{t['settings.retry']}</Button></div>}</div>
91
- const numeric=(key:keyof Values,min=0)=><Input style={{width:'100%',height:36,borderRadius:8}} type="number" min={min} value={String(draft[key])} onChange={e=>patch(key,Number(e.target.value) as never)} />
92
- return <div style={shell}>
93
- <h2 style={settingsHeading}>{t['settings.title']}</h2>
94
- <p style={settingsIntro}>{t['settings.intro']}</p>
95
-
96
- <section style={group}><GroupTitle>{t['section.tools']}</GroupTitle>
97
- <div style={row}><Label title={t['field.enabled.title']} help={draft.enabled?t['field.enabled.helpOn']:t['field.enabled.helpOff']}/><button type="button" role="switch" aria-checked={draft.enabled} aria-label={t['field.enabled.title']} onClick={()=>patch('enabled',!draft.enabled)} style={toggleStyle(draft.enabled)}><span style={toggleThumbStyle(draft.enabled)}/></button></div>
98
- {!draft.enabled&&<p style={{margin:'8px 0 0',fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-warn-label)'}}>{t['field.enabled.warnDisabled']}</p>}
99
- </section>
100
-
101
- <section style={group}><GroupTitle>{t['section.autoVerify']}</GroupTitle>
102
- <div style={row}><Label title={t['field.autoVerifyMode.title']} help={t['field.autoVerifyMode.help']}/><select style={selectStyle} value={draft.autoVerifyMode} onChange={e=>patch('autoVerifyMode',e.target.value as Values['autoVerifyMode'])}><option value="manual">{t['field.autoVerifyMode.manual']}</option><option value="smart">{t['field.autoVerifyMode.smart']}</option><option value="strict">{t['field.autoVerifyMode.strict']}</option></select></div>
103
- <div style={row}><Label title={t['field.autoRouteSemantic.title']} help={t['field.autoRouteSemantic.help']}/><button type="button" role="switch" aria-checked={draft.autoRouteSemantic} aria-label={t['field.autoRouteSemantic.title']} onClick={()=>patch('autoRouteSemantic',!draft.autoRouteSemantic)} style={toggleStyle(draft.autoRouteSemantic)}><span style={toggleThumbStyle(draft.autoRouteSemantic)}/></button></div>
104
- <div style={row}><Label title={t['field.autoRouteMinConfidence.title']} help={t['field.autoRouteMinConfidence.help']}/>{numeric('autoRouteMinConfidence',0)}</div>
105
- <div style={row}><Label title={t['field.autoRouteMaxCandidates.title']} help={t['field.autoRouteMaxCandidates.help']}/>{numeric('autoRouteMaxCandidates',3)}</div>
106
- <div style={row}><Label title={t['field.autoRouteMaxPerTask.title']} help={t['field.autoRouteMaxPerTask.help']}/>{numeric('autoRouteMaxPerTask',1)}</div>
107
- <div style={row}><Label title={t['field.autoRouteMaxPerSession.title']} help={t['field.autoRouteMaxPerSession.help']}/>{numeric('autoRouteMaxPerSession',1)}</div>
108
- <div style={row}><Label title={t['field.autoTrackCompletionThreshold.title']} help={t['field.autoTrackCompletionThreshold.help']}/>{numeric('autoTrackCompletionThreshold',0)}</div>
109
- <div style={row}><Label title={t['field.autoRouteMaxItemChars.title']} help={t['field.autoRouteMaxItemChars.help']}/>{numeric('autoRouteMaxItemChars',100)}</div>
110
- <div style={row}><Label title={t['field.autoRouteMaxInputChars.title']} help={t['field.autoRouteMaxInputChars.help']}/>{numeric('autoRouteMaxInputChars',1000)}</div>
111
- <div style={row}><Label title={t['field.autoMaxModelCallsPerTask.title']} help={t['field.autoMaxModelCallsPerTask.help']}/>{numeric('autoMaxModelCallsPerTask',1)}</div>
112
- <div style={row}><Label title={t['field.autoMaxModelCallsPerSession.title']} help={t['field.autoMaxModelCallsPerSession.help']}/>{numeric('autoMaxModelCallsPerSession',1)}</div>
113
- <div style={row}><Label title={t['field.autoVerifyThreshold.title']} help={t['field.autoVerifyThreshold.help']}/>{numeric('autoVerifyThreshold',0)}</div>
114
- <div style={row}><Label title={t['field.autoVerifyRepeats.title']} help={t['field.autoVerifyRepeats.help']}/>{numeric('autoVerifyRepeats',1)}</div>
115
- <div style={row}><Label title={t['field.autoVerifyMinToolCalls.title']} help={t['field.autoVerifyMinToolCalls.help']}/>{numeric('autoVerifyMinToolCalls',1)}</div>
116
- <div style={row}><Label title={t['field.autoVerifyMaxChars.title']} help={t['field.autoVerifyMaxChars.help']}/>{numeric('autoVerifyMaxChars',1000)}</div>
117
- <div style={row}><Label title={t['field.autoVerifyMaxPerTask.title']} help={t['field.autoVerifyMaxPerTask.help']}/>{numeric('autoVerifyMaxPerTask',1)}</div>
118
- <div style={row}><Label title={t['field.autoVerifyMaxPerSession.title']} help={t['field.autoVerifyMaxPerSession.help']}/>{numeric('autoVerifyMaxPerSession',1)}</div>
119
- {draft.autoVerifyMode!=='manual'&&<p style={{margin:'8px 0 0',fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-warn-label)'}}>{t['field.autoVerify.warnNotice']}</p>}
120
- </section>
121
-
122
- <section style={group}><GroupTitle>{t['section.model']}</GroupTitle>
123
- <div style={row}><Label title={t['field.provider.title']} help={t['field.provider.help']}/><select style={selectStyle} value={draft.provider} onChange={e=>{const provider=e.target.value;const first=loaded.groups.find(g=>g.id===provider)?.models[0];setDraft({...draft,provider,...(first?{model:first.id,reasoningEffort:first.reasoning?.defaultEffort}:{})})}}>{loaded.groups.map(g=><option key={g.id} value={g.id}>{g.name} · {g.id}</option>)}</select></div>
124
- <div style={row}><Label title={t['field.model.title']} help={t['field.model.help']}/><select style={selectStyle} value={draft.model} onChange={e=>{const model=e.target.value;const found=models.find(m=>m.id===model);setDraft({...draft,model,...(found?.reasoning?.defaultEffort?{reasoningEffort:found.reasoning.defaultEffort}:{reasoningEffort:undefined})})}}>{models.map(m=><option key={m.id} value={m.id}>{m.name} · {m.id}</option>)}</select></div>
125
- <div style={row}><Label title={t['field.reasoningEffort.title']} help={t['field.reasoningEffort.help']}/><select style={selectStyle} value={draft.reasoningEffort??''} onChange={e=>patch('reasoningEffort',e.target.value||undefined)}><option value="">{t['field.reasoningEffort.default']}</option>{efforts.map(e=><option key={e.id} value={e.id}>{e.name}</option>)}</select></div>
126
- <div style={row}><Label title={t['field.maxTokens.title']} help={t['field.maxTokens.help']}/>{numeric('maxTokens',1)}</div>
127
- </section>
128
-
129
- <section style={group}><GroupTitle>{t['section.execution']}</GroupTitle>
130
- <div style={row}><Label title={t['field.maxConcurrency.title']} help={t['field.maxConcurrency.help']}/>{numeric('maxConcurrency',1)}</div>
131
- <div style={row}><Label title={t['field.maxRetries.title']} help={t['field.maxRetries.help']}/>{numeric('maxRetries',0)}</div>
132
- <div style={row}><Label title={t['field.timeoutMs.title']} help={t['field.timeoutMs.help']}/>{numeric('timeoutMs',1)}</div>
133
- <div style={row}><Label title={t['field.cacheMaxEntries.title']} help={t['field.cacheMaxEntries.help']}/>{numeric('cacheMaxEntries',1)}</div>
134
- </section>
135
-
136
- <section style={group}><GroupTitle>{t['section.cost']}</GroupTitle>
137
- <div style={row}><Label title={t['field.estimatedInputUsdPerMillion.title']} help={t['field.estimatedInputUsdPerMillion.help']}/>{numeric('estimatedInputUsdPerMillion',0)}</div>
138
- <div style={row}><Label title={t['field.estimatedOutputUsdPerMillion.title']} help={t['field.estimatedOutputUsdPerMillion.help']}/>{numeric('estimatedOutputUsdPerMillion',0)}</div>
139
- </section>
140
-
141
- {loaded.failures.length>0&&<div style={{padding:'10px 12px',borderRadius:8,background:'var(--dsw-alias-state-warn-bg)',color:'var(--dsw-alias-state-warn-label)',fontSize:12,lineHeight:'18px'}}><div style={{fontWeight:500,marginBottom:3}}>{t['settings.catalogFailures']}</div>{loaded.failures.map(x=><div key={x}>{x}</div>)}</div>}
142
- {error&&<p style={{margin:0,fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-error-primary)'}}>{error}</p>}{saved&&<p style={{margin:0,fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-success-primary)'}}>{t['settings.saved']}</p>}
143
- <div style={{display:'flex',justifyContent:'flex-end',gap:8,paddingTop:4}}><Button variant="outline" disabled={busy} onClick={()=>void load()}>{t['settings.reload']}</Button><Button variant="primary" disabled={busy||!loaded.writable} onClick={()=>void save()}>{busy?t['settings.saving']:t['settings.save']}</Button></div>
144
- </div>
145
- }
146
-
147
- function Metric({ label, value, note, accent }:{label:string;value:string;note:string;accent?:string}) {
148
- return <div style={{...dashboardCard,padding:'16px 18px',minWidth:0}}><div style={muted}>{label}</div><div style={{fontSize:25,fontWeight:750,lineHeight:1.2,margin:'7px 0 5px',color:accent}}>{value}</div><div style={{...muted,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{note}</div></div>
149
- }
150
-
151
- function TrendChart({ daily, days, lang }:{daily:DailyStatistics[];days:number;lang:'zh'|'en'}) {
152
- const t = dictionaries[lang]
153
- const rows=useMemo(()=>{const map=new Map(daily.map(row=>[row.date,row]));const values:DailyStatistics[]=[];const start=new Date(startOfRange(days));for(let i=0;i<days;i+=1){const date=new Date(start);date.setDate(start.getDate()+i);const key=[date.getFullYear(),String(date.getMonth()+1).padStart(2,'0'),String(date.getDate()).padStart(2,'0')].join('-');values.push(map.get(key)??{date:key,invocations:0,successes:0,failures:0,calls:0,tokens:0,estimatedCostUsd:0,byTool:{}})}return values},[daily,days])
154
- const width=760,height=230,pad={l:44,r:24,t:20,b:38};const innerW=width-pad.l-pad.r,innerH=height-pad.t-pad.b
155
- const max=Math.max(1,...rows.flatMap(row=>[row.invocations,row.calls]));const x=(index:number)=>pad.l+(rows.length<=1?innerW/2:index*innerW/(rows.length-1));const y=(value:number)=>pad.t+innerH-value/max*innerH
156
- const points=rows.map((row,index)=>`${x(index)},${y(row.calls)}`).join(' ');const step=rows.length>16?Math.ceil(rows.length/7):Math.max(1,Math.ceil(rows.length/7));const barWidth=Math.max(3,Math.min(18,innerW/Math.max(rows.length,1)*.55))
157
- return <div style={{width:'100%',overflowX:'auto'}}><svg viewBox={`0 0 ${width} ${height}`} style={{display:'block',width:'100%',minWidth:620,height:'auto'}} aria-label={t['chart.ariaLabel']}>
158
- {[0,.25,.5,.75,1].map(ratio=><g key={ratio}><line x1={pad.l} x2={width-pad.r} y1={pad.t+innerH*ratio} y2={pad.t+innerH*ratio} stroke="rgba(148,163,184,.16)"/><text x={pad.l-8} y={pad.t+innerH*ratio+4} textAnchor="end" fontSize="10" fill="var(--dsw-text-secondary)">{Math.round(max*(1-ratio))}</text></g>)}
159
- {rows.map((row,index)=><rect key={row.date} x={x(index)-barWidth/2} y={y(row.invocations)} width={barWidth} height={pad.t+innerH-y(row.invocations)} rx="2" fill="#4f8cff" opacity=".82"/>)}
160
- <polyline points={points} fill="none" stroke="#5ed7e8" strokeWidth="2.4" strokeLinejoin="round" strokeLinecap="round"/>
161
- {rows.map((row,index)=>index%step===0||index===rows.length-1?<text key={row.date} x={x(index)} y={height-14} textAnchor="middle" fontSize="10" fill="var(--dsw-text-secondary)">{Number(row.date.slice(5,7))}/{Number(row.date.slice(8,10))}</text>:null)}
162
- </svg><div style={{display:'flex',justifyContent:'center',gap:18,...muted}}><span><i style={{display:'inline-block',width:8,height:8,borderRadius:2,background:'#4f8cff',marginRight:6}}/>{t['chart.legendToolCalls']}</span><span><i style={{display:'inline-block',width:14,height:2,background:'#5ed7e8',marginRight:6,verticalAlign:'middle'}}/>{t['chart.legendModelCalls']}</span></div></div>
163
- }
164
-
165
- export function StatisticsPage({ sessionId, rpc }:StatisticsPageProps) {
166
- const lang = useLanguage()
167
- const t = dictionaries[lang]
168
- const labels = toolLabels[lang]
169
- const [days,setDays]=useState(30);const [sessionOnly,setSessionOnly]=useState(false);const [data,setData]=useState<StatisticsOverview|null>(null);const [loading,setLoading]=useState(true);const [error,setError]=useState<string|null>(null);const [refresh,setRefresh]=useState(0)
170
- useEffect(()=>{const controller=new AbortController();setLoading(true);setError(null);void rpc.call('/llm-verifier','statistics',{fromMs:startOfRange(days),toMs:endOfToday(),timezoneOffsetMinutes:new Date().getTimezoneOffset(),recentLimit:50,...(sessionOnly?{sessionId:String(sessionId)}:{})},controller.signal).then((result: { ok: boolean; value?: unknown; error?: { message: string } })=>{if(!result.ok)throw new Error(result.error?.message??t['stats.requestFailed']);setData(result.value as StatisticsOverview)},(cause: unknown)=>{if(!controller.signal.aborted)throw cause}).catch((cause: unknown)=>{if(!controller.signal.aborted)setError(message(cause))}).finally(()=>{if(!controller.signal.aborted)setLoading(false)});return()=>controller.abort()},[days,sessionOnly,sessionId,refresh,rpc,t])
171
- const totals=data?.totals
172
- return <main style={{height:'100%',overflow:'auto',boxSizing:'border-box',padding:'22px clamp(16px, 3vw, 38px) 48px',color:'var(--dsw-text-primary)',background:'radial-gradient(circle at 10% 0%, rgba(115,77,255,.09), transparent 32%), radial-gradient(circle at 100% 8%, rgba(47,197,201,.07), transparent 28%)'}}>
173
- <div style={{maxWidth:1180,margin:'0 auto',display:'flex',flexDirection:'column',gap:16}}>
174
- <header style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:16,flexWrap:'wrap'}}><div><div style={{display:'flex',alignItems:'center',gap:10}}><span style={{display:'grid',placeItems:'center',width:34,height:34,borderRadius:10,background:'rgba(79,140,255,.14)',color:'#6da0ff'}}><IconDataOutline16 size={18}/></span><h2 style={{margin:0,fontSize:23}}>{t['stats.pageTitle']}</h2></div><p style={{margin:'7px 0 0 44px',...muted}}>{tFormat(t['stats.updatedAt'], { time: data ? dateTime(data.generatedAt, lang) : '--' })}</p></div>
175
- <div style={{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}><div style={{display:'flex',padding:3,borderRadius:10,background:'var(--dsw-surface-sunken)',border:'1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))'}}>{[7,30,90].map(value=><button key={value} onClick={()=>setDays(value)} style={{border:0,borderRadius:7,padding:'6px 10px',cursor:'pointer',color:days===value?'#fff':'var(--dsw-text-secondary)',background:days===value?'#3f68d8':'transparent'}}>{tFormat(t['stats.daysUnit'], { days: value })}</button>)}</div><button onClick={()=>setSessionOnly(value=>!value)} style={{border:'1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.15))',borderRadius:9,padding:'7px 11px',cursor:'pointer',color:'var(--dsw-text-primary)',background:sessionOnly?'rgba(79,140,255,.18)':'var(--dsw-surface-sunken)'}}>{sessionOnly?t['stats.currentSession']:t['stats.allSessions']}</button><button title={t['stats.refresh']} onClick={()=>setRefresh(value=>value+1)} style={{display:'grid',placeItems:'center',width:34,height:34,borderRadius:9,border:'1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.15))',color:'var(--dsw-text-primary)',background:'var(--dsw-surface-sunken)',cursor:'pointer'}}><IconRefreshOutline16 size={16}/></button></div>
176
- </header>
177
- {error&&<div style={{...dashboardCard,padding:18,borderColor:'var(--dsw-danger, #e85858)',color:'var(--dsw-danger, #e85858)'}}>{error}<div style={{...muted,marginTop:6}}>{t['stats.hostRestartHint']}</div></div>}
178
- {loading&&!data?<div style={{...dashboardCard,padding:32,textAlign:'center',...muted}}>{t['stats.loading']}</div>:<>
179
- <section style={{...dashboardCard,padding:'22px 24px',display:'grid',gridTemplateColumns:'minmax(220px,1.4fr) minmax(240px,1fr)',gap:24,alignItems:'center'}}><div><div style={muted}>{tFormat(t['stats.costSummary'], { days })}</div><div style={{fontSize:38,fontWeight:780,letterSpacing:'-.03em',margin:'5px 0'}}>{money(totals?.estimatedCostUsd??0)}</div><div style={muted}>{tFormat(t['stats.callsSummary'], { invocations: compact(totals?.invocations??0, lang), calls: compact(totals?.calls??0, lang) })}</div></div><div style={{display:'grid',gridTemplateColumns:'1fr auto',gap:'9px 16px',fontSize:13}}><span style={muted}>{t['stats.successCalls']}</span><strong>{compact(totals?.successes??0, lang)} <small style={{color:'#77d49b'}}>▲ {((totals?.successRate??0)*100).toFixed(1)}%</small></strong><span style={muted}>{t['stats.failedCalls']}</span><strong>{compact(totals?.failures??0, lang)}</strong><span style={muted}>{t['stats.avgDuration']}</span><strong>{duration(totals?.averageDurationMs??0)}</strong></div></section>
180
- <section style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(180px,1fr))',gap:12}}>
181
- <Metric label={t['metric.cacheHitRate']} value={((totals?.cacheHitRate??0)*100).toFixed(1)+'%'} note={tFormat(t['metric.cacheHitNote'], { hits: compact(totals?.cacheHits??0, lang), total: compact((totals?.cacheHits??0)+(totals?.cacheMisses??0), lang) })} accent="#b7dd64"/>
182
- <Metric label={t['metric.tokens']} value={compact(totals?.tokens??0, lang)} note={tFormat(t['metric.tokensNote'], { input: compact((totals?.inputTokens??0)+(totals?.cachedInputTokens??0), lang), output: compact(totals?.outputTokens??0, lang) })}/>
183
- <Metric label={t['metric.avgModelCalls']} value={(totals?.invocations??0)>0?((totals?.calls??0)/(totals?.invocations??1)).toFixed(1):'0'} note={tFormat(t['metric.avgModelCallsNote'], { attempts: compact(totals?.attempts??0, lang), retries: compact(totals?.retries??0, lang) })}/>
184
- <Metric label={t['metric.scoringMode']} value={compact(totals?.topLogprobScores??0, lang)} note={tFormat(t['metric.scoringModeNote'], { explicit: compact(totals?.explicitTagScores??0, lang) })}/>
185
- </section>
186
- <section style={{...dashboardCard,padding:'18px 20px 16px'}}><div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}><strong>{t['chart.title']}</strong><span style={muted}>{sessionOnly?t['stats.currentSession']:t['stats.allSessions']}</span></div><TrendChart daily={data?.daily??[]} days={days} lang={lang}/></section>
187
- <section style={{...dashboardCard,padding:'18px 18px 8px',overflow:'hidden'}}><div style={{display:'flex',justifyContent:'space-between',margin:'0 2px 12px'}}><strong>{t['table.title']}</strong><span style={muted}>{tFormat(t['table.toolCount'], { count: data?.tools.length??0 })}</span></div><div style={{overflowX:'auto'}}><table style={{width:'100%',borderCollapse:'collapse',fontSize:13,minWidth:760}}><thead><tr style={{textAlign:'left',color:'var(--dsw-text-secondary)',background:'var(--dsw-surface-sunken)'}}>{[t['table.colTool'],t['table.colInvocations'],t['table.colSuccessRate'],t['table.colAvgDuration'],t['table.colModelCalls'],t['table.colTokens'],t['table.colCacheHits'],t['table.colEstimatedCost']].map(value=><th key={value} style={{padding:'10px 12px',fontWeight:500}}>{value}</th>)}</tr></thead><tbody>{(data?.tools??[]).map(tool=><tr key={tool.toolName} style={{borderTop:'1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.1))'}}><td style={{padding:'13px 12px'}}><span style={{display:'inline-block',width:8,height:8,borderRadius:'50%',background:toolColors[tool.toolName]??'#8691a8',marginRight:8}}/><strong>{labels[tool.toolName]??tool.toolName}</strong><div style={{...muted,margin:'3px 0 0 16px'}}>{tool.toolName}</div></td><td style={{padding:'13px 12px'}}>{compact(tool.invocations, lang)}</td><td style={{padding:'13px 12px',color:tool.successRate>=.9?'#77d49b':tool.successRate>=.7?'#e3bd63':'#ed7777'}}>{(tool.successRate*100).toFixed(1)}%</td><td style={{padding:'13px 12px'}}>{duration(tool.averageDurationMs)}</td><td style={{padding:'13px 12px'}}>{compact(tool.calls, lang)}</td><td style={{padding:'13px 12px'}}>{compact(tool.tokens, lang)}</td><td style={{padding:'13px 12px'}}>{tool.cacheHits}/{tool.cacheHits+tool.cacheMisses}</td><td style={{padding:'13px 12px'}}>{money(tool.estimatedCostUsd)}</td></tr>)}{(data?.tools.length??0)===0&&<tr><td colSpan={8} style={{padding:28,textAlign:'center',...muted}}>{t['table.empty']}</td></tr>}</tbody></table></div></section>
188
- <section style={{display:'grid',gridTemplateColumns:'minmax(0,1fr) minmax(290px,.45fr)',gap:16,alignItems:'start'}}><div style={{...dashboardCard,padding:'18px 18px 8px',overflow:'hidden'}}><div style={{display:'flex',justifyContent:'space-between',margin:'0 2px 12px'}}><strong>{t['recent.title']}</strong><span style={muted}>{t['recent.maxCount']}</span></div><div style={{maxHeight:360,overflow:'auto'}}>{(data?.recent??[]).map(item=><div key={item.id} style={{display:'grid',gridTemplateColumns:'minmax(170px,1fr) auto',gap:12,padding:'11px 8px',borderTop:'1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.1))'}}><div><div style={{display:'flex',alignItems:'center',gap:8}}><span style={{width:7,height:7,borderRadius:'50%',background:item.success?'#59c985':'#e76565'}}/><strong style={{fontSize:13}}>{labels[item.toolName]??item.toolName}</strong><span style={muted}>{item.provider}/{item.model}</span></div>{!item.success&&<div title={item.errorMessage} style={{margin:'5px 0 0 15px',fontSize:11,color:'#e76565',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{item.errorMessage??item.errorName}</div>}</div><div style={{textAlign:'right'}}><div style={{fontSize:12}}>{duration(item.durationMs)}</div><div style={{...muted,marginTop:3}}>{dateTime(item.startedAt, lang)}</div></div></div>)}{(data?.recent.length??0)===0&&<div style={{padding:24,textAlign:'center',...muted}}>{t['recent.empty']}</div>}</div></div>
189
- <div style={{...dashboardCard,padding:'18px'}}><strong>{t['models.title']}</strong><div style={{marginTop:10,display:'flex',flexDirection:'column',gap:10}}>{(data?.models??[]).map(model=><div key={model.provider+'\0'+model.model} style={{padding:'11px 12px',borderRadius:10,background:'var(--dsw-surface-sunken)'}}><div style={{fontWeight:650,fontSize:13,overflow:'hidden',textOverflow:'ellipsis'}}>{model.model}</div><div style={{...muted,marginTop:3}}>{model.provider}</div><div style={{display:'flex',justifyContent:'space-between',marginTop:9,fontSize:12}}><span>{tFormat(t['models.calls'], { calls: compact(model.calls, lang) })}</span><span>{tFormat(t['models.tokens'], { tokens: compact(model.tokens, lang) })}</span><strong>{money(model.estimatedCostUsd)}</strong></div></div>)}{(data?.models.length??0)===0&&<div style={muted}>{t['models.empty']}</div>}</div></div></section>
190
- </>}
191
- </div>
192
- </main>
193
- }
194
-
195
- export const inject = ['slots', 'connection', 'remote', 'remote.session', 'remote.settings']
196
- export function apply(ctx: ClientContext): void {
197
- const connection = ctx.get('connection') as any
198
- const remote = ctx.remote
199
- ctx.slots.inject('settings.section', () => ctx.slots.register({
200
- name: 'settings.section',
201
- id: 'llm-verifier',
202
- order: 35,
203
- label: 'LLM Verifier',
204
- inject: () => ({ remote }),
205
- }, VerifierSettings as never))
206
- ctx.slots.inject('conversation.view', () => ctx.slots.register({
207
- name: 'conversation.view',
208
- id: 'llm-verifier-statistics',
209
- order: 30,
210
- label: detectLanguage() === 'zh' ? zh['slot.statistics'] : en['slot.statistics'],
211
- inject: () => ({ rpc: connection.rpc }),
212
- }, StatisticsPage as never))
213
- }
1
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
+ import type { ModelProviderGroup, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
3
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
4
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
5
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
6
+ import { Button, IconDataOutline16, IconRefreshOutline16, Input } from '@deepseek-ai/dsh-client-ui-primitives'
7
+ import { useEffect, useMemo, useState } from 'react'
8
+ import {
9
+ zh, en, dictionaries, toolLabels, tFormat, useLanguage, detectLanguage,
10
+ compact, money, duration, dateTime, type I18nDict,
11
+ } from './client-i18n.ts'
12
+
13
+ export { zh, en, dictionaries, toolLabels, tFormat, useLanguage, detectLanguage, compact, money, duration, dateTime, type I18nDict }
14
+
15
+ const NS = 'llm-verifier'
16
+ interface Values { enabled: boolean; autoVerifyMode: 'manual'|'smart'|'strict'; autoVerifyThreshold: number; autoVerifyRepeats: number; autoVerifyMinToolCalls: number; autoVerifyMaxChars: number; autoVerifyMaxPerTask: number; autoVerifyMaxPerSession: number; autoRouteSemantic: boolean; autoRouteMinConfidence: number; autoRouteMaxCandidates: number; autoRouteMaxPerTask: number; autoRouteMaxPerSession: number; autoTrackCompletionThreshold: number; autoRouteMaxItemChars: number; autoRouteMaxInputChars: number; autoMaxModelCallsPerTask: number; autoMaxModelCallsPerSession: number; autoVerifyTeamTasks: boolean; autoVerifyPlanMode: boolean; provider: string; model: string; reasoningEffort?: string; maxTokens: number; maxConcurrency: number; maxRetries: number; timeoutMs: number; cacheMaxEntries: number; estimatedInputUsdPerMillion: number; estimatedOutputUsdPerMillion: number }
17
+ interface Loaded { groups: readonly ModelProviderGroup[]; settings: SettingsNamespaceView; writable: boolean; failures: string[] }
18
+ interface RunStats { calls: number; attempts: number; retries: number; inputTokens: number; cachedInputTokens: number; outputTokens: number; reasoningTokens: number; cacheHits: number; cacheMisses: number; estimatedCostUsd: number; topLogprobScores: number; explicitTagScores: number }
19
+ interface InvocationRecord { id: string; toolName: string; sessionId?: string; startedAt: number; finishedAt: number; durationMs: number; success: boolean; errorName?: string; errorMessage?: string; provider: string; model: string; stats: RunStats }
20
+ interface DailyStatistics { date: string; invocations: number; successes: number; failures: number; calls: number; tokens: number; estimatedCostUsd: number; byTool: Record<string, number> }
21
+ interface ToolStatistics { toolName: string; invocations: number; successes: number; failures: number; successRate: number; averageDurationMs: number; calls: number; tokens: number; cacheHits: number; cacheMisses: number; estimatedCostUsd: number }
22
+ interface ModelStatistics { provider: string; model: string; invocations: number; calls: number; tokens: number; estimatedCostUsd: number }
23
+ interface Totals extends RunStats { invocations: number; successes: number; failures: number; successRate: number; averageDurationMs: number; tokens: number; cacheHitRate: number }
24
+ interface StatisticsOverview { generatedAt: number; fromMs: number; toMs: number; sessionId?: string; totals: Totals; daily: DailyStatistics[]; tools: ToolStatistics[]; models: ModelStatistics[]; recent: InvocationRecord[] }
25
+ interface VerifierRemote {
26
+ session: {
27
+ modelCatalog(): Promise<{
28
+ ok: boolean
29
+ value: { groups: readonly ModelProviderGroup[]; failures: readonly { id?: string; provider?: string; name?: string; message: string }[] }
30
+ error: { message: string }
31
+ }>
32
+ }
33
+ settings: {
34
+ describe(): Promise<{
35
+ ok: boolean
36
+ value: { writable: boolean; namespaces: readonly SettingsNamespaceView[] }
37
+ error: { message: string }
38
+ }>
39
+ update(
40
+ ns: string,
41
+ patch: Record<string, unknown>,
42
+ expectedRevision: number | undefined,
43
+ ): Promise<{
44
+ ok: boolean
45
+ value: SettingsNamespaceView
46
+ error: { message: string }
47
+ }>
48
+ }
49
+ }
50
+
51
+ interface VerifierSettingsProps { remote: VerifierRemote }
52
+ interface StatisticsPageProps {
53
+ sessionId?: string
54
+ isGlobal?: boolean
55
+ rpc: {
56
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<{ ok: boolean; value?: unknown; error?: { message: string } }>
57
+ }
58
+ }
59
+
60
+ const shell: React.CSSProperties = { width: '100%', maxWidth: 720, display: 'flex', flexDirection: 'column', gap: 12, padding: '0 0 32px', color: 'var(--dsw-alias-label-primary)' }
61
+ const settingsHeading: React.CSSProperties = { margin: 0, fontSize: 16, fontWeight: 500, lineHeight: '24px', color: 'var(--dsw-alias-label-primary)' }
62
+ const settingsIntro: React.CSSProperties = { margin: 0, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-tertiary)' }
63
+ const group: React.CSSProperties = { width: '100%', display: 'flex', flexDirection: 'column', borderTop: '1px solid var(--dsw-alias-border-l2)' }
64
+ const groupTitle: React.CSSProperties = { margin: 0, padding: '18px 0 8px', fontSize: 14, fontWeight: 500, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' }
65
+ const row: React.CSSProperties = { minHeight: 64, display: 'grid', gridTemplateColumns: 'minmax(180px, 1fr) minmax(230px, 288px)', gap: 24, alignItems: 'center', padding: '12px 0', borderBottom: '1px solid var(--dsw-alias-border-l2)' }
66
+ const selectStyle: React.CSSProperties = { boxSizing: 'border-box', width: '100%', height: 36, padding: '0 34px 0 12px', borderRadius: 8, color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-input)', border: '1px solid var(--dsw-alias-border-l2)', font: 'inherit', fontSize: 14, lineHeight: '22px', outline: 'none' }
67
+ const toggleStyle = (enabled: boolean): React.CSSProperties => ({ position: 'relative', justifySelf: 'end', width: 40, height: 22, padding: 0, border: 0, borderRadius: 999, cursor: 'pointer', transition: 'background .15s ease', background: enabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-input)' })
68
+ const toggleThumbStyle = (enabled: boolean): React.CSSProperties => ({ position: 'absolute', top: 3, left: enabled ? 21 : 3, width: 16, height: 16, borderRadius: '50%', background: 'var(--dsw-static-neutral-00, #fff)', boxShadow: '0 1px 3px rgba(0,0,0,.28)', transition: 'left .15s ease' })
69
+ const dashboardCard: React.CSSProperties = { border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.13))', background: 'color-mix(in srgb, var(--dsw-alias-bg-module, #171925) 88%, transparent)', borderRadius: 16, boxShadow: '0 12px 36px rgba(0,0,0,.12)' }
70
+ const muted: React.CSSProperties = { color: 'var(--dsw-text-secondary)', fontSize: 12 }
71
+ const toolColors: Record<string, string> = { verifier_route_classify: '#d97706', verifier_compare: '#4f8cff', verifier_select: '#8b6df6', verifier_track: '#2fc5c9', verifier_current_session: '#f5a524' }
72
+
73
+ function record(value: unknown): Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {} }
74
+ function values(view: SettingsNamespaceView): Values { const v=record(view.value); const mode=v.autoVerifyMode==='manual'||v.autoVerifyMode==='strict'?v.autoVerifyMode:'smart'; return { enabled:v.enabled!==false,autoVerifyMode:mode,autoVerifyThreshold:Number(v.autoVerifyThreshold??.65),autoVerifyRepeats:Number(v.autoVerifyRepeats??1),autoVerifyMinToolCalls:Number(v.autoVerifyMinToolCalls??3),autoVerifyMaxChars:Number(v.autoVerifyMaxChars??80000),autoVerifyMaxPerTask:Number(v.autoVerifyMaxPerTask??2),autoVerifyMaxPerSession:Number(v.autoVerifyMaxPerSession??8),autoRouteSemantic:v.autoRouteSemantic!==false,autoRouteMinConfidence:Number(v.autoRouteMinConfidence??.9),autoRouteMaxCandidates:Number(v.autoRouteMaxCandidates??8),autoRouteMaxPerTask:Number(v.autoRouteMaxPerTask??2),autoRouteMaxPerSession:Number(v.autoRouteMaxPerSession??8),autoTrackCompletionThreshold:Number(v.autoTrackCompletionThreshold??.8),autoRouteMaxItemChars:Number(v.autoRouteMaxItemChars??20000),autoRouteMaxInputChars:Number(v.autoRouteMaxInputChars??60000),autoMaxModelCallsPerTask:Number(v.autoMaxModelCallsPerTask??48),autoMaxModelCallsPerSession:Number(v.autoMaxModelCallsPerSession??160),autoVerifyTeamTasks:v.autoVerifyTeamTasks!==false,autoVerifyPlanMode:v.autoVerifyPlanMode!==false,provider:String(v.provider??''),model:String(v.model??''),...(typeof v.reasoningEffort==='string'?{reasoningEffort:v.reasoningEffort}:{}),maxTokens:Number(v.maxTokens??32768),maxConcurrency:Number(v.maxConcurrency??8),maxRetries:Number(v.maxRetries??3),timeoutMs:Number(v.timeoutMs??300000),cacheMaxEntries:Number(v.cacheMaxEntries??10000),estimatedInputUsdPerMillion:Number(v.estimatedInputUsdPerMillion??0),estimatedOutputUsdPerMillion:Number(v.estimatedOutputUsdPerMillion??0) } }
75
+ function message(error: unknown): string { return error instanceof Error ? error.message : String(error) }
76
+ function Label({title,help}:{title:string;help:string}) { return <div style={{minWidth:0}}><div style={{fontSize:14,fontWeight:400,lineHeight:'22px',color:'var(--dsw-alias-label-primary)'}}>{title}</div><div style={{fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-label-tertiary)',marginTop:2}}>{help}</div></div> }
77
+ function GroupTitle({children}:{children:React.ReactNode}) { return <h3 style={groupTitle}>{children}</h3> }
78
+ function startOfRange(days: number): number { const date = new Date(); date.setHours(0,0,0,0); date.setDate(date.getDate() - days + 1); return date.getTime() }
79
+ function endOfToday(): number { const date = new Date(); date.setHours(0,0,0,0); date.setDate(date.getDate() + 1); return date.getTime() }
80
+
81
+ export function VerifierSettings({ remote }: VerifierSettingsProps) {
82
+ const lang = useLanguage()
83
+ const t = dictionaries[lang]
84
+ const [loaded,setLoaded]=useState<Loaded|null>(null); const [draft,setDraft]=useState<Values|null>(null); const [busy,setBusy]=useState(false); const [error,setError]=useState<string|null>(null); const [saved,setSaved]=useState(false)
85
+ const load=async()=>{setError(null);try{const [m,s]=await Promise.all([remote.session.modelCatalog(),remote.settings.describe()]);if(!m.ok)throw new Error(m.error.message);if(!s.ok)throw new Error(s.error.message);const view=s.value.namespaces.find((x:SettingsNamespaceView)=>x.ns===NS);if(!view)throw new Error(t['settings.nsUnregistered']);const next={groups:m.value.groups,settings:view,writable:s.value.writable,failures:m.value.failures.map((f: { id?: string; provider?: string; name?: string; message: string })=>(f.id??f.provider??f.name??'unknown')+': '+f.message)};setLoaded(next);setDraft(values(view))}catch(e){setError(message(e))}}
86
+ useEffect(()=>{void load()},[])
87
+ const models=useMemo(()=>loaded?.groups.find(g=>g.id===draft?.provider)?.models??[],[loaded,draft?.provider])
88
+ const selected=models.find(m=>m.id===draft?.model); const efforts=selected?.reasoning?.efforts??[]
89
+ const patch=<K extends keyof Values>(key:K,value:Values[K])=>setDraft(v=>v?{...v,[key]:value}:v)
90
+ const save=async()=>{if(!loaded||!draft)return;setBusy(true);setSaved(false);setError(null);try{const section={...record(loaded.settings.user),...draft};if(!draft.reasoningEffort)delete section.reasoningEffort;const res=await remote.settings.update(NS,section as never,loaded.settings.revision);if(!res.ok)throw new Error(res.error.message);setLoaded(v=>v?{...v,settings:res.value}:v);setDraft(values(res.value));setSaved(true)}catch(e){setError(message(e))}finally{setBusy(false)}}
91
+ if(!loaded||!draft)return <div style={shell}><h2 style={settingsHeading}>{t['settings.title']}</h2><p style={settingsIntro}>{error??t['settings.loading']}</p>{error&&<div><Button variant="outline" onClick={()=>void load()}>{t['settings.retry']}</Button></div>}</div>
92
+ const numeric=(key:keyof Values,min=0)=><Input style={{width:'100%',height:36,borderRadius:8}} type="number" min={min} value={String(draft[key])} onChange={e=>patch(key,Number(e.target.value) as never)} />
93
+ return <div style={shell}>
94
+ <h2 style={settingsHeading}>{t['settings.title']}</h2>
95
+ <p style={settingsIntro}>{t['settings.intro']}</p>
96
+
97
+ <section style={group}><GroupTitle>{t['section.tools']}</GroupTitle>
98
+ <div style={row}><Label title={t['field.enabled.title']} help={draft.enabled?t['field.enabled.helpOn']:t['field.enabled.helpOff']}/><button type="button" role="switch" aria-checked={draft.enabled} aria-label={t['field.enabled.title']} onClick={()=>patch('enabled',!draft.enabled)} style={toggleStyle(draft.enabled)}><span style={toggleThumbStyle(draft.enabled)}/></button></div>
99
+ {!draft.enabled&&<p style={{margin:'8px 0 0',fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-warn-label)'}}>{t['field.enabled.warnDisabled']}</p>}
100
+ </section>
101
+
102
+ <section style={group}><GroupTitle>{t['section.autoVerify']}</GroupTitle>
103
+ <div style={row}><Label title={t['field.autoVerifyMode.title']} help={t['field.autoVerifyMode.help']}/><select style={selectStyle} value={draft.autoVerifyMode} onChange={e=>patch('autoVerifyMode',e.target.value as Values['autoVerifyMode'])}><option value="manual">{t['field.autoVerifyMode.manual']}</option><option value="smart">{t['field.autoVerifyMode.smart']}</option><option value="strict">{t['field.autoVerifyMode.strict']}</option></select></div>
104
+ <div style={row}><Label title={t['field.autoRouteSemantic.title']} help={t['field.autoRouteSemantic.help']}/><button type="button" role="switch" aria-checked={draft.autoRouteSemantic} aria-label={t['field.autoRouteSemantic.title']} onClick={()=>patch('autoRouteSemantic',!draft.autoRouteSemantic)} style={toggleStyle(draft.autoRouteSemantic)}><span style={toggleThumbStyle(draft.autoRouteSemantic)}/></button></div>
105
+ <div style={row}><Label title={t['field.autoVerifyTeamTasks.title']} help={t['field.autoVerifyTeamTasks.help']}/><button type="button" role="switch" aria-checked={draft.autoVerifyTeamTasks} aria-label={t['field.autoVerifyTeamTasks.title']} onClick={()=>patch('autoVerifyTeamTasks',!(draft.autoVerifyTeamTasks))} style={toggleStyle(draft.autoVerifyTeamTasks)}><span style={toggleThumbStyle(draft.autoVerifyTeamTasks)}/></button></div>
106
+ <div style={row}><Label title={t['field.autoVerifyPlanMode.title']} help={t['field.autoVerifyPlanMode.help']}/><button type="button" role="switch" aria-checked={draft.autoVerifyPlanMode} aria-label={t['field.autoVerifyPlanMode.title']} onClick={()=>patch('autoVerifyPlanMode',!(draft.autoVerifyPlanMode))} style={toggleStyle(draft.autoVerifyPlanMode)}><span style={toggleThumbStyle(draft.autoVerifyPlanMode)}/></button></div>
107
+ <div style={row}><Label title={t['field.autoRouteMinConfidence.title']} help={t['field.autoRouteMinConfidence.help']}/>{numeric('autoRouteMinConfidence',0)}</div>
108
+ <div style={row}><Label title={t['field.autoRouteMaxCandidates.title']} help={t['field.autoRouteMaxCandidates.help']}/>{numeric('autoRouteMaxCandidates',3)}</div>
109
+ <div style={row}><Label title={t['field.autoRouteMaxPerTask.title']} help={t['field.autoRouteMaxPerTask.help']}/>{numeric('autoRouteMaxPerTask',1)}</div>
110
+ <div style={row}><Label title={t['field.autoRouteMaxPerSession.title']} help={t['field.autoRouteMaxPerSession.help']}/>{numeric('autoRouteMaxPerSession',1)}</div>
111
+ <div style={row}><Label title={t['field.autoTrackCompletionThreshold.title']} help={t['field.autoTrackCompletionThreshold.help']}/>{numeric('autoTrackCompletionThreshold',0)}</div>
112
+ <div style={row}><Label title={t['field.autoRouteMaxItemChars.title']} help={t['field.autoRouteMaxItemChars.help']}/>{numeric('autoRouteMaxItemChars',100)}</div>
113
+ <div style={row}><Label title={t['field.autoRouteMaxInputChars.title']} help={t['field.autoRouteMaxInputChars.help']}/>{numeric('autoRouteMaxInputChars',1000)}</div>
114
+ <div style={row}><Label title={t['field.autoMaxModelCallsPerTask.title']} help={t['field.autoMaxModelCallsPerTask.help']}/>{numeric('autoMaxModelCallsPerTask',1)}</div>
115
+ <div style={row}><Label title={t['field.autoMaxModelCallsPerSession.title']} help={t['field.autoMaxModelCallsPerSession.help']}/>{numeric('autoMaxModelCallsPerSession',1)}</div>
116
+ <div style={row}><Label title={t['field.autoVerifyThreshold.title']} help={t['field.autoVerifyThreshold.help']}/>{numeric('autoVerifyThreshold',0)}</div>
117
+ <div style={row}><Label title={t['field.autoVerifyRepeats.title']} help={t['field.autoVerifyRepeats.help']}/>{numeric('autoVerifyRepeats',1)}</div>
118
+ <div style={row}><Label title={t['field.autoVerifyMinToolCalls.title']} help={t['field.autoVerifyMinToolCalls.help']}/>{numeric('autoVerifyMinToolCalls',1)}</div>
119
+ <div style={row}><Label title={t['field.autoVerifyMaxChars.title']} help={t['field.autoVerifyMaxChars.help']}/>{numeric('autoVerifyMaxChars',1000)}</div>
120
+ <div style={row}><Label title={t['field.autoVerifyMaxPerTask.title']} help={t['field.autoVerifyMaxPerTask.help']}/>{numeric('autoVerifyMaxPerTask',1)}</div>
121
+ <div style={row}><Label title={t['field.autoVerifyMaxPerSession.title']} help={t['field.autoVerifyMaxPerSession.help']}/>{numeric('autoVerifyMaxPerSession',1)}</div>
122
+ {draft.autoVerifyMode!=='manual'&&<p style={{margin:'8px 0 0',fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-warn-label)'}}>{t['field.autoVerify.warnNotice']}</p>}
123
+ </section>
124
+
125
+ <section style={group}><GroupTitle>{t['section.model']}</GroupTitle>
126
+ <div style={row}><Label title={t['field.provider.title']} help={t['field.provider.help']}/><select style={selectStyle} value={draft.provider} onChange={e=>{const provider=e.target.value;const first=loaded.groups.find(g=>g.id===provider)?.models[0];setDraft({...draft,provider,...(first?{model:first.id,reasoningEffort:first.reasoning?.defaultEffort}:{})})}}>{loaded.groups.map(g=><option key={g.id} value={g.id}>{g.name} · {g.id}</option>)}</select></div>
127
+ <div style={row}><Label title={t['field.model.title']} help={t['field.model.help']}/><select style={selectStyle} value={draft.model} onChange={e=>{const model=e.target.value;const found=models.find(m=>m.id===model);setDraft({...draft,model,...(found?.reasoning?.defaultEffort?{reasoningEffort:found.reasoning.defaultEffort}:{reasoningEffort:undefined})})}}>{models.map(m=><option key={m.id} value={m.id}>{m.name} · {m.id}</option>)}</select></div>
128
+ <div style={row}><Label title={t['field.reasoningEffort.title']} help={t['field.reasoningEffort.help']}/><select style={selectStyle} value={draft.reasoningEffort??''} onChange={e=>patch('reasoningEffort',e.target.value||undefined)}><option value="">{t['field.reasoningEffort.default']}</option>{efforts.map(e=><option key={e.id} value={e.id}>{e.name}</option>)}</select></div>
129
+ <div style={row}><Label title={t['field.maxTokens.title']} help={t['field.maxTokens.help']}/>{numeric('maxTokens',1)}</div>
130
+ </section>
131
+
132
+ <section style={group}><GroupTitle>{t['section.execution']}</GroupTitle>
133
+ <div style={row}><Label title={t['field.maxConcurrency.title']} help={t['field.maxConcurrency.help']}/>{numeric('maxConcurrency',1)}</div>
134
+ <div style={row}><Label title={t['field.maxRetries.title']} help={t['field.maxRetries.help']}/>{numeric('maxRetries',0)}</div>
135
+ <div style={row}><Label title={t['field.timeoutMs.title']} help={t['field.timeoutMs.help']}/>{numeric('timeoutMs',1)}</div>
136
+ <div style={row}><Label title={t['field.cacheMaxEntries.title']} help={t['field.cacheMaxEntries.help']}/>{numeric('cacheMaxEntries',1)}</div>
137
+ </section>
138
+
139
+ <section style={group}><GroupTitle>{t['section.cost']}</GroupTitle>
140
+ <div style={row}><Label title={t['field.estimatedInputUsdPerMillion.title']} help={t['field.estimatedInputUsdPerMillion.help']}/>{numeric('estimatedInputUsdPerMillion',0)}</div>
141
+ <div style={row}><Label title={t['field.estimatedOutputUsdPerMillion.title']} help={t['field.estimatedOutputUsdPerMillion.help']}/>{numeric('estimatedOutputUsdPerMillion',0)}</div>
142
+ </section>
143
+
144
+ {loaded.failures.length>0&&<div style={{padding:'10px 12px',borderRadius:8,background:'var(--dsw-alias-state-warn-bg)',color:'var(--dsw-alias-state-warn-label)',fontSize:12,lineHeight:'18px'}}><div style={{fontWeight:500,marginBottom:3}}>{t['settings.catalogFailures']}</div>{loaded.failures.map(x=><div key={x}>{x}</div>)}</div>}
145
+ {error&&<p style={{margin:0,fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-error-primary)'}}>{error}</p>}{saved&&<p style={{margin:0,fontSize:12,lineHeight:'18px',color:'var(--dsw-alias-state-success-primary)'}}>{t['settings.saved']}</p>}
146
+ <div style={{display:'flex',justifyContent:'flex-end',gap:8,paddingTop:4}}><Button variant="outline" disabled={busy} onClick={()=>void load()}>{t['settings.reload']}</Button><Button variant="primary" disabled={busy||!loaded.writable} onClick={()=>void save()}>{busy?t['settings.saving']:t['settings.save']}</Button></div>
147
+ </div>
148
+ }
149
+
150
+ function Metric({ label, value, note, accent }:{label:string;value:string;note:string;accent?:string}) {
151
+ return <div style={{...dashboardCard,padding:'16px 18px',minWidth:0}}><div style={muted}>{label}</div><div style={{fontSize:25,fontWeight:750,lineHeight:1.2,margin:'7px 0 5px',color:accent}}>{value}</div><div style={{...muted,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{note}</div></div>
152
+ }
153
+
154
+ function TrendChart({ daily, days, lang }:{daily:DailyStatistics[];days:number;lang:'zh'|'en'}) {
155
+ const t = dictionaries[lang]
156
+ const rows=useMemo(()=>{const map=new Map(daily.map(row=>[row.date,row]));const values:DailyStatistics[]=[];const start=new Date(startOfRange(days));for(let i=0;i<days;i+=1){const date=new Date(start);date.setDate(start.getDate()+i);const key=[date.getFullYear(),String(date.getMonth()+1).padStart(2,'0'),String(date.getDate()).padStart(2,'0')].join('-');values.push(map.get(key)??{date:key,invocations:0,successes:0,failures:0,calls:0,tokens:0,estimatedCostUsd:0,byTool:{}})}return values},[daily,days])
157
+ const width=760,height=230,pad={l:44,r:24,t:20,b:38};const innerW=width-pad.l-pad.r,innerH=height-pad.t-pad.b
158
+ const max=Math.max(1,...rows.flatMap(row=>[row.invocations,row.calls]));const x=(index:number)=>pad.l+(rows.length<=1?innerW/2:index*innerW/(rows.length-1));const y=(value:number)=>pad.t+innerH-value/max*innerH
159
+ const points=rows.map((row,index)=>`${x(index)},${y(row.calls)}`).join(' ');const step=rows.length>16?Math.ceil(rows.length/7):Math.max(1,Math.ceil(rows.length/7));const barWidth=Math.max(3,Math.min(18,innerW/Math.max(rows.length,1)*.55))
160
+ return <div style={{width:'100%',overflowX:'auto'}}><svg viewBox={`0 0 ${width} ${height}`} style={{display:'block',width:'100%',minWidth:620,height:'auto'}} aria-label={t['chart.ariaLabel']}>
161
+ {[0,.25,.5,.75,1].map(ratio=><g key={ratio}><line x1={pad.l} x2={width-pad.r} y1={pad.t+innerH*ratio} y2={pad.t+innerH*ratio} stroke="rgba(148,163,184,.16)"/><text x={pad.l-8} y={pad.t+innerH*ratio+4} textAnchor="end" fontSize="10" fill="var(--dsw-text-secondary)">{Math.round(max*(1-ratio))}</text></g>)}
162
+ {rows.map((row,index)=><rect key={row.date} x={x(index)-barWidth/2} y={y(row.invocations)} width={barWidth} height={pad.t+innerH-y(row.invocations)} rx="2" fill="#4f8cff" opacity=".82"/>)}
163
+ <polyline points={points} fill="none" stroke="#5ed7e8" strokeWidth="2.4" strokeLinejoin="round" strokeLinecap="round"/>
164
+ {rows.map((row,index)=>index%step===0||index===rows.length-1?<text key={row.date} x={x(index)} y={height-14} textAnchor="middle" fontSize="10" fill="var(--dsw-text-secondary)">{Number(row.date.slice(5,7))}/{Number(row.date.slice(8,10))}</text>:null)}
165
+ </svg><div style={{display:'flex',justifyContent:'center',gap:18,...muted}}><span><i style={{display:'inline-block',width:8,height:8,borderRadius:2,background:'#4f8cff',marginRight:6}}/>{t['chart.legendToolCalls']}</span><span><i style={{display:'inline-block',width:14,height:2,background:'#5ed7e8',marginRight:6,verticalAlign:'middle'}}/>{t['chart.legendModelCalls']}</span></div></div>
166
+ }
167
+
168
+ export function StatisticsPage({ sessionId, rpc, isGlobal }: StatisticsPageProps) {
169
+ const lang = useLanguage()
170
+ const t = dictionaries[lang]
171
+ const labels = toolLabels[lang]
172
+ const [days, setDays] = useState(30)
173
+ const [sessionOnly, setSessionOnly] = useState(!isGlobal && Boolean(sessionId))
174
+ const [data, setData] = useState<StatisticsOverview | null>(null)
175
+ const [loading, setLoading] = useState(true)
176
+ const [error, setError] = useState<string | null>(null)
177
+ const [refresh, setRefresh] = useState(0)
178
+
179
+ useEffect(() => {
180
+ const controller = new AbortController()
181
+ setLoading(true); setError(null)
182
+ const effectiveSessionId = sessionOnly && sessionId ? String(sessionId) : undefined
183
+ const queryPayload = {
184
+ fromMs: startOfRange(days),
185
+ toMs: endOfToday(),
186
+ timezoneOffsetMinutes: new Date().getTimezoneOffset(),
187
+ recentLimit: 50,
188
+ ...(effectiveSessionId ? { sessionId: effectiveSessionId } : {}),
189
+ }
190
+
191
+ const fetchOverview = async (): Promise<StatisticsOverview> => {
192
+ if (rpc && typeof rpc.call === 'function') {
193
+ try {
194
+ const result = await rpc.call('/api', 'llm-verifier/statistics', queryPayload, controller.signal)
195
+ if (result && result.ok) {
196
+ return result.value as StatisticsOverview
197
+ }
198
+ if (result && result.ok === false) {
199
+ throw new Error(result.error?.message ?? t['stats.requestFailed'])
200
+ }
201
+ } catch (rpcError) {
202
+ if (controller.signal.aborted) throw rpcError
203
+ console.warn('[llm-verifier] rpc.call failed, trying fetch fallback:', rpcError)
204
+ }
205
+ }
206
+
207
+ try {
208
+ const response = await fetch('/api/llm-verifier/statistics', {
209
+ method: 'POST',
210
+ headers: { 'content-type': 'application/json' },
211
+ body: JSON.stringify(queryPayload),
212
+ signal: controller.signal,
213
+ })
214
+ if (!response.ok) {
215
+ throw new Error(`HTTP ${response.status}: ${response.statusText || t['stats.requestFailed']}`)
216
+ }
217
+ const data = await response.json()
218
+ if (data && data.ok === true) {
219
+ return data.value as StatisticsOverview
220
+ }
221
+ if (data && data.type === 'server-response' && data.result?.ok === true) {
222
+ return data.result.value as StatisticsOverview
223
+ }
224
+ throw new Error(data?.error?.message ?? data?.result?.error?.message ?? t['stats.requestFailed'])
225
+ } catch (fetchError) {
226
+ if (controller.signal.aborted) throw fetchError
227
+ // Hosts without the exact Fetch route registry only answer on the plugin's own channel.
228
+ if (rpc && typeof rpc.call === 'function') {
229
+ const legacy = await rpc.call('/llm-verifier', 'statistics', queryPayload, controller.signal)
230
+ if (legacy && legacy.ok) return legacy.value as StatisticsOverview
231
+ }
232
+ throw fetchError
233
+ }
234
+ }
235
+
236
+ void fetchOverview()
237
+ .then(overview => {
238
+ setData(overview)
239
+ })
240
+ .catch((cause: unknown) => {
241
+ if (!controller.signal.aborted) setError(message(cause))
242
+ })
243
+ .finally(() => {
244
+ if (!controller.signal.aborted) setLoading(false)
245
+ })
246
+ return () => controller.abort()
247
+ }, [days, sessionOnly, sessionId, refresh, rpc, t])
248
+
249
+ const totals = data?.totals
250
+ return <main style={{ height: '100%', overflow: 'auto', boxSizing: 'border-box', padding: '22px clamp(16px, 3vw, 38px) 48px', color: 'var(--dsw-text-primary)', background: 'radial-gradient(circle at 10% 0%, rgba(115,77,255,.09), transparent 32%), radial-gradient(circle at 100% 8%, rgba(47,197,201,.07), transparent 28%)' }}>
251
+ <div style={{ maxWidth: 1180, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
252
+ <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
253
+ <div>
254
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
255
+ <span style={{ display: 'grid', placeItems: 'center', width: 34, height: 34, borderRadius: 10, background: 'rgba(79,140,255,.14)', color: '#6da0ff' }}>
256
+ <IconDataOutline16 size={18} />
257
+ </span>
258
+ <h2 style={{ margin: 0, fontSize: 23 }}>{isGlobal ? t['global.panelTitle'] : t['stats.pageTitle']}</h2>
259
+ </div>
260
+ <p style={{ margin: '7px 0 0 44px', ...muted }}>{isGlobal ? t['global.panelIntro'] : tFormat(t['stats.updatedAt'], { time: data ? dateTime(data.generatedAt, lang) : '--' })}</p>
261
+ </div>
262
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
263
+ <div style={{ display: 'flex', padding: 3, borderRadius: 10, background: 'var(--dsw-surface-sunken)', border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))' }}>
264
+ {[7, 30, 90].map(value => <button key={value} onClick={() => setDays(value)} style={{ border: 0, borderRadius: 7, padding: '6px 10px', cursor: 'pointer', color: days === value ? '#fff' : 'var(--dsw-text-secondary)', background: days === value ? '#3f68d8' : 'transparent' }}>{tFormat(t['stats.daysUnit'], { days: value })}</button>)}
265
+ </div>
266
+ {Boolean(sessionId) && <button onClick={() => setSessionOnly(value => !value)} style={{ border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.15))', borderRadius: 9, padding: '7px 11px', cursor: 'pointer', color: 'var(--dsw-text-primary)', background: sessionOnly ? 'rgba(79,140,255,.18)' : 'var(--dsw-surface-sunken)' }}>{sessionOnly ? t['stats.currentSession'] : t['stats.allSessions']}</button>}
267
+ <button title={t['stats.refresh']} onClick={() => setRefresh(value => value + 1)} style={{ display: 'grid', placeItems: 'center', width: 34, height: 34, borderRadius: 9, border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.15))', color: 'var(--dsw-text-primary)', background: 'var(--dsw-surface-sunken)', cursor: 'pointer' }}><IconRefreshOutline16 size={16} /></button>
268
+ </div>
269
+ </header>
270
+ {error && <div style={{ ...dashboardCard, padding: 18, borderColor: 'var(--dsw-danger, #e85858)', color: 'var(--dsw-danger, #e85858)' }}>{error}<div style={{ ...muted, marginTop: 6 }}>{t['stats.hostRestartHint']}</div></div>}
271
+ {loading && !data ? <div style={{ ...dashboardCard, padding: 32, textAlign: 'center', ...muted }}>{t['stats.loading']}</div> : <>
272
+ <section style={{ ...dashboardCard, padding: '22px 24px', display: 'grid', gridTemplateColumns: 'minmax(220px,1.4fr) minmax(240px,1fr)', gap: 24, alignItems: 'center' }}>
273
+ <div>
274
+ <div style={muted}>{tFormat(t['stats.costSummary'], { days })}</div>
275
+ <div style={{ fontSize: 38, fontWeight: 780, letterSpacing: '-.03em', margin: '5px 0' }}>{money(totals?.estimatedCostUsd ?? 0)}</div>
276
+ <div style={muted}>{tFormat(t['stats.callsSummary'], { invocations: compact(totals?.invocations ?? 0, lang), calls: compact(totals?.calls ?? 0, lang) })}</div>
277
+ </div>
278
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '9px 16px', fontSize: 13 }}>
279
+ <span style={muted}>{t['stats.successCalls']}</span><strong>{compact(totals?.successes ?? 0, lang)} <small style={{ color: '#77d49b' }}>▲ {((totals?.successRate ?? 0) * 100).toFixed(1)}%</small></strong>
280
+ <span style={muted}>{t['stats.failedCalls']}</span><strong>{compact(totals?.failures ?? 0, lang)}</strong>
281
+ <span style={muted}>{t['stats.avgDuration']}</span><strong>{duration(totals?.averageDurationMs ?? 0)}</strong>
282
+ </div>
283
+ </section>
284
+ <section style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(180px,1fr))', gap: 12 }}>
285
+ <Metric label={t['metric.cacheHitRate']} value={((totals?.cacheHitRate ?? 0) * 100).toFixed(1) + '%'} note={tFormat(t['metric.cacheHitNote'], { hits: compact(totals?.cacheHits ?? 0, lang), total: compact((totals?.cacheHits ?? 0) + (totals?.cacheMisses ?? 0), lang) })} accent="#b7dd64" />
286
+ <Metric label={t['metric.tokens']} value={compact(totals?.tokens ?? 0, lang)} note={tFormat(t['metric.tokensNote'], { input: compact((totals?.inputTokens ?? 0) + (totals?.cachedInputTokens ?? 0), lang), output: compact(totals?.outputTokens ?? 0, lang) })} />
287
+ <Metric label={t['metric.avgModelCalls']} value={(totals?.invocations ?? 0) > 0 ? ((totals?.calls ?? 0) / (totals?.invocations ?? 1)).toFixed(1) : '0'} note={tFormat(t['metric.avgModelCallsNote'], { attempts: compact(totals?.attempts ?? 0, lang), retries: compact(totals?.retries ?? 0, lang) })} />
288
+ <Metric label={t['metric.scoringMode']} value={compact(totals?.topLogprobScores ?? 0, lang)} note={tFormat(t['metric.scoringModeNote'], { explicit: compact(totals?.explicitTagScores ?? 0, lang) })} />
289
+ </section>
290
+ <section style={{ ...dashboardCard, padding: '18px 20px 16px' }}><div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}><strong>{t['chart.title']}</strong><span style={muted}>{sessionOnly ? t['stats.currentSession'] : t['stats.allSessions']}</span></div><TrendChart daily={data?.daily ?? []} days={days} lang={lang} /></section>
291
+ <section style={{ ...dashboardCard, padding: '18px 18px 8px', overflow: 'hidden' }}><div style={{ display: 'flex', justifyContent: 'space-between', margin: '0 2px 12px' }}><strong>{t['table.title']}</strong><span style={muted}>{tFormat(t['table.toolCount'], { count: data?.tools.length ?? 0 })}</span></div><div style={{ overflowX: 'auto' }}><table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, minWidth: 760 }}><thead><tr style={{ textAlign: 'left', color: 'var(--dsw-text-secondary)', background: 'var(--dsw-surface-sunken)' }}>{[t['table.colTool'], t['table.colInvocations'], t['table.colSuccessRate'], t['table.colAvgDuration'], t['table.colModelCalls'], t['table.colTokens'], t['table.colCacheHits'], t['table.colEstimatedCost']].map(value => <th key={value} style={{ padding: '10px 12px', fontWeight: 500 }}>{value}</th>)}</tr></thead><tbody>{(data?.tools ?? []).map(tool => <tr key={tool.toolName} style={{ borderTop: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.1))' }}><td style={{ padding: '13px 12px' }}><span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: '50%', background: toolColors[tool.toolName] ?? '#8691a8', marginRight: 8 }} /><strong>{labels[tool.toolName] ?? tool.toolName}</strong><div style={{ ...muted, margin: '3px 0 0 16px' }}>{tool.toolName}</div></td><td style={{ padding: '13px 12px' }}>{compact(tool.invocations, lang)}</td><td style={{ padding: '13px 12px', color: tool.successRate >= .9 ? '#77d49b' : tool.successRate >= .7 ? '#e3bd63' : '#ed7777' }}>{(tool.successRate * 100).toFixed(1)}%</td><td style={{ padding: '13px 12px' }}>{duration(tool.averageDurationMs)}</td><td style={{ padding: '13px 12px' }}>{compact(tool.calls, lang)}</td><td style={{ padding: '13px 12px' }}>{compact(tool.tokens, lang)}</td><td style={{ padding: '13px 12px' }}>{tool.cacheHits}/{tool.cacheHits + tool.cacheMisses}</td><td style={{ padding: '13px 12px' }}>{money(tool.estimatedCostUsd)}</td></tr>)}{(data?.tools.length ?? 0) === 0 && <tr><td colSpan={8} style={{ padding: 28, textAlign: 'center', ...muted }}>{t['table.empty']}</td></tr>}</tbody></table></div></section>
292
+ <section style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(290px,.45fr)', gap: 16, alignItems: 'start' }}><div style={{ ...dashboardCard, padding: '18px 18px 8px', overflow: 'hidden' }}><div style={{ display: 'flex', justifyContent: 'space-between', margin: '0 2px 12px' }}><strong>{t['recent.title']}</strong><span style={muted}>{t['recent.maxCount']}</span></div><div style={{ maxHeight: 360, overflow: 'auto' }}>{(data?.recent ?? []).map(item => <div key={item.id} style={{ display: 'grid', gridTemplateColumns: 'minmax(170px,1fr) auto', gap: 12, padding: '11px 8px', borderTop: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.1))' }}><div><div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 7, height: 7, borderRadius: '50%', background: item.success ? '#59c985' : '#e76565' }} /><strong style={{ fontSize: 13 }}>{labels[item.toolName] ?? item.toolName}</strong><span style={muted}>{item.provider}/{item.model}</span></div>{!item.success && <div title={item.errorMessage} style={{ margin: '5px 0 0 15px', fontSize: 11, color: '#e76565', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.errorMessage ?? item.errorName}</div>}</div><div style={{ textAlign: 'right' }}><div style={{ fontSize: 12 }}>{duration(item.durationMs)}</div><div style={{ ...muted, marginTop: 3 }}>{dateTime(item.startedAt, lang)}</div></div></div>)}{(data?.recent.length ?? 0) === 0 && <div style={{ padding: 24, textAlign: 'center', ...muted }}>{t['recent.empty']}</div>}</div></div>
293
+ <div style={{ ...dashboardCard, padding: '18px' }}><strong>{t['models.title']}</strong><div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>{(data?.models ?? []).map(model => <div key={model.provider + '\0' + model.model} style={{ padding: '11px 12px', borderRadius: 10, background: 'var(--dsw-surface-sunken)' }}><div style={{ fontWeight: 650, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis' }}>{model.model}</div><div style={{ ...muted, marginTop: 3 }}>{model.provider}</div><div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 9, fontSize: 12 }}><span>{tFormat(t['models.calls'], { calls: compact(model.calls, lang) })}</span><span>{tFormat(t['models.tokens'], { tokens: compact(model.tokens, lang) })}</span><strong>{money(model.estimatedCostUsd)}</strong></div></div>)}{(data?.models.length ?? 0) === 0 && <div style={muted}>{t['models.empty']}</div>}</div></div></section>
294
+ </>}
295
+ </div>
296
+ </main>
297
+ }
298
+
299
+ export function VerifierSidebarIcon({ size, active }: { size: number; active?: boolean }) {
300
+ return <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: size, height: size, color: active ? 'var(--dsw-alias-brand-primary, #4f8cff)' : 'currentColor' }}><IconDataOutline16 size={Math.min(18, size)} /></span>
301
+ }
302
+
303
+ export function GlobalVerifierDashboard({ rpc }: { rpc: any }) {
304
+ return <StatisticsPage rpc={rpc} isGlobal />
305
+ }
306
+
307
+ export function RightSidebarVerifierPanel({ rpc, sessionId }: { rpc: any; sessionId?: string }) {
308
+ return (
309
+ <div style={{ height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
310
+ <StatisticsPage rpc={rpc} sessionId={sessionId} />
311
+ </div>
312
+ )
313
+ }
314
+
315
+ export function RightSidebarVerifierTitle() {
316
+ const lang = useLanguage()
317
+ const t = dictionaries[lang]
318
+ return <span style={{ fontSize: 13, fontWeight: 500 }}>{t['slot.statistics']}</span>
319
+ }
320
+
321
+ export const inject = ['slots', 'connection', 'remote', 'remote.session', 'remote.settings']
322
+ export function apply(ctx: ClientContext): void {
323
+ const connection = ctx.get('connection') as any
324
+ const remote = ctx.remote
325
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
326
+ name: 'settings.section',
327
+ id: 'llm-verifier',
328
+ order: 35,
329
+ label: 'LLM Verifier',
330
+ inject: () => ({ remote }),
331
+ }, VerifierSettings as never))
332
+ ctx.slots.inject('conversation.view', () => ctx.slots.register({
333
+ name: 'conversation.view',
334
+ id: 'llm-verifier-statistics',
335
+ order: 30,
336
+ label: detectLanguage() === 'zh' ? zh['slot.statistics'] : en['slot.statistics'],
337
+ inject: () => ({ rpc: connection.rpc }),
338
+ }, StatisticsPage as never))
339
+ const VERIFIER_TAB_KIND = 'llm-verifier'
340
+ const VERIFIER_TAB_ID = 'dsh-llm-verifier'
341
+
342
+ const registerRightSidebar = (tabs: any) => {
343
+ if (!tabs || typeof tabs.register !== 'function') return
344
+ try {
345
+ tabs.register({
346
+ id: VERIFIER_TAB_ID,
347
+ kind: VERIFIER_TAB_KIND,
348
+ priority: 'extension',
349
+ title: () => (detectLanguage() === 'zh' ? zh['slot.statistics'] : en['slot.statistics']),
350
+ guide: [{
351
+ order: 45,
352
+ title: () => (detectLanguage() === 'zh' ? zh['guide.verifier.title'] : en['guide.verifier.title']),
353
+ description: () => (detectLanguage() === 'zh' ? zh['guide.verifier.desc'] : en['guide.verifier.desc']),
354
+ icon: VerifierSidebarIcon,
355
+ }],
356
+ })
357
+ } catch {}
358
+ }
359
+
360
+ const existingTabs = typeof (ctx as any).get === 'function' ? (ctx as any).get('sidebarRightTabs') : undefined
361
+ if (existingTabs) {
362
+ registerRightSidebar(existingTabs)
363
+ }
364
+ if (typeof (ctx as any).inject === 'function') {
365
+ try {
366
+ (ctx as any).inject(['sidebarRightTabs'], (subCtx: any) => {
367
+ const subTabs = typeof subCtx?.get === 'function' ? subCtx.get('sidebarRightTabs') : (subCtx as any)?.sidebarRightTabs
368
+ if (subTabs) registerRightSidebar(subTabs)
369
+ })
370
+ } catch {}
371
+ }
372
+
373
+ ctx.slots.inject('sidebar.right.pane.tab' as never, () => ctx.slots.register({
374
+ name: 'sidebar.right.pane.tab' as never,
375
+ key: VERIFIER_TAB_ID,
376
+ inject: () => ({ rpc: connection.rpc }),
377
+ } as never, RightSidebarVerifierPanel as never))
378
+
379
+ ctx.slots.inject('sidebar.right.pane.tab.title' as never, () => ctx.slots.register({
380
+ name: 'sidebar.right.pane.tab.title' as never,
381
+ key: VERIFIER_TAB_ID,
382
+ } as never, RightSidebarVerifierTitle as never))
383
+ }