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,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>
|