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
|
@@ -17,17 +17,28 @@
|
|
|
17
17
|
* 自己的编辑弹窗 —— 同一份数据不因为多了一个入口就多一个写入口。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { useState, type ChangeEvent, type ReactNode } from 'react'
|
|
21
|
-
import { Check, Pencil, Plus, ShieldCheck, Trash2 } from '@tita-data-agent/ui/icons'
|
|
22
|
-
import { Button,
|
|
20
|
+
import { useEffect, useRef, useState, type ChangeEvent, type ReactNode } from 'react'
|
|
21
|
+
import { Check, Pencil, Plus, ShieldCheck, Trash2, X } from '@tita-data-agent/ui/icons'
|
|
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
|
|
|
32
|
+
/**
|
|
33
|
+
* 抽屉该把哪一节推到你眼前。
|
|
34
|
+
*
|
|
35
|
+
* 它由**入口**决定,不是抽屉自己的偏好:从工具栏的「类型定义」进来,先看到的是
|
|
36
|
+
* 「这一栏是给本库定义词汇表的」这件事(来源类型在最上面);而从某张卡片上的
|
|
37
|
+
* 「类型定义」进来,人是奔着 Wiki 类型去的 —— 那时还落在来源类型上,等于要滚一遍
|
|
38
|
+
* 才能看到自己点的那一节。
|
|
39
|
+
*/
|
|
40
|
+
export type TypeDefinitionSection = 'source' | 'wiki'
|
|
41
|
+
|
|
31
42
|
export interface TypeDefinitionDrawerProps {
|
|
32
43
|
open: boolean
|
|
33
44
|
onOpenChange: (open: boolean) => void
|
|
@@ -37,7 +48,6 @@ export interface TypeDefinitionDrawerProps {
|
|
|
37
48
|
kbId: string
|
|
38
49
|
sourceTypes: SourceTypeDef[]
|
|
39
50
|
wikiTypes: WikiTypeView[]
|
|
40
|
-
schemaMode: 'strict' | 'report'
|
|
41
51
|
customized: boolean
|
|
42
52
|
catalog: SourceTypeCatalog | null
|
|
43
53
|
busy: string | null
|
|
@@ -45,15 +55,41 @@ export interface TypeDefinitionDrawerProps {
|
|
|
45
55
|
onToggleSourceType: (def: SourceTypeDef, enabled: boolean) => Promise<void>
|
|
46
56
|
onRemoveSourceType: (def: SourceTypeDef) => Promise<void>
|
|
47
57
|
onApplyWikiTypes: (next: WikiTypeView[], describe: string) => Promise<void>
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
/** 打开时滚到哪一节(默认 `source`)。 */
|
|
59
|
+
section?: TypeDefinitionSection
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
const EMPTY_DRAFT: SourceTypeDraft = { mode: 'create', id: '', label: '', description: '', signals: '', excludes: '' }
|
|
62
|
+
const EMPTY_DRAFT: SourceTypeDraft = { mode: 'create', id: '', label: '', description: '', signals: '', excludes: '', metadataFields: [] }
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 这一节的说明**从正文搬进了图标**(2026-09-26 GUI 反馈)。
|
|
66
|
+
*
|
|
67
|
+
* 原来标题下面挂着两段常驻说明、按钮旁边再挂一段,一共五行解释文字压在十一个分类
|
|
68
|
+
* 上面 —— 打开这个抽屉是来改词汇表的,词汇表本身却被推到第二屏。说明的内容有价值
|
|
69
|
+
* (「兜底分类必须存在且不可停用」这种话不能删),所以挪进 `InfoHint`:要看的时候
|
|
70
|
+
* 悬停或聚焦就有,不看的时候不占版面。下面 Wiki 类型那一节同理。
|
|
71
|
+
*/
|
|
72
|
+
const SOURCE_TYPE_HINT = 'source_type 描述业务场景,不描述文件格式(载体走 source_format)。三档默认值:兜底分类必须存在且不可停用;内置分类可改文案、可停用,但不能删除;自定义分类可增可删,新增时必须写清判定文案——它会拼进内置提示词里交给 Agent 判断。分类下还有来源时不能删除,先迁移。'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 一个分类的判定文案:定义 + 典型信号 + 排除项。
|
|
76
|
+
*
|
|
77
|
+
* 这三样是 Agent 判断「这条来源算不算这一类」的全部依据,但铺在行上会占三四行,
|
|
78
|
+
* 把十一行的列表拉成一屏。所以它们进行首那枚图标,行本身只留身份与操作。
|
|
79
|
+
*/
|
|
80
|
+
function sourceTypeHint(def: SourceTypeDef): string {
|
|
81
|
+
const parts = [def.description || '还没有写定义 —— Agent 只能靠名字猜它是哪一类。']
|
|
82
|
+
if (def.signals.length > 0) parts.push(`典型信号:${def.signals.join(';')}`)
|
|
83
|
+
if (def.excludes.length > 0) parts.push(`不归这一类:${def.excludes.join(';')}`)
|
|
84
|
+
return parts.join('。')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Wiki 类型那一节的说明,同样从常驻段落搬进图标。 */
|
|
88
|
+
const WIKI_TYPE_HINT = '每个类型规定必填小节与必填字段,写卡片时按它校验。这就是编译设置里的 Schema 类型——早先那种「逗号分隔的类型名文本框」已撤掉,它既不参与校验也不被生成逻辑读取,是个改了不生效的假开关。从原文件生成卡片时:先按「定义 + 例如」判断属于哪一类,再按必填小节写正文、按必填字段填 front matter;模板卡由小节派生,新建卡片时照抄即可。'
|
|
53
89
|
|
|
54
90
|
export function TypeDefinitionDrawer({
|
|
55
|
-
open, onOpenChange, kbName, kbId, sourceTypes, wikiTypes,
|
|
56
|
-
onSaveSourceType, onToggleSourceType, onRemoveSourceType, onApplyWikiTypes,
|
|
91
|
+
open, onOpenChange, kbName, kbId, sourceTypes, wikiTypes, customized, catalog, busy,
|
|
92
|
+
onSaveSourceType, onToggleSourceType, onRemoveSourceType, onApplyWikiTypes, section = 'source',
|
|
57
93
|
}: TypeDefinitionDrawerProps): ReactNode {
|
|
58
94
|
/**
|
|
59
95
|
* 分类编辑弹窗的草稿。它属于这个抽屉 —— 注册表搬进来之后,只有这里会开这个弹窗。
|
|
@@ -65,41 +101,70 @@ export function TypeDefinitionDrawer({
|
|
|
65
101
|
|
|
66
102
|
const openEditor = (def?: SourceTypeDef): void => {
|
|
67
103
|
setDraft(def
|
|
68
|
-
? { open: true, mode: 'edit', id: def.id, label: def.label, description: def.description, signals: def.signals.join('\n'), excludes: def.excludes.join('\n') }
|
|
104
|
+
? { open: true, mode: 'edit', id: def.id, label: def.label, description: def.description, signals: def.signals.join('\n'), excludes: def.excludes.join('\n'), metadataFields: (def.metadataFields ?? []).map(field => ({ ...field })) }
|
|
69
105
|
: { ...EMPTY_DRAFT, open: true })
|
|
70
106
|
}
|
|
71
107
|
|
|
108
|
+
/** Wiki 类型那一节的副标题计数(原本由 `WikiTypeSection` 自己渲染)。 */
|
|
109
|
+
const enabledWikiTypes = wikiTypes.filter(type => type.enabled).length
|
|
110
|
+
|
|
111
|
+
const sourceSectionRef = useRef<HTMLElement | null>(null)
|
|
112
|
+
const wikiSectionRef = useRef<HTMLElement | null>(null)
|
|
113
|
+
/**
|
|
114
|
+
* 打开时把目标那一节滚到顶部。
|
|
115
|
+
*
|
|
116
|
+
* 用 `scrollIntoView({ block: 'start' })` 而不是给容器设 `scrollTop`:滚动容器
|
|
117
|
+
* (`DrawerBody`)会变,写死一个 offsetTop 迟早会错位。`useEffect` 依赖 `open` —— 抽屉
|
|
118
|
+
* 关闭时内容不在 DOM 里,ref 是空的,只有打开的那一次才有效。
|
|
119
|
+
*/
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!open) return
|
|
122
|
+
const target = section === 'wiki' ? wikiSectionRef.current : sourceSectionRef.current
|
|
123
|
+
target?.scrollIntoView({ block: 'start' })
|
|
124
|
+
}, [open, section])
|
|
125
|
+
|
|
72
126
|
return <Drawer open={open} onOpenChange={onOpenChange}>
|
|
73
127
|
{open && <DrawerContent className={styles.settingsDrawer} resizable resizeKey="tita-typedef-drawer">
|
|
74
128
|
<DrawerHeader>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
129
|
+
{/* 标题与说明**包在一个 div 里**:`DrawerHeader` 是 `flex justify-between`,
|
|
130
|
+
两个平级子元素会并排 —— 标题被挤成两行,说明占掉剩下那一行。版本抽屉与
|
|
131
|
+
版本面板也是这么包的(那里同样是「标题 + 一整句说明」)。 */}
|
|
132
|
+
<div>
|
|
133
|
+
<DrawerTitle>类型定义</DrawerTitle>
|
|
134
|
+
{/*
|
|
135
|
+
作用域写在标题下面,只写一次:切库换的是**这一套**,不是方案的参数。
|
|
136
|
+
|
|
137
|
+
这段 2026-09-26 之前是**注释掉的**,而测试断言的是这段文字本身 —— 于是它一直
|
|
138
|
+
绿着,绿色的来源却是注释,抽屉里其实没有任何作用域说明(`kbName` 也跟着成了
|
|
139
|
+
一个没人读的 prop)。现在它是真的渲染出来:这一栏是**按库**的词汇表,而同一个
|
|
140
|
+
面板右上角那个齿轮里是**本方案**的参数,两者「切库会不会跟着换」不一样,
|
|
141
|
+
不说就只能靠用户自己撞出来。
|
|
142
|
+
|
|
143
|
+
设置抽屉的标题下没有对应的一句,那是有意的:齿轮的默认预期本来就是「方案级」,
|
|
144
|
+
而「这个抽屉跟着当前知识库走」才是需要说出来的那一半。
|
|
145
|
+
*/}
|
|
146
|
+
<DrawerDescription>
|
|
147
|
+
本库定义{kbName ? <> · <b>{kbName}</b></> : null} —— 来源分类与 Wiki 类型只属于这个知识库,切库就换一套;
|
|
148
|
+
方案级参数(存储 / 收录闸门 / 构建 / 召回 / 重排)在右上角齿轮的「设置」里。
|
|
149
|
+
</DrawerDescription>
|
|
150
|
+
</div>
|
|
81
151
|
</DrawerHeader>
|
|
82
152
|
<DrawerBody>
|
|
83
153
|
<div className={styles.settingsStack}>
|
|
84
|
-
<section className={styles.settingsSection} aria-labelledby="tita-typedef-source-title">
|
|
154
|
+
<section ref={sourceSectionRef} className={styles.settingsSection} aria-labelledby="tita-typedef-source-title">
|
|
85
155
|
<div className={styles.settingsSectionHeader}>
|
|
86
156
|
<div>
|
|
87
|
-
<h3 id="tita-typedef-source-title"><ShieldCheck size={13} />
|
|
88
|
-
<p><b>source_type</b> 描述业务场景,不描述文件格式(载体走 source_format)。</p>
|
|
157
|
+
<h3 id="tita-typedef-source-title"><ShieldCheck size={13} /> 来源类型<InfoHint text={SOURCE_TYPE_HINT} /></h3>
|
|
89
158
|
</div>
|
|
90
159
|
<span className={styles.kbCount}>{sourceTypes.length} 个分类</span>
|
|
91
160
|
</div>
|
|
92
|
-
<p className={styles.kbHint}>
|
|
93
|
-
三档默认值:<b>兜底分类</b>必须存在且不可停用;<b>内置分类</b>可改文案、可停用,但不能删除;
|
|
94
|
-
<b>自定义分类</b>可增可删,新增时必须写清判定文案——它会拼进下面那段内置提示词里交给 Agent 判断。
|
|
95
|
-
分类下还有来源时不能删除,先迁移。
|
|
96
|
-
</p>
|
|
97
161
|
<ul className={styles.kbSourceList}>
|
|
98
162
|
{sourceTypes.map(def => <li key={def.id} className={styles.kbSourceRow}>
|
|
99
163
|
<span className={styles.kbSourceName}>
|
|
100
164
|
<span className={styles.kbSourceLabel}>{def.label}</span>
|
|
101
165
|
<code className={styles.kbSourceId}>{def.id}</code>
|
|
102
166
|
<span className={cn(styles.kbSourceTag, !def.fallback && styles.kbSourceTagLock)}>{def.fallback ? '兜底' : def.builtin ? '内置' : '自定义'}</span>
|
|
167
|
+
<InfoHint text={sourceTypeHint(def)} />
|
|
103
168
|
</span>
|
|
104
169
|
<span className={styles.kbTypeCount}>{def.sourceCount} 个来源</span>
|
|
105
170
|
<Switch
|
|
@@ -122,7 +187,6 @@ export function TypeDefinitionDrawer({
|
|
|
122
187
|
</li>)}
|
|
123
188
|
</ul>
|
|
124
189
|
<div className={styles.kbSourceAdd}>
|
|
125
|
-
<p className={styles.kbHint}>追加分类(例如 <b>test_unit 测试用例</b>)时必须写清「怎么判断它是这一类」。</p>
|
|
126
190
|
<Button variant="outline" size="sm" onClick={() => openEditor()} disabled={busy !== null}><Plus size={13} />新增类型</Button>
|
|
127
191
|
</div>
|
|
128
192
|
|
|
@@ -142,28 +206,21 @@ export function TypeDefinitionDrawer({
|
|
|
142
206
|
</details>}
|
|
143
207
|
</section>
|
|
144
208
|
|
|
145
|
-
<section className={styles.settingsSection} aria-labelledby="tita-typedef-wiki-title">
|
|
209
|
+
<section ref={wikiSectionRef} className={styles.settingsSection} aria-labelledby="tita-typedef-wiki-title">
|
|
146
210
|
<div className={styles.settingsSectionHeader}>
|
|
147
211
|
<div>
|
|
148
|
-
<h3 id="tita-typedef-wiki-title">Wiki 类型(卡片 schema
|
|
149
|
-
<p>每个类型规定必填小节与必填字段,写卡片时按它校验。</p>
|
|
212
|
+
<h3 id="tita-typedef-wiki-title">Wiki 类型(卡片 schema)<InfoHint text={WIKI_TYPE_HINT} /></h3>
|
|
150
213
|
</div>
|
|
151
|
-
|
|
214
|
+
{/* 计数从 `WikiTypeSection` 的副标题搬到这里:那块副标题与这一节的标题
|
|
215
|
+
是同一个名字,一个抽屉里同一句话写两遍。 */}
|
|
216
|
+
<span className={styles.kbCount}>{enabledWikiTypes} / {wikiTypes.length} 启用{customized ? ' · 已自定义' : ''}</span>
|
|
152
217
|
</div>
|
|
153
|
-
<p className={styles.kbHint}>
|
|
154
|
-
这就是编译设置里的 <b>Schema 类型</b>。早先那种「逗号分隔的类型名文本框」已撤掉 ——
|
|
155
|
-
它既不参与校验也不被生成逻辑读取,是个改了不生效的假开关。
|
|
156
|
-
</p>
|
|
157
218
|
<WikiTypeSection
|
|
158
219
|
kbId={kbId}
|
|
159
220
|
types={wikiTypes}
|
|
160
|
-
mode={schemaMode}
|
|
161
|
-
customized={customized}
|
|
162
221
|
canEdit={busy === null}
|
|
163
222
|
busy={busy !== null}
|
|
164
223
|
onApply={onApplyWikiTypes}
|
|
165
|
-
onModeChange={onModeChange}
|
|
166
|
-
onReset={onResetWikiTypes}
|
|
167
224
|
/>
|
|
168
225
|
</section>
|
|
169
226
|
</div>
|
|
@@ -173,39 +230,82 @@ export function TypeDefinitionDrawer({
|
|
|
173
230
|
{draft.open && <DialogContent>
|
|
174
231
|
<DialogHeader><DialogTitle>{draft.mode === 'create' ? '新增来源分类' : `编辑来源分类 · ${draft.label}`}</DialogTitle></DialogHeader>
|
|
175
232
|
<div className={styles.kbForm}>
|
|
176
|
-
<label
|
|
233
|
+
<Field label="分类 id" hint="小写字母 / 数字 / 下划线,创建后不可修改——它是写进 SourceRaw frontmatter 的稳定标识。">
|
|
177
234
|
<Input value={draft.id} disabled={draft.mode === 'edit'} spellCheck={false} placeholder="test_unit"
|
|
178
235
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<label className={styles.kbField}><Label>显示名</Label>
|
|
236
|
+
</Field>
|
|
237
|
+
<Field label="显示名">
|
|
182
238
|
<Input value={draft.label} placeholder="测试用例"
|
|
183
239
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, label: event.target.value })} />
|
|
184
|
-
</
|
|
185
|
-
<label
|
|
186
|
-
<
|
|
240
|
+
</Field>
|
|
241
|
+
<Field label="一句话定义(必填)" hint="写清「是什么」,不要写成「包含哪些文件」——载体格式走 source_format,Agent 靠这句话判断。">
|
|
242
|
+
<Textarea value={draft.description} spellCheck={false}
|
|
187
243
|
placeholder="以单个可执行验证点为中心、用于判定某项功能是否正确的用例记录。"
|
|
188
244
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, description: event.target.value })} />
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<textarea className={styles.kbTextarea} value={draft.signals} spellCheck={false}
|
|
245
|
+
</Field>
|
|
246
|
+
<Field label="signals · 典型信号(每行一条,至少 2 条)">
|
|
247
|
+
<Textarea value={draft.signals} spellCheck={false}
|
|
193
248
|
placeholder={'包含前置条件 / 操作步骤 / 预期结果三要素\n带 case id、优先级、关联需求编号'}
|
|
194
249
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, signals: event.target.value })} />
|
|
195
|
-
</
|
|
196
|
-
<label
|
|
197
|
-
<
|
|
250
|
+
</Field>
|
|
251
|
+
<Field label="excludes · 排除项(每行一条,至少 1 条)" hint="每条都要写出「该归到哪个分类」(用 id 或标签);指向不存在的分类会被拒绝,否则边界是悬空的。">
|
|
252
|
+
<Textarea value={draft.excludes} spellCheck={false}
|
|
198
253
|
placeholder={'整份测试方案或测试计划归 document\n线上故障根因排查归 question'}
|
|
199
254
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, excludes: event.target.value })} />
|
|
200
|
-
|
|
201
|
-
|
|
255
|
+
</Field>
|
|
256
|
+
{/* 元数据字段声明(0048)。为什么是结构化行而不是「每行一条」的文本:
|
|
257
|
+
一个字段有 key / 标签 / 必填三样,压进一行就得发明一个分隔符语法,
|
|
258
|
+
而这里要填的 key 恰恰是 front matter 的路径(`sources[0].author`),
|
|
259
|
+
它不该被一个自定义语法二次解析。 */}
|
|
260
|
+
<Field label="元数据字段" hint="这一类来源该带哪些元数据。key 用 front matter 的顶层键(或 sources[0].author 这种路径);勾了「必填」而缺失时,录入与详情里会提示,但不会拒收这份来源。">
|
|
261
|
+
<div className={styles.kbFieldRows}>
|
|
262
|
+
{(draft.metadataFields ?? []).map((field, index) => <div className={styles.kbFieldRow} key={`field-${index}`}>
|
|
263
|
+
<Input
|
|
264
|
+
value={field.key} spellCheck={false} placeholder="sources[0].author" aria-label="元数据键"
|
|
265
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({
|
|
266
|
+
...draft,
|
|
267
|
+
metadataFields: (draft.metadataFields ?? []).map((item, at) => at === index ? { ...item, key: event.target.value } : item),
|
|
268
|
+
})}
|
|
269
|
+
/>
|
|
270
|
+
<Input
|
|
271
|
+
value={field.label ?? ''} spellCheck={false} placeholder="作者 / 账号" aria-label="显示名"
|
|
272
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({
|
|
273
|
+
...draft,
|
|
274
|
+
metadataFields: (draft.metadataFields ?? []).map((item, at) => at === index ? { ...item, label: event.target.value } : item),
|
|
275
|
+
})}
|
|
276
|
+
/>
|
|
277
|
+
<label className={styles.kbFieldRequired}>
|
|
278
|
+
<Switch
|
|
279
|
+
checked={field.required === true}
|
|
280
|
+
onCheckedChange={next => setDraft({
|
|
281
|
+
...draft,
|
|
282
|
+
metadataFields: (draft.metadataFields ?? []).map((item, at) => at === index ? { ...item, required: next === true } : item),
|
|
283
|
+
})}
|
|
284
|
+
/>必填
|
|
285
|
+
</label>
|
|
286
|
+
<Button
|
|
287
|
+
variant="ghost" size="icon" aria-label={`删除字段 ${field.key || index + 1}`}
|
|
288
|
+
onClick={() => setDraft({ ...draft, metadataFields: (draft.metadataFields ?? []).filter((_, at) => at !== index) })}
|
|
289
|
+
><X size={13} /></Button>
|
|
290
|
+
</div>)}
|
|
291
|
+
<Button
|
|
292
|
+
variant="outline" size="sm"
|
|
293
|
+
onClick={() => setDraft({ ...draft, metadataFields: [...(draft.metadataFields ?? []), { key: '', label: '' }] })}
|
|
294
|
+
><Plus size={12} />添加字段</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</Field>
|
|
202
297
|
</div>
|
|
203
298
|
<DialogFooter>
|
|
204
299
|
<Button variant="ghost" onClick={closeEditor}>取消</Button>
|
|
205
300
|
<Button
|
|
206
301
|
variant="default"
|
|
207
302
|
disabled={busy !== null || !draft.id.trim() || !draft.label.trim() || !draft.description.trim()}
|
|
208
|
-
onClick={() => void onSaveSourceType({
|
|
303
|
+
onClick={() => void onSaveSourceType({
|
|
304
|
+
mode: draft.mode, id: draft.id, label: draft.label, description: draft.description,
|
|
305
|
+
signals: draft.signals, excludes: draft.excludes,
|
|
306
|
+
// 空的 key 行是「加了但还没填」,直接丢掉:不把半成品写进注册表。
|
|
307
|
+
metadataFields: (draft.metadataFields ?? []).filter(field => field.key.trim() !== '').map(field => ({ ...field, key: field.key.trim(), label: (field.label ?? '').trim() })),
|
|
308
|
+
})}
|
|
209
309
|
><Check size={13} />保存</Button>
|
|
210
310
|
</DialogFooter>
|
|
211
311
|
</DialogContent>}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「review 与备注」—— 0044 的门开在**卡片预览抽屉**里,不再单独占一屏。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么从列表页搬到这里
|
|
5
|
+
*
|
|
6
|
+
* 0044 第一版在 wiki 卡片列表上面插了一块「入库后的 review」待办区,按卡片逐条列出。
|
|
7
|
+
* 那等于把同一张卡片的信息摊在两个地方:列表里一行(状态 / 类型 / 时间),待办区里
|
|
8
|
+
* 一大块(正文、三层校验、备注、动作)。人点开卡片看内容时,恰恰是**他正要判断这张卡
|
|
9
|
+
* 对不对**的那一刻 —— review 的动作就应该长在那只手上,而不是让人退回去、在另一个
|
|
10
|
+
* 区域里重新找到同一张卡。
|
|
11
|
+
*
|
|
12
|
+
* 现在:列表负责「哪几张需要我看」(状态徽标就在行上),抽屉负责「看它、判它、记它」。
|
|
13
|
+
*
|
|
14
|
+
* ## 三件事分得开
|
|
15
|
+
*
|
|
16
|
+
* - **状态**:`已 review` / `没人看过`,写在抽屉头的事实位上(不靠颜色单独表达)。
|
|
17
|
+
* - **待补**:三层校验(格式 / 引用 / 语义)报上来的缺口。缺信息**不拦入库**,所以它
|
|
18
|
+
* 是常态而不是事故;逐条列出是为了让人知道补什么(多半要去改类型定义)。
|
|
19
|
+
* - **人写的话**:备注与异常点。正文只读,人想改判断就写在这里 —— 这是这一块唯一
|
|
20
|
+
* 可写的东西。
|
|
21
|
+
*
|
|
22
|
+
* ## 为什么没有「全部通过」
|
|
23
|
+
*
|
|
24
|
+
* 批量动作一定会取代逐条判断,而 review 的全部价值就是逐条看过。这里一次只服务一张卡。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
28
|
+
import {
|
|
29
|
+
AlertCircle, Check, CheckCheck, MessageSquarePlus, RotateCcw, ShieldCheck, TriangleAlert, X,
|
|
30
|
+
} from '@tita-data-agent/ui/icons'
|
|
31
|
+
import { Badge, Button, Textarea, cn } from '@tita-data-agent/ui/components'
|
|
32
|
+
import { DrawerSectionHead } from './DrawerSection.tsx'
|
|
33
|
+
import { requestJson } from './request.ts'
|
|
34
|
+
import { styles } from './styles.ts'
|
|
35
|
+
import type { KnowledgeCardView } from './KnowledgeBaseTab.tsx'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 三层校验的标签。三层分开报不是洁癖(0021 §6.5):**格式错说明卡片没填完,
|
|
39
|
+
* 引用错说明图断了,语义错说明这条知识本身没用**。三者对应的动作完全不同
|
|
40
|
+
* (补字段 / 修边 / 重写),混成同一个「校验失败」人就学不会怎么补。
|
|
41
|
+
*/
|
|
42
|
+
const LEVEL_LABEL: Record<string, { label: string; tone: 'warning' | 'violet' | 'neutral' }> = {
|
|
43
|
+
format: { label: '格式', tone: 'warning' },
|
|
44
|
+
reference: { label: '引用', tone: 'violet' },
|
|
45
|
+
semantic: { label: '语义', tone: 'neutral' },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 没人看过时的评分(与服务端 `CARD_REVIEW_BASE_SCORE` 同值,只用于显示兜底)。 */
|
|
49
|
+
const BASE_SCORE = 0.35
|
|
50
|
+
|
|
51
|
+
function issuesOf(card: KnowledgeCardView): Array<{ level?: string; field?: string; message?: string }> {
|
|
52
|
+
const raw = card.metadata?.schema_issues
|
|
53
|
+
return Array.isArray(raw) ? raw as Array<{ level?: string; field?: string; message?: string }> : []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function anomaliesOf(card: KnowledgeCardView): NonNullable<KnowledgeCardView['annotations']> {
|
|
57
|
+
return (card.annotations ?? []).filter(item => item.kind === 'anomaly')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function notesOf(card: KnowledgeCardView): NonNullable<KnowledgeCardView['annotations']> {
|
|
61
|
+
return (card.annotations ?? []).filter(item => item.kind !== 'anomaly')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function WikiCardReview({ kbId, card, onChanged, onOpenWikiTypes }: {
|
|
65
|
+
kbId: string
|
|
66
|
+
/** 抽屉当前打开的那张卡片 —— 这一块只服务它一张。 */
|
|
67
|
+
card: KnowledgeCardView
|
|
68
|
+
/** review / 备注写盘成功之后通知父级重新读取这张卡片与卡片列表。 */
|
|
69
|
+
onChanged?: () => void
|
|
70
|
+
/** 「去改类型定义」的入口。待补项多半是类型定义那一边的问题。 */
|
|
71
|
+
onOpenWikiTypes?: () => void
|
|
72
|
+
}): ReactNode {
|
|
73
|
+
const [busy, setBusy] = useState(false)
|
|
74
|
+
const [composing, setComposing] = useState(false)
|
|
75
|
+
const [kind, setKind] = useState<'note' | 'anomaly'>('note')
|
|
76
|
+
const [draft, setDraft] = useState('')
|
|
77
|
+
const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 抽屉换到另一张卡片时把草稿与提示清掉。
|
|
81
|
+
*
|
|
82
|
+
* 少了这一步,`composing` 与 `draft` 会跟着抽屉留下来:人在 A 卡片写了一半,
|
|
83
|
+
* 关掉再开 B 卡片,输入框里还是 A 的话,一按保存就写到 B 头上。
|
|
84
|
+
*/
|
|
85
|
+
useEffect(() => { setComposing(false); setDraft(''); setKind('note'); setNotice(null) }, [card.entityId])
|
|
86
|
+
|
|
87
|
+
const reviewed = card.review?.state === 'reviewed'
|
|
88
|
+
const score = card.review?.score ?? BASE_SCORE
|
|
89
|
+
const issues = issuesOf(card)
|
|
90
|
+
const anomalies = anomaliesOf(card)
|
|
91
|
+
const notes = notesOf(card)
|
|
92
|
+
|
|
93
|
+
const call = async (path: string, body: Record<string, unknown>): Promise<boolean> => {
|
|
94
|
+
setNotice(null)
|
|
95
|
+
try {
|
|
96
|
+
await requestJson(path, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ kb: kbId, ...body }),
|
|
100
|
+
})
|
|
101
|
+
return true
|
|
102
|
+
} catch (error) {
|
|
103
|
+
setNotice({ tone: 'error', text: error instanceof Error ? error.message : String(error) })
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const review = async (action: 'review' | 'unreview'): Promise<void> => {
|
|
109
|
+
setBusy(true)
|
|
110
|
+
const ok = await call('/knowledge/card-review', { entity_id: card.entityId, entity_type: card.entityType, action })
|
|
111
|
+
if (ok) {
|
|
112
|
+
setNotice({
|
|
113
|
+
tone: 'success',
|
|
114
|
+
text: action === 'review'
|
|
115
|
+
? '已 review:评分写进卡片,下一次编译之后召回排序会用到它'
|
|
116
|
+
: '已撤销 review:评分放回出厂值,下一次编译之后召回排序会用到它',
|
|
117
|
+
})
|
|
118
|
+
onChanged?.()
|
|
119
|
+
}
|
|
120
|
+
setBusy(false)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const annotate = async (noteKind: 'note' | 'anomaly', note: string, removeId?: string): Promise<void> => {
|
|
124
|
+
setBusy(true)
|
|
125
|
+
const ok = await call('/knowledge/card-annotate', {
|
|
126
|
+
entity_id: card.entityId,
|
|
127
|
+
entity_type: card.entityType,
|
|
128
|
+
...(removeId ? { remove_id: removeId } : { kind: noteKind, note }),
|
|
129
|
+
})
|
|
130
|
+
if (ok) {
|
|
131
|
+
setNotice({
|
|
132
|
+
tone: 'success',
|
|
133
|
+
text: removeId
|
|
134
|
+
? '备注已撤销'
|
|
135
|
+
: noteKind === 'anomaly'
|
|
136
|
+
? '已标为异常点:它照样召回得到,只是排在后面'
|
|
137
|
+
: '已加一条备注',
|
|
138
|
+
})
|
|
139
|
+
setComposing(false)
|
|
140
|
+
setDraft('')
|
|
141
|
+
onChanged?.()
|
|
142
|
+
}
|
|
143
|
+
setBusy(false)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return <section className={styles.drawerSection}>
|
|
147
|
+
<DrawerSectionHead icon={<ShieldCheck size={13} />} title="review 与备注">
|
|
148
|
+
<span className={styles.drawerHeadFact}>{reviewed ? '已 review' : '没人看过'} · 评分 {score.toFixed(2)}</span>
|
|
149
|
+
</DrawerSectionHead>
|
|
150
|
+
|
|
151
|
+
{/* 这一屏的规矩要写在明面上,而且要说清它与上一版的差别 —— 用户对「编译没生效」的
|
|
152
|
+
印象正来自上一版「提议不落卡」。 */}
|
|
153
|
+
<p className="max-w-[68ch] text-[11px] leading-relaxed text-tita-ink-muted">
|
|
154
|
+
卡片已经在库里、能被召回了 —— review 是事后判断,不是准入:它抬高召回评分。
|
|
155
|
+
正文只读,发现对不上就加备注;标注异常点不会让它消失,只会把它排到后面。
|
|
156
|
+
{card.review?.reviewedBy ? `(${reviewed ? 'review 人' : '上次 review 人'}:${card.review.reviewedBy})` : ''}
|
|
157
|
+
</p>
|
|
158
|
+
|
|
159
|
+
{notice && <p
|
|
160
|
+
role="status"
|
|
161
|
+
className={cn('flex items-start gap-1.5 text-[11px] leading-relaxed', notice.tone === 'error' ? 'text-tita-danger' : 'text-tita-ink-muted')}
|
|
162
|
+
>
|
|
163
|
+
{notice.tone === 'error' ? <AlertCircle size={12} className="mt-0.5 shrink-0" /> : <Check size={12} className="mt-0.5 shrink-0" />}
|
|
164
|
+
<span className="min-w-0 flex-1">{notice.text}</span>
|
|
165
|
+
</p>}
|
|
166
|
+
|
|
167
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
168
|
+
<Button
|
|
169
|
+
variant={reviewed ? 'outline' : 'default'} size="sm"
|
|
170
|
+
className="h-[28px] min-h-[28px] px-2.5 text-[11.5px]"
|
|
171
|
+
disabled={busy}
|
|
172
|
+
title={reviewed ? '撤销 review:评分放回出厂值' : '标记为看过:评分抬高,召回时排在前面一点'}
|
|
173
|
+
onClick={() => void review(reviewed ? 'unreview' : 'review')}
|
|
174
|
+
>{reviewed ? <><RotateCcw size={12} />撤销 review</> : <><CheckCheck size={12} />review 通过</>}</Button>
|
|
175
|
+
<Button
|
|
176
|
+
variant="outline" size="sm"
|
|
177
|
+
className="h-[28px] min-h-[28px] px-2.5 text-[11.5px]"
|
|
178
|
+
aria-expanded={composing}
|
|
179
|
+
onClick={() => composing ? (setComposing(false), setDraft('')) : (setComposing(true), setDraft(''), setKind('note'))}
|
|
180
|
+
><MessageSquarePlus size={12} />加备注</Button>
|
|
181
|
+
{issues.length > 0 && onOpenWikiTypes && <Button
|
|
182
|
+
variant="ghost" size="sm"
|
|
183
|
+
className="h-[28px] min-h-[28px] px-2.5 text-[11.5px]"
|
|
184
|
+
onClick={onOpenWikiTypes}
|
|
185
|
+
>去改类型定义</Button>}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* 待补:三层校验的问题逐条列出。它排在人写的备注前面 —— 「卡片缺什么」是事实,
|
|
189
|
+
「人怎么看它」是判断,事实先读。 */}
|
|
190
|
+
{issues.length > 0 && <div className="flex flex-col gap-1">
|
|
191
|
+
<span className="text-[10.5px] font-semibold text-tita-ink-muted">待补 {issues.length} 项(缺信息不拦入库,后面有涉及的文档会补上)</span>
|
|
192
|
+
<ul className="flex flex-col gap-0.5">
|
|
193
|
+
{issues.map((issue, index) => {
|
|
194
|
+
const level = LEVEL_LABEL[issue.level ?? ''] ?? { label: issue.level ?? '问题', tone: 'neutral' as const }
|
|
195
|
+
return <li key={`${issue.field ?? ''}-${index}`} className="flex items-start gap-1.5 text-[11px] leading-relaxed text-tita-ink-muted">
|
|
196
|
+
<Badge tone={level.tone}>{level.label}</Badge>
|
|
197
|
+
<span className="min-w-0 flex-1">{issue.field ? `${issue.field}:` : ''}{issue.message}</span>
|
|
198
|
+
</li>
|
|
199
|
+
})}
|
|
200
|
+
</ul>
|
|
201
|
+
</div>}
|
|
202
|
+
|
|
203
|
+
{/* 人写的话:异常点在前(它们是这一块里唯一「已经知道有问题」的东西),普通备注在后。 */}
|
|
204
|
+
<div className="flex flex-col gap-1">
|
|
205
|
+
<span className="text-[10.5px] font-semibold text-tita-ink-muted">备注 {anomalies.length + notes.length > 0 ? `· ${anomalies.length + notes.length} 条` : ''}</span>
|
|
206
|
+
{anomalies.length + notes.length === 0
|
|
207
|
+
? <p className="text-[11px] leading-relaxed text-tita-ink-faint">还没有备注。发现卡片与来源对不上,就写一条 —— 原始文档不会因此被改动。</p>
|
|
208
|
+
: <ul className="flex flex-col gap-1">
|
|
209
|
+
{[...anomalies, ...notes].map(item => <li key={item.id} className="flex items-start gap-1.5 text-[11px] leading-relaxed">
|
|
210
|
+
{item.kind === 'anomaly'
|
|
211
|
+
? <TriangleAlert size={12} className="mt-0.5 shrink-0 text-tita-warning" />
|
|
212
|
+
: <MessageSquarePlus size={12} className="mt-0.5 shrink-0 text-tita-ink-faint" />}
|
|
213
|
+
<span className={cn('min-w-0 flex-1', item.kind === 'anomaly' ? 'text-tita-ink' : 'text-tita-ink-muted')}>{item.note}</span>
|
|
214
|
+
<span className="shrink-0 text-[10px] text-tita-ink-faint">{item.author ?? item.createdAt?.slice(0, 10) ?? ''}</span>
|
|
215
|
+
<Button
|
|
216
|
+
variant="ghost" size="icon"
|
|
217
|
+
className="h-[22px] w-[22px] min-w-[22px] shrink-0"
|
|
218
|
+
title="撤销这条备注" aria-label={`撤销备注:${item.note}`}
|
|
219
|
+
disabled={busy}
|
|
220
|
+
onClick={() => void annotate(item.kind, item.note, item.id)}
|
|
221
|
+
><X size={11} /></Button>
|
|
222
|
+
</li>)}
|
|
223
|
+
</ul>}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{composing && <div className="flex flex-col gap-1.5 rounded-lg border border-tita-line bg-tita-canvas p-2">
|
|
227
|
+
{/* 两条路是**两种判断**,不是两个样式:普通备注是解释,异常点是「这里对不上」。 */}
|
|
228
|
+
<div className="flex items-center gap-1.5">
|
|
229
|
+
<Button
|
|
230
|
+
variant={kind === 'note' ? 'default' : 'outline'} size="sm"
|
|
231
|
+
className="h-[26px] min-h-[26px] px-2 text-[11px]"
|
|
232
|
+
onClick={() => setKind('note')}
|
|
233
|
+
><MessageSquarePlus size={11} />普通备注</Button>
|
|
234
|
+
<Button
|
|
235
|
+
variant={kind === 'anomaly' ? 'default' : 'outline'} size="sm"
|
|
236
|
+
className="h-[26px] min-h-[26px] px-2 text-[11px]"
|
|
237
|
+
onClick={() => setKind('anomaly')}
|
|
238
|
+
><TriangleAlert size={11} />异常点</Button>
|
|
239
|
+
</div>
|
|
240
|
+
<Textarea
|
|
241
|
+
value={draft}
|
|
242
|
+
autoFocus
|
|
243
|
+
rows={3}
|
|
244
|
+
aria-label="备注内容"
|
|
245
|
+
placeholder={kind === 'anomaly'
|
|
246
|
+
? '哪里对不上?例如「这个数与 2025-03 复盘里的 12.4% 不一致」'
|
|
247
|
+
: '记一句判断,例如「口径在 Q3 手册里改过,本节还是旧的」'}
|
|
248
|
+
className="min-h-[62px] text-[11.5px] leading-relaxed"
|
|
249
|
+
onChange={event => setDraft(event.target.value)}
|
|
250
|
+
/>
|
|
251
|
+
<div className="flex items-center gap-1.5">
|
|
252
|
+
<Button
|
|
253
|
+
variant="default" size="sm"
|
|
254
|
+
className="h-[28px] min-h-[28px] px-2.5 text-[11.5px]"
|
|
255
|
+
disabled={busy || draft.trim().length === 0}
|
|
256
|
+
onClick={() => void annotate(kind, draft.trim())}
|
|
257
|
+
><Check size={12} />保存备注</Button>
|
|
258
|
+
<Button
|
|
259
|
+
variant="ghost" size="sm"
|
|
260
|
+
className="h-[28px] min-h-[28px] px-2.5 text-[11.5px]"
|
|
261
|
+
onClick={() => { setComposing(false); setDraft('') }}
|
|
262
|
+
>取消</Button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>}
|
|
265
|
+
</section>
|
|
266
|
+
}
|