dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.4
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/README.md +147 -19
- package/cordis.patch.yml +25 -10
- package/lib/client.js +6400 -3326
- package/lib/doc-page.js +7761 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +63 -11
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +7 -4
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +52 -14
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/convert/types.d.ts +2 -2
- package/lib/types/convert/types.d.ts.map +1 -1
- package/lib/types/convert/types.js +2 -2
- package/lib/types/convert/types.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +57 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +245 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +19 -2
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +95 -25
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +297 -166
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/extract-model.d.ts +67 -27
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js +225 -97
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract.d.ts +16 -0
- package/lib/types/extract.d.ts.map +1 -1
- package/lib/types/extract.js +19 -2
- package/lib/types/extract.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +11 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +41 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +466 -63
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/types.js +1 -1
- package/lib/types/intent/types.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +23 -12
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +37 -44
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/layout.d.ts +3 -3
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +4 -4
- package/lib/types/layout.js.map +1 -1
- package/lib/types/migrate.d.ts +1 -1
- package/lib/types/migrate.js +2 -2
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +99 -0
- package/lib/types/model-text.d.ts.map +1 -0
- package/lib/types/model-text.js +103 -0
- package/lib/types/model-text.js.map +1 -0
- package/lib/types/present.d.ts +239 -0
- package/lib/types/present.d.ts.map +1 -0
- package/lib/types/present.js +255 -0
- package/lib/types/present.js.map +1 -0
- package/lib/types/recall-rank.d.ts +37 -0
- package/lib/types/recall-rank.d.ts.map +1 -1
- package/lib/types/recall-rank.js +41 -1
- package/lib/types/recall-rank.js.map +1 -1
- package/lib/types/runtime.d.ts +564 -31
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +2645 -292
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +4 -2
- package/lib/types/settings-schema.d.ts.map +1 -1
- package/lib/types/settings-schema.js +2 -0
- package/lib/types/settings-schema.js.map +1 -1
- package/lib/types/source-images.d.ts +118 -0
- package/lib/types/source-images.d.ts.map +1 -0
- package/lib/types/source-images.js +288 -0
- package/lib/types/source-images.js.map +1 -0
- package/lib/types/types.d.ts +356 -28
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +112 -18
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +140 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -0
- package/lib/types/wiki-finalize.js +269 -0
- package/lib/types/wiki-finalize.js.map +1 -0
- package/lib/types/wiki-types.d.ts +10 -2
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +27 -30
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +25 -10
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/DrawerSection.tsx +18 -0
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +458 -158
- package/src/client/KnowledgePanel.tsx +44 -10
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/KnowledgeToolRow.tsx +331 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +178 -43
- package/src/client/RecallPanel.tsx +418 -127
- package/src/client/TypeDefinitionDrawer.tsx +154 -54
- package/src/client/WikiCardReview.tsx +266 -0
- package/src/client/WikiFlowCard.tsx +57 -95
- package/src/client/WikiTypeEditor.tsx +197 -95
- package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +25 -10
- package/src/client/architecture.tsx +66 -41
- package/src/client/asset-images.ts +125 -0
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +501 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +280 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +36 -4
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +242 -0
- package/src/client/index.ts +18 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/problem-domains.ts +1 -1
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +397 -54
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +68 -9
- package/src/convert/ingest.ts +61 -18
- package/src/convert/types.ts +2 -2
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +251 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +29 -4
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +310 -173
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/extract-model.ts +254 -99
- package/src/extract.ts +35 -3
- package/src/fs-atomic.ts +41 -0
- package/src/index.ts +499 -62
- package/src/intent/types.ts +1 -1
- package/src/knowledge-bases.ts +50 -49
- package/src/layout.ts +4 -4
- package/src/migrate.ts +2 -2
- package/src/model-text.ts +126 -0
- package/src/present.ts +367 -0
- package/src/recall-rank.ts +57 -2
- package/src/runtime.ts +2802 -293
- package/src/settings-schema.ts +2 -0
- package/src/source-images.ts +326 -0
- package/src/types.ts +446 -45
- package/src/wiki-finalize.ts +310 -0
- package/src/wiki-types.ts +28 -32
|
@@ -13,7 +13,7 @@ import { LayoutPreview, useAgentLayout } from './DirectoryPicker.tsx'
|
|
|
13
13
|
import { AgentPresetCard, type AgentPresetRemote } from './AgentPresetCard.tsx'
|
|
14
14
|
import {
|
|
15
15
|
NODE_META, NODE_ORDER, NODE_STATUS_LABEL, noteCountOf, sourceCountOf,
|
|
16
|
-
type KnowledgeStats, type KnowledgeTab, type NodeId, type PanelTab, type Tone,
|
|
16
|
+
type KnowledgeStats, type KnowledgeTab, type NodeId, type PanelTab, type Tone, type WorkspaceTabKind,
|
|
17
17
|
} from './architecture.tsx'
|
|
18
18
|
import { AgentArchitectureGraph } from './arch/AgentArchitectureGraph.tsx'
|
|
19
19
|
import { NodeLogicPanel } from './arch/NodeLogicPanel.tsx'
|
|
@@ -24,13 +24,14 @@ import { AttributionTab } from './attribution/AttributionTab.tsx'
|
|
|
24
24
|
import { KnowledgeBaseTab } from './KnowledgeBaseTab.tsx'
|
|
25
25
|
import { IntentTab } from './IntentTab.tsx'
|
|
26
26
|
import { RecallPanel } from './RecallPanel.tsx'
|
|
27
|
+
import { readRemembered, remember } from './panel-state.ts'
|
|
27
28
|
import { styles, toneSuffix } from './styles.ts'
|
|
28
29
|
import '@tita-data-agent/ui/styles.css'
|
|
29
30
|
import './styles.css'
|
|
30
31
|
|
|
31
32
|
interface Props {
|
|
32
33
|
visible: boolean
|
|
33
|
-
onOpenTab?: (kind:
|
|
34
|
+
onOpenTab?: (kind: WorkspaceTabKind) => void
|
|
34
35
|
settings?: {
|
|
35
36
|
intent?: SettingsScopeLike
|
|
36
37
|
knowledge?: SettingsScopeLike
|
|
@@ -155,7 +156,7 @@ function EvalTab({ visible, settings, onSelect }: { visible: boolean; settings?:
|
|
|
155
156
|
<p className={styles.cardDescription}>评测集接入前,这一栏只声明要评什么;左边的单问题检查已经是真实链路。</p>
|
|
156
157
|
<div className={styles.evalRows}>
|
|
157
158
|
<div><span>编译正确性</span><strong>待运行</strong></div>
|
|
158
|
-
<div><span
|
|
159
|
+
<div><span>Hybrid Recall@10(关键词 + 向量)</span><strong>待运行</strong></div>
|
|
159
160
|
<div><span>引用可追溯性</span><strong>待运行</strong></div>
|
|
160
161
|
<div><span>多模态 Markdown</span><strong>待运行</strong></div>
|
|
161
162
|
<div><span>增量编译复用率</span><strong>待运行</strong></div>
|
|
@@ -167,7 +168,13 @@ function EvalTab({ visible, settings, onSelect }: { visible: boolean; settings?:
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props): ReactNode {
|
|
170
|
-
|
|
171
|
+
/**
|
|
172
|
+
* 一级 tab。**起始值来自记住的位置**(`panel-state.ts`):DSH 会重新挂载这个面板
|
|
173
|
+
* (刷新页面必然、换会话按宿主策略也会),只放在 `useState` 里的话,用户每次回来都
|
|
174
|
+
* 跳回「Agent 概览」。存的值先对着 `VIEW_ORDER` 校验 —— 上一版写下的旧 id 不能让面板
|
|
175
|
+
* 走进一个不存在的 tab。
|
|
176
|
+
*/
|
|
177
|
+
const [activeTab, setActiveTab] = useState<ViewId>(() => readRemembered('view', VIEW_ORDER) ?? 'overview')
|
|
171
178
|
const [stats, setStats] = useState<KnowledgeStats>({})
|
|
172
179
|
const [statsLoading, setStatsLoading] = useState(false)
|
|
173
180
|
const [statsError, setStatsError] = useState<string | null>(null)
|
|
@@ -230,9 +237,19 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
230
237
|
const selectNode = (id: NodeId | null): void => { setSelected(id); setSelectedDomain(null); setNotice(null) }
|
|
231
238
|
const selectDomain = (id: string | null): void => { setSelectedDomain(id); setSelected(null); setNotice(null) }
|
|
232
239
|
|
|
240
|
+
/**
|
|
241
|
+
* **换一级 tab 的唯一入口。** 三处都会换(tab 栏、节点面板的「去配置」、归因空态送人
|
|
242
|
+
* 去知识库录来源),每一处都顺手记一笔 —— 分头写的话,迟早有一处忘了记,
|
|
243
|
+
* 而那种漏表现为「从这个入口过去不记得、从那个入口过去记得」。
|
|
244
|
+
*/
|
|
245
|
+
const changeTab = useCallback((tab: ViewId): void => {
|
|
246
|
+
setActiveTab(tab)
|
|
247
|
+
remember('view', tab)
|
|
248
|
+
}, [])
|
|
249
|
+
|
|
233
250
|
/** 架构图节点 → 功能 tab。这是节点面板上「去配置」按钮唯一的落点。 */
|
|
234
251
|
const jumpTo = (tab: PanelTab, knowledgeTab?: KnowledgeTab): void => {
|
|
235
|
-
|
|
252
|
+
changeTab(tab)
|
|
236
253
|
if (tab === 'knowledge' && knowledgeTab) setKnowledgeFocus({ tab: knowledgeTab, nonce: Date.now() })
|
|
237
254
|
}
|
|
238
255
|
|
|
@@ -258,7 +275,7 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
258
275
|
|
|
259
276
|
return <section className={styles.panel} aria-label="Tita Data Agent">
|
|
260
277
|
{/* Tab 切换是面板外壳的一部分:它不参与 `.tita-content` 的滚动,永远停在顶部。 */}
|
|
261
|
-
<Tabs className={styles.panelTabs} value={activeTab} onValueChange={value =>
|
|
278
|
+
<Tabs className={styles.panelTabs} value={activeTab} onValueChange={value => changeTab(value as ViewId)}>
|
|
262
279
|
<div className={styles.tabBar}>
|
|
263
280
|
<TabsList className={styles.tabsList}>
|
|
264
281
|
{VIEW_ORDER.map(id => {
|
|
@@ -273,7 +290,10 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
273
290
|
><Settings2 size={15} /></Button>
|
|
274
291
|
</div>
|
|
275
292
|
|
|
276
|
-
|
|
293
|
+
{/* 两栏走「舞台」版式(自己占满高度、不参与面板滚动):智能归因是图 + 运行面板,
|
|
294
|
+
概览是画布 + 画布内浮层。同一套语汇,同一个理由 —— 它们的内容都要自己撑满
|
|
295
|
+
可视区,让面板替它们滚会把画布推成一条可以滚出视口的纸。 */}
|
|
296
|
+
<div className={cn(styles.content, (activeTab === 'attribution' || activeTab === 'overview') && styles.contentStage)}>
|
|
277
297
|
|
|
278
298
|
{/* 一级 tab 已经说明「现在是哪一页」,这里不再重复一遍标题与说明。
|
|
279
299
|
只有真的出问题时才占一行:读取失败 / 读取中。 */}
|
|
@@ -286,11 +306,14 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
286
306
|
<Button variant="ghost" size="icon" aria-label="关闭提示" title="关闭提示" onClick={() => setNotice(null)}><X size={14} /></Button>
|
|
287
307
|
</Card>}
|
|
288
308
|
|
|
289
|
-
<TabsContent value="overview">
|
|
309
|
+
<TabsContent value="overview" className={styles.stageContent}>
|
|
290
310
|
{/* 概览 = **一个画布**。0027 之前这里是「画布 + 右侧栏」,而问题域还收在栏底的
|
|
291
311
|
折叠区里 —— 三块内容、两条边框,读起来是三个东西。现在画布是四条带
|
|
292
312
|
(A/B/C 三条链路 + 一条问题域带),点节点或问题域牌子都在**画布内**的
|
|
293
313
|
浮层里展开详情:不占侧栏、不缩字、可关闭。
|
|
314
|
+
0050:这一栏是**一块带发丝线的画布,整图居中、不可拖拽** ——
|
|
315
|
+
两个方向都适应容器,所以「图被拖到一边」这种状态不再存在;
|
|
316
|
+
1:1 档放不下时由容器自己出滚动条。
|
|
294
317
|
设置不在这里:面板上唯一的设置入口在顶部 tab 栏最右边的齿轮里。 */}
|
|
295
318
|
<AgentArchitectureGraph
|
|
296
319
|
stats={stats} version={version} selected={selected}
|
|
@@ -312,7 +335,13 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
312
335
|
<IntentTab visible={visible && activeTab === 'intent'} intent={settings?.intent} />
|
|
313
336
|
</TabsContent>
|
|
314
337
|
|
|
315
|
-
|
|
338
|
+
{/* `forceMount`:知识库这一栏**不要在离开 tab 时卸载**。
|
|
339
|
+
它有一整套已经拉回来的状态(库列表、文件、卡片、图谱、统计),而 Radix 的
|
|
340
|
+
`TabsContent` 默认是卸载的 —— 于是每次从别的 tab 切回来,选择器先显示
|
|
341
|
+
「加载中…」、「类型定义」按钮跟着变灰,整栏白一下再长出来。那些数据并没有
|
|
342
|
+
过期,重拉一遍也不该把已经画好的东西先擦掉。
|
|
343
|
+
只有这一栏这么做:其余五栏拉的是各自独立的只读数据,卸载它们反而省事。 */}
|
|
344
|
+
<TabsContent value="knowledge" forceMount>
|
|
316
345
|
<KnowledgeBaseTab
|
|
317
346
|
visible={visible && activeTab === 'knowledge'}
|
|
318
347
|
onChanged={() => void refreshStats()}
|
|
@@ -323,7 +352,12 @@ export function KnowledgePanel({ visible, onOpenTab, settings, remote }: Props):
|
|
|
323
352
|
</TabsContent>
|
|
324
353
|
|
|
325
354
|
<TabsContent value="attribution" className={styles.stageContent}>
|
|
326
|
-
|
|
355
|
+
{/* 空态的主操作是「去知识库录来源」,所以这一栏要能把人送过去 ——
|
|
356
|
+
排查知识是从来源与卡片编译来的,它自己产不出第一份知识。 */}
|
|
357
|
+
<AttributionTab
|
|
358
|
+
visible={visible && activeTab === 'attribution'}
|
|
359
|
+
onOpenKnowledge={() => changeTab('knowledge')}
|
|
360
|
+
/>
|
|
327
361
|
</TabsContent>
|
|
328
362
|
|
|
329
363
|
<TabsContent value="memory">
|
|
@@ -39,6 +39,9 @@ export function KnowledgeStepRail({ tab, onChange, facts }: KnowledgeStepRailPro
|
|
|
39
39
|
type="button"
|
|
40
40
|
className={cn(styles.kbStep, on && styles.kbStepOn)}
|
|
41
41
|
aria-current={on ? 'step' : undefined}
|
|
42
|
+
// 第二行的说明在窄栏下会省略号截断(它是 `nowrap` + ellipsis),
|
|
43
|
+
// 所以完整那句挂在这里 —— 截断的信息要有地方读得到。
|
|
44
|
+
title={`${item.label}:${fact.value} · ${fact.note}`}
|
|
42
45
|
onClick={() => onChange(item.id)}
|
|
43
46
|
>
|
|
44
47
|
{/* 两行,不是一行挤四样东西。一行放「图标 + 名字 + 动作 + 计数」在宿主侧栏
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识工具在**对话流**里的卡片(0045)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要有它
|
|
5
|
+
*
|
|
6
|
+
* 0045 之前,`import_sources` / `recall_knowledge` 这类调用在对话里只有两样东西:工具名
|
|
7
|
+
* 和一坨 JSON。人看到的是「import_sources」加一屏花括号,于是「Agent 到底导了几份、
|
|
8
|
+
* 编出了几张卡、召回命中哪几条」只能自己去右边面板里找 —— 而右侧面板压根不知道
|
|
9
|
+
* 刚才那一次调用做了什么。
|
|
10
|
+
*
|
|
11
|
+
* 现在这一层注册的是平台的 `tool.call.toolview` 槽位,按**工具名**命中
|
|
12
|
+
* (`key: 'recall_knowledge'`),命中即**替换**默认的通用行。数据有两个来源:
|
|
13
|
+
*
|
|
14
|
+
* 1. **参数**(`block.argsRaw` / `phase === 'start'`):跑起来的那一刻就能显示
|
|
15
|
+
* 「导入 12 份来源并编译」,不必等结果;
|
|
16
|
+
* 2. **`block.meta`**:宿主侧 `presentationMeta` 产出的结构化账本(见 `src/present.ts`),
|
|
17
|
+
* 随会话日志落盘,所以**回放时与当时看到的一模一样**。
|
|
18
|
+
*
|
|
19
|
+
* ## 三条实现约束
|
|
20
|
+
*
|
|
21
|
+
* - 卡片长在**对话区**里,不是右侧面板里:排版密度、字号跟着对话走(13px / 24px 行高),
|
|
22
|
+
* 不套面板那套 11px 的小字。颜色仍然读同一份共享令牌(`--tita-*` 挂在 `:root, body`
|
|
23
|
+
* 上),所以深浅色主题自动一致。
|
|
24
|
+
* - **不猜**。meta 缺字段就少显示一行,不编造 0 或「未知」。宿主没给 meta(旧会话、或
|
|
25
|
+
* 调用在复合传输里跑的)时退回默认通用行 —— 由 `undefined` 返回触发。
|
|
26
|
+
* - 参数解析失败**不抛**:一条工具记录因为渲染报错整块消失,比显示得不好看严重得多。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ReactNode } from 'react'
|
|
30
|
+
import {
|
|
31
|
+
ChevronRight, Database, FileText, Layers3, RefreshCw, Search, TriangleAlert, X,
|
|
32
|
+
} from '@tita-data-agent/ui/icons'
|
|
33
|
+
import { Badge, cn } from '@tita-data-agent/ui/components'
|
|
34
|
+
|
|
35
|
+
/** 本插件注册了专用卡片的工具名 —— 与 `src/index.ts` 里的注册一一对应。 */
|
|
36
|
+
export const KNOWLEDGE_TOOL_KEYS = ['import_sources', 'recall_knowledge', 'rebuild_knowledge', 'add_source', 'propose_wiki_cards'] as const
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 槽位 props 的**结构性子集**。
|
|
40
|
+
*
|
|
41
|
+
* 平台把 `tool.call.toolview` 的完整类型声明在 `@deepseek-ai/dsh-client-ui-tool/client`,
|
|
42
|
+
* 而那个包不在本插件的依赖里(客户端只 inject 了 sidebar-right / slots / renderer /
|
|
43
|
+
* settings 四个)。为一个**纯类型**去加一条 peer dependency,代价是发布面与安装面都变大,
|
|
44
|
+
* 而这里真正用到的只有五个字段。
|
|
45
|
+
*
|
|
46
|
+
* 所以:自己声明这五个字段 + 把槽位登记进 `SlotMap`(下面那段 `declare module`),
|
|
47
|
+
* 形状以平台那份声明为准(`ToolCallCommonProps` + 三个阶段 props)。
|
|
48
|
+
* **平台那份一旦改名,这里会编译不过** —— 那正是我们想要的失败方式。
|
|
49
|
+
*/
|
|
50
|
+
export interface KnowledgeToolViewProps {
|
|
51
|
+
/** 三个阶段二选一:`preparing` 连参数都没有。 */
|
|
52
|
+
phase: 'preparing' | 'start' | 'result'
|
|
53
|
+
/** 平台派发用的工具名(本卡片的键)。 */
|
|
54
|
+
toolName: string
|
|
55
|
+
callId: string
|
|
56
|
+
block: {
|
|
57
|
+
/** 只有 `start` 阶段有完整参数。 */
|
|
58
|
+
argsRaw?: string
|
|
59
|
+
/** 结果阶段:模型可见的内容块。 */
|
|
60
|
+
content?: readonly unknown[]
|
|
61
|
+
isError?: boolean
|
|
62
|
+
/** 宿主 `presentationMeta` 落下来的账本。 */
|
|
63
|
+
meta?: unknown
|
|
64
|
+
}
|
|
65
|
+
/** 平台提供的展开状态钩子:**每个调用各自持有**,切轮次会被重置。 */
|
|
66
|
+
useDisclosure: () => { readonly expanded: boolean; readonly setExpanded: (open: boolean) => void; readonly toggle: () => void }
|
|
67
|
+
/** 在轨迹视图里定位这次调用(可能没有)。 */
|
|
68
|
+
inspect?: () => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 把槽位登记进平台的 `SlotMap`。
|
|
73
|
+
*
|
|
74
|
+
* 平台自己的声明在 `dsh-client-ui-tool/client` 里;这里补一份**同样的形状**,因为那个包
|
|
75
|
+
* 不在本插件的类型可见范围内。运行时不受影响 —— 派发按名字字符串走,这段只在编译期
|
|
76
|
+
* 让 `slots.register({ name: 'tool.call.toolview' })` 通过检查。
|
|
77
|
+
*/
|
|
78
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
79
|
+
interface SlotMap {
|
|
80
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: KnowledgeToolViewProps }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** meta 里那本账的最小形状。所有字段都可缺:旧会话、失败信封都可能是残的。 */
|
|
85
|
+
interface ToolMeta {
|
|
86
|
+
kind?: string
|
|
87
|
+
// 导入
|
|
88
|
+
attempted?: number
|
|
89
|
+
written?: Array<{ sourceId?: string; title?: string; bytes?: number }>
|
|
90
|
+
failed?: Array<{ index?: number; title?: string; code?: string; message?: string }>
|
|
91
|
+
compile?: { cardsCreated?: number; cardsUpdated?: number; cardsReused?: number; notesReused?: number; notesRecompiled?: number; sourcesExtracted?: number; sourcesSkipped?: number; embedding?: string; version?: number } | null
|
|
92
|
+
// 召回
|
|
93
|
+
query?: string
|
|
94
|
+
hits?: Array<{ entityId?: string; entityType?: string; title?: string; score?: number; rerankScore?: number; reviewScore?: number }>
|
|
95
|
+
contextTopN?: number
|
|
96
|
+
backend?: string
|
|
97
|
+
routes?: string[]
|
|
98
|
+
rerankStatus?: string
|
|
99
|
+
insufficientEvidence?: boolean
|
|
100
|
+
// 编译
|
|
101
|
+
only?: string
|
|
102
|
+
version?: number
|
|
103
|
+
cardsCreated?: number
|
|
104
|
+
cardsUpdated?: number
|
|
105
|
+
cardsReused?: number
|
|
106
|
+
notesReused?: number
|
|
107
|
+
notesRecompiled?: number
|
|
108
|
+
sourcesExtracted?: number
|
|
109
|
+
sourcesSkipped?: number
|
|
110
|
+
embedding?: string
|
|
111
|
+
skipped?: boolean
|
|
112
|
+
// 写卡
|
|
113
|
+
ids?: string[]
|
|
114
|
+
// 单份来源
|
|
115
|
+
sourceId?: string
|
|
116
|
+
path?: string
|
|
117
|
+
title?: string
|
|
118
|
+
sourceType?: string
|
|
119
|
+
bytes?: number
|
|
120
|
+
rejected?: Array<{ title?: string; reason?: string; code?: string }>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 参数:`phase === 'preparing'` 时还没有,别去猜。 */
|
|
124
|
+
function argsOf(block: unknown): Record<string, any> {
|
|
125
|
+
const raw = (block as { argsRaw?: unknown } | undefined)?.argsRaw
|
|
126
|
+
if (typeof raw !== 'string' || raw.trim() === '') return {}
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(raw)
|
|
129
|
+
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : {}
|
|
130
|
+
} catch {
|
|
131
|
+
return {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 模型可见的文本结果 —— 结果阶段之外用它兜底(也用于错误态)。 */
|
|
136
|
+
function textOf(block: unknown): string {
|
|
137
|
+
const content = (block as { content?: unknown } | undefined)?.content
|
|
138
|
+
if (!Array.isArray(content)) return ''
|
|
139
|
+
return content
|
|
140
|
+
.map(part => (part && typeof part === 'object' && (part as { type?: string }).type === 'text' ? String((part as { text?: unknown }).text ?? '') : ''))
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join('\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function short(text: string, max = 60): string {
|
|
146
|
+
const flat = text.replace(/\s+/g, ' ').trim()
|
|
147
|
+
return flat.length > max ? `${flat.slice(0, max)}…` : flat
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 标题与摘要:**先写这一下要动什么**,结果回来之后再补上产物。 */
|
|
151
|
+
function headOf(toolName: string, args: Record<string, any>, meta: ToolMeta | null): { icon: ReactNode; title: string; summary: string } {
|
|
152
|
+
const icon = (size = 14): ReactNode => {
|
|
153
|
+
switch (toolName) {
|
|
154
|
+
case 'recall_knowledge': return <Search size={size} />
|
|
155
|
+
case 'import_sources': return <Database size={size} />
|
|
156
|
+
case 'rebuild_knowledge': return <RefreshCw size={size} />
|
|
157
|
+
case 'propose_wiki_cards': return <Layers3 size={size} />
|
|
158
|
+
default: return <FileText size={size} />
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (toolName === 'recall_knowledge') {
|
|
162
|
+
const query = short(String(meta?.query ?? args?.query ?? ''), 36)
|
|
163
|
+
const hits = meta?.hits?.length
|
|
164
|
+
const summary = meta?.insufficientEvidence
|
|
165
|
+
? '没有内容越过相关性阈值'
|
|
166
|
+
: meta
|
|
167
|
+
? [hits !== undefined ? `命中 ${hits} 条` : '', meta.backend ? `${meta.backend}` : '', meta.contextTopN !== undefined ? `进上下文 ${meta.contextTopN} 条` : ''].filter(Boolean).join(' · ')
|
|
168
|
+
: '正在检索…'
|
|
169
|
+
return { icon: icon(), title: `召回「${query}」`, summary }
|
|
170
|
+
}
|
|
171
|
+
if (toolName === 'import_sources') {
|
|
172
|
+
const count = Array.isArray(args?.sources) ? args.sources.length : 0
|
|
173
|
+
const summary = meta
|
|
174
|
+
? [
|
|
175
|
+
`写入 ${meta.written?.length ?? 0} 份`,
|
|
176
|
+
meta.failed && meta.failed.length > 0 ? `失败 ${meta.failed.length} 份` : '',
|
|
177
|
+
meta.compile ? `新建 ${meta.compile.cardsCreated ?? 0} 张卡 · 并入 ${meta.compile.cardsUpdated ?? 0} 张` : '未编译',
|
|
178
|
+
].filter(Boolean).join(' · ')
|
|
179
|
+
: '正在写入并编译…'
|
|
180
|
+
return { icon: icon(), title: `导入 ${count} 份来源${args?.compile === false ? '' : '并编译'}`, summary }
|
|
181
|
+
}
|
|
182
|
+
if (toolName === 'rebuild_knowledge') {
|
|
183
|
+
const only = String(meta?.only ?? args?.only ?? 'all')
|
|
184
|
+
const what = only === 'all' ? '全链路' : only === 'wiki' ? 'wiki 与 wikiNotes' : only === 'embeddings' ? '向量索引' : '注册表'
|
|
185
|
+
const summary = meta
|
|
186
|
+
? meta.skipped
|
|
187
|
+
? '没有变化,全部复用'
|
|
188
|
+
: `新建 ${meta.cardsCreated ?? 0} · 并入 ${meta.cardsUpdated ?? 0} · 复用 ${meta.cardsReused ?? 0}${meta.version !== undefined ? ` · v${meta.version}` : ''}`
|
|
189
|
+
: '正在编译…'
|
|
190
|
+
return { icon: icon(), title: `编译知识库 · ${what}`, summary }
|
|
191
|
+
}
|
|
192
|
+
if (toolName === 'propose_wiki_cards') {
|
|
193
|
+
const count = Array.isArray(args?.proposals) ? args.proposals.length : 0
|
|
194
|
+
const summary = meta
|
|
195
|
+
? `入库 ${meta.ids?.length ?? 0} 张${meta.rejected && meta.rejected.length > 0 ? ` · 被拒 ${meta.rejected.length} 张` : ''}`
|
|
196
|
+
: '正在写入…'
|
|
197
|
+
return { icon: icon(), title: `写入 ${count} 张 wiki 卡片`, summary }
|
|
198
|
+
}
|
|
199
|
+
const title = args?.title ? short(String(args.title), 30) : '(未命名)'
|
|
200
|
+
const summary = meta
|
|
201
|
+
? `${meta.sourceType ?? 'document'} · ${meta.bytes ?? 0} 字符`
|
|
202
|
+
: '正在写入…'
|
|
203
|
+
return { icon: icon(), title: `写入来源「${title}」`, summary }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 展开之后的正文:**这一屏要回答「刚才到底发生了什么」**。 */
|
|
207
|
+
function DetailsOf({ toolName, meta, fallback }: { toolName: string; meta: ToolMeta | null; fallback: string }): ReactNode {
|
|
208
|
+
if (!meta) {
|
|
209
|
+
return fallback
|
|
210
|
+
? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
|
|
211
|
+
: null
|
|
212
|
+
}
|
|
213
|
+
if (meta.kind === 'recall') {
|
|
214
|
+
if (meta.insufficientEvidence) {
|
|
215
|
+
return <p className="text-[12px] leading-relaxed text-tita-ink-muted">库里没有一条越过相关性阈值 —— 这是结论,不是错误。把来源导进来再问一次。</p>
|
|
216
|
+
}
|
|
217
|
+
return <div className="flex flex-col gap-1">
|
|
218
|
+
{(meta.hits ?? []).map((hit, index) => {
|
|
219
|
+
const score = typeof hit.rerankScore === 'number' ? hit.rerankScore : hit.score
|
|
220
|
+
return <div key={`${hit.entityType}:${hit.entityId}:${index}`} className="flex items-baseline gap-2 text-[12px]">
|
|
221
|
+
<span className="w-4 shrink-0 text-right tabular-nums text-tita-ink-faint">{index + 1}</span>
|
|
222
|
+
<span className="min-w-0 flex-1 truncate text-tita-ink" title={hit.title}>{hit.title || hit.entityId}</span>
|
|
223
|
+
<code className="shrink-0 font-mono text-[10.5px] text-tita-ink-faint">{hit.entityType}:{hit.entityId}</code>
|
|
224
|
+
<span className="w-12 shrink-0 text-right tabular-nums text-tita-ink-muted">{typeof score === 'number' ? score.toFixed(3) : '—'}</span>
|
|
225
|
+
{typeof hit.reviewScore === 'number' && <span className="shrink-0 text-[10.5px] text-tita-ink-faint">评分 {hit.reviewScore.toFixed(2)}</span>}
|
|
226
|
+
</div>
|
|
227
|
+
})}
|
|
228
|
+
{/* 路由与重排状态:它们解释「为什么是这个顺序」,所以和命中列在同一块里。 */}
|
|
229
|
+
<p className="text-[11px] text-tita-ink-faint">
|
|
230
|
+
{[meta.backend ? `检索 ${meta.backend}` : '', meta.routes && meta.routes.length > 0 ? `路由 ${meta.routes.join('+')}` : '', meta.rerankStatus ? `重排 ${meta.rerankStatus}` : ''].filter(Boolean).join(' · ')}
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
}
|
|
234
|
+
if (meta.kind === 'import') {
|
|
235
|
+
return <div className="flex flex-col gap-1.5">
|
|
236
|
+
<ul className="flex flex-col gap-0.5">
|
|
237
|
+
{(meta.written ?? []).map(item => <li key={item.sourceId} className="flex items-baseline gap-2 text-[12px]">
|
|
238
|
+
<FileText size={11} className="mt-1 shrink-0 text-tita-ink-faint" />
|
|
239
|
+
<span className="min-w-0 flex-1 truncate text-tita-ink" title={item.title}>{item.title || item.sourceId}</span>
|
|
240
|
+
<span className="shrink-0 tabular-nums text-tita-ink-faint">{item.bytes ?? 0} 字符</span>
|
|
241
|
+
</li>)}
|
|
242
|
+
</ul>
|
|
243
|
+
{(meta.failed ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
|
|
244
|
+
{(meta.failed ?? []).map(item => <li key={`${item.index}-${item.code}`} className="flex items-baseline gap-2 text-[12px] text-tita-danger">
|
|
245
|
+
<X size={11} className="mt-1 shrink-0" />
|
|
246
|
+
<span className="min-w-0 flex-1">第 {(item.index ?? 0) + 1} 份{item.title ? `「${short(item.title, 24)}」` : ''}:{item.message}</span>
|
|
247
|
+
<code className="shrink-0 font-mono text-[10.5px]">{item.code}</code>
|
|
248
|
+
</li>)}
|
|
249
|
+
</ul>}
|
|
250
|
+
{meta.compile
|
|
251
|
+
? <p className="text-[11.5px] text-tita-ink-muted">
|
|
252
|
+
编译:新建 {meta.compile.cardsCreated ?? 0} 张 · 并入 {meta.compile.cardsUpdated ?? 0} 张 · 复用 {meta.compile.cardsReused ?? 0} 张 · wikiNotes 复用 {meta.compile.notesReused ?? 0} / 重编 {meta.compile.notesRecompiled ?? 0}
|
|
253
|
+
{meta.compile.version !== undefined ? ` · 索引 v${meta.compile.version}` : ''}
|
|
254
|
+
</p>
|
|
255
|
+
: <p className="text-[11.5px] text-tita-ink-muted">这次没有编译(<code className="font-mono">compile: false</code>)—— 卡片还没生成,需要时再编译一次。</p>}
|
|
256
|
+
</div>
|
|
257
|
+
}
|
|
258
|
+
if (meta.kind === 'rebuild') {
|
|
259
|
+
return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
|
|
260
|
+
{meta.skipped ? '知识库没有变化:产物逐字复用,没有生成新的索引版本。' : <>新建 {meta.cardsCreated ?? 0} 张 · 并入 {meta.cardsUpdated ?? 0} 张 · 复用 {meta.cardsReused ?? 0} 张 · 重抽来源 {meta.sourcesExtracted ?? 0} 份 / 跳过 {meta.sourcesSkipped ?? 0} 份</>}
|
|
261
|
+
{meta.embedding ? ` · 向量 ${meta.embedding}` : ''}{meta.version !== undefined ? ` · 索引 v${meta.version}` : ''}
|
|
262
|
+
</p>
|
|
263
|
+
}
|
|
264
|
+
if (meta.kind === 'writeCards') {
|
|
265
|
+
return <div className="flex flex-col gap-1">
|
|
266
|
+
<ul className="flex flex-col gap-0.5">
|
|
267
|
+
{(meta.ids ?? []).map(id => <li key={id} className="text-[12px]"><code className="font-mono text-[11px] text-tita-ink-muted">{id}</code></li>)}
|
|
268
|
+
</ul>
|
|
269
|
+
{(meta.rejected ?? []).length > 0 && <ul className="flex flex-col gap-0.5">
|
|
270
|
+
{(meta.rejected ?? []).map((item, index) => <li key={index} className="text-[12px] text-tita-danger">{item.title ? `「${short(item.title, 24)}」` : '一条'}被拒:{item.reason ?? item.code ?? '原因未知'}</li>)}
|
|
271
|
+
</ul>}
|
|
272
|
+
</div>
|
|
273
|
+
}
|
|
274
|
+
if (meta.kind === 'addSource') {
|
|
275
|
+
return <p className="text-[11.5px] leading-relaxed text-tita-ink-muted">
|
|
276
|
+
已写入 <code className="font-mono text-[11px]">{meta.sourceId}</code>({meta.sourceType},{meta.bytes ?? 0} 字符)。下一步:编译它,才会生成 wiki 卡片。
|
|
277
|
+
</p>
|
|
278
|
+
}
|
|
279
|
+
return fallback
|
|
280
|
+
? <pre className="max-h-[240px] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-surface-muted p-2 font-mono text-[11px] leading-relaxed text-tita-ink-muted">{fallback}</pre>
|
|
281
|
+
: null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 一条知识工具调用的卡片。
|
|
286
|
+
*
|
|
287
|
+
* 返回 `null` 会让槽位退回默认通用行 —— 这是**刻意的出口**:拿到不认识的形状时,
|
|
288
|
+
* 宁可显示平台默认的样子,也不要显示一张自己编的卡。
|
|
289
|
+
*/
|
|
290
|
+
export function KnowledgeToolRow(props: KnowledgeToolViewProps): ReactNode {
|
|
291
|
+
const { phase, toolName, block, useDisclosure } = props
|
|
292
|
+
const { expanded, toggle } = useDisclosure()
|
|
293
|
+
|
|
294
|
+
const args = phase === 'preparing' ? {} : argsOf(block)
|
|
295
|
+
const meta = phase === 'result' ? (((block as { meta?: unknown }).meta ?? null) as ToolMeta | null) : null
|
|
296
|
+
const isError = phase === 'result' && (block as { isError?: boolean }).isError === true
|
|
297
|
+
const head = headOf(toolName, args, meta)
|
|
298
|
+
const fallback = phase === 'result' ? textOf(block) : ''
|
|
299
|
+
const details = phase === 'result' || Object.keys(args).length > 0
|
|
300
|
+
const summary = isError ? '失败' : head.summary
|
|
301
|
+
|
|
302
|
+
return <div className="flex flex-col" data-tool={toolName} data-phase={phase}>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
className={cn(
|
|
306
|
+
'group flex w-full min-w-0 items-center gap-1.5 py-0.5 text-left',
|
|
307
|
+
details ? 'cursor-pointer' : 'cursor-default',
|
|
308
|
+
)}
|
|
309
|
+
aria-expanded={details ? expanded : undefined}
|
|
310
|
+
onClick={() => { if (details) toggle() }}
|
|
311
|
+
>
|
|
312
|
+
<span className={cn('flex size-4 shrink-0 items-center justify-center', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>
|
|
313
|
+
{isError ? <TriangleAlert size={14} /> : head.icon}
|
|
314
|
+
</span>
|
|
315
|
+
<span className="shrink-0 text-[13px] text-tita-ink" title={head.title}>{head.title}</span>
|
|
316
|
+
<span className="size-[2px] shrink-0 rounded-full bg-tita-ink-faint" aria-hidden />
|
|
317
|
+
<span className={cn('min-w-0 flex-1 truncate text-[13px]', isError ? 'text-tita-danger' : 'text-tita-ink-faint')}>{summary}</span>
|
|
318
|
+
{meta?.kind === 'import' && (meta.failed?.length ?? 0) > 0 && <Badge tone="warning" className="shrink-0">失败 {meta.failed?.length}</Badge>}
|
|
319
|
+
{meta?.insufficientEvidence && <Badge tone="warning" className="shrink-0">无命中</Badge>}
|
|
320
|
+
{details && <ChevronRight size={12} className={cn('shrink-0 text-tita-ink-faint transition-transform', expanded && 'rotate-90')} />}
|
|
321
|
+
</button>
|
|
322
|
+
{expanded && details && <div className="mt-1 mb-1 ml-[22px]">
|
|
323
|
+
{isError
|
|
324
|
+
? <p className="flex items-start gap-1.5 text-[12px] leading-relaxed text-tita-danger">
|
|
325
|
+
<TriangleAlert size={12} className="mt-0.5 shrink-0" />
|
|
326
|
+
<span className="min-w-0 flex-1">{short(fallback || '调用失败', 400)}</span>
|
|
327
|
+
</p>
|
|
328
|
+
: <DetailsOf toolName={toolName} meta={meta} fallback={fallback} />}
|
|
329
|
+
</div>}
|
|
330
|
+
</div>
|
|
331
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'
|
|
2
|
-
import { Button, Input, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
|
|
2
|
+
import { Button, Field, Input, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
|
|
3
3
|
import { AlertCircle, Check, LoaderCircle, SlidersHorizontal } from '@tita-data-agent/ui/icons'
|
|
4
4
|
import { DirectoryPickerField } from './DirectoryPicker.tsx'
|
|
5
5
|
import { InfoHint } from './InfoHint.tsx'
|
|
@@ -203,9 +203,13 @@ function needsDebounce(spec: FieldSpec): boolean {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean; resolved?: string }) {
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
/* 两条说明各自成行:`Field` 的说明位是一个 span,两条挤在一行会读成一句。 */
|
|
207
|
+
const hint = <>
|
|
208
|
+
{spec.hint ? <span className="block">{spec.hint}</span> : null}
|
|
208
209
|
{/* 留空 = 自动。占位符显示**解析后**的真实路径 —— 否则「自动」等于没说。 */}
|
|
210
|
+
{resolved ? <span className="block">留空 = 自动:<code>{resolved}</code></span> : null}
|
|
211
|
+
</>
|
|
212
|
+
return <Field label={spec.label} hint={hint}>
|
|
209
213
|
<Input
|
|
210
214
|
type={spec.kind === 'number' ? 'number' : 'text'}
|
|
211
215
|
value={value}
|
|
@@ -213,23 +217,20 @@ function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldS
|
|
|
213
217
|
{...(resolved ? { placeholder: resolved } : {})}
|
|
214
218
|
onChange={event => onChange(event.target.value)}
|
|
215
219
|
/>
|
|
216
|
-
|
|
217
|
-
底部那行「字段名:说明;字段名:说明」是把标签和说明各念一遍。 */}
|
|
218
|
-
{spec.hint && <small className="tita-settingHint">{spec.hint}</small>}
|
|
219
|
-
{resolved && <small className="tita-settingHint">留空 = 自动:<code>{resolved}</code></small>}
|
|
220
|
-
</label>
|
|
220
|
+
</Field>
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* A dropdown field.
|
|
225
|
-
*
|
|
226
|
-
* `<label>`
|
|
227
|
-
*
|
|
228
|
-
*
|
|
224
|
+
* A dropdown field.
|
|
225
|
+
*
|
|
226
|
+
* `as="div"`, not the `<label>` the text and toggle fields use: the control is a
|
|
227
|
+
* `<button>` now (Radix's trigger), and a `<label>` around a button makes the
|
|
228
|
+
* visible field name an ambiguous second name — clicking the caption would also
|
|
229
|
+
* open the menu. The name travels on the trigger's `aria-label` instead, and the
|
|
230
|
+
* visible caption stays the thing a person reads.
|
|
229
231
|
*/
|
|
230
232
|
function SelectField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean }) {
|
|
231
|
-
return <
|
|
232
|
-
<span>{spec.label}</span>
|
|
233
|
+
return <Field as="div" label={spec.label} hint={spec.hint}>
|
|
233
234
|
<SelectMenu
|
|
234
235
|
value={value}
|
|
235
236
|
disabled={disabled}
|
|
@@ -237,25 +238,22 @@ function SelectField({ spec, value, onChange, disabled }: { spec: FieldSpec; val
|
|
|
237
238
|
options={spec.options ?? []}
|
|
238
239
|
aria-label={spec.label}
|
|
239
240
|
/>
|
|
240
|
-
|
|
241
|
-
</div>
|
|
241
|
+
</Field>
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* 开关字段。
|
|
246
246
|
*
|
|
247
|
-
*
|
|
247
|
+
* 它和文本 / 下拉 / 目录选择**共用同一套结构**:标签在上、控件在中、说明在下。
|
|
248
248
|
*
|
|
249
249
|
* 原来它是「带边框和底色的整行」,另外三种是无边框的堆叠 —— 四种字段两种长相,而且
|
|
250
250
|
* 标签用 `<strong>`、说明塞在标签里,对齐方式也对不上。更实际的问题是那个边框:
|
|
251
251
|
* 小节本身已经是一张卡片,字段再包一层就是**卡片套卡片**。
|
|
252
252
|
*/
|
|
253
253
|
function ToggleField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: boolean; onChange: (value: boolean) => void; disabled: boolean }) {
|
|
254
|
-
return <label
|
|
255
|
-
<span>{spec.label}</span>
|
|
254
|
+
return <Field label={spec.label} hint={spec.hint}>
|
|
256
255
|
<Switch checked={value} disabled={disabled} onCheckedChange={onChange} />
|
|
257
|
-
|
|
258
|
-
</label>
|
|
256
|
+
</Field>
|
|
259
257
|
}
|
|
260
258
|
|
|
261
259
|
/** 写入状态。`saved` 只短暂出现,`error` 留到下一次改动。 */
|