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,888 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * DatasetDetailDrawer — self-contained full-screen drawer that hosts the whole
4
+ * dataset-detail experience without navigating away.
5
+ *
6
+ * Sliding in from the LEFT (occupies ~88vw so the workspace stays roomy), it
7
+ * contains:
8
+ * - DatasetSidebar on the left (4 nav items switch content in-place)
9
+ * - Right pane switches between DocumentTable / stub / RecallTestingPanelV2
10
+ * / DatasetSettingsPanel based on the internal `tab` state
11
+ * - When a document row is clicked in DocumentTable, right pane in-place
12
+ * replaces with DocumentChunksView plus a "back" button — still inside the
13
+ * drawer, no route change
14
+ *
15
+ * All API access is delegated to the parent via props (`hub` object with
16
+ * async methods). This keeps the component embeddable in any Vue 3 app.
17
+ */
18
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
19
+
20
+ import type {
21
+ Chunk,
22
+ DatasetDetailHub,
23
+ DatasetSummary,
24
+ DatasetTab,
25
+ DocMetadata,
26
+ DocumentRow,
27
+ EmbeddingModelOption,
28
+ ProcessRule,
29
+ RecallHit,
30
+ RecentQuery,
31
+ } from '../types';
32
+ import type { RetrievalMethod } from '../types/retrieval';
33
+ import AddDocumentsWizard from './AddDocumentsWizard.vue';
34
+ import DatasetSettingsPanel from './DatasetSettingsPanel.vue';
35
+ import DatasetSidebar from './DatasetSidebar.vue';
36
+ import DocumentChunksView from './DocumentChunksView.vue';
37
+ import DocumentTable from './DocumentTable.vue';
38
+ import RecallTestingPanelV2 from './RecallTestingPanelV2.vue';
39
+
40
+ interface Props {
41
+ open: boolean;
42
+ /** The dataset id to open. Watched — changing it reloads. */
43
+ datasetId: null | string;
44
+ hub: DatasetDetailHub;
45
+ /** Optional starting tab. Defaults to 'documents'. Re-applied every open. */
46
+ initialTab?: DatasetTab;
47
+ }
48
+
49
+ const props = defineProps<Props>();
50
+
51
+ const emit = defineEmits<{
52
+ (e: 'update:open', v: boolean): void;
53
+ (e: 'deleted', id: string): void;
54
+ (e: 'refresh-list'): void;
55
+ }>();
56
+
57
+ // ---------- state
58
+ const dataset = ref<DatasetSummary | null>(null);
59
+ const documents = ref<DocumentRow[]>([]);
60
+ const documentsLoading = ref(false);
61
+
62
+ const tab = ref<DatasetTab>('documents');
63
+ const openDocId = ref<null | string>(null);
64
+ const segments = ref<Chunk[]>([]);
65
+ const metadata = ref<DocMetadata>({});
66
+ const segmentsLoading = ref(false);
67
+ const chunkPage = ref(1);
68
+ const chunkPageSize = ref(20);
69
+
70
+ const hits = ref<RecallHit[]>([]);
71
+ const recallHistory = ref<RecentQuery[]>([]);
72
+ const recallLoading = ref(false);
73
+
74
+ const embeddingModels = ref<EmbeddingModelOption[]>([]);
75
+ const defaultEmbeddingModelId = ref<null | string>(null);
76
+
77
+ // Segment editor modal state (owned by the drawer since the chunks view emits
78
+ // an event). `editorMode = 'add'` opens the same modal for creating a new
79
+ // segment via `hub.appendSegment`.
80
+ const editingSeg = ref<Chunk | null>(null);
81
+ const editContent = ref('');
82
+ const editorMode = ref<'add' | 'edit' | null>(null);
83
+
84
+ // ---------- lifecycle: reload on open / id change
85
+ watch(
86
+ () => [props.open, props.datasetId] as const,
87
+ async ([isOpen, id]) => {
88
+ if (!isOpen || !id) {
89
+ dataset.value = null;
90
+ documents.value = [];
91
+ tab.value = 'documents';
92
+ openDocId.value = null;
93
+ return;
94
+ }
95
+ tab.value = props.initialTab ?? 'documents';
96
+ await Promise.all([
97
+ loadDataset(),
98
+ loadDocuments(),
99
+ loadEmbeddingModels(),
100
+ loadRerankModels(),
101
+ loadRecallHistory(),
102
+ ]);
103
+ },
104
+ { immediate: true },
105
+ );
106
+
107
+ async function loadDataset() {
108
+ if (!props.datasetId) return;
109
+ try {
110
+ dataset.value = await props.hub.loadDataset(props.datasetId);
111
+ } catch {
112
+ dataset.value = null;
113
+ }
114
+ }
115
+ async function loadDocuments() {
116
+ if (!props.datasetId) return;
117
+ documentsLoading.value = true;
118
+ try {
119
+ documents.value = await props.hub.listDocuments(props.datasetId);
120
+ } finally {
121
+ documentsLoading.value = false;
122
+ }
123
+ scheduleStatusPoll();
124
+ }
125
+
126
+ /**
127
+ * When at least one document is in PROCESSING state, poll listDocuments
128
+ * every 3s until they all settle. This is how the async ingest pipeline
129
+ * surfaces "处理中 → 已完成" to the user without an SSE stream (SSE is a
130
+ * nice next step but polling covers the demand for now).
131
+ */
132
+ let statusPollTimer: ReturnType<typeof setTimeout> | null = null;
133
+ function scheduleStatusPoll() {
134
+ if (statusPollTimer) {
135
+ clearTimeout(statusPollTimer);
136
+ statusPollTimer = null;
137
+ }
138
+ const anyPending = documents.value.some((d) => d.status === 'PROCESSING');
139
+ if (!anyPending) return;
140
+ statusPollTimer = setTimeout(() => {
141
+ loadDocuments();
142
+ }, 3000);
143
+ }
144
+ onBeforeUnmount(() => {
145
+ if (statusPollTimer) clearTimeout(statusPollTimer);
146
+ });
147
+ async function loadEmbeddingModels() {
148
+ try {
149
+ embeddingModels.value = await props.hub.listEmbeddingModels();
150
+ } catch {
151
+ embeddingModels.value = [];
152
+ }
153
+ // Best-effort default id — used by DatasetSettingsPanel to auto-select the
154
+ // tenant default when the dataset has no embedding model set. Prefer the
155
+ // dedicated endpoint if the adapter exposes it; otherwise derive from the
156
+ // `isDefault` flag we already annotated on each row.
157
+ try {
158
+ if (props.hub.getDefaultEmbeddingModelId) {
159
+ defaultEmbeddingModelId.value =
160
+ (await props.hub.getDefaultEmbeddingModelId()) ?? null;
161
+ } else {
162
+ defaultEmbeddingModelId.value =
163
+ embeddingModels.value.find((m) => m.isDefault)?.id ?? null;
164
+ }
165
+ } catch {
166
+ defaultEmbeddingModelId.value =
167
+ embeddingModels.value.find((m) => m.isDefault)?.id ?? null;
168
+ }
169
+ }
170
+ async function loadRecallHistory() {
171
+ if (!props.datasetId) return;
172
+ try {
173
+ recallHistory.value = await props.hub.listRecallHistory(props.datasetId, 20);
174
+ } catch {
175
+ recallHistory.value = [];
176
+ }
177
+ }
178
+
179
+ // ---------- sidebar
180
+ function onNav(next: DatasetTab) {
181
+ // Reset the doc-drilldown when nav switches away from documents.
182
+ if (next !== 'documents') openDocId.value = null;
183
+ tab.value = next;
184
+ }
185
+ function onCopyApi() {
186
+ if (props.datasetId && props.hub.onCopyApi) props.hub.onCopyApi(props.datasetId);
187
+ }
188
+ function close() {
189
+ emit('update:open', false);
190
+ }
191
+
192
+ // ---------- documents tab — "+ 添加文件" opens the 3-step wizard rather than
193
+ // popping the native file picker straight away, mirroring the create-flow.
194
+ const addWizardOpen = ref(false);
195
+ function pickFiles() {
196
+ addWizardOpen.value = true;
197
+ }
198
+ async function onAddWizardConfirm(payload: {
199
+ files: File[];
200
+ processRule?: ProcessRule;
201
+ }) {
202
+ if (!props.datasetId || payload.files.length === 0) return;
203
+ try {
204
+ // If the user tweaked chunking on step 2 of the add wizard, persist those
205
+ // edits onto the dataset first so the upload actually indexes with the
206
+ // new rule. Skipped when the payload matches the current persisted rule
207
+ // (nothing to update) so a bare "just upload more docs" click stays a
208
+ // single request.
209
+ if (payload.processRule && shouldPatchProcessRule(payload.processRule)) {
210
+ await props.hub.updateDataset(props.datasetId, {
211
+ processRule: payload.processRule,
212
+ });
213
+ await loadDataset();
214
+ }
215
+ await props.hub.uploadDocuments(props.datasetId, payload.files);
216
+ await loadDocuments();
217
+ emit('refresh-list');
218
+ } catch {
219
+ // errors surface via the host's onError hook — nothing to do here
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Compare the wizard-emitted ProcessRule against the currently-persisted one
225
+ * so the upload flow only PUTs when the user actually changed something. Deep
226
+ * equality by JSON.stringify is fine for this shape (small, primitive-only).
227
+ */
228
+ function shouldPatchProcessRule(next: ProcessRule): boolean {
229
+ const current = parsedProcessRule.value ?? {};
230
+ return JSON.stringify(next) !== JSON.stringify(current);
231
+ }
232
+
233
+ // Parsed once from the dataset so the wizard can display current chunking /
234
+ // retrieval defaults on step 2 without re-parsing on every render.
235
+ const parsedProcessRule = computed<ProcessRule>(() => {
236
+ try {
237
+ return dataset.value?.processRuleJson
238
+ ? (JSON.parse(dataset.value.processRuleJson) as ProcessRule)
239
+ : {};
240
+ } catch {
241
+ return {};
242
+ }
243
+ });
244
+ const parsedRetrievalConfig = computed(() => {
245
+ try {
246
+ if (dataset.value?.retrievalConfigJson) {
247
+ return JSON.parse(dataset.value.retrievalConfigJson) as {
248
+ method?: RetrievalMethod;
249
+ topK?: number;
250
+ scoreThreshold?: number;
251
+ rerankEnabled?: boolean;
252
+ rerankModelId?: string;
253
+ vectorWeight?: number;
254
+ };
255
+ }
256
+ } catch {
257
+ // fall through to default
258
+ }
259
+ return { method: 'HYBRID' as RetrievalMethod, topK: 3 };
260
+ });
261
+ const parsedRetrievalMethod = computed<RetrievalMethod>(
262
+ () => parsedRetrievalConfig.value.method ?? 'HYBRID',
263
+ );
264
+ // Rerank models come from the hub if it exposes listRerankModels; the recall
265
+ // panel gracefully renders an empty dropdown otherwise.
266
+ const rerankModels = ref<Array<{ id: string; label: string }>>([]);
267
+ async function loadRerankModels() {
268
+ if (!props.hub.listRerankModels) return;
269
+ try {
270
+ rerankModels.value = await props.hub.listRerankModels();
271
+ } catch {
272
+ rerankModels.value = [];
273
+ }
274
+ }
275
+ async function onOpenDoc(d: DocumentRow) {
276
+ if (!props.datasetId) return;
277
+ openDocId.value = d.id;
278
+ chunkPage.value = 1;
279
+ segmentsLoading.value = true;
280
+ try {
281
+ const [segs, meta] = await Promise.all([
282
+ props.hub.listSegments(
283
+ props.datasetId,
284
+ d.id,
285
+ chunkPage.value,
286
+ chunkPageSize.value,
287
+ ),
288
+ props.hub.loadDocumentMetadata(props.datasetId, d.id),
289
+ ]);
290
+ segments.value = segs;
291
+ metadata.value = meta;
292
+ } finally {
293
+ segmentsLoading.value = false;
294
+ }
295
+ }
296
+ async function loadSegmentPage() {
297
+ if (!props.datasetId || !openDocId.value) return;
298
+ segmentsLoading.value = true;
299
+ try {
300
+ segments.value = await props.hub.listSegments(
301
+ props.datasetId,
302
+ openDocId.value,
303
+ chunkPage.value,
304
+ chunkPageSize.value,
305
+ );
306
+ } finally {
307
+ segmentsLoading.value = false;
308
+ }
309
+ }
310
+ function onChunkPageChange(p: number) {
311
+ chunkPage.value = p;
312
+ loadSegmentPage();
313
+ }
314
+ function onChunkPageSizeChange(s: number) {
315
+ chunkPageSize.value = s;
316
+ chunkPage.value = 1;
317
+ loadSegmentPage();
318
+ }
319
+ async function onDeleteDoc(d: DocumentRow) {
320
+ if (!props.datasetId) return;
321
+ await props.hub.deleteDocument(props.datasetId, d.id);
322
+ await loadDocuments();
323
+ emit('refresh-list');
324
+ }
325
+ async function onToggleDocEnabled(d: DocumentRow, next: boolean) {
326
+ if (!props.datasetId || !props.hub.setDocumentEnabled) return;
327
+ await props.hub.setDocumentEnabled(props.datasetId, d.id, next);
328
+ await loadDocuments();
329
+ }
330
+
331
+ // ---------- chunks view (opened when openDocId is set)
332
+ const currentDoc = computed(() =>
333
+ documents.value.find((d) => d.id === openDocId.value) ?? null,
334
+ );
335
+ function backToDocs() {
336
+ openDocId.value = null;
337
+ segments.value = [];
338
+ metadata.value = {};
339
+ chunkPage.value = 1;
340
+ }
341
+ async function onToggleChunk(c: Chunk, next: boolean) {
342
+ if (!props.datasetId || !openDocId.value) return;
343
+ const updated = await props.hub.setSegmentEnabled(
344
+ props.datasetId,
345
+ openDocId.value,
346
+ c.id,
347
+ next,
348
+ );
349
+ const idx = segments.value.findIndex((s) => s.id === c.id);
350
+ if (idx !== -1) segments.value[idx] = { ...segments.value[idx], enabled: !!updated.enabled };
351
+ }
352
+ async function onDeleteChunk(c: Chunk) {
353
+ if (!props.datasetId || !openDocId.value) return;
354
+ await props.hub.deleteSegment(props.datasetId, openDocId.value, c.id);
355
+ metadata.value = await props.hub.loadDocumentMetadata(
356
+ props.datasetId,
357
+ openDocId.value,
358
+ );
359
+ await loadSegmentPage();
360
+ }
361
+ function onEditChunk(c: Chunk) {
362
+ editorMode.value = 'edit';
363
+ editingSeg.value = c;
364
+ editContent.value = c.content;
365
+ }
366
+ function onAddChunk() {
367
+ editorMode.value = 'add';
368
+ editingSeg.value = null;
369
+ editContent.value = '';
370
+ }
371
+ async function saveChunkEdit() {
372
+ if (!props.datasetId || !openDocId.value) return;
373
+ const content = editContent.value.trim();
374
+ if (!content) return;
375
+
376
+ if (editorMode.value === 'add') {
377
+ if (!props.hub.appendSegment) return;
378
+ await props.hub.appendSegment(props.datasetId, openDocId.value, content);
379
+ // Refresh both the current page and the metadata so total + avg update.
380
+ metadata.value = await props.hub.loadDocumentMetadata(
381
+ props.datasetId,
382
+ openDocId.value,
383
+ );
384
+ await loadSegmentPage();
385
+ } else if (editorMode.value === 'edit' && editingSeg.value) {
386
+ const updated = await props.hub.updateSegment(
387
+ props.datasetId,
388
+ openDocId.value,
389
+ editingSeg.value.id,
390
+ content,
391
+ );
392
+ const idx = segments.value.findIndex((s) => s.id === editingSeg.value!.id);
393
+ if (idx !== -1) {
394
+ segments.value[idx] = {
395
+ ...segments.value[idx],
396
+ content: updated.content,
397
+ charCount: updated.content?.length ?? 0,
398
+ tokenCount: updated.tokenCount,
399
+ };
400
+ }
401
+ }
402
+ cancelChunkEdit();
403
+ }
404
+ function cancelChunkEdit() {
405
+ editingSeg.value = null;
406
+ editContent.value = '';
407
+ editorMode.value = null;
408
+ }
409
+
410
+ // ---------- recall
411
+ async function onRunRecall(payload: {
412
+ method: string;
413
+ query: string;
414
+ config?: {
415
+ topK?: number;
416
+ scoreThreshold?: number;
417
+ rerankEnabled?: boolean;
418
+ rerankModelId?: string;
419
+ vectorWeight?: number;
420
+ };
421
+ }) {
422
+ if (!props.datasetId) return;
423
+ recallLoading.value = true;
424
+ try {
425
+ // Forward the popover's full config so topK / score / rerank land on the
426
+ // backend request (previously fixed at topK=10, ignoring the picker).
427
+ hits.value = await props.hub.retrieve(props.datasetId, {
428
+ query: payload.query,
429
+ method: payload.method,
430
+ topK: payload.config?.topK ?? 10,
431
+ scoreThreshold: payload.config?.scoreThreshold,
432
+ rerankEnabled: payload.config?.rerankEnabled,
433
+ rerankModelId: payload.config?.rerankModelId,
434
+ vectorWeight: payload.config?.vectorWeight,
435
+ } as any);
436
+ await loadRecallHistory();
437
+ } finally {
438
+ recallLoading.value = false;
439
+ }
440
+ }
441
+
442
+ // ---------- settings form (adapts dataset to DatasetSettingsPanel shape)
443
+ //
444
+ // Reads BOTH indexingTechnique / retrievalConfig AND the chunking mode from
445
+ // the persisted processRule so the panel opens showing the actual current
446
+ // setup (previously chunkMode was hard-coded to 'general', hiding whatever
447
+ // the user had configured at create time).
448
+ const settingsForm = computed(() => {
449
+ if (!dataset.value) return { id: '', name: '', chunkMode: 'general' as const };
450
+ let retrievalConfig: any = {};
451
+ try {
452
+ if (dataset.value.retrievalConfigJson) {
453
+ retrievalConfig = JSON.parse(dataset.value.retrievalConfigJson);
454
+ }
455
+ } catch {
456
+ retrievalConfig = {};
457
+ }
458
+ let processRule: ProcessRule = {};
459
+ try {
460
+ if (dataset.value.processRuleJson) {
461
+ processRule = JSON.parse(dataset.value.processRuleJson);
462
+ }
463
+ } catch {
464
+ processRule = {};
465
+ }
466
+ return {
467
+ id: dataset.value.id,
468
+ name: dataset.value.name,
469
+ description: dataset.value.description ?? '',
470
+ icon: '📙',
471
+ iconBg: '#FFEAD5',
472
+ permission: 'ONLY_ME' as const,
473
+ chunkMode:
474
+ processRule.template === 'PARENT_CHILD'
475
+ ? ('parent-child' as const)
476
+ : processRule.template === 'QA'
477
+ ? ('qa' as const)
478
+ : ('general' as const),
479
+ // Expose chunk params so the panel's per-mode inputs pre-fill instead of
480
+ // showing wizard defaults every time.
481
+ chunkTokens: processRule.chunkTokens,
482
+ overlapTokens: processRule.overlapTokens,
483
+ parentMode: processRule.parentMode,
484
+ parentChunkTokens: processRule.parentChunkTokens,
485
+ removeExtraWhitespace: processRule.removeExtraWhitespace,
486
+ removeUrlsEmails: processRule.removeUrlsEmails,
487
+ indexingTechnique: dataset.value.indexingTechnique ?? 'HIGH_QUALITY',
488
+ keywordCount: 10,
489
+ embeddingModelId: dataset.value.embeddingModelId,
490
+ autoSummary: false,
491
+ retrievalMethod: (retrievalConfig.method ?? 'VECTOR') as
492
+ | 'FULL_TEXT'
493
+ | 'HYBRID'
494
+ | 'VECTOR',
495
+ rerankEnabled: !!retrievalConfig.rerankEnabled,
496
+ };
497
+ });
498
+
499
+ async function onSaveSettings(payload: any) {
500
+ if (!props.datasetId) return;
501
+ // Rebuild a full ProcessRule from the panel's chunk-mode + per-mode inputs
502
+ // so switching mode in settings actually persists to the backend. Backends
503
+ // that don't understand a given field just ignore it (schema is additive).
504
+ const template =
505
+ payload.chunkMode === 'parent-child'
506
+ ? 'PARENT_CHILD'
507
+ : payload.chunkMode === 'qa'
508
+ ? 'QA'
509
+ : 'NAIVE';
510
+ const processRule: ProcessRule = {
511
+ template,
512
+ chunkTokens: payload.chunkTokens,
513
+ overlapTokens: payload.overlapTokens,
514
+ parentMode: payload.parentMode,
515
+ parentChunkTokens: payload.parentChunkTokens,
516
+ removeExtraWhitespace: payload.removeExtraWhitespace,
517
+ removeUrlsEmails: payload.removeUrlsEmails,
518
+ };
519
+ await props.hub.updateDataset(props.datasetId, {
520
+ name: payload.name,
521
+ description: payload.description,
522
+ embeddingModelId: payload.embeddingModelId,
523
+ indexingTechnique: payload.indexingTechnique,
524
+ processRule,
525
+ retrievalConfig: {
526
+ method: payload.retrievalMethod,
527
+ topK: 10,
528
+ rerankEnabled: !!payload.rerankEnabled,
529
+ },
530
+ });
531
+ await loadDataset();
532
+ emit('refresh-list');
533
+ }
534
+
535
+ // ---------- sidebar data
536
+ const sidebarData = computed(() => ({
537
+ id: props.datasetId ?? '',
538
+ name: dataset.value?.name ?? '加载中...',
539
+ description: dataset.value?.description,
540
+ documentCount: documents.value.length,
541
+ segmentCount: dataset.value?.segmentCount ?? 0,
542
+ }));
543
+ </script>
544
+
545
+ <template>
546
+ <Teleport to="body" :disabled="!open">
547
+ <div v-if="open" class="kh-drawer-mask" @click.self="close">
548
+ <div class="kh-drawer-shell">
549
+ <DatasetSidebar
550
+ :dataset="sidebarData"
551
+ :active="tab"
552
+ @nav="onNav"
553
+ @copy-api="onCopyApi"
554
+ />
555
+
556
+ <div class="kh-drawer-main">
557
+ <!-- Top bar with close button -->
558
+ <div class="kh-drawer-topbar">
559
+ <div class="kh-drawer-topbar-title">
560
+ <template v-if="openDocId && currentDoc">
561
+ <button class="kh-drawer-back" @click="backToDocs">← 返回</button>
562
+ <span class="kh-drawer-doc-name">📄 {{ currentDoc.name }}</span>
563
+ </template>
564
+ <template v-else>
565
+ <span class="kh-drawer-title-name">
566
+ {{ dataset?.name ?? '' }}
567
+ </span>
568
+ <span class="kh-drawer-title-tab">
569
+ · {{ {
570
+ documents: '文档',
571
+ recall: '召回测试',
572
+ settings: '设置',
573
+ }[tab] }}
574
+ </span>
575
+ </template>
576
+ </div>
577
+ <button class="kh-drawer-close" @click="close">×</button>
578
+ </div>
579
+
580
+ <!-- Content -->
581
+ <div class="kh-drawer-content">
582
+ <!-- Chunks view overlays everything else while a document is open -->
583
+ <template v-if="openDocId && currentDoc">
584
+ <DocumentChunksView
585
+ :document-name="currentDoc.name"
586
+ :document-enabled="currentDoc.enabled"
587
+ :chunks="segments"
588
+ :metadata="metadata"
589
+ :loading="segmentsLoading"
590
+ :can-append="!!hub.appendSegment"
591
+ :page="chunkPage"
592
+ :page-size="chunkPageSize"
593
+ :total="metadata.totalChunks ?? 0"
594
+ @toggle-doc-enabled="(v: boolean) => onToggleDocEnabled(currentDoc, v)"
595
+ @edit-chunk="onEditChunk"
596
+ @toggle-chunk="onToggleChunk"
597
+ @delete-chunk="onDeleteChunk"
598
+ @add-chunk="onAddChunk"
599
+ @update:page="onChunkPageChange"
600
+ @update:page-size="onChunkPageSizeChange"
601
+ />
602
+ </template>
603
+ <template v-else>
604
+ <DocumentTable
605
+ v-if="tab === 'documents'"
606
+ :documents="documents"
607
+ :loading="documentsLoading"
608
+ @add-file="pickFiles"
609
+ @open="onOpenDoc"
610
+ @toggle-enabled="onToggleDocEnabled"
611
+ @delete="onDeleteDoc"
612
+ />
613
+ <RecallTestingPanelV2
614
+ v-else-if="tab === 'recall'"
615
+ :hits="hits"
616
+ :history="recallHistory"
617
+ :loading="recallLoading"
618
+ :initial-config="parsedRetrievalConfig"
619
+ :rerank-models="rerankModels"
620
+ :indexing-technique="dataset?.indexingTechnique"
621
+ @run="onRunRecall"
622
+ />
623
+ <DatasetSettingsPanel
624
+ v-else-if="tab === 'settings'"
625
+ :dataset="settingsForm"
626
+ :document-count="documents.length"
627
+ :embedding-models="embeddingModels"
628
+ :default-embedding-model-id="defaultEmbeddingModelId"
629
+ :rerank-models="rerankModels"
630
+ @save="onSaveSettings"
631
+ />
632
+ </template>
633
+ </div>
634
+ </div>
635
+
636
+ <!-- Add-documents wizard (3-step, upload → 分段配置 → 完成). The
637
+ preview-chunks callback runs through the hub so what the user
638
+ sees on step 2 is the real chunker output, not a mock. -->
639
+ <AddDocumentsWizard
640
+ :open="addWizardOpen"
641
+ :dataset-name="dataset?.name"
642
+ :process-rule="parsedProcessRule"
643
+ :retrieval-method="parsedRetrievalMethod"
644
+ :indexing-technique="dataset?.indexingTechnique"
645
+ :preview-chunks="hub.previewChunks"
646
+ @update:open="addWizardOpen = $event"
647
+ @confirm="onAddWizardConfirm"
648
+ />
649
+
650
+ <!-- Inline chunk editor (add & edit reuse the same modal) -->
651
+ <div
652
+ v-if="editorMode"
653
+ class="kh-drawer-editor-mask"
654
+ @click.self="cancelChunkEdit"
655
+ >
656
+ <div class="kh-drawer-editor">
657
+ <div class="kh-drawer-editor-title">
658
+ {{ editorMode === 'add' ? '添加分段' : '编辑分段' }}
659
+ </div>
660
+ <textarea
661
+ v-model="editContent"
662
+ class="kh-drawer-editor-textarea"
663
+ placeholder="分段内容..."
664
+ />
665
+ <div class="kh-drawer-editor-hint">
666
+ {{
667
+ editorMode === 'add'
668
+ ? '新分段会被追加到当前文档末尾,并自动计算 embedding 入向量库。'
669
+ : '保存后会重新计算 embedding 并入向量库,可能需要几秒。'
670
+ }}
671
+ </div>
672
+ <div class="kh-drawer-editor-actions">
673
+ <button class="kh-btn kh-btn-secondary" @click="cancelChunkEdit">
674
+ 取消
675
+ </button>
676
+ <button
677
+ class="kh-btn kh-btn-primary"
678
+ :disabled="!editContent.trim()"
679
+ @click="saveChunkEdit"
680
+ >
681
+ 保存
682
+ </button>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+ </Teleport>
689
+ </template>
690
+
691
+ <style scoped>
692
+ .kh-drawer-mask {
693
+ position: fixed;
694
+ inset: 0;
695
+ z-index: 1040;
696
+ background: rgba(15, 23, 42, 0.35);
697
+ display: flex;
698
+ /* Slide-in from the left — user preference so the workspace tabs stay on the
699
+ right side and the detail view feels closer to a full-page take-over. */
700
+ justify-content: flex-start;
701
+ }
702
+ .kh-drawer-shell {
703
+ /* 88vw / max 1600 — much roomier than the previous ~1200px cap so the doc
704
+ table + right meta pane don't have to fight for horizontal space. */
705
+ width: 88vw;
706
+ max-width: 1600px;
707
+ min-width: 960px;
708
+ height: 100vh;
709
+ background: #fff;
710
+ display: flex;
711
+ box-shadow: 6px 0 22px rgba(15, 23, 42, 0.14);
712
+ animation: kh-drawer-in 0.2s ease-out;
713
+ }
714
+ @keyframes kh-drawer-in {
715
+ from {
716
+ transform: translateX(-20px);
717
+ opacity: 0.9;
718
+ }
719
+ to {
720
+ transform: translateX(0);
721
+ opacity: 1;
722
+ }
723
+ }
724
+
725
+ .kh-drawer-main {
726
+ flex: 1;
727
+ min-width: 0;
728
+ display: flex;
729
+ flex-direction: column;
730
+ background: #fff;
731
+ }
732
+
733
+ .kh-drawer-topbar {
734
+ display: flex;
735
+ justify-content: space-between;
736
+ align-items: center;
737
+ padding: 12px 20px;
738
+ border-bottom: 1px solid #f1f5f9;
739
+ }
740
+ .kh-drawer-topbar-title {
741
+ display: flex;
742
+ align-items: center;
743
+ gap: 8px;
744
+ font-size: 13px;
745
+ color: #475569;
746
+ }
747
+ .kh-drawer-back {
748
+ padding: 4px 10px;
749
+ border: 1px solid #e2e8f0;
750
+ border-radius: 6px;
751
+ background: #fff;
752
+ color: #64748b;
753
+ font-size: 12px;
754
+ cursor: pointer;
755
+ }
756
+ .kh-drawer-back:hover {
757
+ background: #f1f5f9;
758
+ }
759
+ .kh-drawer-doc-name {
760
+ color: #0f172a;
761
+ font-weight: 600;
762
+ }
763
+ .kh-drawer-title-name {
764
+ color: #0f172a;
765
+ font-weight: 600;
766
+ }
767
+ .kh-drawer-title-tab {
768
+ color: #94a3b8;
769
+ }
770
+ .kh-drawer-close {
771
+ width: 32px;
772
+ height: 32px;
773
+ padding: 0;
774
+ border: none;
775
+ border-radius: 8px;
776
+ background: transparent;
777
+ color: #94a3b8;
778
+ font-size: 20px;
779
+ line-height: 1;
780
+ cursor: pointer;
781
+ }
782
+ .kh-drawer-close:hover {
783
+ background: #f1f5f9;
784
+ color: #475569;
785
+ }
786
+
787
+ .kh-drawer-content {
788
+ flex: 1;
789
+ overflow: hidden;
790
+ display: flex;
791
+ flex-direction: column;
792
+ }
793
+
794
+ .kh-drawer-stub {
795
+ padding: 80px 40px;
796
+ text-align: center;
797
+ color: #94a3b8;
798
+ }
799
+ .kh-drawer-stub-icon {
800
+ font-size: 42px;
801
+ opacity: 0.5;
802
+ }
803
+ .kh-drawer-stub-title {
804
+ margin-top: 14px;
805
+ font-size: 15px;
806
+ font-weight: 600;
807
+ color: #64748b;
808
+ }
809
+ .kh-drawer-stub-sub {
810
+ margin-top: 6px;
811
+ font-size: 12px;
812
+ }
813
+
814
+ /* Inline chunk editor */
815
+ .kh-drawer-editor-mask {
816
+ position: fixed;
817
+ inset: 0;
818
+ z-index: 1050;
819
+ background: rgba(15, 23, 42, 0.5);
820
+ display: flex;
821
+ align-items: center;
822
+ justify-content: center;
823
+ }
824
+ .kh-drawer-editor {
825
+ width: min(720px, 90vw);
826
+ max-height: 80vh;
827
+ padding: 20px 22px 18px;
828
+ border-radius: 12px;
829
+ background: #fff;
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 12px;
833
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
834
+ }
835
+ .kh-drawer-editor-title {
836
+ font-size: 15px;
837
+ font-weight: 600;
838
+ color: #0f172a;
839
+ }
840
+ .kh-drawer-editor-textarea {
841
+ flex: 1;
842
+ min-height: 240px;
843
+ padding: 10px 12px;
844
+ border: 1px solid #e2e8f0;
845
+ border-radius: 8px;
846
+ outline: none;
847
+ font-family: inherit;
848
+ font-size: 13px;
849
+ color: #0f172a;
850
+ resize: vertical;
851
+ }
852
+ .kh-drawer-editor-textarea:focus {
853
+ border-color: #6366f1;
854
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
855
+ }
856
+ .kh-drawer-editor-hint {
857
+ font-size: 11px;
858
+ color: #94a3b8;
859
+ }
860
+ .kh-drawer-editor-actions {
861
+ display: flex;
862
+ justify-content: flex-end;
863
+ gap: 8px;
864
+ }
865
+ .kh-btn {
866
+ padding: 6px 18px;
867
+ border: none;
868
+ border-radius: 6px;
869
+ font-size: 13px;
870
+ font-weight: 500;
871
+ cursor: pointer;
872
+ }
873
+ .kh-btn-primary {
874
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
875
+ color: #fff;
876
+ }
877
+ .kh-btn-primary:disabled {
878
+ background: #cbd5e1;
879
+ cursor: not-allowed;
880
+ }
881
+ .kh-btn-secondary {
882
+ background: #f1f5f9;
883
+ color: #475569;
884
+ }
885
+ .kh-btn-secondary:hover {
886
+ background: #e2e8f0;
887
+ }
888
+ </style>