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.
Files changed (169) hide show
  1. package/package.json +30 -0
  2. package/pnpm-workspace.yaml +3 -0
  3. package/src/agent-flow/adapter/backend.ts +99 -0
  4. package/src/agent-flow/components/ChatIframePanel.vue +773 -0
  5. package/src/agent-flow/components/DatasetItemCard.vue +313 -0
  6. package/src/agent-flow/components/MemoryWindow.vue +104 -0
  7. package/src/agent-flow/components/PromptEditor.vue +804 -0
  8. package/src/agent-flow/components/PromptEditorTagPanel.vue +460 -0
  9. package/src/agent-flow/components/SvgIcon.vue +32 -0
  10. package/src/agent-flow/components/ToolItemCard.vue +169 -0
  11. package/src/agent-flow/components/VariableCard.vue +161 -0
  12. package/src/agent-flow/components/VariableSelector.vue +162 -0
  13. package/src/agent-flow/components/chat-iframe-types.ts +35 -0
  14. package/src/agent-flow/components/stubs/DatasetChooser.vue +70 -0
  15. package/src/agent-flow/components/stubs/Embedding.vue +22 -0
  16. package/src/agent-flow/components/stubs/ModelSelect.vue +24 -0
  17. package/src/agent-flow/components/stubs/RestfulSelectCard.vue +24 -0
  18. package/src/agent-flow/components/stubs/ToolChooser.vue +96 -0
  19. package/src/agent-flow/components/stubs/TriggersItemCard.vue +126 -0
  20. package/src/agent-flow/components/stubs/VariableModal.vue +115 -0
  21. package/src/agent-flow/index.ts +73 -0
  22. package/src/agent-flow/stores/workflow.ts +72 -0
  23. package/src/agent-flow/utils/langUtils.ts +173 -0
  24. package/src/agent-flow/workflow/Ability.vue +65 -0
  25. package/src/agent-flow/workflow/AuthorizationItem.vue +101 -0
  26. package/src/agent-flow/workflow/ConversationVar.vue +42 -0
  27. package/src/agent-flow/workflow/CustomEdge.vue +94 -0
  28. package/src/agent-flow/workflow/EnvironmentVar.vue +40 -0
  29. package/src/agent-flow/workflow/FlowDesigner.vue +2802 -0
  30. package/src/agent-flow/workflow/Icon.vue +179 -0
  31. package/src/agent-flow/workflow/NodeConfig.vue +509 -0
  32. package/src/agent-flow/workflow/NodeConfigCard.vue +460 -0
  33. package/src/agent-flow/workflow/NodeHandle.vue +147 -0
  34. package/src/agent-flow/workflow/NodeHeaderToolbar.vue +77 -0
  35. package/src/agent-flow/workflow/NodeHoverToolbar.vue +176 -0
  36. package/src/agent-flow/workflow/OutputItemCard.vue +668 -0
  37. package/src/agent-flow/workflow/ParamTableItem.vue +200 -0
  38. package/src/agent-flow/workflow/StructTable.vue +202 -0
  39. package/src/agent-flow/workflow/VarInsertField.vue +517 -0
  40. package/src/agent-flow/workflow/VarSelectField.vue +223 -0
  41. package/src/agent-flow/workflow/VariableSelectorItem.vue +48 -0
  42. package/src/agent-flow/workflow/WfField.vue +206 -0
  43. package/src/agent-flow/workflow/_config-base.css +470 -0
  44. package/src/agent-flow/workflow/composables/useNode.js +55 -0
  45. package/src/agent-flow/workflow/index.vue +53 -0
  46. package/src/agent-flow/workflow/nodeCard/AgentNodeCard.vue +180 -0
  47. package/src/agent-flow/workflow/nodeCard/AnswerNodeCard.vue +65 -0
  48. package/src/agent-flow/workflow/nodeCard/ClassifierCard.vue +152 -0
  49. package/src/agent-flow/workflow/nodeCard/CodeNodeCard.vue +139 -0
  50. package/src/agent-flow/workflow/nodeCard/ConditionNodeCard.vue +323 -0
  51. package/src/agent-flow/workflow/nodeCard/DocumentExtractorCard.vue +83 -0
  52. package/src/agent-flow/workflow/nodeCard/EndNodeCard.vue +123 -0
  53. package/src/agent-flow/workflow/nodeCard/HttpNodeCard.vue +451 -0
  54. package/src/agent-flow/workflow/nodeCard/HumanInputCard.vue +152 -0
  55. package/src/agent-flow/workflow/nodeCard/IterationCard.vue +66 -0
  56. package/src/agent-flow/workflow/nodeCard/KnowledgeRetrievalCard.vue +48 -0
  57. package/src/agent-flow/workflow/nodeCard/LLMNodeCard.vue +206 -0
  58. package/src/agent-flow/workflow/nodeCard/ListOperatorCard.vue +188 -0
  59. package/src/agent-flow/workflow/nodeCard/ParameterExtractorCard.vue +352 -0
  60. package/src/agent-flow/workflow/nodeCard/ServiceApiNodeCard.vue +188 -0
  61. package/src/agent-flow/workflow/nodeCard/StartNodeCard.vue +216 -0
  62. package/src/agent-flow/workflow/nodeCard/VariableAssignerCard.vue +125 -0
  63. package/src/agent-flow/workflow/nodeCard/VariableNodeCard.vue +121 -0
  64. package/src/agent-flow/workflow/nodes/AgentNode.vue +122 -0
  65. package/src/agent-flow/workflow/nodes/AnswerNode.vue +99 -0
  66. package/src/agent-flow/workflow/nodes/ClassifierNode.vue +199 -0
  67. package/src/agent-flow/workflow/nodes/CodeNode.vue +152 -0
  68. package/src/agent-flow/workflow/nodes/ConditionNode.vue +232 -0
  69. package/src/agent-flow/workflow/nodes/DocumentExtractorNode.vue +96 -0
  70. package/src/agent-flow/workflow/nodes/EndNode.vue +119 -0
  71. package/src/agent-flow/workflow/nodes/FileUploadNode.vue +97 -0
  72. package/src/agent-flow/workflow/nodes/HttpNode.vue +204 -0
  73. package/src/agent-flow/workflow/nodes/HumanInputNode.vue +110 -0
  74. package/src/agent-flow/workflow/nodes/IterationNode.vue +98 -0
  75. package/src/agent-flow/workflow/nodes/KnowledgeRetrievalNode.vue +253 -0
  76. package/src/agent-flow/workflow/nodes/LLMNode.vue +134 -0
  77. package/src/agent-flow/workflow/nodes/ListOperatorNode.vue +111 -0
  78. package/src/agent-flow/workflow/nodes/LoopNode.vue +94 -0
  79. package/src/agent-flow/workflow/nodes/ParameterExtractorNode.vue +147 -0
  80. package/src/agent-flow/workflow/nodes/ServiceApiNode.vue +161 -0
  81. package/src/agent-flow/workflow/nodes/StartNode.vue +199 -0
  82. package/src/agent-flow/workflow/nodes/TemplateNode.vue +103 -0
  83. package/src/agent-flow/workflow/nodes/UserInputNode.vue +92 -0
  84. package/src/agent-flow/workflow/nodes/VariableAssignerNode.vue +145 -0
  85. package/src/agent-flow/workflow/nodes/VariableNode.vue +130 -0
  86. package/src/agent-flow/workflow/nodes/_node-base.css +338 -0
  87. package/src/agent-flow/workflow/utils/node_card_form.ts +335 -0
  88. package/src/agent-flow/workflow/utils/node_config.ts +63 -0
  89. package/src/agent-flow/workflow/utils/workflow_utils.ts +151 -0
  90. package/src/agent-studio/adapters/springAgentStart.ts +390 -0
  91. package/src/agent-studio/api/index.ts +1 -0
  92. package/src/agent-studio/api/types.ts +173 -0
  93. package/src/agent-studio/components/AgentApiDocs.vue +500 -0
  94. package/src/agent-studio/components/AgentAppsPage.vue +1601 -0
  95. package/src/agent-studio/components/AgentCardGrid.vue +259 -0
  96. package/src/agent-studio/components/AgentChatPage.vue +553 -0
  97. package/src/agent-studio/components/AgentDebugPanel.vue +350 -0
  98. package/src/agent-studio/components/AgentLogsPanel.vue +391 -0
  99. package/src/agent-studio/components/AgentMonitorPanel.vue +204 -0
  100. package/src/agent-studio/components/AgentOrchestrate.vue +505 -0
  101. package/src/agent-studio/components/AgentPromptEditor.vue +237 -0
  102. package/src/agent-studio/components/AgentStudioShell.vue +227 -0
  103. package/src/agent-studio/components/AgentToolsPanel.vue +394 -0
  104. package/src/agent-studio/components/AgentVariablesPanel.vue +205 -0
  105. package/src/agent-studio/components/ApiKeyManager.vue +251 -0
  106. package/src/agent-studio/components/AppDesignDrawer.vue +3391 -0
  107. package/src/agent-studio/components/CreateAppModal.vue +737 -0
  108. package/src/agent-studio/components/SparkChart.vue +200 -0
  109. package/src/agent-studio/components/VariableEditorModal.vue +499 -0
  110. package/src/agent-studio/composables/useAgentStudio.ts +347 -0
  111. package/src/agent-studio/index.ts +60 -0
  112. package/src/agent-studio/panels/DrawerFlowDesigner.vue +203 -0
  113. package/src/agent-studio/panels/LogAnnotationPanel.vue +490 -0
  114. package/src/agent-studio/panels/MonitorPanel.vue +535 -0
  115. package/src/agent-studio/types.ts +242 -0
  116. package/src/index.ts +155 -0
  117. package/src/knowledge-hub/adapters/springAgentStart.ts +472 -0
  118. package/src/knowledge-hub/components/AddDocumentsWizard.vue +1452 -0
  119. package/src/knowledge-hub/components/CreateDatasetWizard.vue +1723 -0
  120. package/src/knowledge-hub/components/DatasetCardGrid.vue +467 -0
  121. package/src/knowledge-hub/components/DatasetDetailDrawer.vue +888 -0
  122. package/src/knowledge-hub/components/DatasetPicker.vue +65 -0
  123. package/src/knowledge-hub/components/DatasetPickerModal.vue +526 -0
  124. package/src/knowledge-hub/components/DatasetSettingsPanel.vue +908 -0
  125. package/src/knowledge-hub/components/DatasetSidebar.vue +781 -0
  126. package/src/knowledge-hub/components/DocumentChunksView.vue +802 -0
  127. package/src/knowledge-hub/components/DocumentTable.vue +552 -0
  128. package/src/knowledge-hub/components/HitTestingPanel.vue +74 -0
  129. package/src/knowledge-hub/components/KnowledgeApp.vue +66 -0
  130. package/src/knowledge-hub/components/KnowledgeHubApp.vue +504 -0
  131. package/src/knowledge-hub/components/RecallTestingPanelV2.vue +515 -0
  132. package/src/knowledge-hub/components/RetrievalConfigPopover.vue +270 -0
  133. package/src/knowledge-hub/components/RetrievalMethodPicker.vue +547 -0
  134. package/src/knowledge-hub/components/RetrievedList.vue +24 -0
  135. package/src/knowledge-hub/components/internal/KhDialog.vue +195 -0
  136. package/src/knowledge-hub/composables/useKnowledge.ts +89 -0
  137. package/src/knowledge-hub/i18n/index.ts +2 -0
  138. package/src/knowledge-hub/i18n/messages.ts +422 -0
  139. package/src/knowledge-hub/i18n/useKhI18n.ts +107 -0
  140. package/src/knowledge-hub/index.ts +46 -0
  141. package/src/knowledge-hub/styles/index.css +4 -0
  142. package/src/knowledge-hub/styles/tokens.css +138 -0
  143. package/src/knowledge-hub/types/api.ts +213 -0
  144. package/src/knowledge-hub/types/dataset.ts +88 -0
  145. package/src/knowledge-hub/types/document.ts +45 -0
  146. package/src/knowledge-hub/types/index.ts +9 -0
  147. package/src/knowledge-hub/types/retrieval.ts +46 -0
  148. package/src/provider-hub/components/CredentialForm.vue +103 -0
  149. package/src/provider-hub/components/DefaultModelsPanel.vue +270 -0
  150. package/src/provider-hub/components/GroupedModelSelect.vue +445 -0
  151. package/src/provider-hub/components/ModelCardGrid.vue +332 -0
  152. package/src/provider-hub/components/ModelParameterDrawer.vue +739 -0
  153. package/src/provider-hub/components/ModelPickerPopover.vue +1282 -0
  154. package/src/provider-hub/components/ProviderApp.vue +159 -0
  155. package/src/provider-hub/components/ProviderCredentialModal.vue +321 -0
  156. package/src/provider-hub/components/ProviderGallery.vue +131 -0
  157. package/src/provider-hub/components/ProviderHubShell.vue +1992 -0
  158. package/src/provider-hub/components/ProviderIcon.vue +130 -0
  159. package/src/provider-hub/composables/useProviderHub.ts +358 -0
  160. package/src/provider-hub/index.ts +43 -0
  161. package/src/provider-hub/svg/MiniMax.svg +1 -0
  162. package/src/provider-hub/svg/Volcengine.svg +1 -0
  163. package/src/provider-hub/svg/Zhipu.svg +1 -0
  164. package/src/provider-hub/svg/deepseek.svg +1 -0
  165. package/src/provider-hub/svg/moonshot.svg +1 -0
  166. package/src/provider-hub/svg/openai.svg +1 -0
  167. package/src/provider-hub/svg/qwen.svg +1 -0
  168. package/src/provider-hub/svg/siliconflow.svg +1 -0
  169. package/src/provider-hub/types.ts +256 -0
