vue-agent-start 0.1.0
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/package.json +30 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/agent-flow/adapter/backend.ts +99 -0
- package/src/agent-flow/components/ChatIframePanel.vue +773 -0
- package/src/agent-flow/components/DatasetItemCard.vue +313 -0
- package/src/agent-flow/components/MemoryWindow.vue +104 -0
- package/src/agent-flow/components/PromptEditor.vue +804 -0
- package/src/agent-flow/components/PromptEditorTagPanel.vue +460 -0
- package/src/agent-flow/components/SvgIcon.vue +32 -0
- package/src/agent-flow/components/ToolItemCard.vue +169 -0
- package/src/agent-flow/components/VariableCard.vue +161 -0
- package/src/agent-flow/components/VariableSelector.vue +162 -0
- package/src/agent-flow/components/chat-iframe-types.ts +35 -0
- package/src/agent-flow/components/stubs/DatasetChooser.vue +70 -0
- package/src/agent-flow/components/stubs/Embedding.vue +22 -0
- package/src/agent-flow/components/stubs/ModelSelect.vue +24 -0
- package/src/agent-flow/components/stubs/RestfulSelectCard.vue +24 -0
- package/src/agent-flow/components/stubs/ToolChooser.vue +96 -0
- package/src/agent-flow/components/stubs/TriggersItemCard.vue +126 -0
- package/src/agent-flow/components/stubs/VariableModal.vue +115 -0
- package/src/agent-flow/index.ts +73 -0
- package/src/agent-flow/stores/workflow.ts +72 -0
- package/src/agent-flow/utils/langUtils.ts +173 -0
- package/src/agent-flow/workflow/Ability.vue +65 -0
- package/src/agent-flow/workflow/AuthorizationItem.vue +101 -0
- package/src/agent-flow/workflow/ConversationVar.vue +42 -0
- package/src/agent-flow/workflow/CustomEdge.vue +94 -0
- package/src/agent-flow/workflow/EnvironmentVar.vue +40 -0
- package/src/agent-flow/workflow/FlowDesigner.vue +2802 -0
- package/src/agent-flow/workflow/Icon.vue +179 -0
- package/src/agent-flow/workflow/NodeConfig.vue +509 -0
- package/src/agent-flow/workflow/NodeConfigCard.vue +460 -0
- package/src/agent-flow/workflow/NodeHandle.vue +147 -0
- package/src/agent-flow/workflow/NodeHeaderToolbar.vue +77 -0
- package/src/agent-flow/workflow/NodeHoverToolbar.vue +176 -0
- package/src/agent-flow/workflow/OutputItemCard.vue +668 -0
- package/src/agent-flow/workflow/ParamTableItem.vue +200 -0
- package/src/agent-flow/workflow/StructTable.vue +202 -0
- package/src/agent-flow/workflow/VarInsertField.vue +517 -0
- package/src/agent-flow/workflow/VarSelectField.vue +223 -0
- package/src/agent-flow/workflow/VariableSelectorItem.vue +48 -0
- package/src/agent-flow/workflow/WfField.vue +206 -0
- package/src/agent-flow/workflow/_config-base.css +470 -0
- package/src/agent-flow/workflow/composables/useNode.js +55 -0
- package/src/agent-flow/workflow/index.vue +53 -0
- package/src/agent-flow/workflow/nodeCard/AgentNodeCard.vue +180 -0
- package/src/agent-flow/workflow/nodeCard/AnswerNodeCard.vue +65 -0
- package/src/agent-flow/workflow/nodeCard/ClassifierCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/CodeNodeCard.vue +139 -0
- package/src/agent-flow/workflow/nodeCard/ConditionNodeCard.vue +323 -0
- package/src/agent-flow/workflow/nodeCard/DocumentExtractorCard.vue +83 -0
- package/src/agent-flow/workflow/nodeCard/EndNodeCard.vue +123 -0
- package/src/agent-flow/workflow/nodeCard/HttpNodeCard.vue +451 -0
- package/src/agent-flow/workflow/nodeCard/HumanInputCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/IterationCard.vue +66 -0
- package/src/agent-flow/workflow/nodeCard/KnowledgeRetrievalCard.vue +48 -0
- package/src/agent-flow/workflow/nodeCard/LLMNodeCard.vue +206 -0
- package/src/agent-flow/workflow/nodeCard/ListOperatorCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/ParameterExtractorCard.vue +352 -0
- package/src/agent-flow/workflow/nodeCard/ServiceApiNodeCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/StartNodeCard.vue +216 -0
- package/src/agent-flow/workflow/nodeCard/VariableAssignerCard.vue +125 -0
- package/src/agent-flow/workflow/nodeCard/VariableNodeCard.vue +121 -0
- package/src/agent-flow/workflow/nodes/AgentNode.vue +122 -0
- package/src/agent-flow/workflow/nodes/AnswerNode.vue +99 -0
- package/src/agent-flow/workflow/nodes/ClassifierNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/CodeNode.vue +152 -0
- package/src/agent-flow/workflow/nodes/ConditionNode.vue +232 -0
- package/src/agent-flow/workflow/nodes/DocumentExtractorNode.vue +96 -0
- package/src/agent-flow/workflow/nodes/EndNode.vue +119 -0
- package/src/agent-flow/workflow/nodes/FileUploadNode.vue +97 -0
- package/src/agent-flow/workflow/nodes/HttpNode.vue +204 -0
- package/src/agent-flow/workflow/nodes/HumanInputNode.vue +110 -0
- package/src/agent-flow/workflow/nodes/IterationNode.vue +98 -0
- package/src/agent-flow/workflow/nodes/KnowledgeRetrievalNode.vue +253 -0
- package/src/agent-flow/workflow/nodes/LLMNode.vue +134 -0
- package/src/agent-flow/workflow/nodes/ListOperatorNode.vue +111 -0
- package/src/agent-flow/workflow/nodes/LoopNode.vue +94 -0
- package/src/agent-flow/workflow/nodes/ParameterExtractorNode.vue +147 -0
- package/src/agent-flow/workflow/nodes/ServiceApiNode.vue +161 -0
- package/src/agent-flow/workflow/nodes/StartNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/TemplateNode.vue +103 -0
- package/src/agent-flow/workflow/nodes/UserInputNode.vue +92 -0
- package/src/agent-flow/workflow/nodes/VariableAssignerNode.vue +145 -0
- package/src/agent-flow/workflow/nodes/VariableNode.vue +130 -0
- package/src/agent-flow/workflow/nodes/_node-base.css +338 -0
- package/src/agent-flow/workflow/utils/node_card_form.ts +335 -0
- package/src/agent-flow/workflow/utils/node_config.ts +63 -0
- package/src/agent-flow/workflow/utils/workflow_utils.ts +151 -0
- package/src/agent-studio/adapters/springAgentStart.ts +390 -0
- package/src/agent-studio/api/index.ts +1 -0
- package/src/agent-studio/api/types.ts +173 -0
- package/src/agent-studio/components/AgentApiDocs.vue +500 -0
- package/src/agent-studio/components/AgentAppsPage.vue +1601 -0
- package/src/agent-studio/components/AgentCardGrid.vue +259 -0
- package/src/agent-studio/components/AgentChatPage.vue +553 -0
- package/src/agent-studio/components/AgentDebugPanel.vue +350 -0
- package/src/agent-studio/components/AgentLogsPanel.vue +391 -0
- package/src/agent-studio/components/AgentMonitorPanel.vue +204 -0
- package/src/agent-studio/components/AgentOrchestrate.vue +505 -0
- package/src/agent-studio/components/AgentPromptEditor.vue +237 -0
- package/src/agent-studio/components/AgentStudioShell.vue +227 -0
- package/src/agent-studio/components/AgentToolsPanel.vue +394 -0
- package/src/agent-studio/components/AgentVariablesPanel.vue +205 -0
- package/src/agent-studio/components/ApiKeyManager.vue +251 -0
- package/src/agent-studio/components/AppDesignDrawer.vue +3391 -0
- package/src/agent-studio/components/CreateAppModal.vue +737 -0
- package/src/agent-studio/components/SparkChart.vue +200 -0
- package/src/agent-studio/components/VariableEditorModal.vue +499 -0
- package/src/agent-studio/composables/useAgentStudio.ts +347 -0
- package/src/agent-studio/index.ts +60 -0
- package/src/agent-studio/panels/DrawerFlowDesigner.vue +203 -0
- package/src/agent-studio/panels/LogAnnotationPanel.vue +490 -0
- package/src/agent-studio/panels/MonitorPanel.vue +535 -0
- package/src/agent-studio/types.ts +242 -0
- package/src/index.ts +155 -0
- package/src/knowledge-hub/adapters/springAgentStart.ts +472 -0
- package/src/knowledge-hub/components/AddDocumentsWizard.vue +1452 -0
- package/src/knowledge-hub/components/CreateDatasetWizard.vue +1723 -0
- package/src/knowledge-hub/components/DatasetCardGrid.vue +467 -0
- package/src/knowledge-hub/components/DatasetDetailDrawer.vue +888 -0
- package/src/knowledge-hub/components/DatasetPicker.vue +65 -0
- package/src/knowledge-hub/components/DatasetPickerModal.vue +526 -0
- package/src/knowledge-hub/components/DatasetSettingsPanel.vue +908 -0
- package/src/knowledge-hub/components/DatasetSidebar.vue +781 -0
- package/src/knowledge-hub/components/DocumentChunksView.vue +802 -0
- package/src/knowledge-hub/components/DocumentTable.vue +552 -0
- package/src/knowledge-hub/components/HitTestingPanel.vue +74 -0
- package/src/knowledge-hub/components/KnowledgeApp.vue +66 -0
- package/src/knowledge-hub/components/KnowledgeHubApp.vue +504 -0
- package/src/knowledge-hub/components/RecallTestingPanelV2.vue +515 -0
- package/src/knowledge-hub/components/RetrievalConfigPopover.vue +270 -0
- package/src/knowledge-hub/components/RetrievalMethodPicker.vue +547 -0
- package/src/knowledge-hub/components/RetrievedList.vue +24 -0
- package/src/knowledge-hub/components/internal/KhDialog.vue +195 -0
- package/src/knowledge-hub/composables/useKnowledge.ts +89 -0
- package/src/knowledge-hub/i18n/index.ts +2 -0
- package/src/knowledge-hub/i18n/messages.ts +422 -0
- package/src/knowledge-hub/i18n/useKhI18n.ts +107 -0
- package/src/knowledge-hub/index.ts +46 -0
- package/src/knowledge-hub/styles/index.css +4 -0
- package/src/knowledge-hub/styles/tokens.css +138 -0
- package/src/knowledge-hub/types/api.ts +213 -0
- package/src/knowledge-hub/types/dataset.ts +88 -0
- package/src/knowledge-hub/types/document.ts +45 -0
- package/src/knowledge-hub/types/index.ts +9 -0
- package/src/knowledge-hub/types/retrieval.ts +46 -0
- package/src/provider-hub/components/CredentialForm.vue +103 -0
- package/src/provider-hub/components/DefaultModelsPanel.vue +270 -0
- package/src/provider-hub/components/GroupedModelSelect.vue +445 -0
- package/src/provider-hub/components/ModelCardGrid.vue +332 -0
- package/src/provider-hub/components/ModelParameterDrawer.vue +739 -0
- package/src/provider-hub/components/ModelPickerPopover.vue +1282 -0
- package/src/provider-hub/components/ProviderApp.vue +159 -0
- package/src/provider-hub/components/ProviderCredentialModal.vue +321 -0
- package/src/provider-hub/components/ProviderGallery.vue +131 -0
- package/src/provider-hub/components/ProviderHubShell.vue +1992 -0
- package/src/provider-hub/components/ProviderIcon.vue +130 -0
- package/src/provider-hub/composables/useProviderHub.ts +358 -0
- package/src/provider-hub/index.ts +43 -0
- package/src/provider-hub/svg/MiniMax.svg +1 -0
- package/src/provider-hub/svg/Volcengine.svg +1 -0
- package/src/provider-hub/svg/Zhipu.svg +1 -0
- package/src/provider-hub/svg/deepseek.svg +1 -0
- package/src/provider-hub/svg/moonshot.svg +1 -0
- package/src/provider-hub/svg/openai.svg +1 -0
- package/src/provider-hub/svg/qwen.svg +1 -0
- package/src/provider-hub/svg/siliconflow.svg +1 -0
- package/src/provider-hub/types.ts +256 -0
|
@@ -0,0 +1,1723 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CreateDatasetWizard — Dify-parity 3-step wizard for creating a knowledge base.
|
|
4
|
+
*
|
|
5
|
+
* Screens (match Snipaste_2026-07-09_08-56-09..47.png):
|
|
6
|
+
* 1. 选择数据源 — source cards + file dropzone
|
|
7
|
+
* 2. 文本分段与清洗 — chunking + indexing + retrieval, right-side preview
|
|
8
|
+
* 3. 处理并完成 — success screen with progress + summary
|
|
9
|
+
*
|
|
10
|
+
* Component owns UI state only. The host provides callbacks for file upload
|
|
11
|
+
* and dataset creation via events — this keeps the package framework-agnostic
|
|
12
|
+
* (no baked-in axios / antd deps).
|
|
13
|
+
*/
|
|
14
|
+
import { computed, ref } from 'vue';
|
|
15
|
+
|
|
16
|
+
import type { ChunkPreview } from '../types/api';
|
|
17
|
+
import type {
|
|
18
|
+
ParentMode,
|
|
19
|
+
ProcessRule,
|
|
20
|
+
RetrievalConfig,
|
|
21
|
+
} from '../types/dataset';
|
|
22
|
+
import type { RetrievalMethod } from '../types/retrieval';
|
|
23
|
+
import RetrievalMethodPicker from './RetrievalMethodPicker.vue';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Available embedding models to pick from. */
|
|
27
|
+
embeddingModels?: Array<{ id: string; label: string }>;
|
|
28
|
+
/** Available rerank models — feeds the retrieval settings' Rerank dropdown. */
|
|
29
|
+
rerankModels?: Array<{ id: string; label: string }>;
|
|
30
|
+
/** Show the wizard (host controls open state). */
|
|
31
|
+
open: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Backend preview call. Runs the same extract → clean → chunk pipeline as
|
|
34
|
+
* real ingestion, so the "文本分段与清洗" step's preview matches what the
|
|
35
|
+
* dataset gets on save. Falls back to a mock when omitted so hosts without
|
|
36
|
+
* the backing endpoint still get a functional wizard.
|
|
37
|
+
*/
|
|
38
|
+
previewChunks?: (
|
|
39
|
+
file: File,
|
|
40
|
+
rule: ProcessRule,
|
|
41
|
+
limit?: number,
|
|
42
|
+
) => Promise<ChunkPreview>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
46
|
+
embeddingModels: () => [],
|
|
47
|
+
rerankModels: () => [],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits<{
|
|
51
|
+
(e: 'update:open', v: boolean): void;
|
|
52
|
+
(
|
|
53
|
+
e: 'create',
|
|
54
|
+
payload: {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
files: File[];
|
|
58
|
+
indexingTechnique: 'ECONOMY' | 'HIGH_QUALITY';
|
|
59
|
+
embeddingModelId: string;
|
|
60
|
+
processRule: ProcessRule;
|
|
61
|
+
retrievalConfig: RetrievalConfig;
|
|
62
|
+
},
|
|
63
|
+
): void;
|
|
64
|
+
(e: 'skip-to-empty'): void;
|
|
65
|
+
}>();
|
|
66
|
+
|
|
67
|
+
// ------ step tracker
|
|
68
|
+
type StepId = 1 | 2 | 3;
|
|
69
|
+
const step = ref<StepId>(1);
|
|
70
|
+
|
|
71
|
+
const steps = [
|
|
72
|
+
{ id: 1 as const, label: '选择数据源' },
|
|
73
|
+
{ id: 2 as const, label: '文本分段与清洗' },
|
|
74
|
+
{ id: 3 as const, label: '处理并完成' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
function close() {
|
|
78
|
+
step.value = 1;
|
|
79
|
+
emit('update:open', false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------ step 1: data source
|
|
83
|
+
type SourceKind = 'notion' | 'text' | 'web';
|
|
84
|
+
const source = ref<SourceKind>('text');
|
|
85
|
+
const files = ref<File[]>([]);
|
|
86
|
+
|
|
87
|
+
const SOURCES = [
|
|
88
|
+
{
|
|
89
|
+
id: 'text' as const,
|
|
90
|
+
icon: '📄',
|
|
91
|
+
iconBg: '#EEF4FF',
|
|
92
|
+
label: '导入已有文本',
|
|
93
|
+
enabled: true,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'notion' as const,
|
|
97
|
+
icon: 'N',
|
|
98
|
+
iconBg: '#F1F5F9',
|
|
99
|
+
label: '同步自 Notion 内容',
|
|
100
|
+
enabled: false,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'web' as const,
|
|
104
|
+
icon: '🌐',
|
|
105
|
+
iconBg: '#EEF4FF',
|
|
106
|
+
label: '同步自 Web 站点',
|
|
107
|
+
enabled: false,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const dragOver = ref(false);
|
|
112
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
113
|
+
|
|
114
|
+
const ALLOWED = [
|
|
115
|
+
'TXT',
|
|
116
|
+
'CSV',
|
|
117
|
+
'XLS',
|
|
118
|
+
'MARKDOWN',
|
|
119
|
+
'HTML',
|
|
120
|
+
'MDX',
|
|
121
|
+
'PDF',
|
|
122
|
+
'VTT',
|
|
123
|
+
'DOCX',
|
|
124
|
+
'HTM',
|
|
125
|
+
'PROPERTIES',
|
|
126
|
+
'MD',
|
|
127
|
+
'XLSX',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function onFileInput(e: Event) {
|
|
131
|
+
const input = e.target as HTMLInputElement;
|
|
132
|
+
if (input.files) addFiles([...input.files]);
|
|
133
|
+
input.value = '';
|
|
134
|
+
}
|
|
135
|
+
function onDrop(e: DragEvent) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
dragOver.value = false;
|
|
138
|
+
const dropped = e.dataTransfer?.files;
|
|
139
|
+
if (dropped) addFiles([...dropped]);
|
|
140
|
+
}
|
|
141
|
+
function addFiles(list: File[]) {
|
|
142
|
+
// 15MB per file, 5 files per batch — matches Dify's default.
|
|
143
|
+
const filtered = list.filter((f) => f.size <= 15 * 1024 * 1024);
|
|
144
|
+
files.value = [...files.value, ...filtered].slice(0, 5);
|
|
145
|
+
}
|
|
146
|
+
function removeFile(i: number) {
|
|
147
|
+
files.value.splice(i, 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const step1Ready = computed(() => files.value.length > 0);
|
|
151
|
+
|
|
152
|
+
// ------ step 2: chunking
|
|
153
|
+
const chunkMode = ref<'general' | 'parent-child'>('general');
|
|
154
|
+
const chunkSeparator = ref('\\n\\n');
|
|
155
|
+
const chunkMaxTokens = ref(1024);
|
|
156
|
+
const chunkOverlap = ref(50);
|
|
157
|
+
const removeExtraWhitespace = ref(true);
|
|
158
|
+
const removeUrlsEmails = ref(false);
|
|
159
|
+
const qaMode = ref(false);
|
|
160
|
+
const qaLanguage = ref('Chinese Simplified');
|
|
161
|
+
|
|
162
|
+
// -- parent-child mode config (Dify parity)
|
|
163
|
+
// Parent context: PARAGRAPH slices the doc into paragraph-sized parents; FULL_DOC
|
|
164
|
+
// treats the whole document as one parent (max context, higher token cost).
|
|
165
|
+
const parentMode = ref<ParentMode>('PARAGRAPH');
|
|
166
|
+
const parentSeparator = ref('\\n\\n');
|
|
167
|
+
const parentMaxTokens = ref(1024);
|
|
168
|
+
const childSeparator = ref('\\n');
|
|
169
|
+
const childMaxTokens = ref(512);
|
|
170
|
+
|
|
171
|
+
const indexingTechnique = ref<'ECONOMY' | 'HIGH_QUALITY'>('HIGH_QUALITY');
|
|
172
|
+
const embeddingModelId = ref(props.embeddingModels[0]?.id ?? '');
|
|
173
|
+
|
|
174
|
+
// Retrieval settings live in a single {@link RetrievalConfig} v-model'd
|
|
175
|
+
// through the shared {@link RetrievalMethodPicker}. Wizard doesn't manage
|
|
176
|
+
// method / topK / rerank refs individually anymore — one shape, one place.
|
|
177
|
+
const retrievalConfig = ref<RetrievalConfig>({
|
|
178
|
+
method: 'HYBRID',
|
|
179
|
+
topK: 3,
|
|
180
|
+
vectorWeight: 0.7,
|
|
181
|
+
rerankEnabled: false,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/** Convert an escaped `\\n` literal typed in the input back to a real newline. */
|
|
185
|
+
function unescapeSeparator(raw: string): string {
|
|
186
|
+
return raw.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildProcessRule(): ProcessRule {
|
|
190
|
+
if (chunkMode.value === 'parent-child') {
|
|
191
|
+
return {
|
|
192
|
+
template: 'PARENT_CHILD',
|
|
193
|
+
chunkTokens: childMaxTokens.value,
|
|
194
|
+
overlapTokens: 0,
|
|
195
|
+
parentChunkTokens: parentMaxTokens.value,
|
|
196
|
+
parentMode: parentMode.value,
|
|
197
|
+
separators: [
|
|
198
|
+
unescapeSeparator(parentSeparator.value),
|
|
199
|
+
unescapeSeparator(childSeparator.value),
|
|
200
|
+
],
|
|
201
|
+
removeExtraWhitespace: removeExtraWhitespace.value,
|
|
202
|
+
removeUrlsEmails: removeUrlsEmails.value,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
template: 'NAIVE',
|
|
207
|
+
chunkTokens: chunkMaxTokens.value,
|
|
208
|
+
overlapTokens: chunkOverlap.value,
|
|
209
|
+
separators: [unescapeSeparator(chunkSeparator.value)],
|
|
210
|
+
removeExtraWhitespace: removeExtraWhitespace.value,
|
|
211
|
+
removeUrlsEmails: removeUrlsEmails.value,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildRetrievalConfig(): RetrievalConfig {
|
|
216
|
+
// Shape the picker emits is the same shape we send to createDataset —
|
|
217
|
+
// just return it verbatim.
|
|
218
|
+
return { ...retrievalConfig.value };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ------ step 2 preview — goes through props.previewChunks when the host has
|
|
222
|
+
// wired a backend endpoint; falls back to a synthesized sample otherwise so
|
|
223
|
+
// the panel still comes alive (with an inline warning that it's not real).
|
|
224
|
+
const previewLoaded = ref(false);
|
|
225
|
+
const previewLoading = ref(false);
|
|
226
|
+
const previewError = ref<string | null>(null);
|
|
227
|
+
const previewTotal = ref(0);
|
|
228
|
+
const previewChunks = ref<Array<{ index: number; text: string; tokens: number }>>(
|
|
229
|
+
[],
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
/** Snapshot current step-2 form into a ProcessRule (shared with save path). */
|
|
233
|
+
function currentRule(): ProcessRule {
|
|
234
|
+
return {
|
|
235
|
+
template: chunkMode.value === 'parent-child' ? 'PARENT_CHILD' : 'NAIVE',
|
|
236
|
+
chunkTokens: chunkMaxTokens.value,
|
|
237
|
+
overlapTokens: chunkOverlap.value,
|
|
238
|
+
parentMode: parentMode.value,
|
|
239
|
+
parentChunkTokens: parentMaxTokens.value,
|
|
240
|
+
removeExtraWhitespace: removeExtraWhitespace.value,
|
|
241
|
+
removeUrlsEmails: removeUrlsEmails.value,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function loadPreview() {
|
|
246
|
+
const file = files.value[0];
|
|
247
|
+
if (!file) return;
|
|
248
|
+
previewError.value = null;
|
|
249
|
+
if (props.previewChunks) {
|
|
250
|
+
previewLoading.value = true;
|
|
251
|
+
try {
|
|
252
|
+
const res = await props.previewChunks(file, currentRule(), 10);
|
|
253
|
+
previewTotal.value = res.totalChunks;
|
|
254
|
+
previewChunks.value = res.chunks;
|
|
255
|
+
previewLoaded.value = true;
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
previewError.value = e?.message ?? '预览失败';
|
|
258
|
+
previewLoaded.value = true;
|
|
259
|
+
previewChunks.value = [];
|
|
260
|
+
} finally {
|
|
261
|
+
previewLoading.value = false;
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Mock fallback — kept so hosts that haven't wired the endpoint still get a
|
|
266
|
+
// functional wizard, but flagged so the user isn't misled.
|
|
267
|
+
previewLoaded.value = true;
|
|
268
|
+
previewTotal.value = 6;
|
|
269
|
+
const filename = file.name;
|
|
270
|
+
previewChunks.value = Array.from({ length: 6 }, (_, i) => ({
|
|
271
|
+
index: i + 1,
|
|
272
|
+
text: `SEG-${String(i + 1).padStart(2, '0')} · 来自「${filename}」的第 ${i + 1} 段(示例)。`,
|
|
273
|
+
tokens: 240 + Math.floor(Math.random() * 400),
|
|
274
|
+
}));
|
|
275
|
+
previewError.value = '未接入 previewChunks 接口,当前展示的是占位示例。';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resetChunking() {
|
|
279
|
+
chunkSeparator.value = '\\n\\n';
|
|
280
|
+
chunkMaxTokens.value = 1024;
|
|
281
|
+
chunkOverlap.value = 50;
|
|
282
|
+
previewLoaded.value = false;
|
|
283
|
+
previewError.value = null;
|
|
284
|
+
previewChunks.value = [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ------ step 3: create + done state
|
|
288
|
+
const datasetName = ref('');
|
|
289
|
+
const creating = ref(false);
|
|
290
|
+
const created = ref(false);
|
|
291
|
+
|
|
292
|
+
async function goToStep2() {
|
|
293
|
+
// Default the name to the first file's base name (Dify does this too).
|
|
294
|
+
const first = files.value[0]?.name ?? '';
|
|
295
|
+
datasetName.value = first.replace(/\.[^./\\]+$/, '') || '知识库';
|
|
296
|
+
step.value = 2;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function submitCreate() {
|
|
300
|
+
creating.value = true;
|
|
301
|
+
emit('create', {
|
|
302
|
+
name: datasetName.value,
|
|
303
|
+
description: '',
|
|
304
|
+
files: files.value,
|
|
305
|
+
indexingTechnique: indexingTechnique.value,
|
|
306
|
+
embeddingModelId: embeddingModelId.value,
|
|
307
|
+
processRule: buildProcessRule(),
|
|
308
|
+
retrievalConfig: buildRetrievalConfig(),
|
|
309
|
+
});
|
|
310
|
+
// Move to step 3 to show the progress screen; the host is expected to update
|
|
311
|
+
// `created` via ref exposure or by closing the modal on real success.
|
|
312
|
+
step.value = 3;
|
|
313
|
+
created.value = true; // mock — real caller can prop-drive this
|
|
314
|
+
creating.value = false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function humanSize(bytes: number): string {
|
|
318
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
319
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
320
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<template>
|
|
325
|
+
<Teleport to="body" :disabled="!open">
|
|
326
|
+
<div v-if="open" class="kh-wizard-mask" @click.self="close">
|
|
327
|
+
<div class="kh-wizard-shell">
|
|
328
|
+
<!-- top bar (modal-style: no back button, close × on the right) -->
|
|
329
|
+
<div class="kh-topbar">
|
|
330
|
+
<div class="kh-topbar-title">创建知识库</div>
|
|
331
|
+
<div class="kh-stepper">
|
|
332
|
+
<div
|
|
333
|
+
v-for="(s, i) in steps"
|
|
334
|
+
:key="s.id"
|
|
335
|
+
class="kh-step"
|
|
336
|
+
:class="{ 'kh-step-active': step === s.id, 'kh-step-done': step > s.id }"
|
|
337
|
+
>
|
|
338
|
+
<span class="kh-step-num">
|
|
339
|
+
<span v-if="step === s.id" class="kh-step-num-badge">
|
|
340
|
+
STEP {{ s.id }}
|
|
341
|
+
</span>
|
|
342
|
+
<span v-else>{{ s.id }}</span>
|
|
343
|
+
</span>
|
|
344
|
+
<span class="kh-step-label">{{ s.label }}</span>
|
|
345
|
+
<span v-if="i < steps.length - 1" class="kh-step-dash" />
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<button class="kh-close" aria-label="close" @click="close">×</button>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<!-- STEP 1: 选择数据源 -->
|
|
352
|
+
<div v-if="step === 1" class="kh-body kh-body-center">
|
|
353
|
+
<div class="kh-content-narrow">
|
|
354
|
+
<div class="kh-section-label">选择数据源</div>
|
|
355
|
+
<div class="kh-source-row">
|
|
356
|
+
<button
|
|
357
|
+
v-for="s in SOURCES"
|
|
358
|
+
:key="s.id"
|
|
359
|
+
type="button"
|
|
360
|
+
class="kh-source"
|
|
361
|
+
:class="{
|
|
362
|
+
'kh-source-active': source === s.id,
|
|
363
|
+
'kh-source-disabled': !s.enabled,
|
|
364
|
+
}"
|
|
365
|
+
:disabled="!s.enabled"
|
|
366
|
+
@click="s.enabled && (source = s.id)"
|
|
367
|
+
>
|
|
368
|
+
<div
|
|
369
|
+
class="kh-source-icon"
|
|
370
|
+
:style="{ background: s.iconBg }"
|
|
371
|
+
>
|
|
372
|
+
{{ s.icon }}
|
|
373
|
+
</div>
|
|
374
|
+
<div class="kh-source-label">{{ s.label }}</div>
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div class="kh-section-label" style="margin-top: 22px">上传文本文件</div>
|
|
379
|
+
<div
|
|
380
|
+
class="kh-dropzone"
|
|
381
|
+
:class="{ 'kh-dropzone-over': dragOver }"
|
|
382
|
+
@dragenter.prevent="dragOver = true"
|
|
383
|
+
@dragover.prevent="dragOver = true"
|
|
384
|
+
@dragleave.prevent="dragOver = false"
|
|
385
|
+
@drop="onDrop"
|
|
386
|
+
@click="fileInputRef?.click()"
|
|
387
|
+
>
|
|
388
|
+
<div class="kh-dz-row">
|
|
389
|
+
<span class="kh-dz-icon">☁</span>
|
|
390
|
+
<span class="kh-dz-text">
|
|
391
|
+
拖拽文件或文件夹至此,或者
|
|
392
|
+
<span class="kh-dz-link">选择文件</span>
|
|
393
|
+
</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="kh-dz-hint">
|
|
396
|
+
已支持
|
|
397
|
+
<template v-for="(a, i) in ALLOWED" :key="a">
|
|
398
|
+
<span>{{ a }}</span>
|
|
399
|
+
<span v-if="i < ALLOWED.length - 1">、</span>
|
|
400
|
+
</template>
|
|
401
|
+
。每批最多 5 个文件,每个文件不超过 15 MB。
|
|
402
|
+
</div>
|
|
403
|
+
<input
|
|
404
|
+
ref="fileInputRef"
|
|
405
|
+
type="file"
|
|
406
|
+
multiple
|
|
407
|
+
class="kh-hidden-input"
|
|
408
|
+
@change="onFileInput"
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div v-if="files.length > 0" class="kh-file-list">
|
|
413
|
+
<div
|
|
414
|
+
v-for="(f, i) in files"
|
|
415
|
+
:key="i"
|
|
416
|
+
class="kh-file-row"
|
|
417
|
+
>
|
|
418
|
+
<span class="kh-file-icon">📄</span>
|
|
419
|
+
<span class="kh-file-name">{{ f.name }}</span>
|
|
420
|
+
<span class="kh-file-size">{{ humanSize(f.size) }}</span>
|
|
421
|
+
<button class="kh-file-x" @click="removeFile(i)">×</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div class="kh-step1-actions">
|
|
426
|
+
<button
|
|
427
|
+
class="kh-btn kh-btn-primary"
|
|
428
|
+
:disabled="!step1Ready"
|
|
429
|
+
@click="goToStep2"
|
|
430
|
+
>
|
|
431
|
+
下一步 →
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div class="kh-empty-link" @click="emit('skip-to-empty')">
|
|
436
|
+
📂 创建一个空知识库
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- STEP 2: 分段与清洗 + 索引 + 检索 -->
|
|
442
|
+
<div v-if="step === 2" class="kh-body kh-body-split">
|
|
443
|
+
<!-- LEFT -->
|
|
444
|
+
<div class="kh-split-left">
|
|
445
|
+
<!-- 分段设置 -->
|
|
446
|
+
<div
|
|
447
|
+
class="kh-panel"
|
|
448
|
+
:class="{ 'kh-panel-active': chunkMode === 'general' }"
|
|
449
|
+
@click="chunkMode = 'general'"
|
|
450
|
+
>
|
|
451
|
+
<div class="kh-panel-header">
|
|
452
|
+
<div class="kh-panel-title">
|
|
453
|
+
<span class="kh-panel-radio" :class="{ 'kh-panel-radio-on': chunkMode === 'general' }" />
|
|
454
|
+
<span>通用</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="kh-panel-hint">
|
|
457
|
+
通过文本分块模式,检索和召回块获取相关部分。
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<div v-if="chunkMode === 'general'" class="kh-panel-body">
|
|
461
|
+
<div class="kh-grid-3">
|
|
462
|
+
<div class="kh-field">
|
|
463
|
+
<label>分段标识符</label>
|
|
464
|
+
<input v-model="chunkSeparator" class="kh-input" />
|
|
465
|
+
</div>
|
|
466
|
+
<div class="kh-field">
|
|
467
|
+
<label>分段最大长度</label>
|
|
468
|
+
<div class="kh-input-suffix">
|
|
469
|
+
<input
|
|
470
|
+
v-model.number="chunkMaxTokens"
|
|
471
|
+
type="number"
|
|
472
|
+
class="kh-input"
|
|
473
|
+
/>
|
|
474
|
+
<span class="kh-suffix">characters</span>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="kh-field">
|
|
478
|
+
<label>分段重叠长度</label>
|
|
479
|
+
<div class="kh-input-suffix">
|
|
480
|
+
<input
|
|
481
|
+
v-model.number="chunkOverlap"
|
|
482
|
+
type="number"
|
|
483
|
+
class="kh-input"
|
|
484
|
+
/>
|
|
485
|
+
<span class="kh-suffix">characters</span>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="kh-sub-label">文本预处理规则</div>
|
|
490
|
+
<label class="kh-check">
|
|
491
|
+
<input type="checkbox" v-model="removeExtraWhitespace" />
|
|
492
|
+
<span>替换掉连续的空格、换行符和制表符</span>
|
|
493
|
+
</label>
|
|
494
|
+
<label class="kh-check">
|
|
495
|
+
<input type="checkbox" v-model="removeUrlsEmails" />
|
|
496
|
+
<span>删除所有 URL 和电子邮件地址</span>
|
|
497
|
+
</label>
|
|
498
|
+
<label class="kh-check">
|
|
499
|
+
<input type="checkbox" v-model="qaMode" />
|
|
500
|
+
<span>
|
|
501
|
+
摘要自动生成
|
|
502
|
+
<span class="kh-hint-inline">使用 Q&A 分段,语言</span>
|
|
503
|
+
<select v-model="qaLanguage" class="kh-select-inline" @click.stop>
|
|
504
|
+
<option>Chinese Simplified</option>
|
|
505
|
+
<option>English</option>
|
|
506
|
+
</select>
|
|
507
|
+
</span>
|
|
508
|
+
</label>
|
|
509
|
+
<div class="kh-inline-actions">
|
|
510
|
+
<button class="kh-btn-mini kh-btn-mini-primary" @click.stop="loadPreview">
|
|
511
|
+
ⓘ 预览块
|
|
512
|
+
</button>
|
|
513
|
+
<button class="kh-btn-mini" @click.stop="resetChunking">
|
|
514
|
+
重置
|
|
515
|
+
</button>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<div
|
|
521
|
+
class="kh-panel"
|
|
522
|
+
:class="{ 'kh-panel-active': chunkMode === 'parent-child' }"
|
|
523
|
+
@click="chunkMode = 'parent-child'"
|
|
524
|
+
>
|
|
525
|
+
<div class="kh-panel-header">
|
|
526
|
+
<div class="kh-panel-title">
|
|
527
|
+
<span class="kh-panel-radio" :class="{ 'kh-panel-radio-on': chunkMode === 'parent-child' }" />
|
|
528
|
+
<span>👥 父子分段</span>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="kh-panel-hint">
|
|
531
|
+
地区父子块的父块,子块用于检索,父块作为上下文
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
<div v-if="chunkMode === 'parent-child'" class="kh-panel-body">
|
|
535
|
+
<div class="kh-sub-label">父块用作上下文</div>
|
|
536
|
+
<div class="kh-parent-mode-row">
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
class="kh-parent-mode-card"
|
|
540
|
+
:class="{ 'kh-parent-mode-active': parentMode === 'PARAGRAPH' }"
|
|
541
|
+
@click.stop="parentMode = 'PARAGRAPH'"
|
|
542
|
+
>
|
|
543
|
+
<div class="kh-pm-head">
|
|
544
|
+
<span
|
|
545
|
+
class="kh-panel-radio"
|
|
546
|
+
:class="{ 'kh-panel-radio-on': parentMode === 'PARAGRAPH' }"
|
|
547
|
+
/>
|
|
548
|
+
<span class="kh-pm-title">段落</span>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="kh-pm-desc">
|
|
551
|
+
此模式根据预设的分隔符将文本划分为段落。检索到匹配段后返回所在段落作为父块。
|
|
552
|
+
</div>
|
|
553
|
+
<div v-if="parentMode === 'PARAGRAPH'" class="kh-grid-2" style="margin-top: 10px">
|
|
554
|
+
<div class="kh-field">
|
|
555
|
+
<label>分段标识符</label>
|
|
556
|
+
<input v-model="parentSeparator" class="kh-input" @click.stop />
|
|
557
|
+
</div>
|
|
558
|
+
<div class="kh-field">
|
|
559
|
+
<label>分段最大长度</label>
|
|
560
|
+
<div class="kh-input-suffix">
|
|
561
|
+
<input
|
|
562
|
+
v-model.number="parentMaxTokens"
|
|
563
|
+
type="number"
|
|
564
|
+
class="kh-input"
|
|
565
|
+
@click.stop
|
|
566
|
+
/>
|
|
567
|
+
<span class="kh-suffix">characters</span>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
</button>
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
class="kh-parent-mode-card"
|
|
575
|
+
:class="{ 'kh-parent-mode-active': parentMode === 'FULL_DOC' }"
|
|
576
|
+
@click.stop="parentMode = 'FULL_DOC'"
|
|
577
|
+
>
|
|
578
|
+
<div class="kh-pm-head">
|
|
579
|
+
<span
|
|
580
|
+
class="kh-panel-radio"
|
|
581
|
+
:class="{ 'kh-panel-radio-on': parentMode === 'FULL_DOC' }"
|
|
582
|
+
/>
|
|
583
|
+
<span class="kh-pm-title">全文</span>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="kh-pm-desc">
|
|
586
|
+
此模式会将文本转成全文视图,检索到匹配段后返回整个文档作为父块。适用于短文档。
|
|
587
|
+
</div>
|
|
588
|
+
</button>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div class="kh-sub-label" style="margin-top: 14px">子块用于检索</div>
|
|
592
|
+
<div class="kh-grid-2">
|
|
593
|
+
<div class="kh-field">
|
|
594
|
+
<label>分段标识符</label>
|
|
595
|
+
<input v-model="childSeparator" class="kh-input" @click.stop />
|
|
596
|
+
</div>
|
|
597
|
+
<div class="kh-field">
|
|
598
|
+
<label>分段最大长度</label>
|
|
599
|
+
<div class="kh-input-suffix">
|
|
600
|
+
<input
|
|
601
|
+
v-model.number="childMaxTokens"
|
|
602
|
+
type="number"
|
|
603
|
+
class="kh-input"
|
|
604
|
+
@click.stop
|
|
605
|
+
/>
|
|
606
|
+
<span class="kh-suffix">characters</span>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<div class="kh-sub-label" style="margin-top: 14px">文本预处理规则</div>
|
|
612
|
+
<label class="kh-check">
|
|
613
|
+
<input type="checkbox" v-model="removeExtraWhitespace" @click.stop />
|
|
614
|
+
<span>替换掉连续的空格、换行符和制表符</span>
|
|
615
|
+
</label>
|
|
616
|
+
<label class="kh-check">
|
|
617
|
+
<input type="checkbox" v-model="removeUrlsEmails" @click.stop />
|
|
618
|
+
<span>删除所有 URL 和电子邮件地址</span>
|
|
619
|
+
</label>
|
|
620
|
+
|
|
621
|
+
<div class="kh-inline-actions">
|
|
622
|
+
<button class="kh-btn-mini kh-btn-mini-primary" @click.stop="loadPreview">
|
|
623
|
+
ⓘ 预览块
|
|
624
|
+
</button>
|
|
625
|
+
<button class="kh-btn-mini" @click.stop="resetChunking">
|
|
626
|
+
重置
|
|
627
|
+
</button>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<!-- 索引方式 —— stacked one card per row (Dify parity). Two-up
|
|
633
|
+
wraps their descriptions awkwardly on typical viewport widths;
|
|
634
|
+
stacking gives each card room to breathe. -->
|
|
635
|
+
<div class="kh-section-label" style="margin-top: 18px">索引方式</div>
|
|
636
|
+
<div class="kh-index-row">
|
|
637
|
+
<button
|
|
638
|
+
type="button"
|
|
639
|
+
class="kh-index-card"
|
|
640
|
+
:class="{ 'kh-index-card-active': indexingTechnique === 'HIGH_QUALITY' }"
|
|
641
|
+
@click="indexingTechnique = 'HIGH_QUALITY'"
|
|
642
|
+
>
|
|
643
|
+
<div class="kh-index-head">
|
|
644
|
+
<span class="kh-index-icon" style="background: #FEF3C7; color: #b45309">⭐</span>
|
|
645
|
+
<span>高质量</span>
|
|
646
|
+
<span class="kh-index-badge">推荐</span>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="kh-index-desc">
|
|
649
|
+
调用嵌入模型处理文档以实现更精确的检索,可以帮助 LLM 生成高质量的答案。
|
|
650
|
+
</div>
|
|
651
|
+
</button>
|
|
652
|
+
<button
|
|
653
|
+
type="button"
|
|
654
|
+
class="kh-index-card"
|
|
655
|
+
:class="{ 'kh-index-card-active': indexingTechnique === 'ECONOMY' }"
|
|
656
|
+
@click="indexingTechnique = 'ECONOMY'"
|
|
657
|
+
>
|
|
658
|
+
<div class="kh-index-head">
|
|
659
|
+
<span class="kh-index-icon" style="background: #EEF4FF; color: #4338ca">💰</span>
|
|
660
|
+
<span>经济</span>
|
|
661
|
+
</div>
|
|
662
|
+
<div class="kh-index-desc">
|
|
663
|
+
每个数据块使用 10 个关键词进行检索,不会消耗任何 tokens。你会得到较低的检索准确性。
|
|
664
|
+
</div>
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<div class="kh-tip">
|
|
669
|
+
💡 使用高质量模式引入的数据,无法切换回经济模式。
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div class="kh-section-label" style="margin-top: 16px">Embedding 模型</div>
|
|
673
|
+
<select v-model="embeddingModelId" class="kh-select">
|
|
674
|
+
<option value="">选择一个 Embedding 模型</option>
|
|
675
|
+
<option
|
|
676
|
+
v-for="m in embeddingModels"
|
|
677
|
+
:key="m.id"
|
|
678
|
+
:value="m.id"
|
|
679
|
+
>
|
|
680
|
+
{{ m.label }}
|
|
681
|
+
</option>
|
|
682
|
+
</select>
|
|
683
|
+
|
|
684
|
+
<div class="kh-section-label" style="margin-top: 16px">检索设置</div>
|
|
685
|
+
<div class="kh-hint" style="margin-top: -4px">
|
|
686
|
+
了解更多关于检索方式,你可以随时在知识库的设置中更改此设置。
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<!-- 检索方法 —— 共用 RetrievalMethodPicker,之前这里 230 行
|
|
690
|
+
手写卡片 + rerank + TopK + score 阈值现在一个组件搞定。 -->
|
|
691
|
+
<RetrievalMethodPicker
|
|
692
|
+
v-model="retrievalConfig"
|
|
693
|
+
:rerank-models="rerankModels"
|
|
694
|
+
:indexing-technique="indexingTechnique"
|
|
695
|
+
/>
|
|
696
|
+
|
|
697
|
+
<div class="kh-step-actions">
|
|
698
|
+
<button class="kh-btn kh-btn-secondary" @click="step = 1">
|
|
699
|
+
← 上一步
|
|
700
|
+
</button>
|
|
701
|
+
<button
|
|
702
|
+
class="kh-btn kh-btn-primary"
|
|
703
|
+
:disabled="!embeddingModelId && indexingTechnique === 'HIGH_QUALITY'"
|
|
704
|
+
@click="submitCreate"
|
|
705
|
+
>
|
|
706
|
+
保存并处理 →
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<!-- RIGHT: preview -->
|
|
712
|
+
<div class="kh-split-right">
|
|
713
|
+
<div class="kh-preview-title">
|
|
714
|
+
<span>预览</span>
|
|
715
|
+
<span v-if="files[0]" class="kh-preview-file">
|
|
716
|
+
📄 {{ files[0].name }}
|
|
717
|
+
<template v-if="previewLoaded">
|
|
718
|
+
· 共 {{ previewTotal }} 段
|
|
719
|
+
<template v-if="previewTotal > previewChunks.length">
|
|
720
|
+
(前 {{ previewChunks.length }} 段)
|
|
721
|
+
</template>
|
|
722
|
+
</template>
|
|
723
|
+
</span>
|
|
724
|
+
</div>
|
|
725
|
+
<div v-if="previewLoading" class="kh-preview-empty">
|
|
726
|
+
<div class="kh-preview-empty-text">分段中…</div>
|
|
727
|
+
</div>
|
|
728
|
+
<div v-else-if="!previewLoaded" class="kh-preview-empty">
|
|
729
|
+
<div class="kh-preview-empty-icon">◉</div>
|
|
730
|
+
<div class="kh-preview-empty-text">
|
|
731
|
+
点击左侧的"预览块"按钮来加载预览
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
<div v-else class="kh-preview-body">
|
|
735
|
+
<div v-if="previewError" class="kh-preview-warning">
|
|
736
|
+
⚠️ {{ previewError }}
|
|
737
|
+
</div>
|
|
738
|
+
<div
|
|
739
|
+
v-for="c in previewChunks"
|
|
740
|
+
:key="c.index"
|
|
741
|
+
class="kh-preview-chunk"
|
|
742
|
+
>
|
|
743
|
+
<div class="kh-preview-chunk-head">
|
|
744
|
+
<span class="kh-chunk-tag">SEG-{{ String(c.index).padStart(2, '0') }}</span>
|
|
745
|
+
<span class="kh-chunk-tokens">{{ c.tokens }} tokens</span>
|
|
746
|
+
</div>
|
|
747
|
+
<div class="kh-preview-chunk-body">{{ c.text }}</div>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<!-- STEP 3: 处理并完成 -->
|
|
754
|
+
<div v-if="step === 3" class="kh-body kh-body-center">
|
|
755
|
+
<div class="kh-content-narrow">
|
|
756
|
+
<div class="kh-done-header">
|
|
757
|
+
<span class="kh-done-emoji">🎉</span>
|
|
758
|
+
<div>
|
|
759
|
+
<div class="kh-done-title">知识库已创建</div>
|
|
760
|
+
<div class="kh-done-sub">
|
|
761
|
+
我们自动为您把知识库起了个名称,您也可以随时修改。
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
<div class="kh-done-name-row">
|
|
766
|
+
<span class="kh-done-name-label">知识库名称</span>
|
|
767
|
+
<div class="kh-done-name-input">
|
|
768
|
+
<span class="kh-done-name-icon">📙</span>
|
|
769
|
+
<input v-model="datasetName" class="kh-input" />
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<div class="kh-done-progress-row">
|
|
774
|
+
<span class="kh-done-progress-label">嵌入处理中…</span>
|
|
775
|
+
<span class="kh-done-progress-file">
|
|
776
|
+
📄 {{ files[0]?.name ?? '' }}
|
|
777
|
+
</span>
|
|
778
|
+
<div class="kh-done-progress-bar">
|
|
779
|
+
<div class="kh-done-progress-fill" style="width: 0%" />
|
|
780
|
+
</div>
|
|
781
|
+
<span class="kh-done-progress-pct">0%</span>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<div class="kh-done-facts">
|
|
785
|
+
<div>
|
|
786
|
+
<span class="kh-fact-label">分段模式</span>
|
|
787
|
+
<span class="kh-fact-val">
|
|
788
|
+
{{ chunkMode === 'general' ? '自定义' : '父子分段' }}
|
|
789
|
+
</span>
|
|
790
|
+
</div>
|
|
791
|
+
<div>
|
|
792
|
+
<span class="kh-fact-label">最大分段长度</span>
|
|
793
|
+
<span class="kh-fact-val">
|
|
794
|
+
{{ chunkMode === 'parent-child' ? childMaxTokens : chunkMaxTokens }}
|
|
795
|
+
</span>
|
|
796
|
+
</div>
|
|
797
|
+
<div>
|
|
798
|
+
<span class="kh-fact-label">文本预处理规则</span>
|
|
799
|
+
<span class="kh-fact-val">
|
|
800
|
+
{{ removeExtraWhitespace ? '替换掉连续的空格、换行符和制表符' : '不处理' }}
|
|
801
|
+
</span>
|
|
802
|
+
</div>
|
|
803
|
+
<div>
|
|
804
|
+
<span class="kh-fact-label">索引方式</span>
|
|
805
|
+
<span class="kh-fact-val" style="color: #b45309">
|
|
806
|
+
⭐ {{ indexingTechnique === 'HIGH_QUALITY' ? '高质量' : '经济' }}
|
|
807
|
+
</span>
|
|
808
|
+
</div>
|
|
809
|
+
<div>
|
|
810
|
+
<span class="kh-fact-label">检索设置</span>
|
|
811
|
+
<span class="kh-fact-val" style="color: #4338ca">
|
|
812
|
+
{{
|
|
813
|
+
retrievalMethod === 'VECTOR'
|
|
814
|
+
? '◈ 向量检索'
|
|
815
|
+
: retrievalMethod === 'FULL_TEXT'
|
|
816
|
+
? '≡ 全文检索'
|
|
817
|
+
: '⚡ 混合检索'
|
|
818
|
+
}}
|
|
819
|
+
· Top K {{ topK }}
|
|
820
|
+
</span>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
|
|
824
|
+
<div class="kh-done-actions">
|
|
825
|
+
<button class="kh-btn kh-btn-secondary">◈ Access the API</button>
|
|
826
|
+
<button class="kh-btn kh-btn-primary" @click="close">
|
|
827
|
+
前往文档 →
|
|
828
|
+
</button>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
</Teleport>
|
|
835
|
+
</template>
|
|
836
|
+
|
|
837
|
+
<style scoped>
|
|
838
|
+
/* Modal-style overlay: centered card with fade-in. Consumers close with the
|
|
839
|
+
× in the top-right or by clicking the backdrop. */
|
|
840
|
+
.kh-wizard-mask {
|
|
841
|
+
position: fixed;
|
|
842
|
+
inset: 0;
|
|
843
|
+
z-index: 1040;
|
|
844
|
+
background: rgba(15, 23, 42, 0.45);
|
|
845
|
+
display: flex;
|
|
846
|
+
align-items: center;
|
|
847
|
+
justify-content: center;
|
|
848
|
+
padding: 24px;
|
|
849
|
+
}
|
|
850
|
+
.kh-wizard-shell {
|
|
851
|
+
width: 100%;
|
|
852
|
+
max-width: 1200px;
|
|
853
|
+
height: min(920px, calc(100vh - 48px));
|
|
854
|
+
background: #fff;
|
|
855
|
+
border-radius: 14px;
|
|
856
|
+
display: flex;
|
|
857
|
+
flex-direction: column;
|
|
858
|
+
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
|
|
859
|
+
overflow: hidden;
|
|
860
|
+
animation: kh-wizard-in 0.2s ease-out;
|
|
861
|
+
}
|
|
862
|
+
@keyframes kh-wizard-in {
|
|
863
|
+
from {
|
|
864
|
+
transform: scale(0.98);
|
|
865
|
+
opacity: 0.8;
|
|
866
|
+
}
|
|
867
|
+
to {
|
|
868
|
+
transform: scale(1);
|
|
869
|
+
opacity: 1;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* Top bar */
|
|
874
|
+
.kh-topbar {
|
|
875
|
+
display: grid;
|
|
876
|
+
grid-template-columns: 200px 1fr 60px;
|
|
877
|
+
align-items: center;
|
|
878
|
+
padding: 14px 24px;
|
|
879
|
+
border-bottom: 1px solid #f1f5f9;
|
|
880
|
+
background: #fff;
|
|
881
|
+
}
|
|
882
|
+
.kh-topbar-title {
|
|
883
|
+
font-size: 14px;
|
|
884
|
+
font-weight: 600;
|
|
885
|
+
color: #0f172a;
|
|
886
|
+
}
|
|
887
|
+
.kh-close {
|
|
888
|
+
justify-self: end;
|
|
889
|
+
width: 32px;
|
|
890
|
+
height: 32px;
|
|
891
|
+
padding: 0;
|
|
892
|
+
border: none;
|
|
893
|
+
border-radius: 8px;
|
|
894
|
+
background: transparent;
|
|
895
|
+
color: #94a3b8;
|
|
896
|
+
font-size: 20px;
|
|
897
|
+
line-height: 1;
|
|
898
|
+
cursor: pointer;
|
|
899
|
+
}
|
|
900
|
+
.kh-close:hover {
|
|
901
|
+
background: #f1f5f9;
|
|
902
|
+
color: #475569;
|
|
903
|
+
}
|
|
904
|
+
.kh-stepper {
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
justify-content: center;
|
|
908
|
+
gap: 6px;
|
|
909
|
+
}
|
|
910
|
+
.kh-step {
|
|
911
|
+
display: inline-flex;
|
|
912
|
+
align-items: center;
|
|
913
|
+
gap: 6px;
|
|
914
|
+
font-size: 13px;
|
|
915
|
+
color: #94a3b8;
|
|
916
|
+
}
|
|
917
|
+
.kh-step-num {
|
|
918
|
+
min-width: 20px;
|
|
919
|
+
text-align: center;
|
|
920
|
+
}
|
|
921
|
+
.kh-step-num-badge {
|
|
922
|
+
display: inline-block;
|
|
923
|
+
padding: 2px 8px;
|
|
924
|
+
border-radius: 10px;
|
|
925
|
+
background: #4f46e5;
|
|
926
|
+
color: #fff;
|
|
927
|
+
font-size: 10px;
|
|
928
|
+
font-weight: 600;
|
|
929
|
+
letter-spacing: 0.05em;
|
|
930
|
+
}
|
|
931
|
+
.kh-step-active {
|
|
932
|
+
color: #4f46e5;
|
|
933
|
+
font-weight: 600;
|
|
934
|
+
}
|
|
935
|
+
.kh-step-done {
|
|
936
|
+
color: #10b981;
|
|
937
|
+
}
|
|
938
|
+
.kh-step-dash {
|
|
939
|
+
display: inline-block;
|
|
940
|
+
width: 24px;
|
|
941
|
+
height: 1px;
|
|
942
|
+
background: #cbd5e1;
|
|
943
|
+
margin: 0 4px;
|
|
944
|
+
}
|
|
945
|
+
/* Body layouts */
|
|
946
|
+
.kh-body {
|
|
947
|
+
flex: 1;
|
|
948
|
+
overflow-y: auto;
|
|
949
|
+
padding: 40px 40px 80px;
|
|
950
|
+
}
|
|
951
|
+
.kh-body-center {
|
|
952
|
+
display: flex;
|
|
953
|
+
justify-content: center;
|
|
954
|
+
}
|
|
955
|
+
.kh-body-split {
|
|
956
|
+
display: grid;
|
|
957
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
958
|
+
gap: 32px;
|
|
959
|
+
max-width: 1400px;
|
|
960
|
+
margin: 0 auto;
|
|
961
|
+
/* Let each column own its scroll — preview on the right can scroll without
|
|
962
|
+
dragging the segmentation strategy on the left along with it. */
|
|
963
|
+
padding: 0;
|
|
964
|
+
overflow: hidden;
|
|
965
|
+
}
|
|
966
|
+
.kh-split-left {
|
|
967
|
+
min-height: 0;
|
|
968
|
+
overflow-y: auto;
|
|
969
|
+
padding: 40px 12px 80px 40px;
|
|
970
|
+
}
|
|
971
|
+
.kh-content-narrow {
|
|
972
|
+
width: 660px;
|
|
973
|
+
max-width: 100%;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* Section labels */
|
|
977
|
+
.kh-section-label {
|
|
978
|
+
margin-bottom: 10px;
|
|
979
|
+
font-size: 13px;
|
|
980
|
+
font-weight: 600;
|
|
981
|
+
color: #334155;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/* Step 1 — source cards */
|
|
985
|
+
.kh-source-row {
|
|
986
|
+
display: grid;
|
|
987
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
988
|
+
gap: 12px;
|
|
989
|
+
}
|
|
990
|
+
.kh-source {
|
|
991
|
+
display: flex;
|
|
992
|
+
align-items: center;
|
|
993
|
+
gap: 10px;
|
|
994
|
+
padding: 14px 16px;
|
|
995
|
+
background: #fff;
|
|
996
|
+
border: 1px solid #e2e8f0;
|
|
997
|
+
border-radius: 10px;
|
|
998
|
+
text-align: left;
|
|
999
|
+
cursor: pointer;
|
|
1000
|
+
transition:
|
|
1001
|
+
border-color 0.15s,
|
|
1002
|
+
background 0.15s;
|
|
1003
|
+
}
|
|
1004
|
+
.kh-source:hover:not(.kh-source-disabled) {
|
|
1005
|
+
border-color: #cbd5e1;
|
|
1006
|
+
}
|
|
1007
|
+
.kh-source-active {
|
|
1008
|
+
border-color: transparent !important;
|
|
1009
|
+
outline: 1.5px solid #6366f1;
|
|
1010
|
+
background: #eef2ff;
|
|
1011
|
+
}
|
|
1012
|
+
.kh-source-disabled {
|
|
1013
|
+
opacity: 0.55;
|
|
1014
|
+
cursor: not-allowed;
|
|
1015
|
+
}
|
|
1016
|
+
.kh-source-icon {
|
|
1017
|
+
width: 32px;
|
|
1018
|
+
height: 32px;
|
|
1019
|
+
display: flex;
|
|
1020
|
+
align-items: center;
|
|
1021
|
+
justify-content: center;
|
|
1022
|
+
border-radius: 6px;
|
|
1023
|
+
font-size: 16px;
|
|
1024
|
+
font-weight: 600;
|
|
1025
|
+
color: #4338ca;
|
|
1026
|
+
}
|
|
1027
|
+
.kh-source-label {
|
|
1028
|
+
font-size: 14px;
|
|
1029
|
+
color: #0f172a;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* Step 1 — dropzone */
|
|
1033
|
+
.kh-dropzone {
|
|
1034
|
+
padding: 20px 24px;
|
|
1035
|
+
border: 1px dashed #cbd5e1;
|
|
1036
|
+
border-radius: 10px;
|
|
1037
|
+
background: #f8fafc;
|
|
1038
|
+
cursor: pointer;
|
|
1039
|
+
transition:
|
|
1040
|
+
background 0.15s,
|
|
1041
|
+
border-color 0.15s;
|
|
1042
|
+
}
|
|
1043
|
+
.kh-dropzone:hover,
|
|
1044
|
+
.kh-dropzone-over {
|
|
1045
|
+
border-color: #6366f1;
|
|
1046
|
+
background: #eef2ff;
|
|
1047
|
+
}
|
|
1048
|
+
.kh-dz-row {
|
|
1049
|
+
display: flex;
|
|
1050
|
+
justify-content: center;
|
|
1051
|
+
align-items: center;
|
|
1052
|
+
gap: 8px;
|
|
1053
|
+
font-size: 13px;
|
|
1054
|
+
color: #475569;
|
|
1055
|
+
}
|
|
1056
|
+
.kh-dz-icon {
|
|
1057
|
+
font-size: 18px;
|
|
1058
|
+
color: #94a3b8;
|
|
1059
|
+
}
|
|
1060
|
+
.kh-dz-link {
|
|
1061
|
+
color: #4338ca;
|
|
1062
|
+
font-weight: 500;
|
|
1063
|
+
}
|
|
1064
|
+
.kh-dz-hint {
|
|
1065
|
+
margin-top: 8px;
|
|
1066
|
+
font-size: 11px;
|
|
1067
|
+
color: #94a3b8;
|
|
1068
|
+
line-height: 1.6;
|
|
1069
|
+
}
|
|
1070
|
+
.kh-hidden-input {
|
|
1071
|
+
display: none;
|
|
1072
|
+
}
|
|
1073
|
+
.kh-file-list {
|
|
1074
|
+
margin-top: 10px;
|
|
1075
|
+
display: flex;
|
|
1076
|
+
flex-direction: column;
|
|
1077
|
+
gap: 6px;
|
|
1078
|
+
}
|
|
1079
|
+
.kh-file-row {
|
|
1080
|
+
display: flex;
|
|
1081
|
+
align-items: center;
|
|
1082
|
+
gap: 8px;
|
|
1083
|
+
padding: 6px 10px;
|
|
1084
|
+
background: #fff;
|
|
1085
|
+
border: 1px solid #e2e8f0;
|
|
1086
|
+
border-radius: 6px;
|
|
1087
|
+
font-size: 12px;
|
|
1088
|
+
color: #334155;
|
|
1089
|
+
}
|
|
1090
|
+
.kh-file-icon {
|
|
1091
|
+
color: #4338ca;
|
|
1092
|
+
}
|
|
1093
|
+
.kh-file-name {
|
|
1094
|
+
flex: 1;
|
|
1095
|
+
overflow: hidden;
|
|
1096
|
+
text-overflow: ellipsis;
|
|
1097
|
+
white-space: nowrap;
|
|
1098
|
+
}
|
|
1099
|
+
.kh-file-size {
|
|
1100
|
+
color: #94a3b8;
|
|
1101
|
+
}
|
|
1102
|
+
.kh-file-x {
|
|
1103
|
+
padding: 0 6px;
|
|
1104
|
+
border: none;
|
|
1105
|
+
background: transparent;
|
|
1106
|
+
color: #94a3b8;
|
|
1107
|
+
font-size: 16px;
|
|
1108
|
+
cursor: pointer;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/* Step 1 — actions */
|
|
1112
|
+
.kh-step1-actions {
|
|
1113
|
+
margin-top: 22px;
|
|
1114
|
+
display: flex;
|
|
1115
|
+
justify-content: flex-end;
|
|
1116
|
+
}
|
|
1117
|
+
.kh-empty-link {
|
|
1118
|
+
margin-top: 20px;
|
|
1119
|
+
font-size: 13px;
|
|
1120
|
+
color: #4338ca;
|
|
1121
|
+
cursor: pointer;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/* Buttons */
|
|
1125
|
+
.kh-btn {
|
|
1126
|
+
padding: 8px 18px;
|
|
1127
|
+
border: none;
|
|
1128
|
+
border-radius: 8px;
|
|
1129
|
+
font-size: 13px;
|
|
1130
|
+
font-weight: 500;
|
|
1131
|
+
cursor: pointer;
|
|
1132
|
+
}
|
|
1133
|
+
.kh-btn-primary {
|
|
1134
|
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
1135
|
+
color: #fff;
|
|
1136
|
+
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3);
|
|
1137
|
+
}
|
|
1138
|
+
.kh-btn-primary:disabled {
|
|
1139
|
+
background: #cbd5e1;
|
|
1140
|
+
box-shadow: none;
|
|
1141
|
+
cursor: not-allowed;
|
|
1142
|
+
}
|
|
1143
|
+
.kh-btn-secondary {
|
|
1144
|
+
background: #f1f5f9;
|
|
1145
|
+
color: #475569;
|
|
1146
|
+
}
|
|
1147
|
+
.kh-btn-secondary:hover {
|
|
1148
|
+
background: #e2e8f0;
|
|
1149
|
+
}
|
|
1150
|
+
.kh-btn-mini {
|
|
1151
|
+
padding: 4px 10px;
|
|
1152
|
+
font-size: 12px;
|
|
1153
|
+
border: 1px solid #e2e8f0;
|
|
1154
|
+
border-radius: 6px;
|
|
1155
|
+
background: #fff;
|
|
1156
|
+
color: #475569;
|
|
1157
|
+
cursor: pointer;
|
|
1158
|
+
}
|
|
1159
|
+
.kh-btn-mini:hover {
|
|
1160
|
+
background: #f8fafc;
|
|
1161
|
+
}
|
|
1162
|
+
.kh-btn-mini-primary {
|
|
1163
|
+
color: #4338ca;
|
|
1164
|
+
border-color: #c7d2fe;
|
|
1165
|
+
background: #eef2ff;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/* Step 2 — chunking panels */
|
|
1169
|
+
.kh-panel {
|
|
1170
|
+
padding: 14px 16px;
|
|
1171
|
+
margin-bottom: 12px;
|
|
1172
|
+
background: #fff;
|
|
1173
|
+
border: 1px solid #e2e8f0;
|
|
1174
|
+
border-radius: 10px;
|
|
1175
|
+
cursor: pointer;
|
|
1176
|
+
transition:
|
|
1177
|
+
border-color 0.15s,
|
|
1178
|
+
background 0.15s;
|
|
1179
|
+
}
|
|
1180
|
+
.kh-panel:hover {
|
|
1181
|
+
border-color: #cbd5e1;
|
|
1182
|
+
}
|
|
1183
|
+
.kh-panel-active {
|
|
1184
|
+
border-color: transparent;
|
|
1185
|
+
outline: 1.5px solid #6366f1;
|
|
1186
|
+
background: #f5f6ff;
|
|
1187
|
+
}
|
|
1188
|
+
.kh-panel-header {
|
|
1189
|
+
display: flex;
|
|
1190
|
+
align-items: center;
|
|
1191
|
+
justify-content: space-between;
|
|
1192
|
+
gap: 8px;
|
|
1193
|
+
}
|
|
1194
|
+
.kh-panel-title {
|
|
1195
|
+
display: inline-flex;
|
|
1196
|
+
align-items: center;
|
|
1197
|
+
gap: 8px;
|
|
1198
|
+
font-size: 14px;
|
|
1199
|
+
font-weight: 600;
|
|
1200
|
+
color: #0f172a;
|
|
1201
|
+
}
|
|
1202
|
+
.kh-panel-hint {
|
|
1203
|
+
font-size: 11px;
|
|
1204
|
+
color: #94a3b8;
|
|
1205
|
+
}
|
|
1206
|
+
.kh-panel-radio {
|
|
1207
|
+
width: 14px;
|
|
1208
|
+
height: 14px;
|
|
1209
|
+
border-radius: 50%;
|
|
1210
|
+
border: 1.5px solid #cbd5e1;
|
|
1211
|
+
background: #fff;
|
|
1212
|
+
display: inline-block;
|
|
1213
|
+
}
|
|
1214
|
+
.kh-panel-radio-on {
|
|
1215
|
+
border-color: #6366f1;
|
|
1216
|
+
background:
|
|
1217
|
+
radial-gradient(circle, #6366f1 30%, transparent 32%) center / cover;
|
|
1218
|
+
}
|
|
1219
|
+
.kh-panel-body {
|
|
1220
|
+
margin-top: 12px;
|
|
1221
|
+
padding-top: 12px;
|
|
1222
|
+
border-top: 1px dashed #e2e8f0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/* Fields */
|
|
1226
|
+
.kh-grid-3 {
|
|
1227
|
+
display: grid;
|
|
1228
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1229
|
+
gap: 10px;
|
|
1230
|
+
}
|
|
1231
|
+
.kh-grid-2 {
|
|
1232
|
+
display: grid;
|
|
1233
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1234
|
+
gap: 10px;
|
|
1235
|
+
}
|
|
1236
|
+
/* Indexing-mode cards: stacked one-per-row (Dify parity). Descriptions
|
|
1237
|
+
need horizontal room; a 2-column layout wraps 高质量/经济 desc awkwardly. */
|
|
1238
|
+
.kh-index-row {
|
|
1239
|
+
display: flex;
|
|
1240
|
+
flex-direction: column;
|
|
1241
|
+
gap: 10px;
|
|
1242
|
+
}
|
|
1243
|
+
.kh-field {
|
|
1244
|
+
display: flex;
|
|
1245
|
+
flex-direction: column;
|
|
1246
|
+
gap: 4px;
|
|
1247
|
+
}
|
|
1248
|
+
.kh-field label {
|
|
1249
|
+
font-size: 11px;
|
|
1250
|
+
color: #64748b;
|
|
1251
|
+
}
|
|
1252
|
+
.kh-input {
|
|
1253
|
+
padding: 7px 10px;
|
|
1254
|
+
font-size: 13px;
|
|
1255
|
+
border: 1px solid #d1d5db;
|
|
1256
|
+
border-radius: 6px;
|
|
1257
|
+
outline: none;
|
|
1258
|
+
background: #fff;
|
|
1259
|
+
color: #0f172a;
|
|
1260
|
+
width: 100%;
|
|
1261
|
+
}
|
|
1262
|
+
.kh-input:focus {
|
|
1263
|
+
border-color: #6366f1;
|
|
1264
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
1265
|
+
}
|
|
1266
|
+
.kh-input-suffix {
|
|
1267
|
+
position: relative;
|
|
1268
|
+
}
|
|
1269
|
+
.kh-input-suffix .kh-input {
|
|
1270
|
+
padding-right: 74px;
|
|
1271
|
+
}
|
|
1272
|
+
.kh-suffix {
|
|
1273
|
+
position: absolute;
|
|
1274
|
+
right: 10px;
|
|
1275
|
+
top: 50%;
|
|
1276
|
+
transform: translateY(-50%);
|
|
1277
|
+
font-size: 11px;
|
|
1278
|
+
color: #94a3b8;
|
|
1279
|
+
}
|
|
1280
|
+
.kh-select,
|
|
1281
|
+
.kh-select-inline {
|
|
1282
|
+
padding: 7px 28px 7px 10px;
|
|
1283
|
+
font-size: 13px;
|
|
1284
|
+
border: 1px solid #d1d5db;
|
|
1285
|
+
border-radius: 6px;
|
|
1286
|
+
background: #fff;
|
|
1287
|
+
outline: none;
|
|
1288
|
+
color: #0f172a;
|
|
1289
|
+
}
|
|
1290
|
+
.kh-select {
|
|
1291
|
+
width: 100%;
|
|
1292
|
+
}
|
|
1293
|
+
.kh-select-inline {
|
|
1294
|
+
padding: 3px 20px 3px 8px;
|
|
1295
|
+
font-size: 11px;
|
|
1296
|
+
margin-left: 4px;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.kh-sub-label {
|
|
1300
|
+
margin-top: 12px;
|
|
1301
|
+
margin-bottom: 6px;
|
|
1302
|
+
font-size: 12px;
|
|
1303
|
+
color: #64748b;
|
|
1304
|
+
}
|
|
1305
|
+
.kh-check {
|
|
1306
|
+
display: inline-flex;
|
|
1307
|
+
align-items: center;
|
|
1308
|
+
gap: 6px;
|
|
1309
|
+
margin-right: 12px;
|
|
1310
|
+
font-size: 12px;
|
|
1311
|
+
color: #475569;
|
|
1312
|
+
cursor: pointer;
|
|
1313
|
+
}
|
|
1314
|
+
.kh-check input {
|
|
1315
|
+
accent-color: #6366f1;
|
|
1316
|
+
}
|
|
1317
|
+
.kh-hint-inline {
|
|
1318
|
+
color: #94a3b8;
|
|
1319
|
+
font-size: 11px;
|
|
1320
|
+
margin-left: 4px;
|
|
1321
|
+
}
|
|
1322
|
+
.kh-inline-actions {
|
|
1323
|
+
display: flex;
|
|
1324
|
+
gap: 6px;
|
|
1325
|
+
margin-top: 12px;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/* Indexing cards */
|
|
1329
|
+
.kh-index-card {
|
|
1330
|
+
padding: 12px 14px;
|
|
1331
|
+
background: #fff;
|
|
1332
|
+
border: 1px solid #e2e8f0;
|
|
1333
|
+
border-radius: 10px;
|
|
1334
|
+
text-align: left;
|
|
1335
|
+
cursor: pointer;
|
|
1336
|
+
transition:
|
|
1337
|
+
border-color 0.15s,
|
|
1338
|
+
background 0.15s;
|
|
1339
|
+
}
|
|
1340
|
+
.kh-index-card:hover {
|
|
1341
|
+
border-color: #cbd5e1;
|
|
1342
|
+
}
|
|
1343
|
+
.kh-index-card-active {
|
|
1344
|
+
border-color: transparent;
|
|
1345
|
+
outline: 1.5px solid #6366f1;
|
|
1346
|
+
background: #f5f6ff;
|
|
1347
|
+
}
|
|
1348
|
+
.kh-index-head {
|
|
1349
|
+
display: flex;
|
|
1350
|
+
align-items: center;
|
|
1351
|
+
gap: 8px;
|
|
1352
|
+
font-size: 13px;
|
|
1353
|
+
font-weight: 600;
|
|
1354
|
+
color: #0f172a;
|
|
1355
|
+
}
|
|
1356
|
+
.kh-index-icon {
|
|
1357
|
+
width: 22px;
|
|
1358
|
+
height: 22px;
|
|
1359
|
+
display: flex;
|
|
1360
|
+
align-items: center;
|
|
1361
|
+
justify-content: center;
|
|
1362
|
+
border-radius: 6px;
|
|
1363
|
+
font-size: 12px;
|
|
1364
|
+
}
|
|
1365
|
+
.kh-index-badge {
|
|
1366
|
+
padding: 1px 6px;
|
|
1367
|
+
font-size: 10px;
|
|
1368
|
+
border-radius: 4px;
|
|
1369
|
+
background: #b45309;
|
|
1370
|
+
color: #fff;
|
|
1371
|
+
}
|
|
1372
|
+
.kh-index-desc {
|
|
1373
|
+
margin-top: 6px;
|
|
1374
|
+
font-size: 11px;
|
|
1375
|
+
color: #64748b;
|
|
1376
|
+
line-height: 1.5;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.kh-tip {
|
|
1380
|
+
margin-top: 10px;
|
|
1381
|
+
padding: 8px 12px;
|
|
1382
|
+
border-radius: 6px;
|
|
1383
|
+
background: #fef3c7;
|
|
1384
|
+
color: #92400e;
|
|
1385
|
+
font-size: 11px;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/* Retrieval card */
|
|
1389
|
+
.kh-hint {
|
|
1390
|
+
font-size: 11px;
|
|
1391
|
+
color: #94a3b8;
|
|
1392
|
+
margin-bottom: 8px;
|
|
1393
|
+
}
|
|
1394
|
+
.kh-retrieve-card {
|
|
1395
|
+
padding: 12px 14px;
|
|
1396
|
+
margin-bottom: 8px;
|
|
1397
|
+
border-radius: 10px;
|
|
1398
|
+
background: #fff;
|
|
1399
|
+
border: 1px solid #e2e8f0;
|
|
1400
|
+
cursor: pointer;
|
|
1401
|
+
transition:
|
|
1402
|
+
border-color 0.15s,
|
|
1403
|
+
background 0.15s;
|
|
1404
|
+
}
|
|
1405
|
+
.kh-retrieve-card:hover {
|
|
1406
|
+
border-color: #cbd5e1;
|
|
1407
|
+
}
|
|
1408
|
+
.kh-retrieve-card-active {
|
|
1409
|
+
border-color: transparent;
|
|
1410
|
+
outline: 1.5px solid #6366f1;
|
|
1411
|
+
background: #f5f6ff;
|
|
1412
|
+
}
|
|
1413
|
+
.kh-retrieve-head {
|
|
1414
|
+
display: flex;
|
|
1415
|
+
align-items: center;
|
|
1416
|
+
gap: 8px;
|
|
1417
|
+
}
|
|
1418
|
+
.kh-retrieve-title {
|
|
1419
|
+
font-size: 13px;
|
|
1420
|
+
font-weight: 600;
|
|
1421
|
+
color: #0f172a;
|
|
1422
|
+
}
|
|
1423
|
+
.kh-retrieve-icon {
|
|
1424
|
+
padding: 3px 8px;
|
|
1425
|
+
border-radius: 4px;
|
|
1426
|
+
background: #4f46e5;
|
|
1427
|
+
color: #fff;
|
|
1428
|
+
font-size: 11px;
|
|
1429
|
+
}
|
|
1430
|
+
.kh-retrieve-desc {
|
|
1431
|
+
margin-top: 6px;
|
|
1432
|
+
font-size: 11px;
|
|
1433
|
+
color: #64748b;
|
|
1434
|
+
}
|
|
1435
|
+
.kh-retrieve-body {
|
|
1436
|
+
margin-top: 12px;
|
|
1437
|
+
padding-top: 12px;
|
|
1438
|
+
border-top: 1px dashed #e2e8f0;
|
|
1439
|
+
cursor: default;
|
|
1440
|
+
}
|
|
1441
|
+
.kh-slider-row {
|
|
1442
|
+
margin-top: 10px;
|
|
1443
|
+
}
|
|
1444
|
+
.kh-slider-label {
|
|
1445
|
+
display: flex;
|
|
1446
|
+
justify-content: space-between;
|
|
1447
|
+
align-items: center;
|
|
1448
|
+
font-size: 11px;
|
|
1449
|
+
color: #64748b;
|
|
1450
|
+
margin-bottom: 4px;
|
|
1451
|
+
}
|
|
1452
|
+
.kh-slider-val {
|
|
1453
|
+
font-size: 11px;
|
|
1454
|
+
color: #4338ca;
|
|
1455
|
+
font-weight: 600;
|
|
1456
|
+
}
|
|
1457
|
+
.kh-range {
|
|
1458
|
+
width: 100%;
|
|
1459
|
+
accent-color: #6366f1;
|
|
1460
|
+
}
|
|
1461
|
+
.kh-range:disabled {
|
|
1462
|
+
opacity: 0.4;
|
|
1463
|
+
cursor: not-allowed;
|
|
1464
|
+
}
|
|
1465
|
+
.kh-check-inline {
|
|
1466
|
+
margin: 0;
|
|
1467
|
+
}
|
|
1468
|
+
.kh-hybrid-tabs {
|
|
1469
|
+
display: inline-flex;
|
|
1470
|
+
gap: 4px;
|
|
1471
|
+
padding: 3px;
|
|
1472
|
+
background: #eef2ff;
|
|
1473
|
+
border-radius: 6px;
|
|
1474
|
+
margin-bottom: 8px;
|
|
1475
|
+
}
|
|
1476
|
+
.kh-hybrid-tab {
|
|
1477
|
+
padding: 4px 12px;
|
|
1478
|
+
border: none;
|
|
1479
|
+
border-radius: 4px;
|
|
1480
|
+
background: transparent;
|
|
1481
|
+
color: #64748b;
|
|
1482
|
+
font-size: 11px;
|
|
1483
|
+
cursor: pointer;
|
|
1484
|
+
}
|
|
1485
|
+
.kh-hybrid-tab-active {
|
|
1486
|
+
background: #fff;
|
|
1487
|
+
color: #4338ca;
|
|
1488
|
+
font-weight: 600;
|
|
1489
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/* Parent-child parent-mode cards */
|
|
1493
|
+
.kh-parent-mode-row {
|
|
1494
|
+
display: grid;
|
|
1495
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1496
|
+
gap: 10px;
|
|
1497
|
+
}
|
|
1498
|
+
.kh-parent-mode-card {
|
|
1499
|
+
padding: 10px 12px;
|
|
1500
|
+
background: #fff;
|
|
1501
|
+
border: 1px solid #e2e8f0;
|
|
1502
|
+
border-radius: 8px;
|
|
1503
|
+
text-align: left;
|
|
1504
|
+
cursor: pointer;
|
|
1505
|
+
transition:
|
|
1506
|
+
border-color 0.15s,
|
|
1507
|
+
background 0.15s;
|
|
1508
|
+
}
|
|
1509
|
+
.kh-parent-mode-card:hover {
|
|
1510
|
+
border-color: #cbd5e1;
|
|
1511
|
+
}
|
|
1512
|
+
.kh-parent-mode-active {
|
|
1513
|
+
border-color: transparent;
|
|
1514
|
+
outline: 1.5px solid #6366f1;
|
|
1515
|
+
background: #f5f6ff;
|
|
1516
|
+
}
|
|
1517
|
+
.kh-pm-head {
|
|
1518
|
+
display: flex;
|
|
1519
|
+
align-items: center;
|
|
1520
|
+
gap: 8px;
|
|
1521
|
+
font-size: 13px;
|
|
1522
|
+
}
|
|
1523
|
+
.kh-pm-title {
|
|
1524
|
+
font-weight: 600;
|
|
1525
|
+
color: #0f172a;
|
|
1526
|
+
}
|
|
1527
|
+
.kh-pm-desc {
|
|
1528
|
+
margin-top: 6px;
|
|
1529
|
+
font-size: 11px;
|
|
1530
|
+
color: #64748b;
|
|
1531
|
+
line-height: 1.5;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
.kh-step-actions {
|
|
1535
|
+
margin-top: 20px;
|
|
1536
|
+
display: flex;
|
|
1537
|
+
justify-content: space-between;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/* Preview pane (right) */
|
|
1541
|
+
.kh-split-right {
|
|
1542
|
+
min-height: 0;
|
|
1543
|
+
overflow-y: auto;
|
|
1544
|
+
padding: 40px 40px 80px 24px;
|
|
1545
|
+
border-left: 1px solid #f1f5f9;
|
|
1546
|
+
}
|
|
1547
|
+
.kh-preview-title {
|
|
1548
|
+
display: flex;
|
|
1549
|
+
justify-content: space-between;
|
|
1550
|
+
align-items: center;
|
|
1551
|
+
margin-bottom: 10px;
|
|
1552
|
+
font-size: 13px;
|
|
1553
|
+
font-weight: 600;
|
|
1554
|
+
color: #334155;
|
|
1555
|
+
}
|
|
1556
|
+
.kh-preview-file {
|
|
1557
|
+
font-size: 11px;
|
|
1558
|
+
font-weight: 400;
|
|
1559
|
+
color: #94a3b8;
|
|
1560
|
+
}
|
|
1561
|
+
.kh-preview-empty {
|
|
1562
|
+
padding: 100px 20px;
|
|
1563
|
+
text-align: center;
|
|
1564
|
+
border: 1px dashed #e2e8f0;
|
|
1565
|
+
border-radius: 10px;
|
|
1566
|
+
background: #f8fafc;
|
|
1567
|
+
}
|
|
1568
|
+
.kh-preview-empty-icon {
|
|
1569
|
+
font-size: 34px;
|
|
1570
|
+
color: #cbd5e1;
|
|
1571
|
+
}
|
|
1572
|
+
.kh-preview-empty-text {
|
|
1573
|
+
margin-top: 12px;
|
|
1574
|
+
font-size: 12px;
|
|
1575
|
+
color: #94a3b8;
|
|
1576
|
+
}
|
|
1577
|
+
.kh-preview-body {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
flex-direction: column;
|
|
1580
|
+
gap: 8px;
|
|
1581
|
+
}
|
|
1582
|
+
.kh-preview-warning {
|
|
1583
|
+
padding: 8px 10px;
|
|
1584
|
+
font-size: 12px;
|
|
1585
|
+
color: #92400e;
|
|
1586
|
+
background: #fef3c7;
|
|
1587
|
+
border: 1px solid #fde68a;
|
|
1588
|
+
border-radius: 6px;
|
|
1589
|
+
line-height: 1.5;
|
|
1590
|
+
}
|
|
1591
|
+
.kh-preview-chunk {
|
|
1592
|
+
padding: 10px 12px;
|
|
1593
|
+
border: 1px solid #e2e8f0;
|
|
1594
|
+
border-radius: 8px;
|
|
1595
|
+
background: #fff;
|
|
1596
|
+
}
|
|
1597
|
+
.kh-preview-chunk-head {
|
|
1598
|
+
display: flex;
|
|
1599
|
+
justify-content: space-between;
|
|
1600
|
+
align-items: center;
|
|
1601
|
+
font-size: 10px;
|
|
1602
|
+
margin-bottom: 6px;
|
|
1603
|
+
}
|
|
1604
|
+
.kh-chunk-tag {
|
|
1605
|
+
padding: 2px 6px;
|
|
1606
|
+
background: #eef2ff;
|
|
1607
|
+
color: #4338ca;
|
|
1608
|
+
border-radius: 4px;
|
|
1609
|
+
font-weight: 600;
|
|
1610
|
+
}
|
|
1611
|
+
.kh-chunk-tokens {
|
|
1612
|
+
color: #94a3b8;
|
|
1613
|
+
}
|
|
1614
|
+
.kh-preview-chunk-body {
|
|
1615
|
+
font-size: 12px;
|
|
1616
|
+
color: #334155;
|
|
1617
|
+
line-height: 1.6;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/* Step 3 — done */
|
|
1621
|
+
.kh-done-header {
|
|
1622
|
+
display: flex;
|
|
1623
|
+
align-items: center;
|
|
1624
|
+
gap: 12px;
|
|
1625
|
+
margin-bottom: 22px;
|
|
1626
|
+
}
|
|
1627
|
+
.kh-done-emoji {
|
|
1628
|
+
font-size: 34px;
|
|
1629
|
+
}
|
|
1630
|
+
.kh-done-title {
|
|
1631
|
+
font-size: 18px;
|
|
1632
|
+
font-weight: 600;
|
|
1633
|
+
color: #0f172a;
|
|
1634
|
+
}
|
|
1635
|
+
.kh-done-sub {
|
|
1636
|
+
margin-top: 4px;
|
|
1637
|
+
font-size: 12px;
|
|
1638
|
+
color: #94a3b8;
|
|
1639
|
+
}
|
|
1640
|
+
.kh-done-name-row {
|
|
1641
|
+
margin-bottom: 22px;
|
|
1642
|
+
}
|
|
1643
|
+
.kh-done-name-label {
|
|
1644
|
+
display: block;
|
|
1645
|
+
font-size: 12px;
|
|
1646
|
+
color: #64748b;
|
|
1647
|
+
margin-bottom: 6px;
|
|
1648
|
+
}
|
|
1649
|
+
.kh-done-name-input {
|
|
1650
|
+
position: relative;
|
|
1651
|
+
display: flex;
|
|
1652
|
+
align-items: center;
|
|
1653
|
+
}
|
|
1654
|
+
.kh-done-name-icon {
|
|
1655
|
+
position: absolute;
|
|
1656
|
+
left: 12px;
|
|
1657
|
+
font-size: 18px;
|
|
1658
|
+
}
|
|
1659
|
+
.kh-done-name-input .kh-input {
|
|
1660
|
+
padding-left: 40px;
|
|
1661
|
+
background: #f8fafc;
|
|
1662
|
+
}
|
|
1663
|
+
.kh-done-progress-row {
|
|
1664
|
+
padding: 12px 16px;
|
|
1665
|
+
border-radius: 10px;
|
|
1666
|
+
background: #f8fafc;
|
|
1667
|
+
border: 1px solid #e2e8f0;
|
|
1668
|
+
margin-bottom: 22px;
|
|
1669
|
+
display: grid;
|
|
1670
|
+
grid-template-columns: auto 1fr auto;
|
|
1671
|
+
gap: 12px;
|
|
1672
|
+
align-items: center;
|
|
1673
|
+
}
|
|
1674
|
+
.kh-done-progress-label {
|
|
1675
|
+
font-size: 12px;
|
|
1676
|
+
color: #64748b;
|
|
1677
|
+
grid-column: 1 / -1;
|
|
1678
|
+
}
|
|
1679
|
+
.kh-done-progress-file {
|
|
1680
|
+
grid-column: 1 / 2;
|
|
1681
|
+
font-size: 12px;
|
|
1682
|
+
color: #334155;
|
|
1683
|
+
}
|
|
1684
|
+
.kh-done-progress-bar {
|
|
1685
|
+
grid-column: 2 / 3;
|
|
1686
|
+
height: 8px;
|
|
1687
|
+
background: #e2e8f0;
|
|
1688
|
+
border-radius: 4px;
|
|
1689
|
+
overflow: hidden;
|
|
1690
|
+
}
|
|
1691
|
+
.kh-done-progress-fill {
|
|
1692
|
+
height: 100%;
|
|
1693
|
+
background: linear-gradient(90deg, #6366f1, #4f46e5);
|
|
1694
|
+
transition: width 0.3s;
|
|
1695
|
+
}
|
|
1696
|
+
.kh-done-progress-pct {
|
|
1697
|
+
grid-column: 3 / 4;
|
|
1698
|
+
font-size: 12px;
|
|
1699
|
+
color: #64748b;
|
|
1700
|
+
}
|
|
1701
|
+
.kh-done-facts {
|
|
1702
|
+
display: grid;
|
|
1703
|
+
grid-template-columns: 1fr;
|
|
1704
|
+
gap: 6px;
|
|
1705
|
+
margin-bottom: 26px;
|
|
1706
|
+
}
|
|
1707
|
+
.kh-done-facts > div {
|
|
1708
|
+
display: grid;
|
|
1709
|
+
grid-template-columns: 120px 1fr;
|
|
1710
|
+
font-size: 12px;
|
|
1711
|
+
padding: 4px 0;
|
|
1712
|
+
}
|
|
1713
|
+
.kh-fact-label {
|
|
1714
|
+
color: #94a3b8;
|
|
1715
|
+
}
|
|
1716
|
+
.kh-fact-val {
|
|
1717
|
+
color: #334155;
|
|
1718
|
+
}
|
|
1719
|
+
.kh-done-actions {
|
|
1720
|
+
display: flex;
|
|
1721
|
+
gap: 10px;
|
|
1722
|
+
}
|
|
1723
|
+
</style>
|