dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.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 +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -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 +18 -1
- 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 +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- 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/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -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 +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
|
|
20
20
|
import { useState, type ChangeEvent, type ReactNode } from 'react'
|
|
21
21
|
import { Check, Pencil, Plus, ShieldCheck, Trash2 } from '@tita-data-agent/ui/icons'
|
|
22
|
-
import { Button,
|
|
22
|
+
import { Button, Field, Input, Switch, Textarea, cn } from '@tita-data-agent/ui/components'
|
|
23
23
|
import {
|
|
24
24
|
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
|
|
25
25
|
Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle,
|
|
26
26
|
} from '@tita-data-agent/ui/overlays'
|
|
27
27
|
import { styles } from './styles.ts'
|
|
28
|
+
import { InfoHint } from './InfoHint.tsx'
|
|
28
29
|
import type { SourceTypeCatalog, SourceTypeDef, SourceTypeDraft } from './KnowledgeBaseTab.tsx'
|
|
29
30
|
import { WikiTypeSection, type WikiTypeView } from './WikiTypeEditor.tsx'
|
|
30
31
|
|
|
@@ -51,6 +52,32 @@ export interface TypeDefinitionDrawerProps {
|
|
|
51
52
|
|
|
52
53
|
const EMPTY_DRAFT: SourceTypeDraft = { mode: 'create', id: '', label: '', description: '', signals: '', excludes: '' }
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 这一节的说明**从正文搬进了图标**(2026-09-26 GUI 反馈)。
|
|
57
|
+
*
|
|
58
|
+
* 原来标题下面挂着两段常驻说明、按钮旁边再挂一段,一共五行解释文字压在十一个分类
|
|
59
|
+
* 上面 —— 打开这个抽屉是来改词汇表的,词汇表本身却被推到第二屏。说明的内容有价值
|
|
60
|
+
* (「兜底分类必须存在且不可停用」这种话不能删),所以挪进 `InfoHint`:要看的时候
|
|
61
|
+
* 悬停或聚焦就有,不看的时候不占版面。下面 Wiki 类型那一节同理。
|
|
62
|
+
*/
|
|
63
|
+
const SOURCE_TYPE_HINT = 'source_type 描述业务场景,不描述文件格式(载体走 source_format)。三档默认值:兜底分类必须存在且不可停用;内置分类可改文案、可停用,但不能删除;自定义分类可增可删,新增时必须写清判定文案——它会拼进内置提示词里交给 Agent 判断。分类下还有来源时不能删除,先迁移。'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 一个分类的判定文案:定义 + 典型信号 + 排除项。
|
|
67
|
+
*
|
|
68
|
+
* 这三样是 Agent 判断「这条来源算不算这一类」的全部依据,但铺在行上会占三四行,
|
|
69
|
+
* 把十一行的列表拉成一屏。所以它们进行首那枚图标,行本身只留身份与操作。
|
|
70
|
+
*/
|
|
71
|
+
function sourceTypeHint(def: SourceTypeDef): string {
|
|
72
|
+
const parts = [def.description || '还没有写定义 —— Agent 只能靠名字猜它是哪一类。']
|
|
73
|
+
if (def.signals.length > 0) parts.push(`典型信号:${def.signals.join(';')}`)
|
|
74
|
+
if (def.excludes.length > 0) parts.push(`不归这一类:${def.excludes.join(';')}`)
|
|
75
|
+
return parts.join('。')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wiki 类型那一节的说明,同样从常驻段落搬进图标。 */
|
|
79
|
+
const WIKI_TYPE_HINT = '每个类型规定必填小节与必填字段,写卡片时按它校验。这就是编译设置里的 Schema 类型——早先那种「逗号分隔的类型名文本框」已撤掉,它既不参与校验也不被生成逻辑读取,是个改了不生效的假开关。从原文件生成卡片时:先按「定义 + 例如」判断属于哪一类,再按必填小节写正文、按必填字段填 front matter;模板卡由小节派生,新建卡片时照抄即可。'
|
|
80
|
+
|
|
54
81
|
export function TypeDefinitionDrawer({
|
|
55
82
|
open, onOpenChange, kbName, kbId, sourceTypes, wikiTypes, schemaMode, customized, catalog, busy,
|
|
56
83
|
onSaveSourceType, onToggleSourceType, onRemoveSourceType, onApplyWikiTypes, onModeChange, onResetWikiTypes,
|
|
@@ -69,37 +96,51 @@ export function TypeDefinitionDrawer({
|
|
|
69
96
|
: { ...EMPTY_DRAFT, open: true })
|
|
70
97
|
}
|
|
71
98
|
|
|
99
|
+
/** Wiki 类型那一节的副标题计数(原本由 `WikiTypeSection` 自己渲染)。 */
|
|
100
|
+
const enabledWikiTypes = wikiTypes.filter(type => type.enabled).length
|
|
101
|
+
|
|
72
102
|
return <Drawer open={open} onOpenChange={onOpenChange}>
|
|
73
103
|
{open && <DrawerContent className={styles.settingsDrawer} resizable resizeKey="tita-typedef-drawer">
|
|
74
104
|
<DrawerHeader>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
{/* 标题与说明**包在一个 div 里**:`DrawerHeader` 是 `flex justify-between`,
|
|
106
|
+
两个平级子元素会并排 —— 标题被挤成两行,说明占掉剩下那一行。版本抽屉与
|
|
107
|
+
版本面板也是这么包的(那里同样是「标题 + 一整句说明」)。 */}
|
|
108
|
+
<div>
|
|
109
|
+
<DrawerTitle>类型定义</DrawerTitle>
|
|
110
|
+
{/*
|
|
111
|
+
作用域写在标题下面,只写一次:切库换的是**这一套**,不是方案的参数。
|
|
112
|
+
|
|
113
|
+
这段 2026-09-26 之前是**注释掉的**,而测试断言的是这段文字本身 —— 于是它一直
|
|
114
|
+
绿着,绿色的来源却是注释,抽屉里其实没有任何作用域说明(`kbName` 也跟着成了
|
|
115
|
+
一个没人读的 prop)。现在它是真的渲染出来:这一栏是**按库**的词汇表,而同一个
|
|
116
|
+
面板右上角那个齿轮里是**本方案**的参数,两者「切库会不会跟着换」不一样,
|
|
117
|
+
不说就只能靠用户自己撞出来。
|
|
118
|
+
|
|
119
|
+
设置抽屉的标题下没有对应的一句,那是有意的:齿轮的默认预期本来就是「方案级」,
|
|
120
|
+
而「这个抽屉跟着当前知识库走」才是需要说出来的那一半。
|
|
121
|
+
*/}
|
|
122
|
+
<DrawerDescription>
|
|
123
|
+
本库定义{kbName ? <> · <b>{kbName}</b></> : null} —— 来源分类与 Wiki 类型只属于这个知识库,切库就换一套;
|
|
124
|
+
方案级参数(存储 / 收录闸门 / 构建 / 召回 / 重排)在右上角齿轮的「设置」里。
|
|
125
|
+
</DrawerDescription>
|
|
126
|
+
</div>
|
|
81
127
|
</DrawerHeader>
|
|
82
128
|
<DrawerBody>
|
|
83
129
|
<div className={styles.settingsStack}>
|
|
84
130
|
<section className={styles.settingsSection} aria-labelledby="tita-typedef-source-title">
|
|
85
131
|
<div className={styles.settingsSectionHeader}>
|
|
86
132
|
<div>
|
|
87
|
-
<h3 id="tita-typedef-source-title"><ShieldCheck size={13} />
|
|
88
|
-
<p><b>source_type</b> 描述业务场景,不描述文件格式(载体走 source_format)。</p>
|
|
133
|
+
<h3 id="tita-typedef-source-title"><ShieldCheck size={13} /> 来源类型<InfoHint text={SOURCE_TYPE_HINT} /></h3>
|
|
89
134
|
</div>
|
|
90
135
|
<span className={styles.kbCount}>{sourceTypes.length} 个分类</span>
|
|
91
136
|
</div>
|
|
92
|
-
<p className={styles.kbHint}>
|
|
93
|
-
三档默认值:<b>兜底分类</b>必须存在且不可停用;<b>内置分类</b>可改文案、可停用,但不能删除;
|
|
94
|
-
<b>自定义分类</b>可增可删,新增时必须写清判定文案——它会拼进下面那段内置提示词里交给 Agent 判断。
|
|
95
|
-
分类下还有来源时不能删除,先迁移。
|
|
96
|
-
</p>
|
|
97
137
|
<ul className={styles.kbSourceList}>
|
|
98
138
|
{sourceTypes.map(def => <li key={def.id} className={styles.kbSourceRow}>
|
|
99
139
|
<span className={styles.kbSourceName}>
|
|
100
140
|
<span className={styles.kbSourceLabel}>{def.label}</span>
|
|
101
141
|
<code className={styles.kbSourceId}>{def.id}</code>
|
|
102
142
|
<span className={cn(styles.kbSourceTag, !def.fallback && styles.kbSourceTagLock)}>{def.fallback ? '兜底' : def.builtin ? '内置' : '自定义'}</span>
|
|
143
|
+
<InfoHint text={sourceTypeHint(def)} />
|
|
103
144
|
</span>
|
|
104
145
|
<span className={styles.kbTypeCount}>{def.sourceCount} 个来源</span>
|
|
105
146
|
<Switch
|
|
@@ -122,7 +163,6 @@ export function TypeDefinitionDrawer({
|
|
|
122
163
|
</li>)}
|
|
123
164
|
</ul>
|
|
124
165
|
<div className={styles.kbSourceAdd}>
|
|
125
|
-
<p className={styles.kbHint}>追加分类(例如 <b>test_unit 测试用例</b>)时必须写清「怎么判断它是这一类」。</p>
|
|
126
166
|
<Button variant="outline" size="sm" onClick={() => openEditor()} disabled={busy !== null}><Plus size={13} />新增类型</Button>
|
|
127
167
|
</div>
|
|
128
168
|
|
|
@@ -145,15 +185,12 @@ export function TypeDefinitionDrawer({
|
|
|
145
185
|
<section className={styles.settingsSection} aria-labelledby="tita-typedef-wiki-title">
|
|
146
186
|
<div className={styles.settingsSectionHeader}>
|
|
147
187
|
<div>
|
|
148
|
-
<h3 id="tita-typedef-wiki-title">Wiki 类型(卡片 schema
|
|
149
|
-
<p>每个类型规定必填小节与必填字段,写卡片时按它校验。</p>
|
|
188
|
+
<h3 id="tita-typedef-wiki-title">Wiki 类型(卡片 schema)<InfoHint text={WIKI_TYPE_HINT} /></h3>
|
|
150
189
|
</div>
|
|
151
|
-
|
|
190
|
+
{/* 计数从 `WikiTypeSection` 的副标题搬到这里:那块副标题与这一节的标题
|
|
191
|
+
是同一个名字,一个抽屉里同一句话写两遍。 */}
|
|
192
|
+
<span className={styles.kbCount}>{enabledWikiTypes} / {wikiTypes.length} 启用{customized ? ' · 已自定义' : ''}</span>
|
|
152
193
|
</div>
|
|
153
|
-
<p className={styles.kbHint}>
|
|
154
|
-
这就是编译设置里的 <b>Schema 类型</b>。早先那种「逗号分隔的类型名文本框」已撤掉 ——
|
|
155
|
-
它既不参与校验也不被生成逻辑读取,是个改了不生效的假开关。
|
|
156
|
-
</p>
|
|
157
194
|
<WikiTypeSection
|
|
158
195
|
kbId={kbId}
|
|
159
196
|
types={wikiTypes}
|
|
@@ -173,32 +210,29 @@ export function TypeDefinitionDrawer({
|
|
|
173
210
|
{draft.open && <DialogContent>
|
|
174
211
|
<DialogHeader><DialogTitle>{draft.mode === 'create' ? '新增来源分类' : `编辑来源分类 · ${draft.label}`}</DialogTitle></DialogHeader>
|
|
175
212
|
<div className={styles.kbForm}>
|
|
176
|
-
<label
|
|
213
|
+
<Field label="分类 id" hint="小写字母 / 数字 / 下划线,创建后不可修改——它是写进 SourceRaw frontmatter 的稳定标识。">
|
|
177
214
|
<Input value={draft.id} disabled={draft.mode === 'edit'} spellCheck={false} placeholder="test_unit"
|
|
178
215
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<label className={styles.kbField}><Label>显示名</Label>
|
|
216
|
+
</Field>
|
|
217
|
+
<Field label="显示名">
|
|
182
218
|
<Input value={draft.label} placeholder="测试用例"
|
|
183
219
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, label: event.target.value })} />
|
|
184
|
-
</
|
|
185
|
-
<label
|
|
186
|
-
<
|
|
220
|
+
</Field>
|
|
221
|
+
<Field label="一句话定义(必填)" hint="写清「是什么」,不要写成「包含哪些文件」——载体格式走 source_format,Agent 靠这句话判断。">
|
|
222
|
+
<Textarea value={draft.description} spellCheck={false}
|
|
187
223
|
placeholder="以单个可执行验证点为中心、用于判定某项功能是否正确的用例记录。"
|
|
188
224
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, description: event.target.value })} />
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<textarea className={styles.kbTextarea} value={draft.signals} spellCheck={false}
|
|
225
|
+
</Field>
|
|
226
|
+
<Field label="signals · 典型信号(每行一条,至少 2 条)">
|
|
227
|
+
<Textarea value={draft.signals} spellCheck={false}
|
|
193
228
|
placeholder={'包含前置条件 / 操作步骤 / 预期结果三要素\n带 case id、优先级、关联需求编号'}
|
|
194
229
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, signals: event.target.value })} />
|
|
195
|
-
</
|
|
196
|
-
<label
|
|
197
|
-
<
|
|
230
|
+
</Field>
|
|
231
|
+
<Field label="excludes · 排除项(每行一条,至少 1 条)" hint="每条都要写出「该归到哪个分类」(用 id 或标签);指向不存在的分类会被拒绝,否则边界是悬空的。">
|
|
232
|
+
<Textarea value={draft.excludes} spellCheck={false}
|
|
198
233
|
placeholder={'整份测试方案或测试计划归 document\n线上故障根因排查归 question'}
|
|
199
234
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, excludes: event.target.value })} />
|
|
200
|
-
|
|
201
|
-
</label>
|
|
235
|
+
</Field>
|
|
202
236
|
</div>
|
|
203
237
|
<DialogFooter>
|
|
204
238
|
<Button variant="ghost" onClick={closeEditor}>取消</Button>
|
|
@@ -9,15 +9,26 @@
|
|
|
9
9
|
* 组件自包含:列表与编辑弹窗在这里,数据由父级(知识库面板)以 props 传入,
|
|
10
10
|
* 写入统一走 `onApply`(整表替换)。它另外自己拉一次 `/knowledge/wiki-types`
|
|
11
11
|
* 取"待补"清单与内置只读判定规则——那是展示信息,不影响写入契约。
|
|
12
|
+
*
|
|
13
|
+
* 2026-09-26 的呈现修正(0040 §4.3 / §4.4)都收在同一个毛病上:**只读内容与可编辑
|
|
14
|
+
* 内容原来长得一样**。所以这一屏现在有三条硬规则:
|
|
15
|
+
*
|
|
16
|
+
* 1. 不可编辑的值不给控件形态:内置类型的 id 用 `readOnly`(沉底 + 浅描边 + 文本
|
|
17
|
+
* 光标,可选中复制),模板卡预览用只读排版块,都不再是"看起来能点"的输入框。
|
|
18
|
+
* 2. 模板卡预览**按层分两块**:必填字段落在 front matter、必填小节是正文标题,
|
|
19
|
+
* 三层校验本来就分开报错,这里就分开呈现。
|
|
20
|
+
* 3. `diagnosis/*` 这类带命名空间的 id 在列表里按前缀折成一组(0040 §4.3)。
|
|
12
21
|
*/
|
|
13
22
|
|
|
14
23
|
import { useCallback, useEffect, useState, type ChangeEvent, type ReactNode } from 'react'
|
|
15
24
|
import {
|
|
16
|
-
AlertCircle, Check,
|
|
25
|
+
AlertCircle, Check, ChevronRight, Eye, Pencil, Plus, RotateCcw, ShieldCheck, Trash2, X,
|
|
17
26
|
} from '@tita-data-agent/ui/icons'
|
|
18
|
-
import { Badge, Button,
|
|
27
|
+
import { Badge, Button, Field, Input, SelectMenu, Switch, Textarea, cn } from '@tita-data-agent/ui/components'
|
|
19
28
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@tita-data-agent/ui/overlays'
|
|
20
29
|
import { styles } from './styles.ts'
|
|
30
|
+
import { InfoHint } from './InfoHint.tsx'
|
|
31
|
+
import { wikiTypeRows } from './wiki-type-groups.ts'
|
|
21
32
|
|
|
22
33
|
export interface WikiSectionSpec { label: string; required: boolean }
|
|
23
34
|
export interface WikiFieldSpec { name: string; required: boolean; note: string }
|
|
@@ -29,7 +40,6 @@ export interface WikiTypeView {
|
|
|
29
40
|
layer: 'type' | 'instance'
|
|
30
41
|
sections: WikiSectionSpec[]
|
|
31
42
|
fields: WikiFieldSpec[]
|
|
32
|
-
sourceTypes: string[]
|
|
33
43
|
examples: string[]
|
|
34
44
|
template: string
|
|
35
45
|
enabled: boolean
|
|
@@ -56,12 +66,10 @@ interface WikiTypeDraft {
|
|
|
56
66
|
sections: string
|
|
57
67
|
/** 每行一条,`name|说明`;`name|说明|可选` 表示非必填。 */
|
|
58
68
|
fields: string
|
|
59
|
-
/** 每行一个来源分类 id;留空表示不限制。 */
|
|
60
|
-
sourceTypes: string
|
|
61
69
|
examples: string
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
const EMPTY_DRAFT: WikiTypeDraft = { open: false, mode: 'create', id: '', label: '', description: '', layer: 'type', sections: '', fields: '',
|
|
72
|
+
const EMPTY_DRAFT: WikiTypeDraft = { open: false, mode: 'create', id: '', label: '', description: '', layer: 'type', sections: '', fields: '', examples: '' }
|
|
65
73
|
|
|
66
74
|
const TYPE_ID_PATTERN = /^[a-z][a-z0-9_/-]{1,60}$/
|
|
67
75
|
|
|
@@ -103,6 +111,43 @@ function parseFields(text: string): WikiFieldSpec[] {
|
|
|
103
111
|
|
|
104
112
|
const LAYER_LABEL: Record<WikiTypeView['layer'], string> = { type: '类型层', instance: '实例层' }
|
|
105
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 只读字段的记号:一个 `data-` 属性,样式写在插件的 `styles.css` 里。
|
|
116
|
+
*
|
|
117
|
+
* 为什么不直接给工具类:**插件没有自己的 Tailwind 构建**(`packages/ui` 的
|
|
118
|
+
* `tailwind.css` 只在它自己构建时按源码扫一遍,插件改不了那份清单),所以
|
|
119
|
+
* `disabled:opacity-50` 这类只在恰好被别处用到时才存在。而 `Input` 的 `readonly`
|
|
120
|
+
* 样式只解决"沉底 + 浅描边",缺"这不能改"的那点灰(0040 §4.4 第 1 条)。
|
|
121
|
+
* 给一个属性而不是直接改 `input[readonly]`:这条规约只该作用于**声明了它**的字段,
|
|
122
|
+
* 不能顺手把别处的只读输入框也一起改掉。
|
|
123
|
+
*/
|
|
124
|
+
const READ_ONLY_FIELD = { 'data-tita-readonly': 'true' } as const
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 带命名空间的前缀中文别名(0040 §4.3)。
|
|
128
|
+
*
|
|
129
|
+
* 组名默认取前缀本身 —— 只有真正需要翻译的才写进这张表。写死五个 `diagnosis/*`
|
|
130
|
+
* 的 id 是不行的:自定义类型也允许带斜杠,`diagnosis` 前缀下的类型是可增删的。
|
|
131
|
+
*/
|
|
132
|
+
/** 预览里没有任何可显示的必填字段 / 小节时,也要把这一块画出来:两块的分工本身是信息。 */
|
|
133
|
+
const PREVIEW_EMPTY = '(没有必填项 —— 这一类不受它约束)'
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 一个类型的口径:定义 + 必填小节 + 必填字段。
|
|
137
|
+
*
|
|
138
|
+
* 这三样原来铺在每一行卡片上(十一个类型就是三十多行小字),把列表拉成一屏多 ——
|
|
139
|
+
* 而这一页要做的事是「看一眼有哪些类型、哪个没启用」。所以它们进行首那枚图标,
|
|
140
|
+
* 行本身只留身份、张数与操作。同样的处理也用在「来源类型」的每一行上。
|
|
141
|
+
*/
|
|
142
|
+
function wikiTypeHint(type: WikiTypeView): string {
|
|
143
|
+
const required = type.sections.filter(section => section.required).map(section => section.label)
|
|
144
|
+
const fields = type.fields.filter(field => field.required).map(field => field.name)
|
|
145
|
+
const parts = [type.description || '(没有定义:模型只能靠猜)']
|
|
146
|
+
parts.push(`必填小节:${required.join(' / ') || '(无)'}`)
|
|
147
|
+
if (fields.length > 0) parts.push(`必填字段:${fields.join(' / ')}`)
|
|
148
|
+
return parts.join('。')
|
|
149
|
+
}
|
|
150
|
+
|
|
106
151
|
export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy, onApply, onModeChange, onReset }: Props): ReactNode {
|
|
107
152
|
const [draft, setDraft] = useState<WikiTypeDraft>(EMPTY_DRAFT)
|
|
108
153
|
const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
@@ -129,7 +174,6 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
129
174
|
open: true, mode: 'edit', id: type.id, label: type.label, description: type.description, layer: type.layer,
|
|
130
175
|
sections: type.sections.map(section => section.required ? section.label : `${section.label}?`).join('\n'),
|
|
131
176
|
fields: type.fields.map(field => [field.name, field.note, field.required ? '' : '可选'].filter(Boolean).join('|')).join('\n'),
|
|
132
|
-
sourceTypes: type.sourceTypes.join('\n'),
|
|
133
177
|
examples: type.examples.join('\n'),
|
|
134
178
|
}
|
|
135
179
|
: { ...EMPTY_DRAFT, open: true })
|
|
@@ -154,7 +198,6 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
154
198
|
layer: draft.layer,
|
|
155
199
|
sections,
|
|
156
200
|
fields,
|
|
157
|
-
sourceTypes: draft.sourceTypes.split('\n').map(line => line.trim()).filter(Boolean),
|
|
158
201
|
examples: draft.examples.split('\n').map(line => line.trim()).filter(Boolean),
|
|
159
202
|
template: templatePreview(sections, fields),
|
|
160
203
|
enabled: types.find(type => type.id === id)?.enabled ?? true,
|
|
@@ -172,18 +215,35 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
172
215
|
const remove = (type: WikiTypeView): Promise<void> =>
|
|
173
216
|
onApply(types.filter(item => item.id !== type.id), `已删除卡片类型「${type.label}」`)
|
|
174
217
|
|
|
175
|
-
|
|
218
|
+
/**
|
|
219
|
+
* 一行类型。**一行就是一行**:身份(名字 / id / 层级 / 自定义)+ 说明图标,
|
|
220
|
+
* 右边是张数、开关、编辑、删除。
|
|
221
|
+
*
|
|
222
|
+
* 提出来是因为它现在有两条渲染路径 —— 平铺的行、分组里的行 —— 而两条路径必须是
|
|
223
|
+
* **同一个行**。复制一份出来,迟早会在其中一份上改样式或加操作,然后两处不一样。
|
|
224
|
+
*/
|
|
225
|
+
const renderRow = (type: WikiTypeView): ReactNode => <li key={type.id} className={styles.wikiTypeRow}>
|
|
226
|
+
<span className={styles.wikiTypeName}>
|
|
227
|
+
<span className={styles.wikiTypeLabel}>{type.label}</span>
|
|
228
|
+
<code className={styles.kbSourceId}>{type.id}</code>
|
|
229
|
+
<Badge tone={type.layer === 'instance' ? 'warning' : 'neutral'}>{LAYER_LABEL[type.layer]}</Badge>
|
|
230
|
+
{type.builtin ? '' : <span className={styles.wikiTypeTag}>自定义</span>}
|
|
231
|
+
<InfoHint text={wikiTypeHint(type)} />
|
|
232
|
+
</span>
|
|
233
|
+
{/* 张数是**算出来的**,不是填进去的:它没有编辑入口,所以也不该长得像控件。 */}
|
|
234
|
+
<span className={styles.kbTypeCount} title="由该库已落库卡片实时算出,不可编辑">{type.cardCount} 张</span>
|
|
235
|
+
<Switch checked={type.enabled} disabled={!canEdit || busy} onCheckedChange={checked => void toggle(type, checked)} />
|
|
236
|
+
<Button variant="ghost" size="icon" title={`编辑 ${type.label}`} aria-label={`编辑类型 ${type.label}`} disabled={!canEdit || busy} onClick={() => openEditor(type)}><Pencil size={13} /></Button>
|
|
237
|
+
<Button
|
|
238
|
+
variant="ghost" size="icon"
|
|
239
|
+
disabled={!canEdit || busy || type.builtin || type.cardCount > 0}
|
|
240
|
+
title={type.builtin ? '内置类型不可删除,只能停用' : type.cardCount > 0 ? `该类型下还有 ${type.cardCount} 张卡片,先改类型再删除` : `删除类型 ${type.label}`}
|
|
241
|
+
aria-label={`删除类型 ${type.label}`}
|
|
242
|
+
onClick={() => void remove(type)}
|
|
243
|
+
><Trash2 size={13} /></Button>
|
|
244
|
+
</li>
|
|
176
245
|
|
|
177
246
|
return <>
|
|
178
|
-
<div className={styles.kbSubHead}>
|
|
179
|
-
<h4><ClipboardCheck size={13} />Wiki 类型(卡片 schema)</h4>
|
|
180
|
-
<span>{enabledCount} / {types.length} 启用{customized ? ' · 已自定义' : ''}</span>
|
|
181
|
-
</div>
|
|
182
|
-
<p className={styles.kbHint}>
|
|
183
|
-
从原文件生成 Wiki 卡片时,<b>类型决定这张卡片该长什么样</b>:先按「定义 + 例如」判断属于哪一类,
|
|
184
|
-
再按<b>必填小节</b>写正文、按<b>必填字段</b>填 front matter。模板卡由小节派生,新建卡片时照抄即可。
|
|
185
|
-
</p>
|
|
186
|
-
|
|
187
247
|
<div className={styles.wikiTypeBar}>
|
|
188
248
|
<label className={styles.wikiTypeMode}>
|
|
189
249
|
<Switch
|
|
@@ -210,31 +270,33 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
210
270
|
<button type="button" className={styles.kbNoticeClose} aria-label="关闭提示" onClick={() => setNotice(null)}><X size={12} /></button>
|
|
211
271
|
</p>}
|
|
212
272
|
|
|
273
|
+
{/*
|
|
274
|
+
列表按 id 前缀分组(0040 §4.3):`diagnosis/*` 是**一套 schema**,五个类型
|
|
275
|
+
铺在一级看起来就是五个互不相干的条目。分组是呈现层行为 —— 注册表结构、
|
|
276
|
+
`entity_type`、组内顺序都不动。
|
|
277
|
+
|
|
278
|
+
**「摆成几行」这件事不在 JSX 里算**,在 `wikiTypeRows()` 里算。上一版把判断写成
|
|
279
|
+
一个三元,`prefix === ''` 那一支只渲染了 `members[0]` —— 于是六个没有斜杠的通用
|
|
280
|
+
类型在界面上只剩第一个,另外五个不见了,而且没有任何报错、所有测试都是绿的。
|
|
281
|
+
把计划抽成纯函数之后,这一条由 `tests/wiki-type-groups.test.mjs` 钉住:
|
|
282
|
+
**行的总数必须与注册表一一对应,一个都不能少**。
|
|
283
|
+
*/}
|
|
213
284
|
<ul className={styles.wikiTypeList}>
|
|
214
|
-
{types.map(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<Button variant="ghost" size="icon" title={`编辑 ${type.label}`} aria-label={`编辑类型 ${type.label}`} disabled={!canEdit || busy} onClick={() => openEditor(type)}><Pencil size={13} /></Button>
|
|
230
|
-
<Button
|
|
231
|
-
variant="ghost" size="icon"
|
|
232
|
-
disabled={!canEdit || busy || type.builtin || type.cardCount > 0}
|
|
233
|
-
title={type.builtin ? '内置类型不可删除,只能停用' : type.cardCount > 0 ? `该类型下还有 ${type.cardCount} 张卡片,先改类型再删除` : `删除类型 ${type.label}`}
|
|
234
|
-
aria-label={`删除类型 ${type.label}`}
|
|
235
|
-
onClick={() => void remove(type)}
|
|
236
|
-
><Trash2 size={13} /></Button>
|
|
237
|
-
</li>)}
|
|
285
|
+
{wikiTypeRows(types).map(row => row.kind === 'row'
|
|
286
|
+
? renderRow(row.type)
|
|
287
|
+
: <li key={`group:${row.prefix}`} className={styles.wikiTypeGroup}>
|
|
288
|
+
<details className={styles.wikiTypeGroupBox} open>
|
|
289
|
+
<summary className={styles.wikiTypeGroupHead}>
|
|
290
|
+
{/* 折叠箭头用 lucide 的实体图标,不用 `▾` 这种字符顶替:那个字符的
|
|
291
|
+
描边粗细、基线、大小都不属于这套图标系统,缩放时先散架的就是它。 */}
|
|
292
|
+
<ChevronRight size={12} className={styles.wikiTypeGroupCaret} aria-hidden />
|
|
293
|
+
<span className={styles.wikiTypeGroupLabel}>{row.label}</span>
|
|
294
|
+
<code className={styles.kbSourceId}>{row.prefix}/*</code>
|
|
295
|
+
<span className={styles.wikiTypeGroupCount}>{row.members.length} 个类型</span>
|
|
296
|
+
</summary>
|
|
297
|
+
<ul className={styles.wikiTypeGroupList}>{row.members.map(renderRow)}</ul>
|
|
298
|
+
</details>
|
|
299
|
+
</li>)}
|
|
238
300
|
</ul>
|
|
239
301
|
|
|
240
302
|
{issueCount > 0 && <details className={styles.kbPrompt}>
|
|
@@ -258,22 +320,34 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
258
320
|
{draft.open && <DialogContent>
|
|
259
321
|
<DialogHeader><DialogTitle>{draft.mode === 'create' ? '新增 Wiki 卡片类型' : `编辑类型 · ${draft.label}`}</DialogTitle></DialogHeader>
|
|
260
322
|
<div className={styles.kbForm}>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
323
|
+
{/*
|
|
324
|
+
id 两态两种控件形态(0040 §4.4 第 1 条):
|
|
325
|
+
- 新建:真的是一个要填的输入框;
|
|
326
|
+
- 编辑:**现在是只读值**,不是"看起来能点的灰框"。用 `readOnly` 而不是
|
|
327
|
+
`disabled`——只读的值还该能被选中复制(它是卡片的 `entity_type`,
|
|
328
|
+
人常常要把它拷去写卡片),`disabled` 连选中都做不到。样式见
|
|
329
|
+
`READ_ONLY_FIELD`:沉底 + 浅描边 + 文本光标,由共享 `Input` 自带。
|
|
330
|
+
*/}
|
|
331
|
+
{draft.mode === 'edit'
|
|
332
|
+
? <Field as="div" label={<>类型 id<Badge tone="neutral">只读</Badge></>} hint={<>创建后不可修改——它是卡片的 <code>entity_type</code>,改了等于换一个类型。</>}>
|
|
333
|
+
<Input value={draft.id} readOnly spellCheck={false} {...READ_ONLY_FIELD} />
|
|
334
|
+
</Field>
|
|
335
|
+
: <Field label="类型 id" hint={<>小写字母开头,可含数字 / 下划线 / 连字符 / 斜杠(如 <code>diagnosis/symptom</code>);创建后不可修改——它是卡片的 <code>entity_type</code>。</>}>
|
|
336
|
+
<Input value={draft.id} spellCheck={false} placeholder="metric"
|
|
337
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
|
|
338
|
+
</Field>}
|
|
339
|
+
<Field label="显示名">
|
|
267
340
|
<Input value={draft.label} placeholder="指标"
|
|
268
341
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, label: event.target.value })} />
|
|
269
|
-
</
|
|
270
|
-
<label
|
|
271
|
-
<
|
|
342
|
+
</Field>
|
|
343
|
+
<Field label="定义(必填)" hint="写清这一类「是什么」——它是模型判断卡片属于哪一类的第一依据。">
|
|
344
|
+
<Textarea value={draft.description} spellCheck={false}
|
|
272
345
|
placeholder="一个可计算、可归属的量化口径,回答「这个数是怎么算出来的」。"
|
|
273
346
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, description: event.target.value })} />
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
347
|
+
</Field>
|
|
348
|
+
{/* 选择控件不是 labelable 元素里可被包裹的那种:Radix 的 trigger 是 `<button>`,
|
|
349
|
+
外层再套一个 `<label>` 会让点标题也把菜单打开。所以这一格用 `as="div"`。 */}
|
|
350
|
+
<Field as="div" label="层级">
|
|
277
351
|
<SelectMenu
|
|
278
352
|
value={draft.layer}
|
|
279
353
|
onValueChange={next => setDraft({ ...draft, layer: next as 'type' | 'instance' })}
|
|
@@ -283,35 +357,23 @@ export function WikiTypeSection({ kbId, types, mode, customized, canEdit, busy,
|
|
|
283
357
|
{ value: 'instance', label: '实例层(只发生过一次,如案例)' },
|
|
284
358
|
]}
|
|
285
359
|
/>
|
|
286
|
-
</
|
|
287
|
-
<label
|
|
288
|
-
<
|
|
360
|
+
</Field>
|
|
361
|
+
<Field label="必填小节(每行一条,结尾加 ? 表示可选)" hint="小节就是正文的 Markdown 标题,也是模板卡与校验的依据。">
|
|
362
|
+
<Textarea value={draft.sections} spellCheck={false}
|
|
289
363
|
placeholder={'口径定义\n计算逻辑\n负责人?'}
|
|
290
364
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, sections: event.target.value })} />
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<textarea className={styles.kbTextarea} value={draft.fields} spellCheck={false}
|
|
365
|
+
</Field>
|
|
366
|
+
<Field label={<>字段(每行一条:<code>名称|说明</code>,说明后加 <code>|可选</code> 表示非必填)</>} hint="字段落在 front matter / metadata 上,是机器读的部分。">
|
|
367
|
+
<Textarea value={draft.fields} spellCheck={false}
|
|
295
368
|
placeholder={'formula|可执行的表达式或伪代码\nowner|谁对这个口径负责'}
|
|
296
369
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, fields: event.target.value })} />
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
<textarea className={styles.kbTextarea} value={draft.sourceTypes} spellCheck={false}
|
|
301
|
-
placeholder={'document\nprd'}
|
|
302
|
-
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, sourceTypes: event.target.value })} />
|
|
303
|
-
<span className={styles.kbHint}>来自「来源类型」的 id。来源不匹配时只提示,不阻断写作。</span>
|
|
304
|
-
</label>
|
|
305
|
-
<label className={styles.kbField}><Label>例如 · 用户会怎么提到它(每行一条)</Label>
|
|
306
|
-
<textarea className={styles.kbTextarea} value={draft.examples} spellCheck={false}
|
|
370
|
+
</Field>
|
|
371
|
+
<Field label="例如 · 用户会怎么提到它(每行一条)" hint="按用户的真实说法写,模型靠它判断该生成哪一类卡片。">
|
|
372
|
+
<Textarea value={draft.examples} spellCheck={false}
|
|
307
373
|
placeholder={'上个月的 GMV 怎么算\n转化率的口径是什么'}
|
|
308
374
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, examples: event.target.value })} />
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
{(draft.sections.trim() || draft.fields.trim()) && <div className={styles.kbField}>
|
|
312
|
-
<Label>模板卡预览(由小节派生,只读)</Label>
|
|
313
|
-
<pre className={styles.kbPre}>{templatePreview(parseSections(draft.sections), parseFields(draft.fields))}</pre>
|
|
314
|
-
</div>}
|
|
375
|
+
</Field>
|
|
376
|
+
<TemplateCardPreview sections={parseSections(draft.sections)} fields={parseFields(draft.fields)} />
|
|
315
377
|
</div>
|
|
316
378
|
<DialogFooter>
|
|
317
379
|
<Button variant="ghost" onClick={() => setDraft(EMPTY_DRAFT)}>取消</Button>
|
|
@@ -330,3 +392,57 @@ export function WikiTemplateHint({ template }: { template: string }): ReactNode
|
|
|
330
392
|
<pre className={styles.kbPre}>{template}</pre>
|
|
331
393
|
</details>
|
|
332
394
|
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 模板卡预览(0040 §4.4 第 3、4 条)。**它是只读的**:内容由 `fields` / `sections`
|
|
398
|
+
* 派生,人在这里什么都改不了 —— 真源是上面那两个输入框。
|
|
399
|
+
*
|
|
400
|
+
* 所以它不用输入框形态。原来是一个 `<textarea>`;人第一反应是"这里能编辑",
|
|
401
|
+
* 试两次才发现点不动,那是纯粹的误导。现在是一块沉底的只读排版块 + 一枚「只读」
|
|
402
|
+
* 徽标 + 文本光标可选中的等宽内容,形态与旁边那几个控件明显不同。
|
|
403
|
+
*
|
|
404
|
+
* 并且**按层分成两块**:
|
|
405
|
+
* - front matter:必填字段。落在卡片的 metadata 上,是机器读的部分;
|
|
406
|
+
* - 正文小节:必填小节。正文的 Markdown 标题。
|
|
407
|
+
*
|
|
408
|
+
* 这两件事原来混在一段文本里。而 `write_knowledge_card` 的三层校验本来就分开报错
|
|
409
|
+
* (少字段报字段、少小节报小节),定义界面按同样的分法展示,人看到"格式错误"
|
|
410
|
+
* 才知道该往哪补。
|
|
411
|
+
*/
|
|
412
|
+
function TemplateCardPreview({ sections, fields }: { sections: WikiSectionSpec[]; fields: WikiFieldSpec[] }): ReactNode {
|
|
413
|
+
const requiredFields = fields.filter(field => field.required)
|
|
414
|
+
const requiredSections = sections.filter(section => section.required)
|
|
415
|
+
return <section className={styles.wikiPreview} aria-labelledby="tita-wiki-preview-title">
|
|
416
|
+
<div className={styles.wikiPreviewHead}>
|
|
417
|
+
<h4 id="tita-wiki-preview-title">模板卡预览</h4>
|
|
418
|
+
<Badge tone="neutral">只读</Badge>
|
|
419
|
+
<span className={styles.wikiPreviewOrigin}>由上面的字段与小节派生,不单独保存</span>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className={styles.wikiPreviewBlock}>
|
|
423
|
+
<h5 className={styles.wikiPreviewBlockTitle}>front matter · 必填字段</h5>
|
|
424
|
+
<p className={styles.wikiPreviewBlockNote}>写在卡片正文之前,卡片的机器可读部分;顺序按你填的顺序。</p>
|
|
425
|
+
{requiredFields.length > 0
|
|
426
|
+
? <dl className={styles.wikiPreviewFields}>
|
|
427
|
+
{requiredFields.map(field => <div key={field.name} className={styles.wikiPreviewField}>
|
|
428
|
+
<dt><code>{field.name}</code>:</dt>
|
|
429
|
+
<dd>{field.note || '(还没写说明)'}</dd>
|
|
430
|
+
</div>)}
|
|
431
|
+
</dl>
|
|
432
|
+
: <p className={styles.wikiPreviewEmpty}>{PREVIEW_EMPTY}</p>}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div className={styles.wikiPreviewBlock}>
|
|
436
|
+
<h5 className={styles.wikiPreviewBlockTitle}>正文 · 必填小节</h5>
|
|
437
|
+
<p className={styles.wikiPreviewBlockNote}>写在正文里的 Markdown 标题,缺哪一个都会被点名。</p>
|
|
438
|
+
{requiredSections.length > 0
|
|
439
|
+
? <ul className={styles.wikiPreviewSections}>
|
|
440
|
+
{requiredSections.map(section => <li key={section.label}>
|
|
441
|
+
<code>{`## ${section.label}`}</code>
|
|
442
|
+
<span className={styles.wikiPreviewPlaceholder}>(写什么)</span>
|
|
443
|
+
</li>)}
|
|
444
|
+
</ul>
|
|
445
|
+
: <p className={styles.wikiPreviewEmpty}>{PREVIEW_EMPTY}</p>}
|
|
446
|
+
</div>
|
|
447
|
+
</section>
|
|
448
|
+
}
|