@@ -0,0 +1,1452 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * AddDocumentsWizard — 3-step wizard for adding documents to an existing
4
+ * dataset, mirroring the CreateDatasetWizard UX so the user experience of
5
+ * "+ 添加文件" matches the "+ 新建" flow.
6
+ *
7
+ * Screens:
8
+ * 1. 选择数据源 — source cards + file dropzone
9
+ * 2. 文本分段与清洗 — chunking + retrieval preview (defaults seeded from the
10
+ * current dataset's processRule so the user can review before confirming)
11
+ * 3. 处理并完成 — progress + summary
12
+ *
13
+ * Difference from CreateDatasetWizard: no dataset name/description, no dataset
14
+ * creation. The final action just uploads the picked files into the current
15
+ * dataset — chunking/retrieval settings on step 2 are informational (backend
16
+ * uses the dataset's existing config for the upload endpoint).
17
+ */
18
+ import { computed, ref, watch } from 'vue';
19
+
20
+ import type { ChunkPreview } from '../types/api';
21
+ import type { ParentMode, ProcessRule } from '../types/dataset';
22
+ import type { RetrievalMethod } from '../types/retrieval';
23
+
24
+ interface Props {
25
+ /** Show the wizard (host controls open state). */
26
+ open: boolean;
27
+ /** Existing dataset name — shown in the back button for context. */
28
+ datasetName?: string;
29
+ /** Existing dataset processRule (parsed) — seeds step 2 defaults. */
30
+ processRule?: ProcessRule;
31
+ /** Existing retrieval method — seeds step 2 defaults. */
32
+ retrievalMethod?: RetrievalMethod;
33
+ /** Existing dataset indexing technique — shown on step 2/3 for context. */
34
+ indexingTechnique?: 'ECONOMY' | 'HIGH_QUALITY';
35
+ /**
36
+ * Backend preview call — same reader/cleaner/chunker as ingestion, so what
37
+ * the user sees on step 2 matches what actually lands on save. When omitted
38
+ * the wizard falls back to a mock preview so hosts without the backing
39
+ * endpoint still ship a working wizard.
40
+ */
41
+ previewChunks?: (
42
+ file: File,
43
+ rule: ProcessRule,
44
+ limit?: number,
45
+ ) => Promise<ChunkPreview>;
46
+ }
47
+
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ datasetName: '',
50
+ processRule: () => ({}),
51
+ retrievalMethod: 'HYBRID',
52
+ indexingTechnique: 'HIGH_QUALITY',
53
+ });
54
+
55
+ const emit = defineEmits<{
56
+ (e: 'update:open', v: boolean): void;
57
+ /**
58
+ * Emitted on step-2 "保存并处理". Now carries {@code processRule} in
59
+ * addition to the picked files so the host can persist chunking overrides
60
+ * back onto the dataset before the upload runs — the previous behaviour
61
+ * silently discarded any edits the user made on this step, giving the
62
+ * impression that "分段模式 就不再支持" after the first upload.
63
+ */
64
+ (
65
+ e: 'confirm',
66
+ payload: { files: File[]; processRule: ProcessRule },
67
+ ): void;
68
+ }>();
69
+
70
+ // ------ step tracker
71
+ type StepId = 1 | 2 | 3;
72
+ const step = ref<StepId>(1);
73
+
74
+ const steps = [
75
+ { id: 1 as const, label: '选择数据源' },
76
+ { id: 2 as const, label: '文本分段与清洗' },
77
+ { id: 3 as const, label: '处理并完成' },
78
+ ];
79
+
80
+ function close() {
81
+ step.value = 1;
82
+ files.value = [];
83
+ previewLoaded.value = false;
84
+ previewChunksList.value = [];
85
+ emit('update:open', false);
86
+ }
87
+
88
+ // ------ step 1: data source
89
+ type SourceKind = 'notion' | 'text' | 'web';
90
+ const source = ref<SourceKind>('text');
91
+ const files = ref<File[]>([]);
92
+
93
+ const SOURCES = [
94
+ {
95
+ id: 'text' as const,
96
+ icon: '📄',
97
+ iconBg: '#EEF4FF',
98
+ label: '导入已有文本',
99
+ enabled: true,
100
+ },
101
+ {
102
+ id: 'notion' as const,
103
+ icon: 'N',
104
+ iconBg: '#F1F5F9',
105
+ label: '同步自 Notion 内容',
106
+ enabled: false,
107
+ },
108
+ {
109
+ id: 'web' as const,
110
+ icon: '🌐',
111
+ iconBg: '#EEF4FF',
112
+ label: '同步自 Web 站点',
113
+ enabled: false,
114
+ },
115
+ ];
116
+
117
+ const dragOver = ref(false);
118
+ const fileInputRef = ref<HTMLInputElement | null>(null);
119
+
120
+ const ALLOWED = [
121
+ 'TXT',
122
+ 'CSV',
123
+ 'XLS',
124
+ 'MARKDOWN',
125
+ 'HTML',
126
+ 'MDX',
127
+ 'PDF',
128
+ 'VTT',
129
+ 'DOCX',
130
+ 'HTM',
131
+ 'PROPERTIES',
132
+ 'MD',
133
+ 'XLSX',
134
+ ];
135
+
136
+ function onFileInput(e: Event) {
137
+ const input = e.target as HTMLInputElement;
138
+ if (input.files) addFiles([...input.files]);
139
+ input.value = '';
140
+ }
141
+ function onDrop(e: DragEvent) {
142
+ e.preventDefault();
143
+ dragOver.value = false;
144
+ const dropped = e.dataTransfer?.files;
145
+ if (dropped) addFiles([...dropped]);
146
+ }
147
+ function addFiles(list: File[]) {
148
+ const filtered = list.filter((f) => f.size <= 15 * 1024 * 1024);
149
+ files.value = [...files.value, ...filtered].slice(0, 5);
150
+ }
151
+ function removeFile(i: number) {
152
+ files.value.splice(i, 1);
153
+ }
154
+
155
+ const step1Ready = computed(() => files.value.length > 0);
156
+
157
+ // ------ step 2: chunking (seeded from the dataset's existing processRule)
158
+ const chunkMode = ref<'general' | 'parent-child'>(
159
+ props.processRule?.template === 'PARENT_CHILD' ? 'parent-child' : 'general',
160
+ );
161
+ const chunkSeparator = ref('\\n\\n');
162
+ const chunkMaxTokens = ref(props.processRule?.chunkTokens ?? 1024);
163
+ const chunkOverlap = ref(props.processRule?.overlapTokens ?? 50);
164
+ const removeExtraWhitespace = ref(
165
+ props.processRule?.removeExtraWhitespace ?? true,
166
+ );
167
+ const removeUrlsEmails = ref(props.processRule?.removeUrlsEmails ?? false);
168
+
169
+ const parentMode = ref<ParentMode>(
170
+ props.processRule?.parentMode ?? 'PARAGRAPH',
171
+ );
172
+ const parentSeparator = ref('\\n\\n');
173
+ const parentMaxTokens = ref(props.processRule?.parentChunkTokens ?? 1024);
174
+ const childSeparator = ref('\\n');
175
+ const childMaxTokens = ref(props.processRule?.chunkTokens ?? 512);
176
+
177
+ const retrievalMethodDisplay = computed(() => props.retrievalMethod);
178
+
179
+ // Re-seed defaults if the dataset context changes while the wizard is closed.
180
+ watch(
181
+ () => [props.processRule, props.retrievalMethod] as const,
182
+ ([rule]) => {
183
+ if (props.open) return;
184
+ chunkMode.value =
185
+ rule?.template === 'PARENT_CHILD' ? 'parent-child' : 'general';
186
+ chunkMaxTokens.value = rule?.chunkTokens ?? 1024;
187
+ chunkOverlap.value = rule?.overlapTokens ?? 50;
188
+ removeExtraWhitespace.value = rule?.removeExtraWhitespace ?? true;
189
+ removeUrlsEmails.value = rule?.removeUrlsEmails ?? false;
190
+ parentMode.value = rule?.parentMode ?? 'PARAGRAPH';
191
+ parentMaxTokens.value = rule?.parentChunkTokens ?? 1024;
192
+ childMaxTokens.value = rule?.chunkTokens ?? 512;
193
+ },
194
+ { deep: true },
195
+ );
196
+
197
+ // ------ step 2 preview
198
+ const previewLoaded = ref(false);
199
+ const previewLoading = ref(false);
200
+ const previewTotal = ref(0);
201
+ const previewError = ref<string | null>(null);
202
+ const previewChunksList = ref<
203
+ Array<{ index: number; text: string; tokens: number }>
204
+ >([]);
205
+
206
+ /**
207
+ * Snapshot the current step-2 form into a {@link ProcessRule} for a preview
208
+ * or a real save. Kept as a computed-style function so both {@code
209
+ * loadPreview} and {@code submit} produce identical payloads.
210
+ */
211
+ function currentRule(): ProcessRule {
212
+ return {
213
+ template: chunkMode.value === 'parent-child' ? 'PARENT_CHILD' : 'NAIVE',
214
+ chunkTokens: chunkMaxTokens.value,
215
+ overlapTokens: chunkOverlap.value,
216
+ parentMode: parentMode.value,
217
+ parentChunkTokens: parentMaxTokens.value,
218
+ removeExtraWhitespace: removeExtraWhitespace.value,
219
+ removeUrlsEmails: removeUrlsEmails.value,
220
+ };
221
+ }
222
+
223
+ async function loadPreview() {
224
+ const file = files.value[0];
225
+ if (!file) return;
226
+ previewError.value = null;
227
+ if (props.previewChunks) {
228
+ previewLoading.value = true;
229
+ try {
230
+ const res = await props.previewChunks(file, currentRule(), 10);
231
+ previewTotal.value = res.totalChunks;
232
+ previewChunksList.value = res.chunks;
233
+ previewLoaded.value = true;
234
+ } catch (e: any) {
235
+ previewError.value = e?.message ?? '预览失败';
236
+ previewLoaded.value = true;
237
+ previewChunksList.value = [];
238
+ } finally {
239
+ previewLoading.value = false;
240
+ }
241
+ return;
242
+ }
243
+ // Fallback for hosts that haven't wired the preview endpoint yet — a mock
244
+ // still lets the wizard flow through, but with an inline note that this is
245
+ // NOT the real chunking result.
246
+ previewLoaded.value = true;
247
+ previewTotal.value = 6;
248
+ const filename = file.name;
249
+ previewChunksList.value = Array.from({ length: 6 }, (_, i) => ({
250
+ index: i + 1,
251
+ text: `SEG-${String(i + 1).padStart(2, '0')} · 来自「${filename}」的第 ${i + 1} 段(示例)。`,
252
+ tokens: 240 + Math.floor(Math.random() * 400),
253
+ }));
254
+ previewError.value = '未接入 previewChunks 接口,当前展示的是占位示例。';
255
+ }
256
+ function resetChunking() {
257
+ chunkSeparator.value = '\\n\\n';
258
+ chunkMaxTokens.value = props.processRule?.chunkTokens ?? 1024;
259
+ chunkOverlap.value = props.processRule?.overlapTokens ?? 50;
260
+ previewLoaded.value = false;
261
+ previewChunksList.value = [];
262
+ }
263
+
264
+ // ------ step 3: submit
265
+ function goToStep2() {
266
+ step.value = 2;
267
+ }
268
+ function submit() {
269
+ const processRule: ProcessRule = {
270
+ template: chunkMode.value === 'parent-child' ? 'PARENT_CHILD' : 'NAIVE',
271
+ chunkTokens: chunkMaxTokens.value,
272
+ overlapTokens: chunkOverlap.value,
273
+ parentMode: parentMode.value,
274
+ parentChunkTokens: parentMaxTokens.value,
275
+ removeExtraWhitespace: removeExtraWhitespace.value,
276
+ removeUrlsEmails: removeUrlsEmails.value,
277
+ };
278
+ emit('confirm', { files: files.value, processRule });
279
+ step.value = 3;
280
+ }
281
+
282
+ function humanSize(bytes: number): string {
283
+ if (bytes < 1024) return `${bytes} B`;
284
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
285
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
286
+ }
287
+
288
+ const backLabel = computed(() => props.datasetName || '知识库');
289
+ </script>
290
+
291
+ <template>
292
+ <Teleport to="body" :disabled="!open">
293
+ <div v-if="open" class="kh-wizard-mask" @click.self="close">
294
+ <div class="kh-wizard-shell">
295
+ <!-- top bar (modal-style: title + stepper + close ×) -->
296
+ <div class="kh-topbar">
297
+ <div class="kh-topbar-title">添加文件 · {{ backLabel }}</div>
298
+ <div class="kh-stepper">
299
+ <div
300
+ v-for="(s, i) in steps"
301
+ :key="s.id"
302
+ class="kh-step"
303
+ :class="{
304
+ 'kh-step-active': step === s.id,
305
+ 'kh-step-done': step > s.id,
306
+ }"
307
+ >
308
+ <span class="kh-step-num">
309
+ <span v-if="step === s.id" class="kh-step-num-badge">
310
+ STEP {{ s.id }}
311
+ </span>
312
+ <span v-else>{{ s.id }}</span>
313
+ </span>
314
+ <span class="kh-step-label">{{ s.label }}</span>
315
+ <span v-if="i < steps.length - 1" class="kh-step-dash" />
316
+ </div>
317
+ </div>
318
+ <button class="kh-close" aria-label="close" @click="close">×</button>
319
+ </div>
320
+
321
+ <!-- STEP 1: 选择数据源 -->
322
+ <div v-if="step === 1" class="kh-body kh-body-center">
323
+ <div class="kh-content-narrow">
324
+ <div class="kh-section-label">选择数据源</div>
325
+ <div class="kh-source-row">
326
+ <button
327
+ v-for="s in SOURCES"
328
+ :key="s.id"
329
+ type="button"
330
+ class="kh-source"
331
+ :class="{
332
+ 'kh-source-active': source === s.id,
333
+ 'kh-source-disabled': !s.enabled,
334
+ }"
335
+ :disabled="!s.enabled"
336
+ @click="s.enabled && (source = s.id)"
337
+ >
338
+ <div class="kh-source-icon" :style="{ background: s.iconBg }">
339
+ {{ s.icon }}
340
+ </div>
341
+ <div class="kh-source-label">{{ s.label }}</div>
342
+ </button>
343
+ </div>
344
+
345
+ <div class="kh-section-label" style="margin-top: 22px">上传文本文件</div>
346
+ <div
347
+ class="kh-dropzone"
348
+ :class="{ 'kh-dropzone-over': dragOver }"
349
+ @dragenter.prevent="dragOver = true"
350
+ @dragover.prevent="dragOver = true"
351
+ @dragleave.prevent="dragOver = false"
352
+ @drop="onDrop"
353
+ @click="fileInputRef?.click()"
354
+ >
355
+ <div class="kh-dz-row">
356
+ <span class="kh-dz-icon">☁</span>
357
+ <span class="kh-dz-text">
358
+ 拖拽文件或文件夹至此,或者
359
+ <span class="kh-dz-link">选择文件</span>
360
+ </span>
361
+ </div>
362
+ <div class="kh-dz-hint">
363
+ 已支持
364
+ <template v-for="(a, i) in ALLOWED" :key="a">
365
+ <span>{{ a }}</span>
366
+ <span v-if="i < ALLOWED.length - 1">、</span>
367
+ </template>
368
+ 。每批最多 5 个文件,每个文件不超过 15 MB。
369
+ </div>
370
+ <input
371
+ ref="fileInputRef"
372
+ type="file"
373
+ multiple
374
+ class="kh-hidden-input"
375
+ @change="onFileInput"
376
+ />
377
+ </div>
378
+
379
+ <div v-if="files.length > 0" class="kh-file-list">
380
+ <div v-for="(f, i) in files" :key="i" class="kh-file-row">
381
+ <span class="kh-file-icon">📄</span>
382
+ <span class="kh-file-name">{{ f.name }}</span>
383
+ <span class="kh-file-size">{{ humanSize(f.size) }}</span>
384
+ <button class="kh-file-x" @click="removeFile(i)">×</button>
385
+ </div>
386
+ </div>
387
+
388
+ <div class="kh-step1-actions">
389
+ <button
390
+ class="kh-btn kh-btn-primary"
391
+ :disabled="!step1Ready"
392
+ @click="goToStep2"
393
+ >
394
+ 下一步 →
395
+ </button>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <!-- STEP 2: 分段与清洗 -->
401
+ <div v-if="step === 2" class="kh-body kh-body-split">
402
+ <!-- LEFT -->
403
+ <div class="kh-split-left">
404
+ <div class="kh-hint kh-hint-block">
405
+ 💡 新文件将沿用知识库当前的分段与检索配置。你可以在下方查看,如需修改整套配置请在"设置"标签中调整。
406
+ </div>
407
+
408
+ <div
409
+ class="kh-panel"
410
+ :class="{ 'kh-panel-active': chunkMode === 'general' }"
411
+ @click="chunkMode = 'general'"
412
+ >
413
+ <div class="kh-panel-header">
414
+ <div class="kh-panel-title">
415
+ <span
416
+ class="kh-panel-radio"
417
+ :class="{ 'kh-panel-radio-on': chunkMode === 'general' }"
418
+ />
419
+ <span>通用</span>
420
+ </div>
421
+ <div class="kh-panel-hint">
422
+ 通过文本分块模式,检索和召回块获取相关部分。
423
+ </div>
424
+ </div>
425
+ <div v-if="chunkMode === 'general'" class="kh-panel-body">
426
+ <div class="kh-grid-3">
427
+ <div class="kh-field">
428
+ <label>分段标识符</label>
429
+ <input v-model="chunkSeparator" class="kh-input" />
430
+ </div>
431
+ <div class="kh-field">
432
+ <label>分段最大长度</label>
433
+ <div class="kh-input-suffix">
434
+ <input
435
+ v-model.number="chunkMaxTokens"
436
+ type="number"
437
+ class="kh-input"
438
+ />
439
+ <span class="kh-suffix">characters</span>
440
+ </div>
441
+ </div>
442
+ <div class="kh-field">
443
+ <label>分段重叠长度</label>
444
+ <div class="kh-input-suffix">
445
+ <input
446
+ v-model.number="chunkOverlap"
447
+ type="number"
448
+ class="kh-input"
449
+ />
450
+ <span class="kh-suffix">characters</span>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ <div class="kh-sub-label">文本预处理规则</div>
455
+ <label class="kh-check">
456
+ <input v-model="removeExtraWhitespace" type="checkbox" />
457
+ <span>替换掉连续的空格、换行符和制表符</span>
458
+ </label>
459
+ <label class="kh-check">
460
+ <input v-model="removeUrlsEmails" type="checkbox" />
461
+ <span>删除所有 URL 和电子邮件地址</span>
462
+ </label>
463
+ <div class="kh-inline-actions">
464
+ <button
465
+ class="kh-btn-mini kh-btn-mini-primary"
466
+ @click.stop="loadPreview"
467
+ >
468
+ ⓘ 预览块
469
+ </button>
470
+ <button class="kh-btn-mini" @click.stop="resetChunking">
471
+ 重置
472
+ </button>
473
+ </div>
474
+ </div>
475
+ </div>
476
+
477
+ <div
478
+ class="kh-panel"
479
+ :class="{ 'kh-panel-active': chunkMode === 'parent-child' }"
480
+ @click="chunkMode = 'parent-child'"
481
+ >
482
+ <div class="kh-panel-header">
483
+ <div class="kh-panel-title">
484
+ <span
485
+ class="kh-panel-radio"
486
+ :class="{
487
+ 'kh-panel-radio-on': chunkMode === 'parent-child',
488
+ }"
489
+ />
490
+ <span>👥 父子分段</span>
491
+ </div>
492
+ <div class="kh-panel-hint">
493
+ 地区父子块的父块,子块用于检索,父块作为上下文
494
+ </div>
495
+ </div>
496
+ <div v-if="chunkMode === 'parent-child'" class="kh-panel-body">
497
+ <div class="kh-sub-label">父块用作上下文</div>
498
+ <div class="kh-parent-mode-row">
499
+ <button
500
+ type="button"
501
+ class="kh-parent-mode-card"
502
+ :class="{
503
+ 'kh-parent-mode-active': parentMode === 'PARAGRAPH',
504
+ }"
505
+ @click.stop="parentMode = 'PARAGRAPH'"
506
+ >
507
+ <div class="kh-pm-head">
508
+ <span
509
+ class="kh-panel-radio"
510
+ :class="{
511
+ 'kh-panel-radio-on': parentMode === 'PARAGRAPH',
512
+ }"
513
+ />
514
+ <span class="kh-pm-title">段落</span>
515
+ </div>
516
+ <div class="kh-pm-desc">
517
+ 此模式根据预设的分隔符将文本划分为段落。检索到匹配段后返回所在段落作为父块。
518
+ </div>
519
+ <div
520
+ v-if="parentMode === 'PARAGRAPH'"
521
+ class="kh-grid-2"
522
+ style="margin-top: 10px"
523
+ >
524
+ <div class="kh-field">
525
+ <label>分段标识符</label>
526
+ <input
527
+ v-model="parentSeparator"
528
+ class="kh-input"
529
+ @click.stop
530
+ />
531
+ </div>
532
+ <div class="kh-field">
533
+ <label>分段最大长度</label>
534
+ <div class="kh-input-suffix">
535
+ <input
536
+ v-model.number="parentMaxTokens"
537
+ type="number"
538
+ class="kh-input"
539
+ @click.stop
540
+ />
541
+ <span class="kh-suffix">characters</span>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ </button>
546
+ <button
547
+ type="button"
548
+ class="kh-parent-mode-card"
549
+ :class="{
550
+ 'kh-parent-mode-active': parentMode === 'FULL_DOC',
551
+ }"
552
+ @click.stop="parentMode = 'FULL_DOC'"
553
+ >
554
+ <div class="kh-pm-head">
555
+ <span
556
+ class="kh-panel-radio"
557
+ :class="{
558
+ 'kh-panel-radio-on': parentMode === 'FULL_DOC',
559
+ }"
560
+ />
561
+ <span class="kh-pm-title">全文</span>
562
+ </div>
563
+ <div class="kh-pm-desc">
564
+ 此模式会将文本转成全文视图,检索到匹配段后返回整个文档作为父块。适用于短文档。
565
+ </div>
566
+ </button>
567
+ </div>
568
+
569
+ <div class="kh-sub-label" style="margin-top: 14px">
570
+ 子块用于检索
571
+ </div>
572
+ <div class="kh-grid-2">
573
+ <div class="kh-field">
574
+ <label>分段标识符</label>
575
+ <input v-model="childSeparator" class="kh-input" @click.stop />
576
+ </div>
577
+ <div class="kh-field">
578
+ <label>分段最大长度</label>
579
+ <div class="kh-input-suffix">
580
+ <input
581
+ v-model.number="childMaxTokens"
582
+ type="number"
583
+ class="kh-input"
584
+ @click.stop
585
+ />
586
+ <span class="kh-suffix">characters</span>
587
+ </div>
588
+ </div>
589
+ </div>
590
+
591
+ <div class="kh-inline-actions">
592
+ <button
593
+ class="kh-btn-mini kh-btn-mini-primary"
594
+ @click.stop="loadPreview"
595
+ >
596
+ ⓘ 预览块
597
+ </button>
598
+ <button class="kh-btn-mini" @click.stop="resetChunking">
599
+ 重置
600
+ </button>
601
+ </div>
602
+ </div>
603
+ </div>
604
+
605
+ <div class="kh-section-label" style="margin-top: 18px">
606
+ 索引方式(沿用当前配置)
607
+ </div>
608
+ <div class="kh-readonly-card">
609
+ <span class="kh-readonly-icon">⭐</span>
610
+ <div>
611
+ <div class="kh-readonly-title">
612
+ {{ indexingTechnique === 'HIGH_QUALITY' ? '高质量' : '经济' }}
613
+ </div>
614
+ <div class="kh-readonly-desc">
615
+ {{
616
+ indexingTechnique === 'HIGH_QUALITY'
617
+ ? '调用嵌入模型处理文档以实现更精确的检索。'
618
+ : '使用关键词检索,不消耗 tokens,准确性较低。'
619
+ }}
620
+ </div>
621
+ </div>
622
+ </div>
623
+
624
+ <div class="kh-section-label" style="margin-top: 16px">
625
+ 检索方式(沿用当前配置)
626
+ </div>
627
+ <div class="kh-readonly-card">
628
+ <span class="kh-readonly-icon">
629
+ {{
630
+ retrievalMethodDisplay === 'VECTOR'
631
+ ? '◈'
632
+ : retrievalMethodDisplay === 'FULL_TEXT'
633
+ ? '≡'
634
+ : '⚡'
635
+ }}
636
+ </span>
637
+ <div>
638
+ <div class="kh-readonly-title">
639
+ {{
640
+ retrievalMethodDisplay === 'VECTOR'
641
+ ? '向量检索'
642
+ : retrievalMethodDisplay === 'FULL_TEXT'
643
+ ? '全文检索'
644
+ : '混合检索'
645
+ }}
646
+ </div>
647
+ <div class="kh-readonly-desc">
648
+ 可以在知识库的"设置"标签中调整检索方式。
649
+ </div>
650
+ </div>
651
+ </div>
652
+
653
+ <div class="kh-step-actions">
654
+ <button class="kh-btn kh-btn-secondary" @click="step = 1">
655
+ ← 上一步
656
+ </button>
657
+ <button
658
+ class="kh-btn kh-btn-primary"
659
+ :disabled="files.length === 0"
660
+ @click="submit"
661
+ >
662
+ 保存并处理 →
663
+ </button>
664
+ </div>
665
+ </div>
666
+
667
+ <!-- RIGHT: preview -->
668
+ <div class="kh-split-right">
669
+ <div class="kh-preview-title">
670
+ <span>预览</span>
671
+ <span v-if="files[0]" class="kh-preview-file">
672
+ 📄 {{ files[0].name }}
673
+ <template v-if="previewLoaded">
674
+ · 共 {{ previewTotal }} 段
675
+ <template v-if="previewTotal > previewChunksList.length">
676
+ (前 {{ previewChunksList.length }} 段)
677
+ </template>
678
+ </template>
679
+ </span>
680
+ </div>
681
+ <div v-if="previewLoading" class="kh-preview-empty">
682
+ <div class="kh-preview-empty-text">分段中…</div>
683
+ </div>
684
+ <div v-else-if="!previewLoaded" class="kh-preview-empty">
685
+ <div class="kh-preview-empty-icon">◉</div>
686
+ <div class="kh-preview-empty-text">
687
+ 点击左侧的"预览块"按钮来加载预览
688
+ </div>
689
+ </div>
690
+ <div v-else class="kh-preview-body">
691
+ <div v-if="previewError" class="kh-preview-warning">
692
+ ⚠️ {{ previewError }}
693
+ </div>
694
+ <div
695
+ v-for="c in previewChunksList"
696
+ :key="c.index"
697
+ class="kh-preview-chunk"
698
+ >
699
+ <div class="kh-preview-chunk-head">
700
+ <span class="kh-chunk-tag">
701
+ SEG-{{ String(c.index).padStart(2, '0') }}
702
+ </span>
703
+ <span class="kh-chunk-tokens">{{ c.tokens }} tokens</span>
704
+ </div>
705
+ <div class="kh-preview-chunk-body">{{ c.text }}</div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+ </div>
710
+
711
+ <!-- STEP 3: 处理并完成 -->
712
+ <div v-if="step === 3" class="kh-body kh-body-center">
713
+ <div class="kh-content-narrow">
714
+ <div class="kh-done-header">
715
+ <span class="kh-done-emoji">🎉</span>
716
+ <div>
717
+ <div class="kh-done-title">文件已提交</div>
718
+ <div class="kh-done-sub">
719
+ 文件已开始处理,稍后将出现在文档列表中。
720
+ </div>
721
+ </div>
722
+ </div>
723
+
724
+ <div class="kh-done-files">
725
+ <div v-for="(f, i) in files" :key="i" class="kh-done-file-row">
726
+ <span class="kh-file-icon">📄</span>
727
+ <span class="kh-file-name">{{ f.name }}</span>
728
+ <span class="kh-file-size">{{ humanSize(f.size) }}</span>
729
+ </div>
730
+ </div>
731
+
732
+ <div class="kh-done-facts">
733
+ <div>
734
+ <span class="kh-fact-label">分段模式</span>
735
+ <span class="kh-fact-val">
736
+ {{ chunkMode === 'general' ? '自定义' : '父子分段' }}
737
+ </span>
738
+ </div>
739
+ <div>
740
+ <span class="kh-fact-label">最大分段长度</span>
741
+ <span class="kh-fact-val">
742
+ {{
743
+ chunkMode === 'parent-child'
744
+ ? childMaxTokens
745
+ : chunkMaxTokens
746
+ }}
747
+ </span>
748
+ </div>
749
+ <div>
750
+ <span class="kh-fact-label">索引方式</span>
751
+ <span class="kh-fact-val" style="color: #b45309">
752
+
753
+ {{ indexingTechnique === 'HIGH_QUALITY' ? '高质量' : '经济' }}
754
+ </span>
755
+ </div>
756
+ <div>
757
+ <span class="kh-fact-label">检索设置</span>
758
+ <span class="kh-fact-val" style="color: #4338ca">
759
+ {{
760
+ retrievalMethodDisplay === 'VECTOR'
761
+ ? '◈ 向量检索'
762
+ : retrievalMethodDisplay === 'FULL_TEXT'
763
+ ? '≡ 全文检索'
764
+ : '⚡ 混合检索'
765
+ }}
766
+ </span>
767
+ </div>
768
+ </div>
769
+
770
+ <div class="kh-done-actions">
771
+ <button class="kh-btn kh-btn-primary" @click="close">
772
+ 前往文档 →
773
+ </button>
774
+ </div>
775
+ </div>
776
+ </div>
777
+ </div>
778
+ </div>
779
+ </Teleport>
780
+ </template>
781
+
782
+ <style scoped>
783
+ .kh-wizard-mask {
784
+ position: fixed;
785
+ inset: 0;
786
+ z-index: 1045;
787
+ background: rgba(15, 23, 42, 0.45);
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ padding: 24px;
792
+ }
793
+ .kh-wizard-shell {
794
+ width: 100%;
795
+ max-width: 1200px;
796
+ height: min(920px, calc(100vh - 48px));
797
+ background: #fff;
798
+ border-radius: 14px;
799
+ display: flex;
800
+ flex-direction: column;
801
+ box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
802
+ overflow: hidden;
803
+ animation: kh-wizard-in 0.2s ease-out;
804
+ }
805
+ @keyframes kh-wizard-in {
806
+ from {
807
+ transform: scale(0.98);
808
+ opacity: 0.8;
809
+ }
810
+ to {
811
+ transform: scale(1);
812
+ opacity: 1;
813
+ }
814
+ }
815
+
816
+ .kh-topbar {
817
+ display: grid;
818
+ grid-template-columns: 260px 1fr 60px;
819
+ align-items: center;
820
+ padding: 14px 24px;
821
+ border-bottom: 1px solid #f1f5f9;
822
+ background: #fff;
823
+ }
824
+ .kh-topbar-title {
825
+ font-size: 14px;
826
+ font-weight: 600;
827
+ color: #0f172a;
828
+ overflow: hidden;
829
+ text-overflow: ellipsis;
830
+ white-space: nowrap;
831
+ }
832
+ .kh-close {
833
+ justify-self: end;
834
+ width: 32px;
835
+ height: 32px;
836
+ padding: 0;
837
+ border: none;
838
+ border-radius: 8px;
839
+ background: transparent;
840
+ color: #94a3b8;
841
+ font-size: 20px;
842
+ line-height: 1;
843
+ cursor: pointer;
844
+ }
845
+ .kh-close:hover {
846
+ background: #f1f5f9;
847
+ color: #475569;
848
+ }
849
+ .kh-stepper {
850
+ display: flex;
851
+ align-items: center;
852
+ justify-content: center;
853
+ gap: 6px;
854
+ }
855
+ .kh-step {
856
+ display: inline-flex;
857
+ align-items: center;
858
+ gap: 6px;
859
+ font-size: 13px;
860
+ color: #94a3b8;
861
+ }
862
+ .kh-step-num {
863
+ min-width: 20px;
864
+ text-align: center;
865
+ }
866
+ .kh-step-num-badge {
867
+ display: inline-block;
868
+ padding: 2px 8px;
869
+ border-radius: 10px;
870
+ background: #4f46e5;
871
+ color: #fff;
872
+ font-size: 10px;
873
+ font-weight: 600;
874
+ letter-spacing: 0.05em;
875
+ }
876
+ .kh-step-active {
877
+ color: #4f46e5;
878
+ font-weight: 600;
879
+ }
880
+ .kh-step-done {
881
+ color: #10b981;
882
+ }
883
+ .kh-step-dash {
884
+ display: inline-block;
885
+ width: 24px;
886
+ height: 1px;
887
+ background: #cbd5e1;
888
+ margin: 0 4px;
889
+ }
890
+ .kh-body {
891
+ flex: 1;
892
+ overflow-y: auto;
893
+ padding: 40px 40px 80px;
894
+ }
895
+ .kh-body-center {
896
+ display: flex;
897
+ justify-content: center;
898
+ }
899
+ .kh-body-split {
900
+ display: grid;
901
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
902
+ gap: 32px;
903
+ max-width: 1400px;
904
+ margin: 0 auto;
905
+ /* Let each column own its scroll — preview on the right can scroll without
906
+ dragging the segmentation strategy on the left along with it. */
907
+ padding: 0;
908
+ overflow: hidden;
909
+ }
910
+ .kh-split-left {
911
+ min-height: 0;
912
+ overflow-y: auto;
913
+ padding: 40px 12px 80px 40px;
914
+ }
915
+ .kh-content-narrow {
916
+ width: 660px;
917
+ max-width: 100%;
918
+ }
919
+
920
+ .kh-section-label {
921
+ margin-bottom: 10px;
922
+ font-size: 13px;
923
+ font-weight: 600;
924
+ color: #334155;
925
+ }
926
+
927
+ .kh-source-row {
928
+ display: grid;
929
+ grid-template-columns: repeat(3, minmax(0, 1fr));
930
+ gap: 12px;
931
+ }
932
+ .kh-source {
933
+ display: flex;
934
+ align-items: center;
935
+ gap: 10px;
936
+ padding: 14px 16px;
937
+ background: #fff;
938
+ border: 1px solid #e2e8f0;
939
+ border-radius: 10px;
940
+ text-align: left;
941
+ cursor: pointer;
942
+ transition:
943
+ border-color 0.15s,
944
+ background 0.15s;
945
+ }
946
+ .kh-source:hover:not(.kh-source-disabled) {
947
+ border-color: #cbd5e1;
948
+ }
949
+ .kh-source-active {
950
+ border-color: transparent !important;
951
+ outline: 1.5px solid #6366f1;
952
+ background: #eef2ff;
953
+ }
954
+ .kh-source-disabled {
955
+ opacity: 0.55;
956
+ cursor: not-allowed;
957
+ }
958
+ .kh-source-icon {
959
+ width: 32px;
960
+ height: 32px;
961
+ display: flex;
962
+ align-items: center;
963
+ justify-content: center;
964
+ border-radius: 6px;
965
+ font-size: 16px;
966
+ font-weight: 600;
967
+ color: #4338ca;
968
+ }
969
+ .kh-source-label {
970
+ font-size: 14px;
971
+ color: #0f172a;
972
+ }
973
+
974
+ .kh-dropzone {
975
+ padding: 20px 24px;
976
+ border: 1px dashed #cbd5e1;
977
+ border-radius: 10px;
978
+ background: #f8fafc;
979
+ cursor: pointer;
980
+ transition:
981
+ background 0.15s,
982
+ border-color 0.15s;
983
+ }
984
+ .kh-dropzone:hover,
985
+ .kh-dropzone-over {
986
+ border-color: #6366f1;
987
+ background: #eef2ff;
988
+ }
989
+ .kh-dz-row {
990
+ display: flex;
991
+ justify-content: center;
992
+ align-items: center;
993
+ gap: 8px;
994
+ font-size: 13px;
995
+ color: #475569;
996
+ }
997
+ .kh-dz-icon {
998
+ font-size: 18px;
999
+ color: #94a3b8;
1000
+ }
1001
+ .kh-dz-link {
1002
+ color: #4338ca;
1003
+ font-weight: 500;
1004
+ }
1005
+ .kh-dz-hint {
1006
+ margin-top: 8px;
1007
+ font-size: 11px;
1008
+ color: #94a3b8;
1009
+ line-height: 1.6;
1010
+ }
1011
+ .kh-hidden-input {
1012
+ display: none;
1013
+ }
1014
+ .kh-file-list {
1015
+ margin-top: 10px;
1016
+ display: flex;
1017
+ flex-direction: column;
1018
+ gap: 6px;
1019
+ }
1020
+ .kh-file-row {
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 8px;
1024
+ padding: 6px 10px;
1025
+ background: #fff;
1026
+ border: 1px solid #e2e8f0;
1027
+ border-radius: 6px;
1028
+ font-size: 12px;
1029
+ color: #334155;
1030
+ }
1031
+ .kh-file-icon {
1032
+ color: #4338ca;
1033
+ }
1034
+ .kh-file-name {
1035
+ flex: 1;
1036
+ overflow: hidden;
1037
+ text-overflow: ellipsis;
1038
+ white-space: nowrap;
1039
+ }
1040
+ .kh-file-size {
1041
+ color: #94a3b8;
1042
+ }
1043
+ .kh-file-x {
1044
+ padding: 0 6px;
1045
+ border: none;
1046
+ background: transparent;
1047
+ color: #94a3b8;
1048
+ font-size: 16px;
1049
+ cursor: pointer;
1050
+ }
1051
+
1052
+ .kh-step1-actions {
1053
+ margin-top: 22px;
1054
+ display: flex;
1055
+ justify-content: flex-end;
1056
+ }
1057
+
1058
+ .kh-btn {
1059
+ padding: 8px 18px;
1060
+ border: none;
1061
+ border-radius: 8px;
1062
+ font-size: 13px;
1063
+ font-weight: 500;
1064
+ cursor: pointer;
1065
+ }
1066
+ .kh-btn-primary {
1067
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
1068
+ color: #fff;
1069
+ box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3);
1070
+ }
1071
+ .kh-btn-primary:disabled {
1072
+ background: #cbd5e1;
1073
+ box-shadow: none;
1074
+ cursor: not-allowed;
1075
+ }
1076
+ .kh-btn-secondary {
1077
+ background: #f1f5f9;
1078
+ color: #475569;
1079
+ }
1080
+ .kh-btn-secondary:hover {
1081
+ background: #e2e8f0;
1082
+ }
1083
+ .kh-btn-mini {
1084
+ padding: 4px 10px;
1085
+ font-size: 12px;
1086
+ border: 1px solid #e2e8f0;
1087
+ border-radius: 6px;
1088
+ background: #fff;
1089
+ color: #475569;
1090
+ cursor: pointer;
1091
+ }
1092
+ .kh-btn-mini:hover {
1093
+ background: #f8fafc;
1094
+ }
1095
+ .kh-btn-mini-primary {
1096
+ color: #4338ca;
1097
+ border-color: #c7d2fe;
1098
+ background: #eef2ff;
1099
+ }
1100
+
1101
+ .kh-panel {
1102
+ padding: 14px 16px;
1103
+ margin-bottom: 12px;
1104
+ background: #fff;
1105
+ border: 1px solid #e2e8f0;
1106
+ border-radius: 10px;
1107
+ cursor: pointer;
1108
+ transition:
1109
+ border-color 0.15s,
1110
+ background 0.15s;
1111
+ }
1112
+ .kh-panel:hover {
1113
+ border-color: #cbd5e1;
1114
+ }
1115
+ .kh-panel-active {
1116
+ border-color: transparent;
1117
+ outline: 1.5px solid #6366f1;
1118
+ background: #f5f6ff;
1119
+ }
1120
+ .kh-panel-header {
1121
+ display: flex;
1122
+ align-items: center;
1123
+ justify-content: space-between;
1124
+ gap: 8px;
1125
+ }
1126
+ .kh-panel-title {
1127
+ display: inline-flex;
1128
+ align-items: center;
1129
+ gap: 8px;
1130
+ font-size: 14px;
1131
+ font-weight: 600;
1132
+ color: #0f172a;
1133
+ }
1134
+ .kh-panel-hint {
1135
+ font-size: 11px;
1136
+ color: #94a3b8;
1137
+ }
1138
+ .kh-panel-radio {
1139
+ width: 14px;
1140
+ height: 14px;
1141
+ border-radius: 50%;
1142
+ border: 1.5px solid #cbd5e1;
1143
+ background: #fff;
1144
+ display: inline-block;
1145
+ }
1146
+ .kh-panel-radio-on {
1147
+ border-color: #6366f1;
1148
+ background: radial-gradient(circle, #6366f1 30%, transparent 32%) center /
1149
+ cover;
1150
+ }
1151
+ .kh-panel-body {
1152
+ margin-top: 12px;
1153
+ padding-top: 12px;
1154
+ border-top: 1px dashed #e2e8f0;
1155
+ }
1156
+
1157
+ .kh-grid-3 {
1158
+ display: grid;
1159
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1160
+ gap: 10px;
1161
+ }
1162
+ .kh-grid-2 {
1163
+ display: grid;
1164
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1165
+ gap: 10px;
1166
+ }
1167
+ .kh-field {
1168
+ display: flex;
1169
+ flex-direction: column;
1170
+ gap: 4px;
1171
+ }
1172
+ .kh-field label {
1173
+ font-size: 11px;
1174
+ color: #64748b;
1175
+ }
1176
+ .kh-input {
1177
+ padding: 7px 10px;
1178
+ font-size: 13px;
1179
+ border: 1px solid #d1d5db;
1180
+ border-radius: 6px;
1181
+ outline: none;
1182
+ background: #fff;
1183
+ color: #0f172a;
1184
+ width: 100%;
1185
+ }
1186
+ .kh-input:focus {
1187
+ border-color: #6366f1;
1188
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
1189
+ }
1190
+ .kh-input-suffix {
1191
+ position: relative;
1192
+ }
1193
+ .kh-input-suffix .kh-input {
1194
+ padding-right: 74px;
1195
+ }
1196
+ .kh-suffix {
1197
+ position: absolute;
1198
+ right: 10px;
1199
+ top: 50%;
1200
+ transform: translateY(-50%);
1201
+ font-size: 11px;
1202
+ color: #94a3b8;
1203
+ }
1204
+
1205
+ .kh-sub-label {
1206
+ margin-top: 12px;
1207
+ margin-bottom: 6px;
1208
+ font-size: 12px;
1209
+ color: #64748b;
1210
+ }
1211
+ .kh-check {
1212
+ display: inline-flex;
1213
+ align-items: center;
1214
+ gap: 6px;
1215
+ margin-right: 12px;
1216
+ font-size: 12px;
1217
+ color: #475569;
1218
+ cursor: pointer;
1219
+ }
1220
+ .kh-check input {
1221
+ accent-color: #6366f1;
1222
+ }
1223
+ .kh-inline-actions {
1224
+ display: flex;
1225
+ gap: 6px;
1226
+ margin-top: 12px;
1227
+ }
1228
+
1229
+ .kh-hint {
1230
+ font-size: 11px;
1231
+ color: #94a3b8;
1232
+ }
1233
+ .kh-hint-block {
1234
+ padding: 10px 12px;
1235
+ border-radius: 8px;
1236
+ background: #eef2ff;
1237
+ color: #4338ca;
1238
+ margin-bottom: 14px;
1239
+ font-size: 12px;
1240
+ line-height: 1.5;
1241
+ }
1242
+
1243
+ .kh-readonly-card {
1244
+ display: flex;
1245
+ align-items: flex-start;
1246
+ gap: 10px;
1247
+ padding: 12px 14px;
1248
+ border: 1px solid #e2e8f0;
1249
+ border-radius: 10px;
1250
+ background: #f8fafc;
1251
+ margin-bottom: 8px;
1252
+ }
1253
+ .kh-readonly-icon {
1254
+ font-size: 20px;
1255
+ }
1256
+ .kh-readonly-title {
1257
+ font-size: 13px;
1258
+ font-weight: 600;
1259
+ color: #0f172a;
1260
+ }
1261
+ .kh-readonly-desc {
1262
+ margin-top: 4px;
1263
+ font-size: 11px;
1264
+ color: #64748b;
1265
+ line-height: 1.5;
1266
+ }
1267
+
1268
+ .kh-parent-mode-row {
1269
+ display: grid;
1270
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1271
+ gap: 10px;
1272
+ }
1273
+ .kh-parent-mode-card {
1274
+ padding: 10px 12px;
1275
+ background: #fff;
1276
+ border: 1px solid #e2e8f0;
1277
+ border-radius: 8px;
1278
+ text-align: left;
1279
+ cursor: pointer;
1280
+ transition:
1281
+ border-color 0.15s,
1282
+ background 0.15s;
1283
+ }
1284
+ .kh-parent-mode-card:hover {
1285
+ border-color: #cbd5e1;
1286
+ }
1287
+ .kh-parent-mode-active {
1288
+ border-color: transparent;
1289
+ outline: 1.5px solid #6366f1;
1290
+ background: #f5f6ff;
1291
+ }
1292
+ .kh-pm-head {
1293
+ display: flex;
1294
+ align-items: center;
1295
+ gap: 8px;
1296
+ font-size: 13px;
1297
+ }
1298
+ .kh-pm-title {
1299
+ font-weight: 600;
1300
+ color: #0f172a;
1301
+ }
1302
+ .kh-pm-desc {
1303
+ margin-top: 6px;
1304
+ font-size: 11px;
1305
+ color: #64748b;
1306
+ line-height: 1.5;
1307
+ }
1308
+
1309
+ .kh-step-actions {
1310
+ margin-top: 20px;
1311
+ display: flex;
1312
+ justify-content: space-between;
1313
+ }
1314
+
1315
+ .kh-split-right {
1316
+ min-height: 0;
1317
+ overflow-y: auto;
1318
+ padding: 40px 40px 80px 24px;
1319
+ border-left: 1px solid #f1f5f9;
1320
+ }
1321
+ .kh-preview-title {
1322
+ display: flex;
1323
+ justify-content: space-between;
1324
+ align-items: center;
1325
+ margin-bottom: 10px;
1326
+ font-size: 13px;
1327
+ font-weight: 600;
1328
+ color: #334155;
1329
+ }
1330
+ .kh-preview-file {
1331
+ font-size: 11px;
1332
+ font-weight: 400;
1333
+ color: #94a3b8;
1334
+ }
1335
+ .kh-preview-empty {
1336
+ padding: 100px 20px;
1337
+ text-align: center;
1338
+ border: 1px dashed #e2e8f0;
1339
+ border-radius: 10px;
1340
+ background: #f8fafc;
1341
+ }
1342
+ .kh-preview-empty-icon {
1343
+ font-size: 34px;
1344
+ color: #cbd5e1;
1345
+ }
1346
+ .kh-preview-empty-text {
1347
+ margin-top: 12px;
1348
+ font-size: 12px;
1349
+ color: #94a3b8;
1350
+ }
1351
+ .kh-preview-body {
1352
+ display: flex;
1353
+ flex-direction: column;
1354
+ gap: 8px;
1355
+ }
1356
+ .kh-preview-warning {
1357
+ padding: 8px 10px;
1358
+ font-size: 12px;
1359
+ color: #92400e;
1360
+ background: #fef3c7;
1361
+ border: 1px solid #fde68a;
1362
+ border-radius: 6px;
1363
+ line-height: 1.5;
1364
+ }
1365
+ .kh-preview-chunk {
1366
+ padding: 10px 12px;
1367
+ border: 1px solid #e2e8f0;
1368
+ border-radius: 8px;
1369
+ background: #fff;
1370
+ }
1371
+ .kh-preview-chunk-head {
1372
+ display: flex;
1373
+ justify-content: space-between;
1374
+ align-items: center;
1375
+ font-size: 10px;
1376
+ margin-bottom: 6px;
1377
+ }
1378
+ .kh-chunk-tag {
1379
+ padding: 2px 6px;
1380
+ background: #eef2ff;
1381
+ color: #4338ca;
1382
+ border-radius: 4px;
1383
+ font-weight: 600;
1384
+ }
1385
+ .kh-chunk-tokens {
1386
+ color: #94a3b8;
1387
+ }
1388
+ .kh-preview-chunk-body {
1389
+ font-size: 12px;
1390
+ color: #334155;
1391
+ line-height: 1.6;
1392
+ }
1393
+
1394
+ .kh-done-header {
1395
+ display: flex;
1396
+ align-items: center;
1397
+ gap: 12px;
1398
+ margin-bottom: 22px;
1399
+ }
1400
+ .kh-done-emoji {
1401
+ font-size: 34px;
1402
+ }
1403
+ .kh-done-title {
1404
+ font-size: 18px;
1405
+ font-weight: 600;
1406
+ color: #0f172a;
1407
+ }
1408
+ .kh-done-sub {
1409
+ margin-top: 4px;
1410
+ font-size: 12px;
1411
+ color: #94a3b8;
1412
+ }
1413
+ .kh-done-files {
1414
+ display: flex;
1415
+ flex-direction: column;
1416
+ gap: 6px;
1417
+ margin-bottom: 22px;
1418
+ }
1419
+ .kh-done-file-row {
1420
+ display: flex;
1421
+ align-items: center;
1422
+ gap: 8px;
1423
+ padding: 8px 12px;
1424
+ background: #f8fafc;
1425
+ border: 1px solid #e2e8f0;
1426
+ border-radius: 8px;
1427
+ font-size: 12px;
1428
+ color: #334155;
1429
+ }
1430
+ .kh-done-facts {
1431
+ display: grid;
1432
+ grid-template-columns: 1fr;
1433
+ gap: 6px;
1434
+ margin-bottom: 26px;
1435
+ }
1436
+ .kh-done-facts > div {
1437
+ display: grid;
1438
+ grid-template-columns: 120px 1fr;
1439
+ font-size: 12px;
1440
+ padding: 4px 0;
1441
+ }
1442
+ .kh-fact-label {
1443
+ color: #94a3b8;
1444
+ }
1445
+ .kh-fact-val {
1446
+ color: #334155;
1447
+ }
1448
+ .kh-done-actions {
1449
+ display: flex;
1450
+ gap: 10px;
1451
+ }
1452
+ </style>