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,908 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DatasetSettingsPanel — 设置 tab (screenshot 6).
|
|
4
|
+
*
|
|
5
|
+
* Vertical stack of "label + control" rows, matching Dify's layout:
|
|
6
|
+
* 名称和图标, 描述, 可见权限, 分段模式 (3 stacked cards),
|
|
7
|
+
* 索引模式 (2 cards, keyword-count slider inside 经济), Embedding 模型,
|
|
8
|
+
* 摘要自动生成 toggle, 检索设置 (向量检索 selected card + Rerank switch).
|
|
9
|
+
*/
|
|
10
|
+
import { computed, ref, watch } from 'vue';
|
|
11
|
+
|
|
12
|
+
import GroupedModelSelect, {
|
|
13
|
+
type GroupedModelSelectOption,
|
|
14
|
+
} from '../../provider-hub/components/GroupedModelSelect.vue';
|
|
15
|
+
import type { EmbeddingModelOption } from '../types/api';
|
|
16
|
+
import type { RetrievalConfig } from '../types/dataset';
|
|
17
|
+
import RetrievalMethodPicker from './RetrievalMethodPicker.vue';
|
|
18
|
+
|
|
19
|
+
interface DatasetForm {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
iconBg?: string;
|
|
25
|
+
permission?: 'ONLY_ME' | 'ORG_MEMBERS';
|
|
26
|
+
chunkMode?: 'general' | 'parent-child' | 'qa';
|
|
27
|
+
// Per-chunk-mode parameters — pre-seeded from the dataset's persisted
|
|
28
|
+
// processRule so the panel opens showing the actual current setup. Empty
|
|
29
|
+
// values fall back to the same wizard defaults (1024 / 50 / PARAGRAPH / …).
|
|
30
|
+
chunkTokens?: number;
|
|
31
|
+
overlapTokens?: number;
|
|
32
|
+
parentMode?: 'FULL_DOC' | 'PARAGRAPH';
|
|
33
|
+
parentChunkTokens?: number;
|
|
34
|
+
removeExtraWhitespace?: boolean;
|
|
35
|
+
removeUrlsEmails?: boolean;
|
|
36
|
+
indexingTechnique?: 'ECONOMY' | 'HIGH_QUALITY';
|
|
37
|
+
keywordCount?: number;
|
|
38
|
+
embeddingModelId?: string;
|
|
39
|
+
autoSummary?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Retrieval settings live in one config object v-model'd through the
|
|
42
|
+
* shared {@link RetrievalMethodPicker}. Kept optional for backward compat
|
|
43
|
+
* with hosts still constructing forms by legacy field names — the parent
|
|
44
|
+
* derives {@code retrievalMethod} etc. from this on save.
|
|
45
|
+
*/
|
|
46
|
+
retrievalConfig?: RetrievalConfig;
|
|
47
|
+
// Legacy — kept so existing DatasetDetailDrawer field-by-field save code
|
|
48
|
+
// still compiles. Panel wraps them into {@code retrievalConfig} on mount.
|
|
49
|
+
retrievalMethod?: 'FULL_TEXT' | 'HYBRID' | 'VECTOR';
|
|
50
|
+
rerankEnabled?: boolean;
|
|
51
|
+
rerankModelId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Props {
|
|
55
|
+
dataset: DatasetForm;
|
|
56
|
+
embeddingModels?: EmbeddingModelOption[];
|
|
57
|
+
rerankModels?: Array<{ id: string; label: string }>;
|
|
58
|
+
/**
|
|
59
|
+
* Total documents already ingested for this dataset. When > 0 the chunk-mode
|
|
60
|
+
* cards are locked: switching the mode after indexing would create chunks
|
|
61
|
+
* inconsistent with the ones already in the vector store. Users are told to
|
|
62
|
+
* re-create the dataset instead.
|
|
63
|
+
*/
|
|
64
|
+
documentCount?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Tenant-scoped default embedding model id from
|
|
67
|
+
* {@code /models/defaults} — auto-selected when the dataset itself has no
|
|
68
|
+
* {@code embeddingModelId} yet, so a fresh dataset opens with the shared
|
|
69
|
+
* default rather than "选择一个 Embedding 模型" placeholder.
|
|
70
|
+
*/
|
|
71
|
+
defaultEmbeddingModelId?: null | string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
75
|
+
embeddingModels: () => [],
|
|
76
|
+
rerankModels: () => [],
|
|
77
|
+
documentCount: 0,
|
|
78
|
+
defaultEmbeddingModelId: null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const emit = defineEmits<{
|
|
82
|
+
(e: 'save', payload: Partial<DatasetForm>): void;
|
|
83
|
+
}>();
|
|
84
|
+
|
|
85
|
+
const form = ref<DatasetForm>({ ...props.dataset });
|
|
86
|
+
|
|
87
|
+
// Retrieval settings are v-model'd through the shared picker as a single
|
|
88
|
+
// RetrievalConfig; local ref stays synced with the legacy per-field props so
|
|
89
|
+
// callers that still read {@code retrievalMethod} / {@code rerankEnabled}
|
|
90
|
+
// separately keep working during the migration.
|
|
91
|
+
const retrievalConfig = ref<RetrievalConfig>({
|
|
92
|
+
method: props.dataset.retrievalMethod ?? 'HYBRID',
|
|
93
|
+
topK: 3,
|
|
94
|
+
vectorWeight: 0.7,
|
|
95
|
+
rerankEnabled: props.dataset.rerankEnabled ?? false,
|
|
96
|
+
rerankModelId: props.dataset.rerankModelId,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Track "dirty" so we can enable the sticky save button. Dify does this too.
|
|
100
|
+
const dirty = ref(false);
|
|
101
|
+
/**
|
|
102
|
+
* When we seed the form ourselves (loading the dataset, auto-picking the
|
|
103
|
+
* tenant default embedding model), suppress the deep watcher's dirty flip.
|
|
104
|
+
* Without this the sticky footer would appear the moment the panel opens.
|
|
105
|
+
*/
|
|
106
|
+
let seeding = false;
|
|
107
|
+
|
|
108
|
+
watch(
|
|
109
|
+
() => props.dataset,
|
|
110
|
+
(d) => {
|
|
111
|
+
seeding = true;
|
|
112
|
+
form.value = { ...d };
|
|
113
|
+
retrievalConfig.value = {
|
|
114
|
+
...(d.retrievalConfig ?? {}),
|
|
115
|
+
method: d.retrievalMethod ?? d.retrievalConfig?.method ?? 'HYBRID',
|
|
116
|
+
topK: d.retrievalConfig?.topK ?? 3,
|
|
117
|
+
vectorWeight: d.retrievalConfig?.vectorWeight ?? 0.7,
|
|
118
|
+
rerankEnabled: d.rerankEnabled ?? d.retrievalConfig?.rerankEnabled ?? false,
|
|
119
|
+
rerankModelId: d.rerankModelId ?? d.retrievalConfig?.rerankModelId,
|
|
120
|
+
};
|
|
121
|
+
dirty.value = false;
|
|
122
|
+
void Promise.resolve().then(() => {
|
|
123
|
+
seeding = false;
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
watch(
|
|
129
|
+
form,
|
|
130
|
+
() => {
|
|
131
|
+
if (seeding) return;
|
|
132
|
+
dirty.value = true;
|
|
133
|
+
},
|
|
134
|
+
{ deep: true },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Picker changes → mirror back into form so `save()` sends everything on the
|
|
138
|
+
// same emit shape callers already expect.
|
|
139
|
+
watch(
|
|
140
|
+
retrievalConfig,
|
|
141
|
+
(c) => {
|
|
142
|
+
dirty.value = true;
|
|
143
|
+
form.value.retrievalMethod = c.method;
|
|
144
|
+
form.value.rerankEnabled = c.rerankEnabled;
|
|
145
|
+
form.value.rerankModelId = c.rerankModelId;
|
|
146
|
+
form.value.retrievalConfig = { ...c };
|
|
147
|
+
},
|
|
148
|
+
{ deep: true },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const CHUNK_MODES = [
|
|
152
|
+
{
|
|
153
|
+
id: 'general' as const,
|
|
154
|
+
icon: '≡',
|
|
155
|
+
title: 'General',
|
|
156
|
+
desc: '通过文本分块模式,检索和召回块获取相关部分。',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'parent-child' as const,
|
|
160
|
+
icon: '👥',
|
|
161
|
+
title: 'Parent-Child',
|
|
162
|
+
desc: '使用父子文档结构,子块用于检索,父块用作上下文。',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'qa' as const,
|
|
166
|
+
icon: '💬',
|
|
167
|
+
title: 'Q&A',
|
|
168
|
+
desc: '使用 Q&A 分段,将检索到的内容作为知识源,构建更精细的问题-答案配对。',
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Chunk-mode cards lock once the dataset has at least one document — reindexing
|
|
174
|
+
* across incompatible chunk shapes is an offline op we don't want users to
|
|
175
|
+
* accidentally trigger from settings. UI shows a hint pointing at "重新创建
|
|
176
|
+
* 知识库" instead.
|
|
177
|
+
*/
|
|
178
|
+
const chunkModeLocked = computed(() => (props.documentCount ?? 0) > 0);
|
|
179
|
+
function selectChunkMode(id: 'general' | 'parent-child' | 'qa') {
|
|
180
|
+
if (chunkModeLocked.value) return;
|
|
181
|
+
form.value.chunkMode = id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Embedding model picker options ----
|
|
185
|
+
// The grouped dropdown lives in the shared `GroupedModelSelect`. We just adapt
|
|
186
|
+
// the wire shape (`EmbeddingModelOption`) to the picker's option shape here —
|
|
187
|
+
// everything else (grouping, search, brand icons) lives in the shared
|
|
188
|
+
// component so 系统默认模型 uses the same UX.
|
|
189
|
+
const embedOptions = computed<GroupedModelSelectOption[]>(() =>
|
|
190
|
+
props.embeddingModels.map((m) => ({
|
|
191
|
+
id: m.id,
|
|
192
|
+
label: m.label,
|
|
193
|
+
providerName: m.providerName,
|
|
194
|
+
providerLabel: m.providerLabel,
|
|
195
|
+
isDefault: m.isDefault,
|
|
196
|
+
})),
|
|
197
|
+
);
|
|
198
|
+
function onPickEmbedding(id: null | string) {
|
|
199
|
+
form.value.embeddingModelId = id ?? undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Auto-select the tenant default embedding model when the dataset opens with
|
|
204
|
+
* an empty `embeddingModelId`. Fires whenever the default id or the model list
|
|
205
|
+
* changes so hosts that load these async still get the preselection.
|
|
206
|
+
*/
|
|
207
|
+
watch(
|
|
208
|
+
() => [
|
|
209
|
+
props.defaultEmbeddingModelId,
|
|
210
|
+
props.embeddingModels.map((m) => m.id).join('|'),
|
|
211
|
+
],
|
|
212
|
+
() => {
|
|
213
|
+
if (form.value.embeddingModelId) return;
|
|
214
|
+
const defaultId =
|
|
215
|
+
props.defaultEmbeddingModelId ??
|
|
216
|
+
props.embeddingModels.find((m) => m.isDefault)?.id ??
|
|
217
|
+
null;
|
|
218
|
+
if (!defaultId) return;
|
|
219
|
+
if (!props.embeddingModels.some((m) => m.id === defaultId)) return;
|
|
220
|
+
seeding = true;
|
|
221
|
+
form.value.embeddingModelId = defaultId;
|
|
222
|
+
// Release the guard on the next tick so any legit user edit right after
|
|
223
|
+
// still flips dirty.
|
|
224
|
+
void Promise.resolve().then(() => {
|
|
225
|
+
seeding = false;
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
{ immediate: true },
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const canSave = computed(() => dirty.value && form.value.name.trim().length > 0);
|
|
232
|
+
|
|
233
|
+
function save() {
|
|
234
|
+
if (!canSave.value) return;
|
|
235
|
+
emit('save', { ...form.value });
|
|
236
|
+
dirty.value = false;
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<div class="kh-settings">
|
|
242
|
+
<div class="kh-settings-head">
|
|
243
|
+
<div class="kh-settings-title">知识库设置</div>
|
|
244
|
+
<div class="kh-settings-sub">
|
|
245
|
+
在这里,您可以修改此知识库的属性和检索设置
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div class="kh-settings-body">
|
|
250
|
+
<!-- 名称和图标 -->
|
|
251
|
+
<div class="kh-settings-row">
|
|
252
|
+
<label class="kh-settings-label">名称和图标</label>
|
|
253
|
+
<div class="kh-settings-name-row">
|
|
254
|
+
<button class="kh-settings-icon-btn" :style="{ background: form.iconBg ?? '#FFEAD5' }">
|
|
255
|
+
{{ form.icon ?? '📙' }}
|
|
256
|
+
</button>
|
|
257
|
+
<input v-model="form.name" class="kh-input" />
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<!-- 描述 -->
|
|
262
|
+
<div class="kh-settings-row">
|
|
263
|
+
<label class="kh-settings-label">描述</label>
|
|
264
|
+
<textarea
|
|
265
|
+
v-model="form.description"
|
|
266
|
+
class="kh-input kh-textarea"
|
|
267
|
+
rows="3"
|
|
268
|
+
placeholder="useful for when you want to answer queries about the …"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- 可见权限 -->
|
|
273
|
+
<div class="kh-settings-row">
|
|
274
|
+
<label class="kh-settings-label">可见权限</label>
|
|
275
|
+
<select v-model="form.permission" class="kh-input">
|
|
276
|
+
<option value="ONLY_ME">🔒 只有我</option>
|
|
277
|
+
<option value="ORG_MEMBERS">👥 团队成员</option>
|
|
278
|
+
</select>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<!-- 分段模式 —— includes per-mode parameter inputs so the panel is the
|
|
282
|
+
single source of truth for dataset chunking config (mirrors the
|
|
283
|
+
create wizard). Previous version only exposed the 3 mode cards,
|
|
284
|
+
forcing users to re-create the dataset just to tweak chunk size. -->
|
|
285
|
+
<div class="kh-settings-row">
|
|
286
|
+
<label class="kh-settings-label">
|
|
287
|
+
分段模式
|
|
288
|
+
<div v-if="chunkModeLocked" class="kh-settings-sub-label">
|
|
289
|
+
已存在文档,无法切换分段模式。如需变更请新建知识库。
|
|
290
|
+
</div>
|
|
291
|
+
<div v-else class="kh-settings-sub-label">了解更多关于分段模式。</div>
|
|
292
|
+
</label>
|
|
293
|
+
<div
|
|
294
|
+
class="kh-settings-cards"
|
|
295
|
+
:class="{ 'kh-settings-cards-locked': chunkModeLocked }"
|
|
296
|
+
>
|
|
297
|
+
<div
|
|
298
|
+
v-for="m in CHUNK_MODES"
|
|
299
|
+
:key="m.id"
|
|
300
|
+
class="kh-mode-card"
|
|
301
|
+
:class="{
|
|
302
|
+
'kh-mode-card-active': form.chunkMode === m.id,
|
|
303
|
+
'kh-mode-card-disabled': chunkModeLocked && form.chunkMode !== m.id,
|
|
304
|
+
}"
|
|
305
|
+
@click="selectChunkMode(m.id)"
|
|
306
|
+
>
|
|
307
|
+
<div class="kh-mode-card-head">
|
|
308
|
+
<span class="kh-mode-card-icon">{{ m.icon }}</span>
|
|
309
|
+
<span class="kh-mode-card-title">{{ m.title }}</span>
|
|
310
|
+
<span
|
|
311
|
+
v-if="chunkModeLocked && form.chunkMode === m.id"
|
|
312
|
+
class="kh-mode-card-lock"
|
|
313
|
+
>
|
|
314
|
+
🔒 已锁定
|
|
315
|
+
</span>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="kh-mode-card-desc">{{ m.desc }}</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<!-- General / QA modes share the same chunk-size + overlap fields.
|
|
321
|
+
Parent-child adds a parent-chunk-size + parent-mode picker. All of
|
|
322
|
+
these are hidden once the chunk mode is locked — showing them
|
|
323
|
+
read-only would just be visual noise since the backend won't act
|
|
324
|
+
on a re-submitted value here. -->
|
|
325
|
+
<template v-if="!chunkModeLocked">
|
|
326
|
+
<div
|
|
327
|
+
v-if="form.chunkMode !== 'parent-child'"
|
|
328
|
+
class="kh-settings-inline-grid"
|
|
329
|
+
>
|
|
330
|
+
<div class="kh-field">
|
|
331
|
+
<label>分段最大长度</label>
|
|
332
|
+
<div class="kh-input-suffix">
|
|
333
|
+
<input
|
|
334
|
+
v-model.number="form.chunkTokens"
|
|
335
|
+
type="number"
|
|
336
|
+
class="kh-input"
|
|
337
|
+
placeholder="1024"
|
|
338
|
+
/>
|
|
339
|
+
<span class="kh-suffix">characters</span>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="kh-field">
|
|
343
|
+
<label>分段重叠长度</label>
|
|
344
|
+
<div class="kh-input-suffix">
|
|
345
|
+
<input
|
|
346
|
+
v-model.number="form.overlapTokens"
|
|
347
|
+
type="number"
|
|
348
|
+
class="kh-input"
|
|
349
|
+
placeholder="50"
|
|
350
|
+
/>
|
|
351
|
+
<span class="kh-suffix">characters</span>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
<div v-else class="kh-settings-inline-grid">
|
|
356
|
+
<div class="kh-field">
|
|
357
|
+
<label>父块最大长度</label>
|
|
358
|
+
<div class="kh-input-suffix">
|
|
359
|
+
<input
|
|
360
|
+
v-model.number="form.parentChunkTokens"
|
|
361
|
+
type="number"
|
|
362
|
+
class="kh-input"
|
|
363
|
+
placeholder="1024"
|
|
364
|
+
/>
|
|
365
|
+
<span class="kh-suffix">characters</span>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
<div class="kh-field">
|
|
369
|
+
<label>子块最大长度</label>
|
|
370
|
+
<div class="kh-input-suffix">
|
|
371
|
+
<input
|
|
372
|
+
v-model.number="form.chunkTokens"
|
|
373
|
+
type="number"
|
|
374
|
+
class="kh-input"
|
|
375
|
+
placeholder="512"
|
|
376
|
+
/>
|
|
377
|
+
<span class="kh-suffix">characters</span>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="kh-field">
|
|
381
|
+
<label>父块模式</label>
|
|
382
|
+
<select v-model="form.parentMode" class="kh-input">
|
|
383
|
+
<option value="PARAGRAPH">段落</option>
|
|
384
|
+
<option value="FULL_DOC">全文</option>
|
|
385
|
+
</select>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
<div class="kh-settings-inline-checks">
|
|
389
|
+
<label class="kh-check">
|
|
390
|
+
<input type="checkbox" v-model="form.removeExtraWhitespace" />
|
|
391
|
+
<span>替换掉连续的空格、换行符和制表符</span>
|
|
392
|
+
</label>
|
|
393
|
+
</div>
|
|
394
|
+
</template>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<!-- 索引模式 — locked once documents are ingested. HIGH_QUALITY vs
|
|
398
|
+
ECONOMY changes the retrieval pipeline (vector store vs keyword
|
|
399
|
+
inverted index), and the backend does not re-embed on switch — so
|
|
400
|
+
allowing the toggle post-ingest would leave chunks stranded in the
|
|
401
|
+
wrong store. Mirrors the chunk-mode lock behaviour. -->
|
|
402
|
+
<div class="kh-settings-row">
|
|
403
|
+
<label class="kh-settings-label">
|
|
404
|
+
索引模式
|
|
405
|
+
<div v-if="chunkModeLocked" class="kh-settings-sub-label">
|
|
406
|
+
已存在文档,无法切换索引模式。
|
|
407
|
+
</div>
|
|
408
|
+
</label>
|
|
409
|
+
<div
|
|
410
|
+
class="kh-settings-cards"
|
|
411
|
+
:class="{ 'kh-settings-cards-locked': chunkModeLocked }"
|
|
412
|
+
>
|
|
413
|
+
<div
|
|
414
|
+
class="kh-mode-card"
|
|
415
|
+
:class="{
|
|
416
|
+
'kh-mode-card-active': form.indexingTechnique === 'HIGH_QUALITY',
|
|
417
|
+
'kh-mode-card-disabled':
|
|
418
|
+
chunkModeLocked && form.indexingTechnique !== 'HIGH_QUALITY',
|
|
419
|
+
}"
|
|
420
|
+
@click="!chunkModeLocked && (form.indexingTechnique = 'HIGH_QUALITY')"
|
|
421
|
+
>
|
|
422
|
+
<div class="kh-mode-card-head">
|
|
423
|
+
<span class="kh-mode-card-icon" style="color: #b45309">⭐</span>
|
|
424
|
+
<span class="kh-mode-card-title">高质量</span>
|
|
425
|
+
<span v-if="!chunkModeLocked" class="kh-mode-badge">推荐</span>
|
|
426
|
+
<span
|
|
427
|
+
v-if="chunkModeLocked && form.indexingTechnique === 'HIGH_QUALITY'"
|
|
428
|
+
class="kh-mode-card-lock"
|
|
429
|
+
>
|
|
430
|
+
🔒 已锁定
|
|
431
|
+
</span>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="kh-mode-card-desc">
|
|
434
|
+
调用嵌入模型处理文档以实现更精确的检索,可以帮助 LLM 生成高质量的答案。
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
<div
|
|
438
|
+
class="kh-mode-card"
|
|
439
|
+
:class="{
|
|
440
|
+
'kh-mode-card-active': form.indexingTechnique === 'ECONOMY',
|
|
441
|
+
'kh-mode-card-disabled':
|
|
442
|
+
chunkModeLocked && form.indexingTechnique !== 'ECONOMY',
|
|
443
|
+
}"
|
|
444
|
+
@click="!chunkModeLocked && (form.indexingTechnique = 'ECONOMY')"
|
|
445
|
+
>
|
|
446
|
+
<div class="kh-mode-card-head">
|
|
447
|
+
<span class="kh-mode-card-icon" style="color: #4338ca">💰</span>
|
|
448
|
+
<span class="kh-mode-card-title">经济</span>
|
|
449
|
+
<span
|
|
450
|
+
v-if="chunkModeLocked && form.indexingTechnique === 'ECONOMY'"
|
|
451
|
+
class="kh-mode-card-lock"
|
|
452
|
+
>
|
|
453
|
+
🔒 已锁定
|
|
454
|
+
</span>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="kh-mode-card-desc">
|
|
457
|
+
每个数据块使用 10 个关键词进行检索,不消耗任何 tokens,你会得到较低的检索准确性。
|
|
458
|
+
</div>
|
|
459
|
+
<div
|
|
460
|
+
v-if="form.indexingTechnique === 'ECONOMY' && !chunkModeLocked"
|
|
461
|
+
class="kh-keyword-row"
|
|
462
|
+
>
|
|
463
|
+
<label class="kh-keyword-lbl">关键词数量</label>
|
|
464
|
+
<input
|
|
465
|
+
v-model.number="form.keywordCount"
|
|
466
|
+
type="range"
|
|
467
|
+
min="1"
|
|
468
|
+
max="20"
|
|
469
|
+
class="kh-keyword-slider"
|
|
470
|
+
/>
|
|
471
|
+
<span class="kh-keyword-val">{{ form.keywordCount ?? 10 }}</span>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<!-- Embedding 模型 — grouped-by-provider dropdown (shared with 系统默认
|
|
478
|
+
模型). Auto-fills from the tenant default when the dataset has no
|
|
479
|
+
embedding model set yet (see `defaultEmbeddingModelId` prop). -->
|
|
480
|
+
<div class="kh-settings-row">
|
|
481
|
+
<label class="kh-settings-label">Embedding 模型</label>
|
|
482
|
+
<GroupedModelSelect
|
|
483
|
+
:model-value="form.embeddingModelId ?? null"
|
|
484
|
+
:options="embedOptions"
|
|
485
|
+
placeholder="选择一个 Embedding 模型"
|
|
486
|
+
empty-text="暂无可用的嵌入模型"
|
|
487
|
+
empty-hint="请先在「模型供应商」里配置并启用"
|
|
488
|
+
@update:model-value="onPickEmbedding"
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<!-- 摘要自动生成 -->
|
|
493
|
+
<div class="kh-settings-row">
|
|
494
|
+
<label class="kh-settings-label">
|
|
495
|
+
摘要自动生成
|
|
496
|
+
<div class="kh-settings-sub-label">
|
|
497
|
+
启用后,将会自动为新添加的文档生成摘要。已有的文档也可以手动使用。
|
|
498
|
+
</div>
|
|
499
|
+
</label>
|
|
500
|
+
<label class="kh-switch">
|
|
501
|
+
<input type="checkbox" v-model="form.autoSummary" />
|
|
502
|
+
<span class="kh-switch-slider" />
|
|
503
|
+
</label>
|
|
504
|
+
<span class="kh-switch-lbl">
|
|
505
|
+
{{ form.autoSummary ? '已启用' : '已禁用' }}
|
|
506
|
+
</span>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<!-- 检索设置 — shared RetrievalMethodPicker so wizard / settings /
|
|
510
|
+
recall popover all use the same UI. -->
|
|
511
|
+
<div class="kh-settings-row">
|
|
512
|
+
<label class="kh-settings-label">
|
|
513
|
+
检索设置
|
|
514
|
+
<div class="kh-settings-sub-label">了解更多关于检索方法。</div>
|
|
515
|
+
</label>
|
|
516
|
+
<RetrievalMethodPicker
|
|
517
|
+
v-model="retrievalConfig"
|
|
518
|
+
:rerank-models="rerankModels"
|
|
519
|
+
:indexing-technique="form.indexingTechnique"
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<div v-if="dirty" class="kh-settings-sticky">
|
|
525
|
+
<button class="kh-btn kh-btn-secondary" @click="form = { ...dataset }; dirty = false">
|
|
526
|
+
取消
|
|
527
|
+
</button>
|
|
528
|
+
<button
|
|
529
|
+
class="kh-btn kh-btn-primary"
|
|
530
|
+
:disabled="!canSave"
|
|
531
|
+
@click="save"
|
|
532
|
+
>
|
|
533
|
+
保存
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</template>
|
|
538
|
+
|
|
539
|
+
<style scoped>
|
|
540
|
+
.kh-settings {
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
height: 100%;
|
|
544
|
+
padding: 24px 32px;
|
|
545
|
+
overflow-y: auto;
|
|
546
|
+
position: relative;
|
|
547
|
+
}
|
|
548
|
+
.kh-settings-head {
|
|
549
|
+
margin-bottom: 22px;
|
|
550
|
+
}
|
|
551
|
+
.kh-settings-title {
|
|
552
|
+
font-size: 15px;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: #0f172a;
|
|
555
|
+
}
|
|
556
|
+
.kh-settings-sub {
|
|
557
|
+
margin-top: 4px;
|
|
558
|
+
font-size: 12px;
|
|
559
|
+
color: #94a3b8;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.kh-settings-body {
|
|
563
|
+
max-width: 720px;
|
|
564
|
+
display: flex;
|
|
565
|
+
flex-direction: column;
|
|
566
|
+
gap: 22px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.kh-settings-row {
|
|
570
|
+
display: grid;
|
|
571
|
+
grid-template-columns: 140px 1fr;
|
|
572
|
+
gap: 20px;
|
|
573
|
+
align-items: flex-start;
|
|
574
|
+
}
|
|
575
|
+
.kh-settings-label {
|
|
576
|
+
padding-top: 6px;
|
|
577
|
+
font-size: 13px;
|
|
578
|
+
font-weight: 500;
|
|
579
|
+
color: #334155;
|
|
580
|
+
}
|
|
581
|
+
.kh-settings-sub-label {
|
|
582
|
+
margin-top: 2px;
|
|
583
|
+
font-size: 10px;
|
|
584
|
+
color: #94a3b8;
|
|
585
|
+
font-weight: 400;
|
|
586
|
+
line-height: 1.4;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.kh-input {
|
|
590
|
+
width: 100%;
|
|
591
|
+
padding: 8px 12px;
|
|
592
|
+
font-size: 13px;
|
|
593
|
+
border: 1px solid #d1d5db;
|
|
594
|
+
border-radius: 8px;
|
|
595
|
+
background: #fff;
|
|
596
|
+
color: #0f172a;
|
|
597
|
+
outline: none;
|
|
598
|
+
font-family: inherit;
|
|
599
|
+
}
|
|
600
|
+
.kh-input:focus {
|
|
601
|
+
border-color: #6366f1;
|
|
602
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
603
|
+
}
|
|
604
|
+
.kh-textarea {
|
|
605
|
+
resize: vertical;
|
|
606
|
+
min-height: 60px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.kh-settings-name-row {
|
|
610
|
+
display: flex;
|
|
611
|
+
gap: 10px;
|
|
612
|
+
align-items: center;
|
|
613
|
+
}
|
|
614
|
+
.kh-settings-icon-btn {
|
|
615
|
+
width: 44px;
|
|
616
|
+
height: 44px;
|
|
617
|
+
padding: 0;
|
|
618
|
+
border: 1px solid #e2e8f0;
|
|
619
|
+
border-radius: 10px;
|
|
620
|
+
font-size: 22px;
|
|
621
|
+
cursor: pointer;
|
|
622
|
+
flex-shrink: 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Per-chunk-mode parameter panels shown below the mode card row. */
|
|
626
|
+
.kh-settings-inline-grid {
|
|
627
|
+
display: grid;
|
|
628
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
629
|
+
gap: 12px;
|
|
630
|
+
margin-top: 10px;
|
|
631
|
+
}
|
|
632
|
+
.kh-settings-inline-checks {
|
|
633
|
+
display: flex;
|
|
634
|
+
flex-direction: column;
|
|
635
|
+
gap: 6px;
|
|
636
|
+
margin-top: 12px;
|
|
637
|
+
}
|
|
638
|
+
.kh-input-suffix {
|
|
639
|
+
position: relative;
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: center;
|
|
642
|
+
}
|
|
643
|
+
.kh-input-suffix .kh-input {
|
|
644
|
+
padding-right: 74px;
|
|
645
|
+
width: 100%;
|
|
646
|
+
}
|
|
647
|
+
.kh-input-suffix .kh-suffix {
|
|
648
|
+
position: absolute;
|
|
649
|
+
right: 10px;
|
|
650
|
+
font-size: 11px;
|
|
651
|
+
color: #94a3b8;
|
|
652
|
+
pointer-events: none;
|
|
653
|
+
}
|
|
654
|
+
.kh-field {
|
|
655
|
+
display: flex;
|
|
656
|
+
flex-direction: column;
|
|
657
|
+
gap: 4px;
|
|
658
|
+
}
|
|
659
|
+
.kh-field label {
|
|
660
|
+
font-size: 12px;
|
|
661
|
+
color: #64748b;
|
|
662
|
+
}
|
|
663
|
+
.kh-check {
|
|
664
|
+
display: flex;
|
|
665
|
+
align-items: center;
|
|
666
|
+
gap: 6px;
|
|
667
|
+
font-size: 13px;
|
|
668
|
+
color: #334155;
|
|
669
|
+
cursor: pointer;
|
|
670
|
+
}
|
|
671
|
+
.kh-check input {
|
|
672
|
+
width: 15px;
|
|
673
|
+
height: 15px;
|
|
674
|
+
accent-color: #6366f1;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* Mode cards */
|
|
678
|
+
.kh-settings-cards {
|
|
679
|
+
display: flex;
|
|
680
|
+
flex-direction: column;
|
|
681
|
+
gap: 10px;
|
|
682
|
+
}
|
|
683
|
+
.kh-mode-card {
|
|
684
|
+
padding: 12px 14px;
|
|
685
|
+
border: 1px solid #e2e8f0;
|
|
686
|
+
border-radius: 10px;
|
|
687
|
+
background: #fff;
|
|
688
|
+
cursor: pointer;
|
|
689
|
+
transition:
|
|
690
|
+
border-color 0.15s,
|
|
691
|
+
background 0.15s;
|
|
692
|
+
}
|
|
693
|
+
.kh-mode-card:hover {
|
|
694
|
+
border-color: #cbd5e1;
|
|
695
|
+
}
|
|
696
|
+
.kh-mode-card-active {
|
|
697
|
+
border-color: transparent;
|
|
698
|
+
outline: 1.5px solid #6366f1;
|
|
699
|
+
background: #f5f6ff;
|
|
700
|
+
}
|
|
701
|
+
.kh-mode-card-disabled {
|
|
702
|
+
cursor: not-allowed;
|
|
703
|
+
opacity: 0.55;
|
|
704
|
+
}
|
|
705
|
+
.kh-mode-card-disabled:hover {
|
|
706
|
+
border-color: #e2e8f0;
|
|
707
|
+
}
|
|
708
|
+
.kh-settings-cards-locked .kh-mode-card-active {
|
|
709
|
+
outline-color: #94a3b8;
|
|
710
|
+
background: #f8fafc;
|
|
711
|
+
}
|
|
712
|
+
.kh-mode-card-lock {
|
|
713
|
+
margin-left: auto;
|
|
714
|
+
padding: 1px 6px;
|
|
715
|
+
border-radius: 4px;
|
|
716
|
+
background: #f1f5f9;
|
|
717
|
+
color: #64748b;
|
|
718
|
+
font-size: 10px;
|
|
719
|
+
font-weight: 500;
|
|
720
|
+
}
|
|
721
|
+
.kh-mode-card-head {
|
|
722
|
+
display: flex;
|
|
723
|
+
align-items: center;
|
|
724
|
+
gap: 8px;
|
|
725
|
+
font-size: 13px;
|
|
726
|
+
font-weight: 600;
|
|
727
|
+
color: #0f172a;
|
|
728
|
+
}
|
|
729
|
+
.kh-mode-card-icon {
|
|
730
|
+
font-size: 16px;
|
|
731
|
+
}
|
|
732
|
+
.kh-mode-card-title {
|
|
733
|
+
color: #0f172a;
|
|
734
|
+
}
|
|
735
|
+
.kh-mode-badge {
|
|
736
|
+
padding: 1px 6px;
|
|
737
|
+
border-radius: 4px;
|
|
738
|
+
background: #b45309;
|
|
739
|
+
color: #fff;
|
|
740
|
+
font-size: 10px;
|
|
741
|
+
}
|
|
742
|
+
.kh-mode-card-desc {
|
|
743
|
+
margin-top: 4px;
|
|
744
|
+
font-size: 11px;
|
|
745
|
+
color: #64748b;
|
|
746
|
+
line-height: 1.5;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/* Keyword slider row */
|
|
750
|
+
.kh-keyword-row {
|
|
751
|
+
margin-top: 12px;
|
|
752
|
+
display: flex;
|
|
753
|
+
align-items: center;
|
|
754
|
+
gap: 12px;
|
|
755
|
+
}
|
|
756
|
+
.kh-keyword-lbl {
|
|
757
|
+
font-size: 11px;
|
|
758
|
+
color: #64748b;
|
|
759
|
+
width: 80px;
|
|
760
|
+
}
|
|
761
|
+
.kh-keyword-slider {
|
|
762
|
+
flex: 1;
|
|
763
|
+
accent-color: #6366f1;
|
|
764
|
+
}
|
|
765
|
+
.kh-keyword-val {
|
|
766
|
+
min-width: 32px;
|
|
767
|
+
padding: 2px 8px;
|
|
768
|
+
border: 1px solid #e2e8f0;
|
|
769
|
+
border-radius: 6px;
|
|
770
|
+
background: #fff;
|
|
771
|
+
font-size: 12px;
|
|
772
|
+
color: #334155;
|
|
773
|
+
text-align: center;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* Switch */
|
|
777
|
+
.kh-switch {
|
|
778
|
+
position: relative;
|
|
779
|
+
display: inline-block;
|
|
780
|
+
width: 32px;
|
|
781
|
+
height: 18px;
|
|
782
|
+
cursor: pointer;
|
|
783
|
+
}
|
|
784
|
+
.kh-switch input {
|
|
785
|
+
display: none;
|
|
786
|
+
}
|
|
787
|
+
.kh-switch-slider {
|
|
788
|
+
position: absolute;
|
|
789
|
+
inset: 0;
|
|
790
|
+
background: #cbd5e1;
|
|
791
|
+
border-radius: 9px;
|
|
792
|
+
transition: background 0.15s;
|
|
793
|
+
}
|
|
794
|
+
.kh-switch-slider::before {
|
|
795
|
+
content: '';
|
|
796
|
+
position: absolute;
|
|
797
|
+
top: 2px;
|
|
798
|
+
left: 2px;
|
|
799
|
+
width: 14px;
|
|
800
|
+
height: 14px;
|
|
801
|
+
background: #fff;
|
|
802
|
+
border-radius: 50%;
|
|
803
|
+
transition: transform 0.15s;
|
|
804
|
+
}
|
|
805
|
+
.kh-switch input:checked + .kh-switch-slider {
|
|
806
|
+
background: #4f46e5;
|
|
807
|
+
}
|
|
808
|
+
.kh-switch input:checked + .kh-switch-slider::before {
|
|
809
|
+
transform: translateX(14px);
|
|
810
|
+
}
|
|
811
|
+
.kh-switch-lbl {
|
|
812
|
+
margin-left: 8px;
|
|
813
|
+
font-size: 12px;
|
|
814
|
+
color: #64748b;
|
|
815
|
+
}
|
|
816
|
+
.kh-switch-row {
|
|
817
|
+
display: inline-flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
gap: 8px;
|
|
820
|
+
font-size: 12px;
|
|
821
|
+
color: #475569;
|
|
822
|
+
cursor: pointer;
|
|
823
|
+
margin-top: 8px;
|
|
824
|
+
}
|
|
825
|
+
.kh-switch-row input {
|
|
826
|
+
accent-color: #6366f1;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* Retrieve card */
|
|
830
|
+
.kh-retrieve-select-card {
|
|
831
|
+
padding: 12px 14px;
|
|
832
|
+
border-radius: 10px;
|
|
833
|
+
background: #f5f6ff;
|
|
834
|
+
border: 1.5px solid #6366f1;
|
|
835
|
+
}
|
|
836
|
+
.kh-retrieve-card-head {
|
|
837
|
+
display: flex;
|
|
838
|
+
align-items: center;
|
|
839
|
+
gap: 8px;
|
|
840
|
+
font-size: 13px;
|
|
841
|
+
font-weight: 600;
|
|
842
|
+
color: #0f172a;
|
|
843
|
+
}
|
|
844
|
+
.kh-retrieve-icon {
|
|
845
|
+
padding: 3px 8px;
|
|
846
|
+
border-radius: 4px;
|
|
847
|
+
background: #4f46e5;
|
|
848
|
+
color: #fff;
|
|
849
|
+
font-size: 11px;
|
|
850
|
+
}
|
|
851
|
+
.kh-retrieve-desc {
|
|
852
|
+
margin-top: 6px;
|
|
853
|
+
font-size: 11px;
|
|
854
|
+
color: #64748b;
|
|
855
|
+
line-height: 1.5;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/* Sticky footer */
|
|
859
|
+
.kh-settings-sticky {
|
|
860
|
+
position: sticky;
|
|
861
|
+
bottom: 0;
|
|
862
|
+
right: 0;
|
|
863
|
+
margin-top: 24px;
|
|
864
|
+
margin-left: auto;
|
|
865
|
+
padding: 12px 20px;
|
|
866
|
+
background: rgba(255, 255, 255, 0.95);
|
|
867
|
+
backdrop-filter: blur(6px);
|
|
868
|
+
border-top: 1px solid #f1f5f9;
|
|
869
|
+
border-radius: 10px;
|
|
870
|
+
display: flex;
|
|
871
|
+
gap: 10px;
|
|
872
|
+
box-shadow: 0 -4px 12px rgba(15, 23, 42, 0.05);
|
|
873
|
+
}
|
|
874
|
+
.kh-btn {
|
|
875
|
+
padding: 6px 20px;
|
|
876
|
+
border: none;
|
|
877
|
+
border-radius: 6px;
|
|
878
|
+
font-size: 13px;
|
|
879
|
+
font-weight: 500;
|
|
880
|
+
cursor: pointer;
|
|
881
|
+
}
|
|
882
|
+
.kh-btn-primary {
|
|
883
|
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
884
|
+
color: #fff;
|
|
885
|
+
}
|
|
886
|
+
.kh-btn-primary:disabled {
|
|
887
|
+
background: #cbd5e1;
|
|
888
|
+
cursor: not-allowed;
|
|
889
|
+
}
|
|
890
|
+
.kh-btn-secondary {
|
|
891
|
+
background: #f1f5f9;
|
|
892
|
+
color: #475569;
|
|
893
|
+
}
|
|
894
|
+
.kh-btn-secondary:hover {
|
|
895
|
+
background: #e2e8f0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* Fix alignment of the remaining process-cleanup checkbox — the input's
|
|
899
|
+
baseline was drifting below the label text on Windows Chrome. */
|
|
900
|
+
.kh-check {
|
|
901
|
+
align-items: flex-start;
|
|
902
|
+
line-height: 1.4;
|
|
903
|
+
}
|
|
904
|
+
.kh-check input {
|
|
905
|
+
flex-shrink: 0;
|
|
906
|
+
margin-top: 1px;
|
|
907
|
+
}
|
|
908
|
+
</style>
|