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.
- package/LICENSE +22 -22
- package/README.md +328 -301
- package/cordis.patch.yml +4 -4
- package/lib/caller-BWwSVNZf.js.map +1 -0
- package/lib/client.js +176 -14
- package/lib/client.js.map +1 -0
- package/lib/core.js.map +1 -0
- package/lib/index.js +400 -51
- package/lib/index.js.map +1 -0
- package/lib/types/auto.d.ts.map +1 -0
- package/lib/types/auto.js +8 -2
- package/lib/types/auto.js.map +1 -0
- package/lib/types/cache.d.ts.map +1 -0
- package/lib/types/cache.js.map +1 -0
- package/lib/types/caller.d.ts.map +1 -0
- package/lib/types/caller.js.map +1 -0
- package/lib/types/client-i18n.d.ts +27 -0
- package/lib/types/client-i18n.d.ts.map +1 -0
- package/lib/types/client-i18n.js +18 -0
- package/lib/types/client-i18n.js.map +1 -0
- package/lib/types/client.d.ts +15 -2
- package/lib/types/client.d.ts.map +1 -0
- package/lib/types/client.js +137 -10
- package/lib/types/client.js.map +1 -0
- package/lib/types/config.d.ts +4 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +5 -3
- package/lib/types/config.js.map +1 -0
- package/lib/types/core.d.ts.map +1 -0
- package/lib/types/core.js.map +1 -0
- package/lib/types/engine.d.ts.map +1 -0
- package/lib/types/engine.js.map +1 -0
- package/lib/types/images.d.ts.map +1 -0
- package/lib/types/images.js.map +1 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +162 -9
- package/lib/types/index.js.map +1 -0
- package/lib/types/plan-gate.d.ts +27 -0
- package/lib/types/plan-gate.d.ts.map +1 -0
- package/lib/types/plan-gate.js +60 -0
- package/lib/types/plan-gate.js.map +1 -0
- package/lib/types/router.d.ts +28 -32
- package/lib/types/router.d.ts.map +1 -0
- package/lib/types/router.js +41 -9
- package/lib/types/router.js.map +1 -0
- package/lib/types/session.d.ts +11 -0
- package/lib/types/session.d.ts.map +1 -0
- package/lib/types/session.js +41 -11
- package/lib/types/session.js.map +1 -0
- package/lib/types/statistics.d.ts.map +1 -0
- package/lib/types/statistics.js.map +1 -0
- package/lib/types/team-gate.d.ts +20 -0
- package/lib/types/team-gate.d.ts.map +1 -0
- package/lib/types/team-gate.js +59 -0
- package/lib/types/team-gate.js.map +1 -0
- package/lib/types/top-logprobs.d.ts.map +1 -0
- package/lib/types/top-logprobs.js.map +1 -0
- package/lib/types/topic-storage.d.ts.map +1 -0
- package/lib/types/topic-storage.js.map +1 -0
- package/package.json +114 -112
- package/src/auto.test.ts +95 -86
- package/src/auto.ts +140 -133
- package/src/cache.ts +115 -115
- package/src/caller.test.ts +34 -34
- package/src/caller.ts +151 -151
- package/src/client-i18n.ts +18 -0
- package/src/client.tsx +383 -213
- package/src/config.ts +189 -183
- package/src/core.test.ts +71 -71
- package/src/core.ts +205 -205
- package/src/engine.test.ts +136 -136
- package/src/engine.ts +140 -140
- package/src/images.ts +33 -33
- package/src/index.ts +394 -247
- package/src/parity.test.ts +85 -85
- package/src/plan-gate.test.ts +56 -0
- package/src/plan-gate.ts +66 -0
- package/src/router.test.ts +107 -89
- package/src/router.ts +361 -309
- package/src/session.test.ts +71 -36
- package/src/session.ts +132 -103
- package/src/statistics.test.ts +80 -80
- package/src/statistics.ts +328 -328
- package/src/team-gate.test.ts +92 -0
- package/src/team-gate.ts +80 -0
- package/src/top-logprobs.test.ts +84 -84
- package/src/top-logprobs.ts +153 -153
- package/src/topic-storage.test.ts +31 -31
- 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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
function
|
|
74
|
-
function
|
|
75
|
-
function
|
|
76
|
-
function
|
|
77
|
-
function
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
if(!loaded||!draft)return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<div style={row}><Label title={t['field.
|
|
104
|
-
<div style={row}><Label title={t['field.
|
|
105
|
-
<div style={row}><Label title={t['field.
|
|
106
|
-
<div style={row}><Label title={t['field.
|
|
107
|
-
<div style={row}><Label title={t['field.
|
|
108
|
-
<div style={row}><Label title={t['field.
|
|
109
|
-
<div style={row}><Label title={t['field.
|
|
110
|
-
<div style={row}><Label title={t['field.
|
|
111
|
-
<div style={row}><Label title={t['field.
|
|
112
|
-
<div style={row}><Label title={t['field.
|
|
113
|
-
<div style={row}><Label title={t['field.
|
|
114
|
-
<div style={row}><Label title={t['field.
|
|
115
|
-
<div style={row}><Label title={t['field.
|
|
116
|
-
<div style={row}><Label title={t['field.
|
|
117
|
-
<div style={row}><Label title={t['field.
|
|
118
|
-
<div style={row}><Label title={t['field.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<div style={row}><Label title={t['field.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div style={row}><Label title={t['field.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
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
|
+
}
|