@anionex/dsh-vision-toolkit 0.1.5
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 +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +383 -0
- package/README.zh.md +383 -0
- package/assets/dsh-conversation-artifact.png +0 -0
- package/assets/dsh-conversation-image-qa-top.png +0 -0
- package/assets/dsh-conversation-image-qa.png +0 -0
- package/assets/dsh-conversation-pixel-diff.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging-top.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging.png +0 -0
- package/assets/dsh-conversation-tool-call.png +0 -0
- package/assets/dsh-conversation-vision-trace.png +0 -0
- package/assets/hero.png +0 -0
- package/assets/social-preview.png +0 -0
- package/assets/upstream/README.md +16 -0
- package/assets/upstream/image-qa.webp +0 -0
- package/assets/upstream/infographic-reference.webp +0 -0
- package/assets/upstream/infographic-result.webp +0 -0
- package/assets/upstream/screenshot-debugging.webp +0 -0
- package/assets/upstream/ui-result.webp +0 -0
- package/assets/upstream/ui-sketch.webp +0 -0
- package/assets/vision-settings.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/assets/vision-settings.png +0 -0
- package/docs/requirements-traceability/README.i18n.yaml +6 -0
- package/docs/requirements-traceability/README.md +75 -0
- package/docs/requirements-traceability/README.zh.md +75 -0
- package/examples/ui-restoration/README.i18n.yaml +6 -0
- package/examples/ui-restoration/README.md +70 -0
- package/examples/ui-restoration/README.zh.md +70 -0
- package/examples/ui-restoration/assets/final-heatmap.png +0 -0
- package/examples/ui-restoration/assets/final-report.json +83 -0
- package/examples/ui-restoration/assets/implementation.png +0 -0
- package/examples/ui-restoration/assets/initial-heatmap.png +0 -0
- package/examples/ui-restoration/assets/initial-report.json +83 -0
- package/examples/ui-restoration/assets/initial.png +0 -0
- package/examples/ui-restoration/assets/metrics.json +12 -0
- package/examples/ui-restoration/assets/reference.png +0 -0
- package/examples/ui-restoration/implementation.html +94 -0
- package/examples/ui-restoration/initial.html +57 -0
- package/lib/artifact-access.js +369 -0
- package/lib/artifact-access.js.map +1 -0
- package/lib/artifacts.js +56 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/client.js +952 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +117 -0
- package/lib/config.js.map +1 -0
- package/lib/errors.js +56 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +213 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +97 -0
- package/lib/index.js.map +1 -0
- package/lib/paste-images.js +199 -0
- package/lib/paste-images.js.map +1 -0
- package/lib/paths.js +325 -0
- package/lib/paths.js.map +1 -0
- package/lib/runtime-install.js +601 -0
- package/lib/runtime-install.js.map +1 -0
- package/lib/runtime-manager.js +126 -0
- package/lib/runtime-manager.js.map +1 -0
- package/lib/runtime.js +1344 -0
- package/lib/runtime.js.map +1 -0
- package/lib/skill.js +139 -0
- package/lib/skill.js.map +1 -0
- package/lib/tools.js +528 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/artifact-access.d.ts +61 -0
- package/lib/types/artifact-access.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +42 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +179 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/paste-images.d.ts +57 -0
- package/lib/types/client/paste-images.d.ts.map +1 -0
- package/lib/types/config.d.ts +73 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/errors.d.ts +35 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +40 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +21 -0
- package/lib/types/paste-images.d.ts.map +1 -0
- package/lib/types/paths.d.ts +107 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/runtime-install.d.ts +49 -0
- package/lib/types/runtime-install.d.ts.map +1 -0
- package/lib/types/runtime-manager.d.ts +60 -0
- package/lib/types/runtime-manager.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +389 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/skill.d.ts +15 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/tools.d.ts +22 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +207 -0
- package/lib/types/upstream.d.ts.map +1 -0
- package/lib/types/version.d.ts +15 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/web-request.d.ts +4 -0
- package/lib/types/web-request.d.ts.map +1 -0
- package/lib/types/web.d.ts +74 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/upstream.js +675 -0
- package/lib/upstream.js.map +1 -0
- package/lib/version.js +18 -0
- package/lib/version.js.map +1 -0
- package/lib/web-request.js +20 -0
- package/lib/web-request.js.map +1 -0
- package/lib/web.js +244 -0
- package/lib/web.js.map +1 -0
- package/package.json +139 -0
- package/runtime/requirements.lock +3 -0
- package/src/artifact-access.ts +386 -0
- package/src/artifacts.ts +85 -0
- package/src/client/index.tsx +866 -0
- package/src/client/paste-images.tsx +426 -0
- package/src/config.ts +177 -0
- package/src/errors.ts +62 -0
- package/src/exposure.ts +227 -0
- package/src/index.ts +122 -0
- package/src/paste-images.ts +234 -0
- package/src/paths.ts +348 -0
- package/src/runtime-install.ts +723 -0
- package/src/runtime-manager.ts +166 -0
- package/src/runtime.ts +1783 -0
- package/src/skill.ts +143 -0
- package/src/tools.ts +668 -0
- package/src/upstream.ts +861 -0
- package/src/version.ts +37 -0
- package/src/web-request.ts +17 -0
- package/src/web.ts +329 -0
- package/vendor/agent-vision-toolkit/CHANGELOG.md +16 -0
- package/vendor/agent-vision-toolkit/LICENSE +21 -0
- package/vendor/agent-vision-toolkit/README.md +399 -0
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +89 -0
- package/vendor/agent-vision-toolkit/bin/crop +90 -0
- package/vendor/agent-vision-toolkit/bin/detect +13 -0
- package/vendor/agent-vision-toolkit/bin/glance +93 -0
- package/vendor/agent-vision-toolkit/bin/ground +13 -0
- package/vendor/agent-vision-toolkit/bin/trace +129 -0
- package/vendor/agent-vision-toolkit/detect.py +56 -0
- package/vendor/agent-vision-toolkit/ground.py +216 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/html_shot.py +108 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/vendor/agent-vision-toolkit/vision_client.py +156 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Vision Toolkit browser plugin: dedicated Tool cards plus the Settings,
|
|
3
|
+
* health, connection-test, and safe Artifact preview experience.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
13
|
+
import type { ClientContext, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-credentials/types'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
21
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
22
|
+
import { installPasteImages } from './paste-images.tsx'
|
|
23
|
+
|
|
24
|
+
const NS = 'vision-toolkit'
|
|
25
|
+
const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings'
|
|
26
|
+
const PRESENTATION_META_KEY = '$dshVisionToolkit'
|
|
27
|
+
|
|
28
|
+
const en = {
|
|
29
|
+
nav: 'Vision',
|
|
30
|
+
settingsTitle: 'Vision Toolkit',
|
|
31
|
+
settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.',
|
|
32
|
+
externalNotice: 'Remote tools send the selected image bytes to the configured external vision API. Local crop, trace, pixel diff, palette, foreground extraction, and HTML rendering do not upload images.',
|
|
33
|
+
provider: 'Vision service',
|
|
34
|
+
baseUrl: 'Base URL',
|
|
35
|
+
credential: 'Credential reference',
|
|
36
|
+
model: 'Model',
|
|
37
|
+
language: 'Output language',
|
|
38
|
+
limits: 'Limits',
|
|
39
|
+
timeout: 'Request timeout (ms)',
|
|
40
|
+
maxBytes: 'Maximum image bytes',
|
|
41
|
+
maxPixels: 'Maximum image pixels',
|
|
42
|
+
concurrency: 'Concurrent calls per session',
|
|
43
|
+
runtime: 'Runtime',
|
|
44
|
+
runtimeMode: 'Runtime mode',
|
|
45
|
+
toolkitPath: 'Pinned checkout path',
|
|
46
|
+
python: 'Python override',
|
|
47
|
+
allowedDirs: 'Additional allowed directories',
|
|
48
|
+
allowedDirsHint: 'One path per line. The session workspace is always allowed.',
|
|
49
|
+
save: 'Save and apply',
|
|
50
|
+
saving: 'Validating runtime…',
|
|
51
|
+
reload: 'Reload',
|
|
52
|
+
saved: 'Settings validated and applied.',
|
|
53
|
+
readOnly: 'The active Settings provider is read-only.',
|
|
54
|
+
configured: 'Configured',
|
|
55
|
+
missing: 'Missing',
|
|
56
|
+
source: 'Source',
|
|
57
|
+
health: 'Health',
|
|
58
|
+
runHealth: 'Run health check',
|
|
59
|
+
testConnection: 'Test connection',
|
|
60
|
+
testing: 'Checking…',
|
|
61
|
+
connectionHint: 'Connection testing explicitly sends the configured credential to GET /models. It uploads no image and creates no completion.',
|
|
62
|
+
pluginVersion: 'Plugin',
|
|
63
|
+
upstreamVersion: 'Upstream',
|
|
64
|
+
activeGeneration: 'Runtime generation',
|
|
65
|
+
runtimeUnavailable: 'Runtime unavailable',
|
|
66
|
+
runtimeCandidateRejected: 'Last runtime candidate was rejected; the active generation remains available.',
|
|
67
|
+
retry: 'Retry',
|
|
68
|
+
open: 'Open file',
|
|
69
|
+
download: 'Download',
|
|
70
|
+
previewUnavailable: 'HTTP preview is unavailable in this host; use Open file.',
|
|
71
|
+
running: 'Running…',
|
|
72
|
+
failed: 'Failed',
|
|
73
|
+
matches: 'matches',
|
|
74
|
+
elements: 'elements',
|
|
75
|
+
dimensions: 'Dimensions',
|
|
76
|
+
coordinates: 'Coordinates',
|
|
77
|
+
artifact: 'Artifact',
|
|
78
|
+
artifacts: 'Artifacts',
|
|
79
|
+
difference: 'Overall difference',
|
|
80
|
+
worstRegions: 'Worst regions',
|
|
81
|
+
colors: 'Dominant colors',
|
|
82
|
+
noResult: 'Structured result unavailable; inspect the raw Tool result.',
|
|
83
|
+
healthy: 'Healthy',
|
|
84
|
+
degraded: 'Needs attention',
|
|
85
|
+
notTested: 'Not tested',
|
|
86
|
+
} as const
|
|
87
|
+
|
|
88
|
+
type LocaleKey = keyof typeof en
|
|
89
|
+
|
|
90
|
+
const zh: Record<LocaleKey, string> = {
|
|
91
|
+
nav: '视觉工具',
|
|
92
|
+
settingsTitle: 'Vision Toolkit',
|
|
93
|
+
settingsIntro: '配置固定版本的视觉工程运行时、外部视觉服务与本地安全限制。',
|
|
94
|
+
externalNotice: '远程工具会把选中的图片字节发送到已配置的外部视觉 API。本地裁剪、几何恢复、像素对比、调色板、前景提取和 HTML 渲染不会上传图片。',
|
|
95
|
+
provider: '视觉服务',
|
|
96
|
+
baseUrl: '服务地址',
|
|
97
|
+
credential: 'Credential 引用',
|
|
98
|
+
model: '模型',
|
|
99
|
+
language: '输出语言',
|
|
100
|
+
limits: '限制',
|
|
101
|
+
timeout: '请求超时(毫秒)',
|
|
102
|
+
maxBytes: '最大图片字节数',
|
|
103
|
+
maxPixels: '最大图片像素数',
|
|
104
|
+
concurrency: '每个 Session 并发数',
|
|
105
|
+
runtime: '运行时',
|
|
106
|
+
runtimeMode: '运行模式',
|
|
107
|
+
toolkitPath: '固定上游 checkout 路径',
|
|
108
|
+
python: 'Python 覆盖',
|
|
109
|
+
allowedDirs: '额外允许目录',
|
|
110
|
+
allowedDirsHint: '每行一个路径;Session 工作区始终允许。',
|
|
111
|
+
save: '保存并应用',
|
|
112
|
+
saving: '正在验证运行时…',
|
|
113
|
+
reload: '重新加载',
|
|
114
|
+
saved: '设置已验证并生效。',
|
|
115
|
+
readOnly: '当前 Settings 提供方为只读。',
|
|
116
|
+
configured: '已配置',
|
|
117
|
+
missing: '缺失',
|
|
118
|
+
source: '来源',
|
|
119
|
+
health: '健康检查',
|
|
120
|
+
runHealth: '运行健康检查',
|
|
121
|
+
testConnection: '测试连接',
|
|
122
|
+
testing: '检查中…',
|
|
123
|
+
connectionHint: '连接测试会显式把已配置 Credential 发送到 GET /models;不会上传图片,也不会创建 completion。',
|
|
124
|
+
pluginVersion: '插件',
|
|
125
|
+
upstreamVersion: '上游',
|
|
126
|
+
activeGeneration: '运行时世代',
|
|
127
|
+
runtimeUnavailable: '运行时不可用',
|
|
128
|
+
runtimeCandidateRejected: '最近的运行时候选已被拒绝;当前世代仍可用。',
|
|
129
|
+
retry: '重试',
|
|
130
|
+
open: '打开文件',
|
|
131
|
+
download: '下载',
|
|
132
|
+
previewUnavailable: '当前宿主不提供 HTTP 预览,请使用“打开文件”。',
|
|
133
|
+
running: '运行中…',
|
|
134
|
+
failed: '失败',
|
|
135
|
+
matches: '个匹配',
|
|
136
|
+
elements: '个元素',
|
|
137
|
+
dimensions: '尺寸',
|
|
138
|
+
coordinates: '坐标',
|
|
139
|
+
artifact: '交付文件',
|
|
140
|
+
artifacts: '交付文件',
|
|
141
|
+
difference: '总体差异',
|
|
142
|
+
worstRegions: '差异最大区域',
|
|
143
|
+
colors: '主色',
|
|
144
|
+
noResult: '结构化结果不可用,请检查原始 Tool 结果。',
|
|
145
|
+
healthy: '健康',
|
|
146
|
+
degraded: '需要处理',
|
|
147
|
+
notTested: '未测试',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type Translate = (key: LocaleKey) => string
|
|
151
|
+
|
|
152
|
+
interface ToolCallOwnerProps {
|
|
153
|
+
callId: string
|
|
154
|
+
toolName: string
|
|
155
|
+
block: ToolCallBlock
|
|
156
|
+
cwd?: string | undefined
|
|
157
|
+
openFile: (path: string) => void
|
|
158
|
+
inspect?: (() => void) | undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
162
|
+
interface SlotMap {
|
|
163
|
+
/** Keyed atomic Tool call view, dispatched by wire Tool name. */
|
|
164
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ToolCallOwnerProps }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface LocaleNamespaceMap {
|
|
168
|
+
/** DSH Vision Toolkit Tool cards and Settings copy. */
|
|
169
|
+
'vision-toolkit': LocaleKey
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type ToolCallViewProps = PropsRuntime<'tool.call.toolview'>
|
|
174
|
+
|
|
175
|
+
interface ArtifactDescriptor {
|
|
176
|
+
path: string
|
|
177
|
+
filename: string
|
|
178
|
+
mimeType: string
|
|
179
|
+
kind: 'image' | 'svg' | 'markdown' | 'json'
|
|
180
|
+
description: string
|
|
181
|
+
sourceTool: string
|
|
182
|
+
previewIntent: 'image' | 'svg' | 'text' | 'download'
|
|
183
|
+
bytes: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ArtifactGrant {
|
|
187
|
+
path: string
|
|
188
|
+
previewUrl: string
|
|
189
|
+
downloadUrl: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface HealthCheck {
|
|
193
|
+
status: 'ok' | 'warning' | 'error' | 'not_tested'
|
|
194
|
+
detail: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface HealthResult {
|
|
198
|
+
pluginVersion: string
|
|
199
|
+
checks: Record<string, HealthCheck>
|
|
200
|
+
healthy: boolean
|
|
201
|
+
connectionTested: boolean
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface SettingsValue {
|
|
205
|
+
provider?: { baseUrl?: string; credential?: string; model?: string }
|
|
206
|
+
language?: 'zh' | 'en'
|
|
207
|
+
timeoutMs?: number
|
|
208
|
+
maxImageBytes?: number
|
|
209
|
+
maxImagePixels?: number
|
|
210
|
+
concurrency?: number
|
|
211
|
+
runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
|
|
212
|
+
allowedDirs?: string[]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface SettingsSnapshot {
|
|
216
|
+
schemaVersion: 1
|
|
217
|
+
writable: boolean
|
|
218
|
+
settings: { value: SettingsValue; revision: number; applies: 'live' }
|
|
219
|
+
credential: { ref: string; configured: boolean; source?: string; writable: boolean }
|
|
220
|
+
runtime: {
|
|
221
|
+
ready: boolean
|
|
222
|
+
generation: number
|
|
223
|
+
activeConfig?: SettingsValue
|
|
224
|
+
upstream?: {
|
|
225
|
+
source: 'managed' | 'external'
|
|
226
|
+
path: string
|
|
227
|
+
runtimeHome: string
|
|
228
|
+
python: string
|
|
229
|
+
pythonVersion: string
|
|
230
|
+
}
|
|
231
|
+
lastError?: string
|
|
232
|
+
}
|
|
233
|
+
release: {
|
|
234
|
+
pluginVersion: string
|
|
235
|
+
upstreamRepository: string
|
|
236
|
+
upstreamVersion: string
|
|
237
|
+
upstreamCommit: string
|
|
238
|
+
}
|
|
239
|
+
artifactRouteAvailable: boolean
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface ApiSuccess<T> { ok: true; value: T }
|
|
243
|
+
interface ApiFailure { ok: false; error: { code: string; message: string } }
|
|
244
|
+
|
|
245
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
246
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function textOfContent(block: ToolCallBlock): string {
|
|
250
|
+
if (!('kind' in block)) return ''
|
|
251
|
+
return block.content
|
|
252
|
+
.filter((entry): entry is Extract<typeof entry, { type: 'text' }> => entry.type === 'text')
|
|
253
|
+
.map(entry => entry.text)
|
|
254
|
+
.join('\n')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Decode canonical presentation metadata with a JSON-text fallback. */
|
|
258
|
+
export function decodeVisionResult(block: ToolCallBlock): Record<string, unknown> | undefined {
|
|
259
|
+
if (!('kind' in block) || block.isError) return undefined
|
|
260
|
+
if (isRecord(block.meta)) return block.meta
|
|
261
|
+
const text = textOfContent(block).trim()
|
|
262
|
+
if (text.length === 0) return undefined
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(text) as unknown
|
|
265
|
+
return isRecord(parsed) ? parsed : undefined
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function accessMap(value: Record<string, unknown> | undefined): Map<string, ArtifactGrant> {
|
|
272
|
+
const map = new Map<string, ArtifactGrant>()
|
|
273
|
+
if (value === undefined) return map
|
|
274
|
+
const envelope = value[PRESENTATION_META_KEY]
|
|
275
|
+
if (!isRecord(envelope) || envelope.schemaVersion !== 1 || !Array.isArray(envelope.artifacts)) return map
|
|
276
|
+
for (const entry of envelope.artifacts) {
|
|
277
|
+
if (!isRecord(entry) || typeof entry.path !== 'string' || typeof entry.previewUrl !== 'string' || typeof entry.downloadUrl !== 'string') continue
|
|
278
|
+
map.set(entry.path, entry as unknown as ArtifactGrant)
|
|
279
|
+
}
|
|
280
|
+
return map
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function artifactFrom(value: unknown): ArtifactDescriptor | undefined {
|
|
284
|
+
if (!isRecord(value)) return undefined
|
|
285
|
+
if (
|
|
286
|
+
typeof value.path !== 'string'
|
|
287
|
+
|| typeof value.filename !== 'string'
|
|
288
|
+
|| typeof value.mimeType !== 'string'
|
|
289
|
+
|| (value.kind !== 'image' && value.kind !== 'svg' && value.kind !== 'markdown' && value.kind !== 'json')
|
|
290
|
+
|| typeof value.description !== 'string'
|
|
291
|
+
|| typeof value.sourceTool !== 'string'
|
|
292
|
+
|| (value.previewIntent !== 'image' && value.previewIntent !== 'svg' && value.previewIntent !== 'text' && value.previewIntent !== 'download')
|
|
293
|
+
|| typeof value.bytes !== 'number'
|
|
294
|
+
) return undefined
|
|
295
|
+
return value as unknown as ArtifactDescriptor
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function collectArtifacts(value: unknown, found = new Map<string, ArtifactDescriptor>(), depth = 0): ArtifactDescriptor[] {
|
|
299
|
+
if (depth > 16) return [...found.values()]
|
|
300
|
+
const artifact = artifactFrom(value)
|
|
301
|
+
if (artifact !== undefined) {
|
|
302
|
+
found.set(artifact.path, artifact)
|
|
303
|
+
return [...found.values()]
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(value)) {
|
|
306
|
+
for (const entry of value) collectArtifacts(entry, found, depth + 1)
|
|
307
|
+
} else if (isRecord(value)) {
|
|
308
|
+
for (const entry of Object.values(value)) collectArtifacts(entry, found, depth + 1)
|
|
309
|
+
}
|
|
310
|
+
return [...found.values()]
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function numberOf(value: unknown): number | undefined {
|
|
314
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function stringOf(value: unknown): string | undefined {
|
|
318
|
+
return typeof value === 'string' ? value : undefined
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatBytes(bytes: number): string {
|
|
322
|
+
if (bytes < 1024) return `${bytes} B`
|
|
323
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
324
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function boxText(value: unknown): string {
|
|
328
|
+
if (!isRecord(value)) return '—'
|
|
329
|
+
const parts = ['x1', 'y1', 'x2', 'y2'].map(key => numberOf(value[key]))
|
|
330
|
+
return parts.every(part => part !== undefined) ? parts.join(', ') : '—'
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function statusText(block: ToolCallBlock, t: Translate): string | undefined {
|
|
334
|
+
if (!('kind' in block)) return t('running')
|
|
335
|
+
if (block.isError) return textOfContent(block).split('\n')[0] || t('failed')
|
|
336
|
+
return undefined
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function VisionIcon({ kind = 'scan' }: { kind?: 'scan' | 'target' | 'layers' | 'shape' | 'diff' | 'palette' }) {
|
|
340
|
+
const path = kind === 'target'
|
|
341
|
+
? 'M8 2v2m0 8v2M2 8h2m8 0h2M5 5h6v6H5z'
|
|
342
|
+
: kind === 'layers'
|
|
343
|
+
? 'm3 6 5-3 5 3-5 3-5-3Zm0 3 5 3 5-3M3 12l5 3 5-3'
|
|
344
|
+
: kind === 'shape'
|
|
345
|
+
? 'M3 12 6 4l7-1-1 7-9 2Zm3-8 6 6'
|
|
346
|
+
: kind === 'diff'
|
|
347
|
+
? 'M3 3h4v4H3V3Zm6 6h4v4H9V9Zm0-6h4M3 11h4'
|
|
348
|
+
: kind === 'palette'
|
|
349
|
+
? 'M8 2a6 6 0 1 0 0 12h1.2a1.3 1.3 0 0 0 0-2.6H8a1.5 1.5 0 0 1 0-3h3.5A2.5 2.5 0 0 0 14 5.9C13.2 3.6 10.9 2 8 2Z'
|
|
350
|
+
: 'M3 5V3h2M11 3h2v2M13 11v2h-2M5 13H3v-2M5 8h6'
|
|
351
|
+
return (
|
|
352
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.35" strokeLinecap="round" strokeLinejoin="round">
|
|
353
|
+
<path d={path} />
|
|
354
|
+
</svg>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function ToolShell({
|
|
359
|
+
block, title, summary, icon, children, t,
|
|
360
|
+
}: {
|
|
361
|
+
block: ToolCallBlock
|
|
362
|
+
title: string
|
|
363
|
+
summary?: string | undefined
|
|
364
|
+
icon: ReactNode
|
|
365
|
+
children?: ReactNode | undefined
|
|
366
|
+
t: Translate
|
|
367
|
+
}) {
|
|
368
|
+
const [open, setOpen] = useState(true)
|
|
369
|
+
const status = statusText(block, t)
|
|
370
|
+
const expandable = children !== undefined && children !== null
|
|
371
|
+
return (
|
|
372
|
+
<section className="dvt-tool" data-state={!('kind' in block) ? 'running' : block.isError ? 'error' : 'success'}>
|
|
373
|
+
<button type="button" className="dvt-tool-head" onClick={() => { if (expandable) setOpen(value => !value) }} aria-expanded={expandable ? open : undefined}>
|
|
374
|
+
<span className="dvt-tool-icon">{icon}</span>
|
|
375
|
+
<span className="dvt-tool-title">{title}</span>
|
|
376
|
+
{summary !== undefined && summary.length > 0 ? <span className="dvt-tool-sep" aria-hidden="true">·</span> : null}
|
|
377
|
+
{summary !== undefined ? <span className="dvt-tool-summary">{summary}</span> : null}
|
|
378
|
+
{status !== undefined ? <span className="dvt-tool-status">{status}</span> : null}
|
|
379
|
+
{expandable ? <span className="dvt-chevron" data-open={open || undefined}>⌄</span> : null}
|
|
380
|
+
</button>
|
|
381
|
+
{expandable && open ? <div className="dvt-tool-body">{children}</div> : null}
|
|
382
|
+
</section>
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function ArtifactActions({ artifact, grant, openFile, t }: {
|
|
387
|
+
artifact: ArtifactDescriptor
|
|
388
|
+
grant?: ArtifactGrant | undefined
|
|
389
|
+
openFile: (path: string) => void
|
|
390
|
+
t: Translate
|
|
391
|
+
}) {
|
|
392
|
+
return (
|
|
393
|
+
<div className="dvt-actions">
|
|
394
|
+
<Button size="sm" variant="outline" onClick={() => { openFile(artifact.path) }}>{t('open')}</Button>
|
|
395
|
+
{grant === undefined ? null : <a className="dvt-download" href={grant.downloadUrl} download={artifact.filename}>{t('download')}</a>}
|
|
396
|
+
</div>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ArtifactPreview({ artifact, grant, openFile, t }: {
|
|
401
|
+
artifact: ArtifactDescriptor
|
|
402
|
+
grant?: ArtifactGrant | undefined
|
|
403
|
+
openFile: (path: string) => void
|
|
404
|
+
t: Translate
|
|
405
|
+
}) {
|
|
406
|
+
const canPreview = grant !== undefined && (artifact.kind === 'image' || artifact.kind === 'svg')
|
|
407
|
+
return (
|
|
408
|
+
<article className="dvt-artifact">
|
|
409
|
+
{canPreview
|
|
410
|
+
? artifact.kind === 'svg'
|
|
411
|
+
? <iframe className="dvt-preview dvt-svg" sandbox="" src={grant.previewUrl} title={artifact.description} />
|
|
412
|
+
: <img className="dvt-preview" src={grant.previewUrl} alt={artifact.description} loading="lazy" />
|
|
413
|
+
: null}
|
|
414
|
+
<div className="dvt-artifact-meta">
|
|
415
|
+
<div>
|
|
416
|
+
<strong>{artifact.filename}</strong>
|
|
417
|
+
<span>{artifact.description}</span>
|
|
418
|
+
<small>{artifact.mimeType} · {formatBytes(artifact.bytes)}</small>
|
|
419
|
+
</div>
|
|
420
|
+
<ArtifactActions artifact={artifact} grant={grant} openFile={openFile} t={t} />
|
|
421
|
+
</div>
|
|
422
|
+
{!canPreview && grant === undefined ? <p className="dvt-muted">{t('previewUnavailable')}</p> : null}
|
|
423
|
+
</article>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
type ViewProps = ToolCallViewProps & { t?: Translate }
|
|
428
|
+
|
|
429
|
+
function GroundView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
430
|
+
const value = decodeVisionResult(block)
|
|
431
|
+
const matches = Array.isArray(value?.matches) ? value.matches.filter(isRecord) : []
|
|
432
|
+
const target = stringOf(value?.target) ?? 'Ground'
|
|
433
|
+
const width = numberOf(value?.imageWidth)
|
|
434
|
+
const height = numberOf(value?.imageHeight)
|
|
435
|
+
const preview = artifactFrom(value?.preview)
|
|
436
|
+
const grants = accessMap(value)
|
|
437
|
+
return (
|
|
438
|
+
<ToolShell block={block} title="Ground" summary={matches.length > 0 ? `${target} · ${matches.length} ${t('matches')}` : target} icon={<VisionIcon kind="target" />} t={t}>
|
|
439
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
440
|
+
<div className="dvt-stack">
|
|
441
|
+
<div className="dvt-metrics">
|
|
442
|
+
<div><span>{t('dimensions')}</span><strong>{width ?? '—'} × {height ?? '—'}</strong></div>
|
|
443
|
+
<div><span>{t('coordinates')}</span><strong>{matches[0] === undefined ? '—' : boxText(matches[0].box)}</strong></div>
|
|
444
|
+
</div>
|
|
445
|
+
{matches.length > 1 ? (
|
|
446
|
+
<ol className="dvt-list">{matches.map((match, index) => <li key={index}><span>{stringOf(match.label) ?? `#${index + 1}`}</span><code>{boxText(match.box)}</code></li>)}</ol>
|
|
447
|
+
) : null}
|
|
448
|
+
{preview === undefined ? null : <ArtifactPreview artifact={preview} grant={grants.get(preview.path)} openFile={openFile} t={t} />}
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
</ToolShell>
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function DetectView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
456
|
+
const value = decodeVisionResult(block)
|
|
457
|
+
const elements = Array.isArray(value?.elements) ? value.elements.filter(isRecord) : []
|
|
458
|
+
const width = numberOf(value?.imageWidth)
|
|
459
|
+
const height = numberOf(value?.imageHeight)
|
|
460
|
+
const preview = artifactFrom(value?.preview)
|
|
461
|
+
const grants = accessMap(value)
|
|
462
|
+
return (
|
|
463
|
+
<ToolShell block={block} title="Detect" summary={`${elements.length} ${t('elements')}`} icon={<VisionIcon kind="layers" />} t={t}>
|
|
464
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
465
|
+
<div className="dvt-stack">
|
|
466
|
+
<div className="dvt-metrics">
|
|
467
|
+
<div><span>{t('dimensions')}</span><strong>{width ?? '—'} × {height ?? '—'}</strong></div>
|
|
468
|
+
<div><span>{t('elements')}</span><strong>{elements.length}</strong></div>
|
|
469
|
+
</div>
|
|
470
|
+
<div className="dvt-table-wrap"><table className="dvt-table"><thead><tr><th>#</th><th>Label</th><th>{t('coordinates')}</th></tr></thead><tbody>
|
|
471
|
+
{elements.map((element, index) => <tr key={index}><td>{numberOf(element.index) ?? index + 1}</td><td>{stringOf(element.label) ?? '—'}</td><td><code>{boxText(element.box)}</code></td></tr>)}
|
|
472
|
+
</tbody></table></div>
|
|
473
|
+
{preview === undefined ? null : <ArtifactPreview artifact={preview} grant={grants.get(preview.path)} openFile={openFile} t={t} />}
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
</ToolShell>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function TraceView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
481
|
+
const value = decodeVisionResult(block)
|
|
482
|
+
const artifact = artifactFrom(value?.artifact)
|
|
483
|
+
const geometry = isRecord(value?.geometry) ? value.geometry : undefined
|
|
484
|
+
const summary = geometry === undefined ? undefined : `${numberOf(geometry.pathCount) ?? 0} paths · ${formatBytes(numberOf(geometry.bytes) ?? 0)}`
|
|
485
|
+
const grants = accessMap(value)
|
|
486
|
+
return (
|
|
487
|
+
<ToolShell block={block} title="Trace SVG" summary={summary} icon={<VisionIcon kind="shape" />} t={t}>
|
|
488
|
+
{artifact === undefined ? <p className="dvt-muted">{t('noResult')}</p> : <ArtifactPreview artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />}
|
|
489
|
+
</ToolShell>
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function PixelDiffView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
494
|
+
const value = decodeVisionResult(block)
|
|
495
|
+
const pct = numberOf(value?.overallDifferencePct)
|
|
496
|
+
const regions = Array.isArray(value?.worstRegions) ? value.worstRegions.filter(isRecord) : []
|
|
497
|
+
const heatmap = artifactFrom(value?.heatmap)
|
|
498
|
+
const report = artifactFrom(value?.report)
|
|
499
|
+
const grants = accessMap(value)
|
|
500
|
+
return (
|
|
501
|
+
<ToolShell block={block} title="Pixel Diff" summary={pct === undefined ? undefined : `${pct.toFixed(3)}%`} icon={<VisionIcon kind="diff" />} t={t}>
|
|
502
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
503
|
+
<div className="dvt-stack">
|
|
504
|
+
<div className="dvt-diff-score"><span>{t('difference')}</span><strong>{pct?.toFixed(4) ?? '—'}%</strong><div><i style={{ width: `${Math.min(100, Math.max(0, pct ?? 0))}%` }} /></div></div>
|
|
505
|
+
{regions.length === 0 ? null : <div><h4>{t('worstRegions')}</h4><ol className="dvt-list">{regions.map((region, index) => <li key={index}><span>{(numberOf(region.differencePct) ?? 0).toFixed(3)}%</span><code>{boxText(region.box)}</code></li>)}</ol></div>}
|
|
506
|
+
{heatmap === undefined ? null : <ArtifactPreview artifact={heatmap} grant={grants.get(heatmap.path)} openFile={openFile} t={t} />}
|
|
507
|
+
{report === undefined ? null : <ArtifactPreview artifact={report} grant={grants.get(report.path)} openFile={openFile} t={t} />}
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
</ToolShell>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function ArtifactView({ block, openFile, toolName, t = key => en[key] }: ViewProps) {
|
|
515
|
+
const value = decodeVisionResult(block)
|
|
516
|
+
const artifacts = collectArtifacts(value)
|
|
517
|
+
const grants = accessMap(value)
|
|
518
|
+
const title = toolName === 'vision_crop' ? 'Crop'
|
|
519
|
+
: toolName === 'vision_long_screenshot_ocr' ? 'Long OCR'
|
|
520
|
+
: toolName === 'vision_extract_foreground' ? 'Extract Foreground'
|
|
521
|
+
: toolName === 'vision_html_screenshot' ? 'HTML Screenshot'
|
|
522
|
+
: 'Vision Artifact'
|
|
523
|
+
return (
|
|
524
|
+
<ToolShell block={block} title={title} summary={artifacts.length > 0 ? `${artifacts.length} ${t('artifacts')}` : undefined} icon={<VisionIcon />} t={t}>
|
|
525
|
+
{artifacts.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-stack">{artifacts.map(artifact => <ArtifactPreview key={artifact.path} artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />)}</div>}
|
|
526
|
+
</ToolShell>
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function PaletteView({ block, t = key => en[key] }: ViewProps) {
|
|
531
|
+
const value = decodeVisionResult(block)
|
|
532
|
+
const analysis = isRecord(value?.analysis) ? value.analysis : undefined
|
|
533
|
+
const colors = Array.isArray(analysis?.colors) ? analysis.colors.filter(isRecord) : []
|
|
534
|
+
return (
|
|
535
|
+
<ToolShell block={block} title="Dominant Colors" summary={`${colors.length} ${t('colors')}`} icon={<VisionIcon kind="palette" />} t={t}>
|
|
536
|
+
{colors.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-palette">{colors.map((color, index) => {
|
|
537
|
+
const hex = stringOf(color.color) ?? '#000000'
|
|
538
|
+
const share = numberOf(color.sharePct)
|
|
539
|
+
return <div key={`${hex}-${index}`}><i style={{ background: hex }} /><span><strong>{hex}</strong><small>{share === undefined ? '' : `${share.toFixed(2)}%`}</small></span></div>
|
|
540
|
+
})}</div>}
|
|
541
|
+
</ToolShell>
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function apiRequest<T>(init?: RequestInit): Promise<T> {
|
|
546
|
+
const response = await fetch(SETTINGS_ROUTE, { credentials: 'same-origin', ...init })
|
|
547
|
+
const body = await response.json() as ApiSuccess<T> | ApiFailure
|
|
548
|
+
if (!response.ok || !body.ok) {
|
|
549
|
+
const failure = body as ApiFailure
|
|
550
|
+
throw new Error(failure.error?.message ?? `Vision Toolkit request failed with HTTP ${response.status}`)
|
|
551
|
+
}
|
|
552
|
+
return body.value
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
interface SettingsState {
|
|
556
|
+
status: 'idle' | 'loading' | 'ready' | 'error'
|
|
557
|
+
snapshot?: SettingsSnapshot | undefined
|
|
558
|
+
health?: HealthResult | undefined
|
|
559
|
+
action?: 'save' | 'health' | 'connection' | undefined
|
|
560
|
+
message?: string | undefined
|
|
561
|
+
error?: string | undefined
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Small external store shared by the Settings route and pushed invalidations. */
|
|
565
|
+
export class VisionSettingsController {
|
|
566
|
+
private state: SettingsState = { status: 'idle' }
|
|
567
|
+
private listeners = new Set<() => void>()
|
|
568
|
+
private generation = 0
|
|
569
|
+
|
|
570
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
571
|
+
this.listeners.add(listener)
|
|
572
|
+
return () => { this.listeners.delete(listener) }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
snapshot = (): SettingsState => this.state
|
|
576
|
+
|
|
577
|
+
private set(next: SettingsState): void {
|
|
578
|
+
this.state = next
|
|
579
|
+
for (const listener of this.listeners) listener()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async load(): Promise<void> {
|
|
583
|
+
const generation = ++this.generation
|
|
584
|
+
this.set({ ...this.state, status: 'loading', error: undefined, message: undefined })
|
|
585
|
+
try {
|
|
586
|
+
const snapshot = await apiRequest<SettingsSnapshot>()
|
|
587
|
+
if (generation !== this.generation) return
|
|
588
|
+
this.set({ status: 'ready', snapshot, health: this.state.health })
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (generation !== this.generation) return
|
|
591
|
+
this.set({ ...this.state, status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
refreshIfLoaded(): void {
|
|
596
|
+
if (this.state.status === 'idle' || this.state.action === 'save') return
|
|
597
|
+
void this.load()
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async save(value: SettingsValue, expectedRevision: number): Promise<void> {
|
|
601
|
+
this.set({ ...this.state, action: 'save', error: undefined, message: undefined })
|
|
602
|
+
try {
|
|
603
|
+
const snapshot = await apiRequest<SettingsSnapshot>({
|
|
604
|
+
method: 'POST',
|
|
605
|
+
headers: { 'Content-Type': 'application/json' },
|
|
606
|
+
body: JSON.stringify({ action: 'save', expectedRevision, value }),
|
|
607
|
+
})
|
|
608
|
+
this.set({ status: 'ready', snapshot, health: this.state.health, message: 'saved' })
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async runHealth(testConnection: boolean): Promise<void> {
|
|
615
|
+
this.set({ ...this.state, action: testConnection ? 'connection' : 'health', error: undefined, message: undefined })
|
|
616
|
+
try {
|
|
617
|
+
const health = await apiRequest<HealthResult>({
|
|
618
|
+
method: 'POST',
|
|
619
|
+
headers: { 'Content-Type': 'application/json' },
|
|
620
|
+
body: JSON.stringify({ action: 'health', testConnection }),
|
|
621
|
+
})
|
|
622
|
+
this.set({ ...this.state, action: undefined, health })
|
|
623
|
+
} catch (error) {
|
|
624
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
interface Draft {
|
|
630
|
+
baseUrl: string
|
|
631
|
+
credential: string
|
|
632
|
+
model: string
|
|
633
|
+
language: 'zh' | 'en'
|
|
634
|
+
timeoutMs: string
|
|
635
|
+
maxImageBytes: string
|
|
636
|
+
maxImagePixels: string
|
|
637
|
+
concurrency: string
|
|
638
|
+
runtimeMode: 'managed' | 'external'
|
|
639
|
+
toolkitPath: string
|
|
640
|
+
python: string
|
|
641
|
+
allowedDirs: string
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function draftOf(value: SettingsValue): Draft {
|
|
645
|
+
return {
|
|
646
|
+
baseUrl: value.provider?.baseUrl ?? 'https://api.inferera.com/v1',
|
|
647
|
+
credential: value.provider?.credential ?? 'VISION_API_KEY',
|
|
648
|
+
model: value.provider?.model ?? 'gemini-3.6-flash',
|
|
649
|
+
language: value.language ?? 'zh',
|
|
650
|
+
timeoutMs: String(value.timeoutMs ?? 60000),
|
|
651
|
+
maxImageBytes: String(value.maxImageBytes ?? 10485760),
|
|
652
|
+
maxImagePixels: String(value.maxImagePixels ?? 40000000),
|
|
653
|
+
concurrency: String(value.concurrency ?? 4),
|
|
654
|
+
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
655
|
+
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
656
|
+
python: value.runtime?.python ?? '',
|
|
657
|
+
allowedDirs: (value.allowedDirs ?? []).join('\n'),
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function positiveInteger(raw: string, label: string): number {
|
|
662
|
+
const value = Number(raw)
|
|
663
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`)
|
|
664
|
+
return value
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function valueOf(draft: Draft): SettingsValue {
|
|
668
|
+
return {
|
|
669
|
+
provider: { baseUrl: draft.baseUrl.trim(), credential: draft.credential.trim(), model: draft.model.trim() },
|
|
670
|
+
language: draft.language,
|
|
671
|
+
timeoutMs: positiveInteger(draft.timeoutMs, 'timeoutMs'),
|
|
672
|
+
maxImageBytes: positiveInteger(draft.maxImageBytes, 'maxImageBytes'),
|
|
673
|
+
maxImagePixels: positiveInteger(draft.maxImagePixels, 'maxImagePixels'),
|
|
674
|
+
concurrency: positiveInteger(draft.concurrency, 'concurrency'),
|
|
675
|
+
runtime: {
|
|
676
|
+
mode: draft.runtimeMode,
|
|
677
|
+
...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
|
|
678
|
+
...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
|
|
679
|
+
},
|
|
680
|
+
allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
interface SettingsInjected {
|
|
685
|
+
controller: VisionSettingsController
|
|
686
|
+
t: Translate
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
type SettingsProps = PropsRuntime<'settings.section'> & Partial<SettingsInjected>
|
|
690
|
+
|
|
691
|
+
function Field({ label, children, hint }: { label: string; children: ReactNode; hint?: string | undefined }) {
|
|
692
|
+
return <label className="dvt-field"><span>{label}</span>{children}{hint === undefined ? null : <small>{hint}</small>}</label>
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function SettingsSection({ controller, t }: SettingsProps) {
|
|
696
|
+
if (controller === undefined || t === undefined) return null
|
|
697
|
+
return <LoadedSettings controller={controller} t={t} />
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
701
|
+
const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot)
|
|
702
|
+
const snapshot = state.snapshot
|
|
703
|
+
const [draft, setDraft] = useState<Draft | undefined>(undefined)
|
|
704
|
+
const [draftError, setDraftError] = useState<string | undefined>(undefined)
|
|
705
|
+
|
|
706
|
+
useEffect(() => { if (state.status === 'idle') void controller.load() }, [controller, state.status])
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (snapshot !== undefined) setDraft(draftOf(snapshot.settings.value))
|
|
709
|
+
}, [snapshot])
|
|
710
|
+
|
|
711
|
+
if (state.status === 'idle' || (state.status === 'loading' && snapshot === undefined)) {
|
|
712
|
+
return <div className="dvt-settings"><div className="dvt-loading">{t('testing')}</div></div>
|
|
713
|
+
}
|
|
714
|
+
if (snapshot === undefined || draft === undefined) {
|
|
715
|
+
return <div className="dvt-settings"><div className="dvt-alert error">{state.error ?? t('runtimeUnavailable')}</div><Button variant="outline" onClick={() => { void controller.load() }}>{t('retry')}</Button></div>
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const update = <K extends keyof Draft>(key: K, value: Draft[K]): void => setDraft(current => current === undefined ? current : { ...current, [key]: value })
|
|
719
|
+
const save = (): void => {
|
|
720
|
+
try {
|
|
721
|
+
setDraftError(undefined)
|
|
722
|
+
void controller.save(valueOf(draft), snapshot.settings.revision)
|
|
723
|
+
} catch (error) {
|
|
724
|
+
setDraftError(error instanceof Error ? error.message : String(error))
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const busy = state.action !== undefined
|
|
728
|
+
const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable')
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<div className="dvt-settings">
|
|
732
|
+
<header className="dvt-settings-header">
|
|
733
|
+
<div><span className="dvt-kicker">DSH native plugin</span><h2>{t('settingsTitle')}</h2><p>{t('settingsIntro')}</p></div>
|
|
734
|
+
<div className="dvt-release"><span>{t('pluginVersion')} <strong>{snapshot.release.pluginVersion}</strong></span><span>{t('upstreamVersion')} <strong>{snapshot.release.upstreamVersion}</strong></span><span>{t('activeGeneration')} <strong>{snapshot.runtime.generation}</strong></span></div>
|
|
735
|
+
</header>
|
|
736
|
+
<div className="dvt-alert notice">{t('externalNotice')}</div>
|
|
737
|
+
{!snapshot.writable ? <div className="dvt-alert warning">{t('readOnly')}</div> : null}
|
|
738
|
+
{draftError === undefined ? null : <div className="dvt-alert error">{draftError}</div>}
|
|
739
|
+
{state.error === undefined ? null : <div className="dvt-alert error">{state.error}</div>}
|
|
740
|
+
{state.message === 'saved' ? <div className="dvt-alert success">{t('saved')}</div> : null}
|
|
741
|
+
{snapshot.runtime.lastError === undefined ? null : <div className="dvt-alert error"><strong>{runtimeErrorTitle}</strong><span>{snapshot.runtime.lastError}</span></div>}
|
|
742
|
+
|
|
743
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('provider')}</h3><span className={`dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`}>{snapshot.credential.configured ? t('configured') : t('missing')}</span></div>
|
|
744
|
+
<div className="dvt-form-grid">
|
|
745
|
+
<Field label={t('baseUrl')}><Input value={draft.baseUrl} onChange={(event) => { update('baseUrl', event.target.value) }} /></Field>
|
|
746
|
+
<Field label={t('model')}><Input value={draft.model} onChange={(event) => { update('model', event.target.value) }} /></Field>
|
|
747
|
+
<Field label={t('credential')} hint={snapshot.credential.source === undefined ? undefined : `${t('source')}: ${snapshot.credential.source}`}><Input value={draft.credential} onChange={(event) => { update('credential', event.target.value) }} /></Field>
|
|
748
|
+
<Field label={t('language')}><select value={draft.language} onChange={(event) => { update('language', event.target.value as 'zh' | 'en') }}><option value="zh">中文</option><option value="en">English</option></select></Field>
|
|
749
|
+
</div>
|
|
750
|
+
</section>
|
|
751
|
+
|
|
752
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('limits')}</h3></div><div className="dvt-form-grid">
|
|
753
|
+
<Field label={t('timeout')}><Input inputMode="numeric" value={draft.timeoutMs} onChange={(event) => { update('timeoutMs', event.target.value) }} /></Field>
|
|
754
|
+
<Field label={t('maxBytes')}><Input inputMode="numeric" value={draft.maxImageBytes} onChange={(event) => { update('maxImageBytes', event.target.value) }} /></Field>
|
|
755
|
+
<Field label={t('maxPixels')}><Input inputMode="numeric" value={draft.maxImagePixels} onChange={(event) => { update('maxImagePixels', event.target.value) }} /></Field>
|
|
756
|
+
<Field label={t('concurrency')}><Input inputMode="numeric" value={draft.concurrency} onChange={(event) => { update('concurrency', event.target.value) }} /></Field>
|
|
757
|
+
</div></section>
|
|
758
|
+
|
|
759
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source ?? 'ready' : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
|
|
760
|
+
<Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">managed</option><option value="external">external</option></select></Field>
|
|
761
|
+
{draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
|
|
762
|
+
<Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
|
|
763
|
+
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
764
|
+
</div>
|
|
765
|
+
{snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
|
|
766
|
+
</section>
|
|
767
|
+
|
|
768
|
+
<div className="dvt-save-row"><Button variant="primary" disabled={!snapshot.writable || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button><Button variant="outline" disabled={busy} onClick={() => { void controller.load() }}>{t('reload')}</Button></div>
|
|
769
|
+
|
|
770
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('health')}</h3><p>{t('connectionHint')}</p></div><div className="dvt-actions"><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth(false) }}>{state.action === 'health' ? t('testing') : t('runHealth')}</Button><Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth(true) }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button></div></div>
|
|
771
|
+
{state.health === undefined ? <p className="dvt-muted">{t('notTested')}</p> : <div className="dvt-health-grid">{Object.entries(state.health.checks).map(([name, check]) => <div key={name} data-status={check.status}><span>{name}</span><strong>{check.status}</strong><p>{check.detail}</p></div>)}</div>}
|
|
772
|
+
</section>
|
|
773
|
+
</div>
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const CSS = `
|
|
778
|
+
.dvt-tool{margin:4px 0;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;box-shadow:var(--dsw-shadow-lv1)}
|
|
779
|
+
.dvt-tool-head{width:100%;min-height:38px;display:flex;align-items:center;gap:7px;padding:8px 10px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;font:inherit}.dvt-tool-head:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.dvt-tool-icon{width:20px;height:20px;display:grid;place-items:center;border-radius:6px;color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);flex:none}.dvt-tool-title{font-size:12px;font-weight:650;white-space:nowrap}.dvt-tool-sep{opacity:.35}.dvt-tool-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-secondary)}.dvt-tool-status{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-tool[data-state=error] .dvt-tool-status{color:var(--dsw-alias-state-error-primary)}.dvt-chevron{margin-left:auto;transition:transform .16s ease;opacity:.55}.dvt-chevron[data-open=true]{transform:rotate(180deg)}.dvt-tool-body{padding:0 10px 10px}.dvt-stack{display:grid;gap:10px}.dvt-muted{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
780
|
+
.dvt-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dvt-metrics>div,.dvt-diff-score{padding:10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);display:grid;gap:4px}.dvt-metrics span,.dvt-diff-score span{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-secondary)}.dvt-metrics strong,.dvt-diff-score strong{font-size:13px}.dvt-list{list-style:none;margin:0;padding:0;display:grid;gap:4px;max-height:160px;overflow:auto}.dvt-list li{display:flex;justify-content:space-between;gap:12px;padding:6px 8px;border-radius:7px;background:var(--dsw-alias-bg-layer-2);font-size:11px}.dvt-list code{color:var(--dsw-alias-state-business-primary)}.dvt-table-wrap{max-height:220px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-table{width:100%;border-collapse:collapse;font-size:11px}.dvt-table th,.dvt-table td{padding:7px 8px;text-align:left;border-bottom:1px solid var(--dsw-alias-border-l1)}.dvt-table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em}.dvt-table tr:last-child td{border-bottom:0}
|
|
781
|
+
.dvt-artifact{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}.dvt-preview{display:block;width:100%;max-height:360px;object-fit:contain;background:repeating-conic-gradient(var(--dsw-alias-bg-module-platform) 0 25%,var(--dsw-alias-bg-layer-1) 0 50%) 50%/18px 18px;border:0}.dvt-svg{height:280px}.dvt-artifact-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px}.dvt-artifact-meta>div:first-child{min-width:0;display:grid;gap:2px}.dvt-artifact-meta strong{font-size:12px;overflow:hidden;text-overflow:ellipsis}.dvt-artifact-meta span,.dvt-artifact-meta small{font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dvt-download{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);text-decoration:none;font-size:12px;font-weight:600}.dvt-download:hover{background:var(--dsw-alias-button-primary-hover)}.dvt-download:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.dvt-artifact>.dvt-muted{padding:0 10px 10px}.dvt-diff-score>div{height:5px;border-radius:99px;background:var(--dsw-alias-border-l2);overflow:hidden}.dvt-diff-score i{display:block;height:100%;min-width:2px;background:linear-gradient(90deg,var(--dsw-alias-state-warn-primary),var(--dsw-alias-state-error-primary));border-radius:99px}.dvt-tool h4{font-size:11px;margin:0 0 6px}.dvt-palette{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:7px}.dvt-palette>div{display:flex;align-items:center;gap:8px;padding:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-palette i{width:28px;height:28px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}.dvt-palette span{display:grid}.dvt-palette strong{font-size:11px}.dvt-palette small{font-size:10px;color:var(--dsw-alias-label-secondary)}
|
|
782
|
+
.dvt-settings{display:grid;gap:14px;max-width:900px;padding:8px 2px 32px;color:var(--dsw-alias-label-primary)}.dvt-settings-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:8px 2px}.dvt-settings-header h2{font-size:25px;letter-spacing:-.025em;margin:3px 0 6px}.dvt-settings-header p{max-width:620px;margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.55}.dvt-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dsw-alias-state-business-primary);font-weight:700}.dvt-release{display:grid;gap:4px;min-width:170px;padding:9px 11px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-release span{display:flex;justify-content:space-between;gap:12px}.dvt-release strong{color:var(--dsw-alias-label-primary)}.dvt-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5;display:grid;gap:3px}.dvt-alert.notice{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}.dvt-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-alert.success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-panel{display:grid;gap:12px;padding:15px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-panel-title{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.dvt-panel-title h3{font-size:14px;margin:0}.dvt-panel-title p{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary);margin:4px 0 0;max-width:620px}.dvt-badge{font-size:10px;padding:3px 7px;border-radius:999px;font-weight:650}.dvt-badge.ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-badge.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.dvt-field{display:grid;gap:6px;align-content:start}.dvt-field>span{font-size:11px;font-weight:600}.dvt-field>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dvt-field select,.dvt-field textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:8px 10px}.dvt-field select{height:36px}.dvt-field textarea{resize:vertical;min-height:76px}.dvt-runtime-facts{display:grid;gap:4px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:auto}.dvt-runtime-facts code{font-size:10px;white-space:nowrap;color:var(--dsw-alias-label-secondary)}.dvt-save-row{display:flex;gap:8px;padding:2px 0}
|
|
783
|
+
.dvt-health-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.dvt-health-grid>div{padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-border-l4)}.dvt-health-grid>div[data-status=ok]{border-left-color:var(--dsw-alias-state-success-primary)}.dvt-health-grid>div[data-status=warning],.dvt-health-grid>div[data-status=not_tested]{border-left-color:var(--dsw-alias-state-warn-primary)}.dvt-health-grid>div[data-status=error]{border-left-color:var(--dsw-alias-state-error-primary)}.dvt-health-grid span{font-size:10px;text-transform:capitalize}.dvt-health-grid strong{float:right;font-size:9px;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}.dvt-health-grid p{clear:both;margin:5px 0 0;font-size:10px;line-height:1.4;color:var(--dsw-alias-label-secondary)}.dvt-loading{padding:24px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
784
|
+
.dvt-paste-dock{box-sizing:border-box;width:calc(100% - 32px);max-width:var(--dsh-composer-card-max-width,960px);margin:0 auto;display:flex;flex-wrap:wrap;gap:6px;padding:0 2px 6px}.dvt-paste-chip{max-width:100%;height:32px;box-sizing:border-box;display:flex;align-items:center;gap:7px;padding:0 6px 0 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-specific-tip);font-size:12px}.dvt-paste-chip[data-status=copying]{border-color:var(--dsw-alias-state-business-primary)}.dvt-paste-chip[data-status=error]{border-color:var(--dsw-alias-state-error-primary)}.dvt-paste-name{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-detail{color:var(--dsw-alias-label-caption);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-chip[data-status=error] .dvt-paste-detail{color:var(--dsw-alias-state-error-primary)}.dvt-paste-chip button{width:20px;height:20px;display:grid;place-items:center;border:0;border-radius:50%;padding:0;background:transparent;color:var(--dsw-alias-label-caption);font:inherit;font-size:16px;cursor:pointer}.dvt-paste-chip button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dvt-paste-chip button:disabled{opacity:.4;cursor:default}
|
|
785
|
+
@media(max-width:720px){.dvt-settings-header{display:grid}.dvt-release{width:auto}.dvt-form-grid{grid-template-columns:1fr}.dvt-metrics{grid-template-columns:1fr}.dvt-artifact-meta{align-items:flex-start;flex-direction:column}.dvt-panel-title{flex-direction:column}}
|
|
786
|
+
`
|
|
787
|
+
|
|
788
|
+
function installStyles(): () => void {
|
|
789
|
+
const id = '@anionex/dsh-vision-toolkit/client'
|
|
790
|
+
const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
|
|
791
|
+
if (existing !== null) return () => {}
|
|
792
|
+
const style = document.createElement('style')
|
|
793
|
+
style.dataset.plugin = '@anionex/dsh-vision-toolkit'
|
|
794
|
+
style.dataset.pluginCss = id
|
|
795
|
+
style.textContent = CSS
|
|
796
|
+
document.head.appendChild(style)
|
|
797
|
+
return () => { style.remove() }
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Required client services. The pasted-image codec attaches to either trigger-service generation after load. */
|
|
801
|
+
export const inject = ['slots', 'locale', 'remote', 'conversation', 'sessions']
|
|
802
|
+
|
|
803
|
+
/** Register dedicated Tool views and the Vision Settings section. */
|
|
804
|
+
export function apply(ctx: ClientContext): void {
|
|
805
|
+
ctx.effect(installStyles, 'dsh-vision-toolkit: styles')
|
|
806
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-vision-toolkit: locale')
|
|
807
|
+
installPasteImages(ctx)
|
|
808
|
+
const t = ctx.locale.bind(NS)
|
|
809
|
+
const injected = () => ({ t })
|
|
810
|
+
const entries: Array<[string, (props: ViewProps) => ReactNode]> = [
|
|
811
|
+
['vision_ground', GroundView],
|
|
812
|
+
['vision_detect', DetectView],
|
|
813
|
+
['vision_trace', TraceView],
|
|
814
|
+
['vision_pixel_diff', PixelDiffView],
|
|
815
|
+
['vision_crop', ArtifactView],
|
|
816
|
+
['vision_long_screenshot_ocr', ArtifactView],
|
|
817
|
+
['vision_extract_foreground', ArtifactView],
|
|
818
|
+
['vision_html_screenshot', ArtifactView],
|
|
819
|
+
['vision_dominant_colors', PaletteView],
|
|
820
|
+
]
|
|
821
|
+
ctx.slots.inject('tool.call.toolview', function* () {
|
|
822
|
+
for (const [key, component] of entries) {
|
|
823
|
+
yield ctx.slots.register({ name: 'tool.call.toolview', key, inject: injected }, component)
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
const controller = new VisionSettingsController()
|
|
828
|
+
ctx.effect(() => {
|
|
829
|
+
const refreshSettings = (namespace: string): void => {
|
|
830
|
+
if (namespace === 'vision-toolkit') controller.refreshIfLoaded()
|
|
831
|
+
}
|
|
832
|
+
const refreshCredential = (ref: string): void => {
|
|
833
|
+
const current = controller.snapshot().snapshot
|
|
834
|
+
if (current?.credential.ref === ref) controller.refreshIfLoaded()
|
|
835
|
+
}
|
|
836
|
+
const legacyRemote = ctx.remote as typeof ctx.remote & {
|
|
837
|
+
$on?: (event: string, listener: (value: string) => void) => () => void
|
|
838
|
+
}
|
|
839
|
+
const currentEvents = ctx as unknown as {
|
|
840
|
+
on(event: 'settings/changed', listener: (namespace: string) => void): () => void
|
|
841
|
+
on(event: 'credentials/changed', listener: (ref: string) => void): () => void
|
|
842
|
+
}
|
|
843
|
+
const disposers = typeof legacyRemote.$on === 'function'
|
|
844
|
+
? [
|
|
845
|
+
legacyRemote.$on('settings/document-updated', refreshSettings),
|
|
846
|
+
legacyRemote.$on('credentials/updated', refreshCredential),
|
|
847
|
+
]
|
|
848
|
+
: [
|
|
849
|
+
currentEvents.on('settings/changed', (namespace) => {
|
|
850
|
+
refreshSettings(namespace)
|
|
851
|
+
}),
|
|
852
|
+
currentEvents.on('credentials/changed', (ref) => {
|
|
853
|
+
refreshCredential(ref)
|
|
854
|
+
}),
|
|
855
|
+
]
|
|
856
|
+
disposers.push(ctx.on('connection/reset', () => { controller.refreshIfLoaded() }))
|
|
857
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
858
|
+
}, 'dsh-vision-toolkit: Settings invalidations')
|
|
859
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
860
|
+
name: 'settings.section',
|
|
861
|
+
id: 'vision-toolkit',
|
|
862
|
+
order: 30,
|
|
863
|
+
label: () => t('nav'),
|
|
864
|
+
inject: () => ({ controller, t }),
|
|
865
|
+
}, SettingsSection))
|
|
866
|
+
}
|