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,3391 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * AppDesignDrawer — Dify workspace-style full-screen design drawer.
4
+ *
5
+ * Layout parity with dify workspace / spring-agent-start's workspace/design:
6
+ *
7
+ * ┌──────────────────────────────────────────────────────────────────────┐
8
+ * │ 🤖 app-name [ 保存 ][关闭×] │
9
+ * ├───────────────────┬──────────────────────────────────────────────────┤
10
+ * │ ┌─────────────┐ │ │
11
+ * │ │ 🤖 App icon │ │ 编排 pane │
12
+ * │ │ app-name │ │ ┌───────────────────┬──────────────────────┐ │
13
+ * │ │ description │ │ │ 提示词 │ 调试与预览 │ │
14
+ * │ │ 2024-01-15 │ │ │ 变量 +添加 │ ───── │ │
15
+ * │ │ v1.2 · 运行 │ │ │ 知识库 +添加 │ 聊天消息... │ │
16
+ * │ └─────────────┘ │ │ 工具 +添加 │ ───── │ │
17
+ * │ │ │ 视觉 ⚪⚫ │ [ 和 Bot 聊天 ] │ │
18
+ * │ ⚙ 编排 │ │ │ │ │
19
+ * │ 🔌 访问 API │ └───────────────────┴──────────────────────┘ │
20
+ * │ 📋 日志与标注 │ │
21
+ * │ 📈 监测 │ * For workflow / chatflow modes, the pane is │
22
+ * │ │ replaced by a full-width slot=designer. │
23
+ * │ «« 折叠 │ │
24
+ * └───────────────────┴──────────────────────────────────────────────────┘
25
+ *
26
+ * The drawer is UI-only — persistence + preview streaming go through the host
27
+ * via the emitted `save` and `preview` events so the parent can call whatever
28
+ * backend API it wants.
29
+ */
30
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
31
+
32
+ import {
33
+ ApiOutlined,
34
+ AppstoreOutlined,
35
+ BugOutlined,
36
+ CloudUploadOutlined,
37
+ CodeOutlined,
38
+ CompassOutlined,
39
+ FileTextOutlined,
40
+ LineChartOutlined,
41
+ PlayCircleOutlined,
42
+ RocketOutlined,
43
+ SaveOutlined,
44
+ ShopOutlined,
45
+ } from '@ant-design/icons-vue';
46
+
47
+ import ChatIframePanel from '../../agent-flow/components/ChatIframePanel.vue';
48
+ import type {
49
+ ChatDebugVariable,
50
+ ChatIframeConfig,
51
+ } from '../../agent-flow/components/chat-iframe-types';
52
+ import DatasetPickerModal from '../../knowledge-hub/components/DatasetPickerModal.vue';
53
+ import { useKnowledge } from '../../knowledge-hub/composables/useKnowledge';
54
+ import type { DatasetSummary } from '../../knowledge-hub/types';
55
+ import ModelPickerPopover from '../../provider-hub/components/ModelPickerPopover.vue';
56
+ import type { SelectedModel } from '../../provider-hub/types';
57
+
58
+ import type {
59
+ AgentEntity,
60
+ AgentVariable,
61
+ StudioChatMessage,
62
+ StudioKnowledge,
63
+ StudioModelOption,
64
+ StudioTool,
65
+ } from '../types';
66
+ import type { AppStudioApi } from '../api';
67
+ import DrawerFlowDesigner from '../panels/DrawerFlowDesigner.vue';
68
+ import LogAnnotationPanel from '../panels/LogAnnotationPanel.vue';
69
+ import MonitorPanel from '../panels/MonitorPanel.vue';
70
+ import AgentApiDocs, { type ApiEndpoint } from './AgentApiDocs.vue';
71
+ import VariableEditorModal from './VariableEditorModal.vue';
72
+
73
+ interface Props {
74
+ open: boolean;
75
+ /** The app row being edited — null while the drawer is closed. */
76
+ app: AgentEntity | null;
77
+ /**
78
+ * Raw workflow-graph JSON for flow-mode apps. Provided by the host after it
79
+ * has fetched the draft (Dify parity — the app row itself no longer carries
80
+ * the graph). Leave empty for non-flow modes; the drawer only threads it
81
+ * into the `#designer` slot.
82
+ */
83
+ initialGraphJson?: string;
84
+ /**
85
+ * @deprecated Legacy dropdown data source — left in for backward compat, but
86
+ * the top-right picker now uses {@link ModelPickerPopover}, which pulls the
87
+ * enabled-model catalog itself via provider-hub. Only meaningful if you have
88
+ * an existing caller passing a hand-curated model list.
89
+ */
90
+ models?: StudioModelOption[];
91
+ tools?: StudioTool[];
92
+ knowledgeBases?: StudioKnowledge[];
93
+ /** Passed to the provider-hub picker for multi-tenant deployments. */
94
+ tenantId?: string;
95
+ /**
96
+ * Callback bag for the drawer's built-in panels (日志/标注/监测). Every
97
+ * method is optional — a panel whose backing call is missing degrades to
98
+ * an empty-state placeholder rather than crashing. Consumers who want full
99
+ * custom panels can still override via the {@code #logs} / {@code #monitor}
100
+ * / {@code #designer} slots, in which case {@code api} is ignored.
101
+ */
102
+ api?: AppStudioApi;
103
+ /**
104
+ * 聊天调试 iframe 配置。
105
+ * - 非 flow 模式(agent / chat / completion):右侧「调试与预览」列直接
106
+ * 渲染 iframe,AgentDebugPanel 兜底不再使用;
107
+ * - flow 模式(workflow / chatflow):透传给 FlowDesigner 的
108
+ * 「发起调试」按钮,点击后弹右侧抽屉。
109
+ * 不传 → 保留原有的简单 previewMessages 面板。
110
+ */
111
+ chatConfig?: ChatIframeConfig | null;
112
+ /**
113
+ * 宿主的代理前缀,只用于「访问 API」tab 里生成 curl 示例的 base URL;
114
+ * 组件内部真正发请求走的是 :api 里的回调(AgentAppsPage 会把它拼上
115
+ * /agent-start 命名空间)。默认 `/api`,最终展示成
116
+ * `${window.location.origin}/api/agent-start`。
117
+ */
118
+ apiBase?: string;
119
+ }
120
+
121
+ const props = withDefaults(defineProps<Props>(), {
122
+ models: () => [],
123
+ tools: () => [],
124
+ knowledgeBases: () => [],
125
+ api: () => ({}),
126
+ apiBase: '/api',
127
+ });
128
+
129
+ const emit = defineEmits<{
130
+ (e: 'update:open', v: boolean): void;
131
+ (
132
+ e: 'save',
133
+ payload: {
134
+ appId: string;
135
+ mode: string;
136
+ graphJson?: string;
137
+ name?: string;
138
+ instructions?: string;
139
+ openingStatement?: string;
140
+ /** Plain vendor model name (e.g. {@code qwen3.6-plus}). */
141
+ modelName?: string;
142
+ /** Provider key that owns {@link modelName} (e.g. {@code qwen}). */
143
+ modelProvider?: string;
144
+ /** Structured selection (kept for callers that want the full record). */
145
+ modelSelection?: SelectedModel;
146
+ toolNames?: string[];
147
+ datasetIds?: string[];
148
+ /** Serialised RetrievalConfig — top-k / method / rerank. */
149
+ retrievalConfigJson?: string;
150
+ },
151
+ ): void;
152
+ (
153
+ e: 'preview',
154
+ payload: {
155
+ appId: string;
156
+ query: string;
157
+ onChunk: (text: string) => void;
158
+ onDone: () => void;
159
+ onError: (msg: string) => void;
160
+ },
161
+ ): void;
162
+ /**
163
+ * Publish the current draft as an immutable snapshot (workflow / chatflow
164
+ * only). Host implements `POST /apps/{appId}/workflow/publish` — the drawer
165
+ * doesn't send a graph payload because the backend reads the persisted
166
+ * draft directly.
167
+ */
168
+ (
169
+ e: 'publish',
170
+ payload: { appId: string },
171
+ ): void;
172
+ /** Restore the most recently published snapshot into the draft. */
173
+ (e: 'restore', payload: { appId: string }): void;
174
+ /** Open the app's runtime page in a new tab (Dify parity: "运行"). */
175
+ (e: 'run', payload: { appId: string }): void;
176
+ /** Show the embed-snippet dialog. */
177
+ (e: 'embed', payload: { appId: string }): void;
178
+ /** Open the app inside the Explore marketplace. */
179
+ (e: 'openInExplore', payload: { appId: string }): void;
180
+ /** Publish the app to the tenant's marketplace / plaza. */
181
+ (e: 'publishToMarket', payload: { appId: string }): void;
182
+ /**
183
+ * Open the "编辑基本信息" surface — fired from the top-left brand popover.
184
+ * The drawer only exposes the trigger; the host owns the actual edit modal.
185
+ */
186
+ (e: 'editInfo', payload: { appId: string }): void;
187
+ /** Duplicate the app row — brand popover shortcut. */
188
+ (e: 'duplicate', payload: { appId: string }): void;
189
+ /** Export the app as a Dify-compatible DSL YAML. */
190
+ (e: 'exportDsl', payload: { appId: string }): void;
191
+ }>();
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Nav state — Dify's 4 canonical sections for an app's design page.
195
+ // ---------------------------------------------------------------------------
196
+ type NavKey = 'api' | 'logs' | 'monitor' | 'orchestrate';
197
+ const nav = ref<NavKey>('orchestrate');
198
+ const collapsed = ref(false);
199
+
200
+ const isFlowMode = computed(
201
+ () => props.app?.mode === 'workflow' || props.app?.mode === 'chatflow',
202
+ );
203
+ const modeLabel = computed(() => {
204
+ const m = props.app?.mode ?? 'agent';
205
+ if (m === 'chat') return 'CHAT';
206
+ if (m === 'agent') return 'AGENT';
207
+ if (m === 'workflow') return 'WORKFLOW';
208
+ if (m === 'chatflow') return 'CHATFLOW';
209
+ if (m === 'completion') return 'COMPLETION';
210
+ return String(m).toUpperCase();
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Editable form (staged locally, only committed on 保存).
215
+ // ---------------------------------------------------------------------------
216
+ interface EditableForm {
217
+ name: string;
218
+ instructions: string;
219
+ openingStatement: string;
220
+ /** Plain vendor model name (e.g. {@code qwen3.6-plus}). */
221
+ modelName: string;
222
+ /** Provider key that owns {@link modelName} (e.g. {@code qwen}). */
223
+ modelProvider: string;
224
+ modelSelection: SelectedModel;
225
+ toolNames: string[];
226
+ datasetIds: string[];
227
+ /**
228
+ * Pass-through storage for the app-level RetrievalConfig JSON. The drawer
229
+ * doesn't edit this itself yet (召回设置 opens a placeholder), but we still
230
+ * round-trip it on save so a value written elsewhere isn't lost.
231
+ */
232
+ retrievalConfigJson: string;
233
+ graphJson: string;
234
+ vision: boolean;
235
+ metadataFilter: boolean;
236
+ }
237
+ const form = ref<EditableForm>(makeEmptyForm());
238
+
239
+ function makeEmptyForm(): EditableForm {
240
+ return {
241
+ name: '',
242
+ instructions: '',
243
+ openingStatement: '',
244
+ modelName: '',
245
+ modelProvider: '',
246
+ modelSelection: {},
247
+ toolNames: [],
248
+ datasetIds: [],
249
+ retrievalConfigJson: '',
250
+ graphJson: '',
251
+ vision: false,
252
+ metadataFilter: false,
253
+ };
254
+ }
255
+
256
+ function onPickModel(sel: SelectedModel) {
257
+ form.value.modelSelection = sel;
258
+ form.value.modelName = sel.modelName ?? '';
259
+ form.value.modelProvider = sel.providerName ?? sel.provider ?? '';
260
+ }
261
+
262
+ /**
263
+ * Rebuild the popover's structured selection from the two flat columns the
264
+ * backend carries. LLM is the only type the app-level picker supports, so we
265
+ * default {@code modelType} rather than round-tripping it.
266
+ * <p>
267
+ * Also folds the persisted {@code modelSettingsJson} — {temperature, topP,
268
+ * maxTokens, thinkingMode, enable_thinking, …} — back onto
269
+ * {@code completionParams} so the picker's parameter panel opens with the
270
+ * values the user last saved, not the vendor defaults.
271
+ */
272
+ function hydrateSelection(app: AgentEntity): SelectedModel {
273
+ if (!app.modelName || !app.modelProvider) return {};
274
+ const sel: SelectedModel = {
275
+ providerName: app.modelProvider,
276
+ modelName: app.modelName,
277
+ modelType: 'LLM',
278
+ };
279
+ if (app.modelSettingsJson) {
280
+ try {
281
+ const parsed = JSON.parse(app.modelSettingsJson);
282
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
283
+ sel.completionParams = parsed as Record<
284
+ string,
285
+ boolean | number | string
286
+ >;
287
+ }
288
+ } catch {
289
+ // Ignore malformed JSON — picker will fall back to defaults.
290
+ }
291
+ }
292
+ return sel;
293
+ }
294
+
295
+ watch(
296
+ () => [props.open, props.app, props.initialGraphJson] as const,
297
+ ([open, app, graphJson]) => {
298
+ if (!open || !app) return;
299
+ form.value = {
300
+ name: app.name ?? '',
301
+ instructions: app.instructions ?? '',
302
+ openingStatement: app.openingStatement ?? '',
303
+ modelName: app.modelName ?? '',
304
+ modelProvider: app.modelProvider ?? '',
305
+ modelSelection: hydrateSelection(app),
306
+ toolNames: safeParseArray(app.toolNamesJson),
307
+ datasetIds: safeParseArray(app.datasetIdsJson),
308
+ retrievalConfigJson: app.retrievalConfigJson ?? '',
309
+ // Graph now flows via the `initialGraphJson` prop — the host fetches the
310
+ // draft from /apps/{id}/workflow/draft and passes it in. AgentEntity no
311
+ // longer carries the graph itself.
312
+ graphJson: graphJson ?? '',
313
+ vision: false,
314
+ metadataFilter: false,
315
+ };
316
+ nav.value = 'orchestrate';
317
+ previewMessages.value = [];
318
+ previewInput.value = '';
319
+ },
320
+ { immediate: true },
321
+ );
322
+
323
+ function safeParseArray(json?: string): string[] {
324
+ if (!json) return [];
325
+ try {
326
+ const v = JSON.parse(json);
327
+ return Array.isArray(v) ? v.map(String) : [];
328
+ } catch {
329
+ return [];
330
+ }
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Flow designer wiring — host slots in @agent-start/agent-flow's FlowDesigner
335
+ // via `#designer`, hands us a `registerDesigner(inst)` callback so we can
336
+ // call getFlowInfo() / reloadGraph() without a hard dep.
337
+ // ---------------------------------------------------------------------------
338
+ const designerRef = ref<null | {
339
+ getFlowInfo: () => unknown;
340
+ reloadGraph: (g: unknown) => void;
341
+ }>(null);
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // Save handler.
345
+ //
346
+ // Two-phase Dify parity:
347
+ // - "保存" (submitSave) — persists the current canvas as the mutable draft.
348
+ // App metadata → /agents/{id}; graph → the draft.
349
+ // - "发布" (submitPublish) — saves first, then asks the host to snapshot the
350
+ // draft as an immutable published workflow.
351
+ // ---------------------------------------------------------------------------
352
+ const saving = ref(false);
353
+ const publishing = ref(false);
354
+
355
+ function collectSavePayload() {
356
+ if (!props.app) return null;
357
+ let graphJson: string | undefined;
358
+ if (isFlowMode.value && designerRef.value) {
359
+ try {
360
+ graphJson = JSON.stringify(designerRef.value.getFlowInfo() ?? {});
361
+ } catch {
362
+ graphJson = undefined;
363
+ }
364
+ }
365
+ // Prefer the picker's selection (it also carries the parameter drawer's
366
+ // completionParams — temperature / topP / maxTokens / thinkingMode / …).
367
+ // Falls back to any pre-existing flat modelName/modelProvider values so a
368
+ // save that never re-opened the picker still round-trips the previous model.
369
+ const sel = form.value.modelSelection;
370
+ const modelSettings =
371
+ sel?.completionParams && Object.keys(sel.completionParams).length > 0
372
+ ? sel.completionParams
373
+ : undefined;
374
+ return {
375
+ appId: props.app.id,
376
+ mode: props.app.mode ?? 'agent',
377
+ graphJson,
378
+ name: form.value.name,
379
+ instructions: form.value.instructions,
380
+ openingStatement: form.value.openingStatement,
381
+ modelName: sel?.modelName || form.value.modelName || undefined,
382
+ modelProvider:
383
+ sel?.providerName || sel?.provider || form.value.modelProvider || undefined,
384
+ modelSelection: sel?.providerName ? sel : undefined,
385
+ // Flat blob the backend persists on app_model_configs.configs. Vendor
386
+ // translation of thinkingMode into enable_thinking / think / thinking.type
387
+ // happens server-side in AgentChatOptionsFactory.
388
+ modelSettings,
389
+ toolNames: form.value.toolNames,
390
+ datasetIds: form.value.datasetIds,
391
+ retrievalConfigJson: form.value.retrievalConfigJson || undefined,
392
+ };
393
+ }
394
+
395
+ async function submitSave() {
396
+ const payload = collectSavePayload();
397
+ if (!payload) return;
398
+ saving.value = true;
399
+ try {
400
+ emit('save', payload);
401
+ } finally {
402
+ saving.value = false;
403
+ }
404
+ }
405
+
406
+ async function submitPublish() {
407
+ const payload = collectSavePayload();
408
+ if (!payload) return;
409
+ publishing.value = true;
410
+ try {
411
+ // Persist the current draft first so the snapshot captures what the user
412
+ // sees on screen. Host is responsible for awaiting the save before firing
413
+ // the publish (see /agent/list.vue::onDrawerSave + onDrawerPublish).
414
+ emit('save', payload);
415
+ emit('publish', { appId: payload.appId });
416
+ } finally {
417
+ publishing.value = false;
418
+ }
419
+ }
420
+
421
+ function close() {
422
+ emit('update:open', false);
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // 知识库 (dataset) picker — Dify parity for the 编排 "知识库 + 添加" card.
427
+ //
428
+ // The shared DatasetPickerModal (from knowledge-hub) does the actual selection
429
+ // UI; here we keep a lazily-fetched catalog around so:
430
+ // • the modal can skip its own fetch (we hand it :datasets), and
431
+ // • the card can render the selected rows with names / rerank badges without
432
+ // the caller having to preload {@link StudioKnowledge} objects.
433
+ // ---------------------------------------------------------------------------
434
+ const datasetPickerOpen = ref(false);
435
+ const datasetCatalog = ref<DatasetSummary[]>([]);
436
+ const datasetCatalogLoaded = ref(false);
437
+
438
+ async function loadDatasetCatalog() {
439
+ if (datasetCatalogLoaded.value) return;
440
+ try {
441
+ const { listDatasets } = useKnowledge();
442
+ datasetCatalog.value = await listDatasets(props.tenantId);
443
+ datasetCatalogLoaded.value = true;
444
+ } catch {
445
+ // Swallow — the card still works with just ids, it just shows the raw id
446
+ // as the row label when the catalog can't be loaded.
447
+ datasetCatalog.value = [];
448
+ }
449
+ }
450
+
451
+ function openDatasetPicker() {
452
+ void loadDatasetCatalog();
453
+ datasetPickerOpen.value = true;
454
+ }
455
+
456
+ function onDatasetsPicked(datasets: DatasetSummary[]) {
457
+ // Merge any newly-picked rows into the catalog so the card can render them
458
+ // immediately (in case the fetch returned a stale list).
459
+ const seen = new Set(datasetCatalog.value.map((d) => d.id));
460
+ for (const d of datasets) {
461
+ if (!seen.has(d.id)) datasetCatalog.value.push(d);
462
+ }
463
+ form.value.datasetIds = datasets.map((d) => d.id);
464
+ }
465
+
466
+ function removeDataset(id: string) {
467
+ form.value.datasetIds = form.value.datasetIds.filter((x) => x !== id);
468
+ }
469
+
470
+ /** Render helpers for the selected-list rows. */
471
+ const selectedDatasets = computed(() => {
472
+ const byId = new Map<string, DatasetSummary>();
473
+ for (const d of datasetCatalog.value) byId.set(d.id, d);
474
+ // Also fold in the legacy `knowledgeBases` prop as a fallback lookup so
475
+ // existing hosts that pre-load a slim {id,name} list still see names.
476
+ const fallback = new Map((props.knowledgeBases ?? []).map((k) => [k.id, k]));
477
+ return form.value.datasetIds.map((id) => {
478
+ const full = byId.get(id);
479
+ if (full) return full;
480
+ const slim = fallback.get(id);
481
+ return {
482
+ id,
483
+ name: slim?.name ?? id,
484
+ documentCount: slim?.documentCount,
485
+ } as DatasetSummary;
486
+ });
487
+ });
488
+
489
+ function datasetTechniqueLabel(d: DatasetSummary): null | string {
490
+ if (!d.indexingTechnique) return null;
491
+ return d.indexingTechnique === 'ECONOMY' ? '经济' : '高质量';
492
+ }
493
+ function datasetMethodLabel(d: DatasetSummary): null | string {
494
+ if (!d.retrievalConfigJson) return null;
495
+ try {
496
+ const cfg = JSON.parse(d.retrievalConfigJson) as { method?: string };
497
+ switch (cfg.method) {
498
+ case 'VECTOR':
499
+ return '向量检索';
500
+ case 'FULL_TEXT':
501
+ return '全文检索';
502
+ case 'HYBRID':
503
+ return '混合检索';
504
+ default:
505
+ return null;
506
+ }
507
+ } catch {
508
+ return null;
509
+ }
510
+ }
511
+
512
+ // Preload the catalog the first time the drawer opens with an app that
513
+ // already has attached datasets — otherwise the rows would render as bare
514
+ // ids until the user clicks 添加.
515
+ watch(
516
+ () => [props.open, props.app?.id] as const,
517
+ ([open, appId]) => {
518
+ if (!open || !appId) return;
519
+ if (form.value.datasetIds.length > 0) void loadDatasetCatalog();
520
+ },
521
+ { immediate: true },
522
+ );
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // 发布 dropdown menu — Dify workspace parity.
526
+ //
527
+ // Clicking 发布 opens a menu with:
528
+ // ▸ 最新发布 header (last-published timestamp + 恢复 button)
529
+ // ▸ 发布更新 primary action (Ctrl+Shift+P shortcut)
530
+ // ▸ 运行 / 嵌入网站 / 在"探索"中打开 / 访问 API quick links
531
+ // ▸ 发布到市场 (marketplace push)
532
+ //
533
+ // Each quick-link emits a dedicated event so the host wires it to whatever
534
+ // route / modal makes sense — the drawer stays UI-only.
535
+ // ---------------------------------------------------------------------------
536
+ const publishMenuOpen = ref(false);
537
+ const publishMenuRef = ref<HTMLElement | null>(null);
538
+
539
+ function togglePublishMenu() {
540
+ publishMenuOpen.value = !publishMenuOpen.value;
541
+ }
542
+ function closePublishMenu() {
543
+ publishMenuOpen.value = false;
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Top-left brand popover — click the caret next to the app name to reveal
548
+ // the Dify-style "basic info" panel (编辑信息 / 复制 / 导出 DSL / …更多,
549
+ // then Web App / 后端服务 API / MCP 服务 status blocks).
550
+ // ---------------------------------------------------------------------------
551
+ const brandMenuOpen = ref(false);
552
+ const brandMenuRef = ref<HTMLElement | null>(null);
553
+
554
+ function toggleBrandMenu() {
555
+ brandMenuOpen.value = !brandMenuOpen.value;
556
+ }
557
+ function closeBrandMenu() {
558
+ brandMenuOpen.value = false;
559
+ }
560
+
561
+ /** Public Web-App URL under the drawer's own origin — Dify parity. */
562
+ const webAppUrl = computed(() => {
563
+ if (!props.app?.id) return '';
564
+ return `${window.location.origin}/embed/agent/${props.app.id}`;
565
+ });
566
+ /**
567
+ * Backend REST base — apps run under {@code ${apiBase}/agent-start} on the same host
568
+ * in dev (apiBase 默认 `/api`,最终成 `${origin}/api/agent-start`)。
569
+ */
570
+ const apiBaseUrl = computed(() => {
571
+ if (!props.app?.id) return '';
572
+ const base = props.apiBase.replace(/\/+$/, '');
573
+ return `${window.location.origin}${base}/agent-start`;
574
+ });
575
+
576
+ /**
577
+ * Endpoint samples for the 访问 API tab. Focused on the two entry points a
578
+ * customer integrates against — the OpenAI-compatible chat endpoint (POSTing
579
+ * with a Bearer api-key resolves to the app) and the Dify /chat-messages
580
+ * shim. Rendered by {@link AgentApiDocs}; anchors on the right auto-populate.
581
+ */
582
+ const apiEndpoints = computed<ApiEndpoint[]>(() => {
583
+ const base = apiBaseUrl.value || `${window.location.origin || 'http://localhost:18090'}/api/agent-start`;
584
+ return [
585
+ {
586
+ id: 'chat-messages',
587
+ title: '发起对话(Dify 兼容)',
588
+ method: 'POST',
589
+ path: '/chat-messages',
590
+ description:
591
+ '推荐入口。传 Authorization: Bearer <api-key>;后端根据 key 反查所属应用,无需再传 app_id。',
592
+ code: `curl -X POST '${base}/chat-messages' \\
593
+ -H 'Authorization: Bearer {API_KEY}' \\
594
+ -H 'Content-Type: application/json' \\
595
+ --data-raw '{
596
+ "query": "你好",
597
+ "inputs": {},
598
+ "response_mode": "streaming",
599
+ "user": "end-user-1",
600
+ "conversation_id": ""
601
+ }'`,
602
+ },
603
+ {
604
+ id: 'chat-completions',
605
+ title: '发起对话(OpenAI 兼容)',
606
+ method: 'POST',
607
+ path: '/chat/completions/{app_id}',
608
+ description:
609
+ 'OpenAI SDK 直连风格。Authorization: Bearer <api-key> 必填;path 里的 app_id 会与 key 归属做一致性校验。',
610
+ code: `curl -X POST '${base}/chat/completions/${props.app?.id ?? '{app_id}'}' \\
611
+ -H 'Authorization: Bearer {API_KEY}' \\
612
+ -H 'Content-Type: application/json' \\
613
+ --data-raw '{
614
+ "model": "app-runtime",
615
+ "messages": [{"role": "user", "content": "你好"}],
616
+ "stream": true
617
+ }'`,
618
+ },
619
+ ];
620
+ });
621
+
622
+ /** Web-App / API / MCP 三块的运行开关 —— 目前是 UI-only 状态展示。 */
623
+ const brandWebAppOn = ref(true);
624
+ const brandApiOn = ref(true);
625
+ const brandMcpOn = ref(false);
626
+
627
+ async function copyToClipboard(text: string) {
628
+ if (!text) return;
629
+ try {
630
+ await navigator.clipboard.writeText(text);
631
+ } catch {
632
+ // silently no-op — the drawer stays UI-only
633
+ }
634
+ }
635
+
636
+ function onBrandEdit() {
637
+ if (!props.app) return;
638
+ emit('editInfo', { appId: props.app.id });
639
+ closeBrandMenu();
640
+ }
641
+ function onBrandDuplicate() {
642
+ if (!props.app) return;
643
+ emit('duplicate', { appId: props.app.id });
644
+ closeBrandMenu();
645
+ }
646
+ function onBrandExport() {
647
+ if (!props.app) return;
648
+ emit('exportDsl', { appId: props.app.id });
649
+ closeBrandMenu();
650
+ }
651
+
652
+ const publishedAgo = computed(() => relativeTime(props.app?.updatedAt));
653
+
654
+ function relativeTime(ts?: string | number | null): string {
655
+ if (!ts) return '刚刚';
656
+ const d = new Date(ts);
657
+ const t = d.getTime();
658
+ if (Number.isNaN(t)) return String(ts);
659
+ const diff = Date.now() - t;
660
+ if (diff < 60_000) return '刚刚';
661
+ const mins = Math.floor(diff / 60_000);
662
+ if (mins < 60) return `${mins} 分钟前`;
663
+ const hours = Math.floor(diff / 3_600_000);
664
+ if (hours < 24) return `${hours} 小时前`;
665
+ const days = Math.floor(diff / 86_400_000);
666
+ if (days < 30) return `${days} 天前`;
667
+ const months = Math.floor(days / 30);
668
+ if (months < 12) return `${months} 个月前`;
669
+ return `${Math.floor(months / 12)} 年前`;
670
+ }
671
+
672
+ async function onPublishUpdate() {
673
+ closePublishMenu();
674
+ await submitPublish();
675
+ }
676
+ function onRestore() {
677
+ if (!props.app) return;
678
+ emit('restore', { appId: props.app.id });
679
+ closePublishMenu();
680
+ }
681
+ function onRun() {
682
+ if (!props.app) return;
683
+ emit('run', { appId: props.app.id });
684
+ closePublishMenu();
685
+ }
686
+ function onEmbed() {
687
+ if (!props.app) return;
688
+ emit('embed', { appId: props.app.id });
689
+ closePublishMenu();
690
+ }
691
+ function onOpenInExplore() {
692
+ if (!props.app) return;
693
+ emit('openInExplore', { appId: props.app.id });
694
+ closePublishMenu();
695
+ }
696
+ function onAccessApi() {
697
+ nav.value = 'api';
698
+ closePublishMenu();
699
+ }
700
+ function onPublishToMarket() {
701
+ if (!props.app) return;
702
+ emit('publishToMarket', { appId: props.app.id });
703
+ closePublishMenu();
704
+ }
705
+
706
+ function onDocClick(e: MouseEvent) {
707
+ if (publishMenuOpen.value) {
708
+ const wrap = publishMenuRef.value;
709
+ if (wrap && !wrap.contains(e.target as Node)) closePublishMenu();
710
+ }
711
+ if (brandMenuOpen.value) {
712
+ const wrap = brandMenuRef.value;
713
+ if (wrap && !wrap.contains(e.target as Node)) closeBrandMenu();
714
+ }
715
+ }
716
+ function onDocKeydown(e: KeyboardEvent) {
717
+ if (!props.open) return;
718
+ if (e.key === 'Escape' && publishMenuOpen.value) {
719
+ closePublishMenu();
720
+ return;
721
+ }
722
+ if (e.key === 'Escape' && brandMenuOpen.value) {
723
+ closeBrandMenu();
724
+ return;
725
+ }
726
+ // Ctrl+Shift+P → publish update, matches Dify's shortcut hint.
727
+ const key = e.key.toLowerCase();
728
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === 'p') {
729
+ e.preventDefault();
730
+ void submitPublish();
731
+ }
732
+ }
733
+
734
+ onMounted(() => {
735
+ document.addEventListener('mousedown', onDocClick);
736
+ document.addEventListener('keydown', onDocKeydown);
737
+ });
738
+ onBeforeUnmount(() => {
739
+ document.removeEventListener('mousedown', onDocClick);
740
+ document.removeEventListener('keydown', onDocKeydown);
741
+ });
742
+
743
+ // ---------------------------------------------------------------------------
744
+ // Preview chat (chat / agent / completion modes only).
745
+ // ---------------------------------------------------------------------------
746
+ const previewMessages = ref<StudioChatMessage[]>([]);
747
+ const previewInput = ref('');
748
+ const previewSending = ref(false);
749
+
750
+ function sendPreview() {
751
+ const q = previewInput.value.trim();
752
+ if (!q || !props.app || previewSending.value) return;
753
+ previewMessages.value.push({ role: 'user', content: q });
754
+ previewMessages.value.push({ role: 'assistant', content: '' });
755
+ const idx = previewMessages.value.length - 1;
756
+ previewInput.value = '';
757
+ previewSending.value = true;
758
+ emit('preview', {
759
+ appId: props.app.id,
760
+ query: q,
761
+ onChunk: (text) => {
762
+ const m = previewMessages.value[idx];
763
+ if (m) m.content = (m.content ?? '') + text;
764
+ },
765
+ onDone: () => {
766
+ previewSending.value = false;
767
+ },
768
+ onError: (msg) => {
769
+ const m = previewMessages.value[idx];
770
+ if (m) {
771
+ m.content = msg;
772
+ m.failed = true;
773
+ }
774
+ previewSending.value = false;
775
+ },
776
+ });
777
+ }
778
+
779
+ // ---------------------------------------------------------------------------
780
+ // Variable list (a Dify-parity concept — parses out `{{#user.xxx#}}` variables
781
+ // used by the prompt into a form-editable list). Editing is delegated to
782
+ // VariableEditorModal to match the flow designer's StartNode UX — same shape,
783
+ // same interaction, no more inline single-line inputs.
784
+ // ---------------------------------------------------------------------------
785
+ const variables = ref<AgentVariable[]>([]);
786
+
787
+ const variableEditorRef = ref<InstanceType<typeof VariableEditorModal> | null>(
788
+ null,
789
+ );
790
+
791
+ function openVariableEditor(item?: AgentVariable) {
792
+ variableEditorRef.value?.open(item);
793
+ }
794
+
795
+ function onVariableSubmit(payload: AgentVariable) {
796
+ const items = variables.value;
797
+ const idx = items.findIndex((v) => v.id === payload.id);
798
+ if (idx === -1) items.push(payload);
799
+ else items[idx] = { ...payload };
800
+ }
801
+
802
+ function removeVariable(i: number) {
803
+ variables.value.splice(i, 1);
804
+ }
805
+
806
+ /**
807
+ * 把 AgentVariable 转成 ChatIframePanel 需要的 ChatDebugVariable。
808
+ * 类型统一小写;defaultValue 直接透传,由 ChatIframePanel 内部按类型解析。
809
+ */
810
+ const chatDebugVariables = computed<ChatDebugVariable[]>(() =>
811
+ variables.value
812
+ .filter((v) => !!v.name)
813
+ .map((v) => ({
814
+ name: v.name,
815
+ label: v.label || v.name,
816
+ type: (v.type || 'string').toLowerCase(),
817
+ required: !!v.required,
818
+ description: v.description,
819
+ defaultValue: v.defaultValue,
820
+ })),
821
+ );
822
+
823
+ // ---------------------------------------------------------------------------
824
+ // 「调试」按钮 & 浮层 — 挂在抽屉顶部保存/发布按钮之前。
825
+ // 非 flow 模式的调试聊天在右侧编排面板里常驻,不需要再弹浮层;flow 模式
826
+ // (workflow / chatflow) 画布占满整屏,才用这个按钮开右侧浮层。
827
+ // ---------------------------------------------------------------------------
828
+ const debugPanelOpen = ref(false);
829
+ /**
830
+ * 每次打开按当前时间刷新,用作 ChatIframePanel 的 key,强制重挂 iframe:
831
+ * 用户改完变量之后再点一次「调试」希望是"新一轮",避免旧会话残留。
832
+ */
833
+ const debugSessionSuffix = ref(0);
834
+
835
+ /** 从当前画布上的 START 节点抽出 variables,转成 ChatDebugVariable。 */
836
+ const flowDebugVariables = computed<ChatDebugVariable[]>(() => {
837
+ if (!isFlowMode.value) return [];
838
+ const info = designerRef.value?.getFlowInfo?.() as
839
+ | { nodes?: Array<{ type?: string; data?: any }> }
840
+ | undefined;
841
+ const startNode = info?.nodes?.find(
842
+ (n) => String(n?.type ?? '').toUpperCase() === 'START',
843
+ );
844
+ const raw = startNode?.data?.variables;
845
+ if (!Array.isArray(raw)) return [];
846
+ return raw
847
+ .filter((v: any) => v && v.name)
848
+ .map((v: any) => ({
849
+ name: String(v.name),
850
+ label: v.label ? String(v.label) : String(v.name),
851
+ type: v.type ? String(v.type).toLowerCase() : 'string',
852
+ required: !!v.required,
853
+ description: v.description ? String(v.description) : undefined,
854
+ defaultValue: v.defaultValue ?? v.value,
855
+ }));
856
+ });
857
+
858
+ function openDebug() {
859
+ if (!props.chatConfig?.src) return;
860
+ debugSessionSuffix.value = Date.now();
861
+ debugPanelOpen.value = true;
862
+ }
863
+ function closeDebug() {
864
+ debugPanelOpen.value = false;
865
+ }
866
+
867
+ // ---------------------------------------------------------------------------
868
+ // 浮动 & 拖动 —— 面板浮在抽屉右上层,不加蒙版;用户可拖 header 换位。
869
+ // 位置写入 localStorage,多次打开维持上次位置;resize 视口时夹回可视区。
870
+ // ---------------------------------------------------------------------------
871
+ const DEBUG_POS_KEY = 'agent-start-debug-panel-pos';
872
+ const DEBUG_PANEL_W = 460;
873
+ const DEBUG_PANEL_H_RATIO = 0.86; // 相对抽屉高度的比例
874
+ const DRAG_HANDLE_HEIGHT = 46; // 与 ChatIframePanel .cip-head 保持一致
875
+ const DRAG_MARGIN = 8;
876
+
877
+ const debugPos = ref<{ left: number; top: number } | null>(null);
878
+ const debugDragging = ref(false);
879
+ let debugDragStart:
880
+ | { pointerX: number; pointerY: number; origLeft: number; origTop: number }
881
+ | null = null;
882
+
883
+ function clampDebugPos(pos: { left: number; top: number }): {
884
+ left: number;
885
+ top: number;
886
+ } {
887
+ const w = DEBUG_PANEL_W;
888
+ const h = Math.max(320, Math.round(window.innerHeight * DEBUG_PANEL_H_RATIO));
889
+ const maxLeft = Math.max(DRAG_MARGIN, window.innerWidth - w - DRAG_MARGIN);
890
+ const maxTop = Math.max(DRAG_MARGIN, window.innerHeight - h - DRAG_MARGIN);
891
+ return {
892
+ left: Math.min(Math.max(DRAG_MARGIN, pos.left), maxLeft),
893
+ top: Math.min(Math.max(DRAG_MARGIN, pos.top), maxTop),
894
+ };
895
+ }
896
+
897
+ function loadDebugPos(): { left: number; top: number } | null {
898
+ try {
899
+ const raw = window.localStorage.getItem(DEBUG_POS_KEY);
900
+ if (!raw) return null;
901
+ const parsed = JSON.parse(raw);
902
+ if (
903
+ !parsed ||
904
+ typeof parsed.left !== 'number' ||
905
+ typeof parsed.top !== 'number'
906
+ ) {
907
+ return null;
908
+ }
909
+ return clampDebugPos(parsed);
910
+ } catch {
911
+ return null;
912
+ }
913
+ }
914
+
915
+ function saveDebugPos(pos: { left: number; top: number }) {
916
+ try {
917
+ window.localStorage.setItem(DEBUG_POS_KEY, JSON.stringify(pos));
918
+ } catch {
919
+ /* localStorage 不可用则退化为仅本次会话生效 */
920
+ }
921
+ }
922
+
923
+ /** 默认落在抽屉右上角(右缘留 24px、header 下 80px) */
924
+ function defaultDebugPos(): { left: number; top: number } {
925
+ return clampDebugPos({
926
+ left: window.innerWidth - DEBUG_PANEL_W - 24,
927
+ top: 80,
928
+ });
929
+ }
930
+
931
+ function onDebugPanelPointerDown(e: PointerEvent) {
932
+ // 只从"顶部 handle 区域"起手,避开按钮 / iframe / 变量气泡卡里的输入
933
+ const el = e.target as HTMLElement | null;
934
+ if (!el) return;
935
+ if (
936
+ el.closest(
937
+ 'button, input, textarea, select, a, iframe, .cip-var-panel',
938
+ )
939
+ ) {
940
+ return;
941
+ }
942
+ const panelEl = e.currentTarget as HTMLElement;
943
+ const rect = panelEl.getBoundingClientRect();
944
+ if (e.clientY - rect.top > DRAG_HANDLE_HEIGHT) return;
945
+ // 首次拖动时把面板从 CSS 的 right 定位切成 left/top 定位
946
+ if (!debugPos.value) {
947
+ debugPos.value = clampDebugPos({ left: rect.left, top: rect.top });
948
+ }
949
+ debugDragStart = {
950
+ pointerX: e.clientX,
951
+ pointerY: e.clientY,
952
+ origLeft: debugPos.value.left,
953
+ origTop: debugPos.value.top,
954
+ };
955
+ debugDragging.value = true;
956
+ panelEl.setPointerCapture?.(e.pointerId);
957
+ e.preventDefault();
958
+ }
959
+
960
+ function onDebugPanelPointerMove(e: PointerEvent) {
961
+ if (!debugDragging.value || !debugDragStart) return;
962
+ debugPos.value = clampDebugPos({
963
+ left: debugDragStart.origLeft + (e.clientX - debugDragStart.pointerX),
964
+ top: debugDragStart.origTop + (e.clientY - debugDragStart.pointerY),
965
+ });
966
+ }
967
+
968
+ function onDebugPanelPointerUp(e: PointerEvent) {
969
+ if (!debugDragging.value) return;
970
+ debugDragging.value = false;
971
+ debugDragStart = null;
972
+ (e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
973
+ if (debugPos.value) saveDebugPos(debugPos.value);
974
+ }
975
+
976
+ /** 打开时装填初始坐标(之前拖过 → 恢复;否则默认右上角) */
977
+ watch(
978
+ () => debugPanelOpen.value,
979
+ (open) => {
980
+ if (open) {
981
+ debugPos.value = loadDebugPos() ?? defaultDebugPos();
982
+ }
983
+ },
984
+ );
985
+
986
+ function onWindowResizeForDebugPos() {
987
+ if (!debugPanelOpen.value || !debugPos.value) return;
988
+ const clamped = clampDebugPos(debugPos.value);
989
+ if (clamped.left !== debugPos.value.left || clamped.top !== debugPos.value.top) {
990
+ debugPos.value = clamped;
991
+ saveDebugPos(clamped);
992
+ }
993
+ }
994
+
995
+ onMounted(() => {
996
+ window.addEventListener('resize', onWindowResizeForDebugPos);
997
+ });
998
+ onBeforeUnmount(() => {
999
+ window.removeEventListener('resize', onWindowResizeForDebugPos);
1000
+ });
1001
+
1002
+ function variableTypeLabel(type: AgentVariable['type']): string {
1003
+ switch (type) {
1004
+ case 'boolean':
1005
+ return 'Boolean';
1006
+ case 'number':
1007
+ return 'Number';
1008
+ case 'array':
1009
+ return 'Array';
1010
+ case 'object':
1011
+ return 'Object';
1012
+ case 'file':
1013
+ return 'File';
1014
+ default:
1015
+ return 'String';
1016
+ }
1017
+ }
1018
+ </script>
1019
+
1020
+ <template>
1021
+ <!-- disabled="!open": 抽屉关闭时 Teleport 直接跳过挂载,DOM 就留在组件树内
1022
+ (且被 v-if 吃掉,不渲染任何东西)。避免 <KeepAlive> deactivate 时抽屉
1023
+ 残留在 body 上遮住后续路由 —— 表现为切换路由后全屏空白,只能 F5 才好。
1024
+ Vue3 <Teleport :disabled> 就是为此场景设计。-->
1025
+ <Teleport to="body" :disabled="!open">
1026
+ <div v-if="open" class="dr-mask">
1027
+ <div class="dr-panel">
1028
+ <!-- ==== Top header bar ==== -->
1029
+ <div class="dr-header">
1030
+ <div class="dr-brand">
1031
+ <div
1032
+ class="dr-icon-sm"
1033
+ :style="{ background: app?.iconBackground || '#EEF4FF' }"
1034
+ >
1035
+ {{ app?.icon || '🤖' }}
1036
+ </div>
1037
+ <div class="dr-title-inline">
1038
+ <span class="dr-title">{{ form.name || app?.name }}</span>
1039
+ <span class="dr-mode-badge">{{ modeLabel }}</span>
1040
+ </div>
1041
+ </div>
1042
+
1043
+ <div class="dr-header-right">
1044
+ <!-- Model picker (right-side of the header) — hidden for workflow
1045
+ mode: the model is selected per-node inside the canvas (each
1046
+ LLM/Agent node has its own model config), so a drawer-level
1047
+ picker would be misleading. Chat / chatflow / agent modes still
1048
+ show it as the app-wide default. -->
1049
+ <div v-if="!isFlowMode" class="dr-header-model">
1050
+ <ModelPickerPopover
1051
+ :model-value="form.modelSelection"
1052
+ model-type="LLM"
1053
+ :tenant-id="tenantId"
1054
+ placement="bottomRight"
1055
+ :width="380"
1056
+ placeholder="选择模型"
1057
+ @update:model-value="onPickModel"
1058
+ />
1059
+ </div>
1060
+
1061
+ <div class="dr-actions">
1062
+ <!-- 「调试」— 只在 flow 模式露出(agent/chat 已经在右侧编排面板里
1063
+ 常驻 ChatIframePanel,不需要再叠层)。宿主没配 chatConfig 时
1064
+ disabled + 提示 —— 避免用户点了没反应。 -->
1065
+ <button
1066
+ v-if="isFlowMode"
1067
+ class="dr-btn dr-btn-secondary"
1068
+ :class="{ 'dr-btn-active': debugPanelOpen }"
1069
+ :disabled="!chatConfig?.src"
1070
+ :title="chatConfig?.src ? '打开聊天调试' : '未配置调试 iframe:宿主传入 :chat-config 即可启用'"
1071
+ @click="openDebug"
1072
+ >
1073
+ <BugOutlined class="dr-btn-icon dr-btn-icon-debug" />
1074
+ <span>调试</span>
1075
+ </button>
1076
+ <button
1077
+ class="dr-btn dr-btn-secondary"
1078
+ :disabled="saving || publishing"
1079
+ @click="submitSave"
1080
+ >
1081
+ <SaveOutlined class="dr-btn-icon" />
1082
+ <span>{{ saving ? '保存中…' : '保存' }}</span>
1083
+ </button>
1084
+ <div ref="publishMenuRef" class="dr-publish-wrap">
1085
+ <button
1086
+ class="dr-btn dr-btn-primary dr-publish-btn"
1087
+ :class="{ 'dr-publish-btn-open': publishMenuOpen }"
1088
+ :disabled="saving || publishing"
1089
+ @click="togglePublishMenu"
1090
+ >
1091
+ <RocketOutlined class="dr-btn-icon" />
1092
+ <span>{{ publishing ? '发布中…' : '发布' }}</span>
1093
+ <span class="dr-publish-caret" aria-hidden="true">▾</span>
1094
+ </button>
1095
+
1096
+ <div v-if="publishMenuOpen" class="dr-publish-menu">
1097
+ <div class="dr-publish-head">
1098
+ <div class="dr-publish-head-title">最新发布</div>
1099
+ <div class="dr-publish-head-row">
1100
+ <span class="dr-publish-head-time">
1101
+ 发布于 {{ publishedAgo }}
1102
+ </span>
1103
+ <button class="dr-publish-restore" @click="onRestore">
1104
+ 恢复
1105
+ </button>
1106
+ </div>
1107
+ </div>
1108
+
1109
+ <button
1110
+ class="dr-publish-primary"
1111
+ :disabled="saving || publishing"
1112
+ @click="onPublishUpdate"
1113
+ >
1114
+ <span class="dr-publish-primary-main">
1115
+ <CloudUploadOutlined class="dr-publish-primary-icon" />
1116
+ <span>{{ publishing ? '发布中…' : '发布更新' }}</span>
1117
+ </span>
1118
+ <span class="dr-publish-shortcut">
1119
+ <kbd>Ctrl</kbd>
1120
+ <kbd>⇧</kbd>
1121
+ <kbd>P</kbd>
1122
+ </span>
1123
+ </button>
1124
+
1125
+ <div class="dr-publish-divider" />
1126
+
1127
+ <button class="dr-publish-item" @click="onRun">
1128
+ <PlayCircleOutlined class="dr-publish-item-icon" />
1129
+ <span class="dr-publish-item-label">运行</span>
1130
+ <span class="dr-publish-item-arrow">↗</span>
1131
+ </button>
1132
+ <button class="dr-publish-item" @click="onEmbed">
1133
+ <CodeOutlined class="dr-publish-item-icon" />
1134
+ <span class="dr-publish-item-label">嵌入网站</span>
1135
+ <span class="dr-publish-item-arrow">↗</span>
1136
+ </button>
1137
+ <button class="dr-publish-item" @click="onOpenInExplore">
1138
+ <CompassOutlined class="dr-publish-item-icon" />
1139
+ <span class="dr-publish-item-label">在“探索”中打开</span>
1140
+ <span class="dr-publish-item-arrow">↗</span>
1141
+ </button>
1142
+ <button class="dr-publish-item" @click="onAccessApi">
1143
+ <ApiOutlined class="dr-publish-item-icon" />
1144
+ <span class="dr-publish-item-label">访问 API</span>
1145
+ <span class="dr-publish-item-arrow">↗</span>
1146
+ </button>
1147
+
1148
+ <div class="dr-publish-divider" />
1149
+
1150
+ <button class="dr-publish-item" @click="onPublishToMarket">
1151
+ <ShopOutlined class="dr-publish-item-icon" />
1152
+ <span class="dr-publish-item-label">发布到市场</span>
1153
+ <span class="dr-publish-item-arrow">↗</span>
1154
+ </button>
1155
+ </div>
1156
+ </div>
1157
+ <button class="dr-btn dr-btn-ghost" title="关闭" @click="close">
1158
+ <span class="dr-btn-icon dr-btn-icon-x" aria-hidden="true">✕</span>
1159
+ <span>关闭</span>
1160
+ </button>
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+
1165
+ <!-- ==== Two-column body: LEFT rail + MAIN content ==== -->
1166
+ <div class="dr-body">
1167
+ <!-- ── LEFT rail: app info card + 4-nav ── -->
1168
+ <aside class="dr-rail" :class="{ 'dr-rail-collapsed': collapsed }">
1169
+ <!-- App info card — click the ⋯ icon (top-right on hover) to open
1170
+ the basic-info popover (Web App / API / MCP status). -->
1171
+ <div
1172
+ ref="brandMenuRef"
1173
+ class="dr-info-card"
1174
+ :class="{ 'dr-info-card-active': brandMenuOpen }"
1175
+ >
1176
+ <button
1177
+ v-show="!collapsed"
1178
+ type="button"
1179
+ class="dr-info-trigger"
1180
+ :class="{ 'dr-info-trigger-open': brandMenuOpen }"
1181
+ :title="brandMenuOpen ? '收起基本信息' : '展开基本信息'"
1182
+ @click.stop="toggleBrandMenu"
1183
+ >
1184
+
1185
+ </button>
1186
+
1187
+ <div
1188
+ class="dr-info-avatar"
1189
+ :style="{ background: app?.iconBackground || '#EEF4FF' }"
1190
+ >
1191
+ {{ app?.icon || '🤖' }}
1192
+ </div>
1193
+ <div v-show="!collapsed" class="dr-info-body">
1194
+ <div class="dr-info-name">{{ app?.name }}</div>
1195
+ <div class="dr-info-desc">
1196
+ {{ app?.description || 'AI 驱动的智能应用' }}
1197
+ </div>
1198
+ <div class="dr-info-meta">
1199
+ <span>🕒 {{ app?.updatedAt || '刚刚' }}</span>
1200
+ </div>
1201
+ <div class="dr-info-tags">
1202
+ <span class="dr-tag dr-tag-live">● 运行中</span>
1203
+ <span class="dr-tag dr-tag-ver">v1.0</span>
1204
+ </div>
1205
+ </div>
1206
+ <div v-show="collapsed" class="dr-info-collapsed-dot" />
1207
+
1208
+ <div v-if="brandMenuOpen" class="dr-brand-panel" @click.stop>
1209
+ <!-- Header: icon + name + mode -->
1210
+ <div class="dr-bp-head">
1211
+ <div
1212
+ class="dr-bp-icon"
1213
+ :style="{ background: app?.iconBackground || '#EEF4FF' }"
1214
+ >
1215
+ {{ app?.icon || '🤖' }}
1216
+ </div>
1217
+ <div class="dr-bp-title-wrap">
1218
+ <div class="dr-bp-title">{{ form.name || app?.name }}</div>
1219
+ <div class="dr-bp-mode">{{ modeLabel }}</div>
1220
+ </div>
1221
+ </div>
1222
+
1223
+ <!-- Quick actions row -->
1224
+ <div class="dr-bp-actions">
1225
+ <button class="dr-bp-action" @click="onBrandEdit">
1226
+ <span class="dr-bp-action-icon">✎</span>
1227
+ <span>编辑信息</span>
1228
+ </button>
1229
+ <button class="dr-bp-action" @click="onBrandDuplicate">
1230
+ <span class="dr-bp-action-icon">⧉</span>
1231
+ <span>复制</span>
1232
+ </button>
1233
+ <button class="dr-bp-action" @click="onBrandExport">
1234
+ <span class="dr-bp-action-icon">⇩</span>
1235
+ <span>导出 DSL</span>
1236
+ </button>
1237
+ <button class="dr-bp-action">
1238
+ <span class="dr-bp-action-icon">⋯</span>
1239
+ <span>更多</span>
1240
+ </button>
1241
+ </div>
1242
+
1243
+ <!-- Web App section -->
1244
+ <section class="dr-bp-section">
1245
+ <div class="dr-bp-section-head">
1246
+ <span class="dr-bp-section-icon dr-bp-section-icon-web">◧</span>
1247
+ <span class="dr-bp-section-title">Web App</span>
1248
+ <span class="dr-bp-status" :class="{ on: brandWebAppOn }">
1249
+ ● {{ brandWebAppOn ? '运行中' : '已停用' }}
1250
+ </span>
1251
+ <label class="dr-bp-switch">
1252
+ <input type="checkbox" v-model="brandWebAppOn" />
1253
+ <span class="dr-bp-switch-track" />
1254
+ </label>
1255
+ </div>
1256
+ <div class="dr-bp-section-label">公开访问 URL</div>
1257
+ <div class="dr-bp-url-row">
1258
+ <span class="dr-bp-url" :title="webAppUrl">{{ webAppUrl }}</span>
1259
+ <button
1260
+ class="dr-bp-icon-btn"
1261
+ title="复制链接"
1262
+ @click="copyToClipboard(webAppUrl)"
1263
+ >
1264
+
1265
+ </button>
1266
+ <button class="dr-bp-icon-btn" title="二维码">▦</button>
1267
+ <button class="dr-bp-icon-btn" title="刷新">↻</button>
1268
+ </div>
1269
+ <div class="dr-bp-links">
1270
+ <button class="dr-bp-link">↗ 启动</button>
1271
+ <button class="dr-bp-link">◱ 嵌入</button>
1272
+ <button class="dr-bp-link">🎨 定制化</button>
1273
+ <button class="dr-bp-link">⚙ 设置</button>
1274
+ </div>
1275
+ </section>
1276
+
1277
+ <!-- Backend API section -->
1278
+ <section class="dr-bp-section">
1279
+ <div class="dr-bp-section-head">
1280
+ <span class="dr-bp-section-icon dr-bp-section-icon-api">🖳</span>
1281
+ <span class="dr-bp-section-title">后端服务 API</span>
1282
+ <span class="dr-bp-status" :class="{ on: brandApiOn }">
1283
+ ● {{ brandApiOn ? '运行中' : '已停用' }}
1284
+ </span>
1285
+ <label class="dr-bp-switch">
1286
+ <input type="checkbox" v-model="brandApiOn" />
1287
+ <span class="dr-bp-switch-track" />
1288
+ </label>
1289
+ </div>
1290
+ <div class="dr-bp-section-label">API 访问地址</div>
1291
+ <div class="dr-bp-url-row">
1292
+ <span class="dr-bp-url" :title="apiBaseUrl">{{ apiBaseUrl }}</span>
1293
+ <button
1294
+ class="dr-bp-icon-btn"
1295
+ title="复制"
1296
+ @click="copyToClipboard(apiBaseUrl)"
1297
+ >
1298
+
1299
+ </button>
1300
+ </div>
1301
+ <div class="dr-bp-links">
1302
+ <button class="dr-bp-link">🔑 API 密钥</button>
1303
+ <button class="dr-bp-link" @click="onAccessApi">
1304
+ 📄 查看 API 文档
1305
+ </button>
1306
+ </div>
1307
+ </section>
1308
+
1309
+ <!-- MCP Server section -->
1310
+ <section class="dr-bp-section">
1311
+ <div class="dr-bp-section-head">
1312
+ <span class="dr-bp-section-icon dr-bp-section-icon-mcp">◈</span>
1313
+ <span class="dr-bp-section-title">MCP 服务</span>
1314
+ <span class="dr-bp-status" :class="{ on: brandMcpOn }">
1315
+ ● {{ brandMcpOn ? '运行中' : '已停用' }}
1316
+ </span>
1317
+ <label class="dr-bp-switch">
1318
+ <input type="checkbox" v-model="brandMcpOn" />
1319
+ <span class="dr-bp-switch-track" />
1320
+ </label>
1321
+ </div>
1322
+ <div class="dr-bp-section-label">服务器的 URL</div>
1323
+ <div class="dr-bp-url-row">
1324
+ <span class="dr-bp-url dr-bp-url-muted">
1325
+ {{ brandMcpOn ? apiBaseUrl + '/mcp' : '···········' }}
1326
+ </span>
1327
+ </div>
1328
+ <div class="dr-bp-links">
1329
+ <button class="dr-bp-link">✎ 添加描述</button>
1330
+ </div>
1331
+ </section>
1332
+ </div>
1333
+ </div>
1334
+
1335
+ <!-- Nav items -->
1336
+ <nav class="dr-nav">
1337
+ <button
1338
+ v-for="item in [
1339
+ { id: 'orchestrate', icon: AppstoreOutlined, label: '编排' },
1340
+ { id: 'api', icon: ApiOutlined, label: '访问 API' },
1341
+ { id: 'logs', icon: FileTextOutlined, label: '日志与标注' },
1342
+ { id: 'monitor', icon: LineChartOutlined, label: '监测' },
1343
+ ] as const"
1344
+ :key="item.id"
1345
+ :class="['dr-nav-item', { on: nav === item.id }]"
1346
+ @click="nav = item.id"
1347
+ >
1348
+ <component :is="item.icon" class="dr-nav-icon" />
1349
+ <span v-show="!collapsed" class="dr-nav-label">
1350
+ {{ item.label }}
1351
+ </span>
1352
+ </button>
1353
+ </nav>
1354
+
1355
+ <button
1356
+ class="dr-rail-collapse"
1357
+ @click="collapsed = !collapsed"
1358
+ :title="collapsed ? '展开' : '折叠'"
1359
+ >
1360
+ {{ collapsed ? '»' : '«' }}
1361
+ </button>
1362
+ </aside>
1363
+
1364
+ <!-- ── MAIN content ── -->
1365
+ <main class="dr-main">
1366
+ <!-- 编排 tab -->
1367
+ <div v-if="nav === 'orchestrate'" class="dr-orch">
1368
+ <!-- Flow modes → full-width designer. Built-in
1369
+ DrawerFlowDesigner covers 99% of use cases; a host that
1370
+ wants full control can still override via the
1371
+ {@code #designer} slot. -->
1372
+ <div v-if="isFlowMode" class="dr-flow-slot">
1373
+ <slot
1374
+ name="designer"
1375
+ :register-designer="
1376
+ (inst: any) => (designerRef = inst)
1377
+ "
1378
+ :initial-graph-json="form.graphJson"
1379
+ >
1380
+ <DrawerFlowDesigner
1381
+ :initial-graph-json="form.graphJson"
1382
+ :app-id="app?.id"
1383
+ :app-mode="app?.mode"
1384
+ :register-designer="
1385
+ (inst: any) => (designerRef = inst)
1386
+ "
1387
+ />
1388
+ </slot>
1389
+ </div>
1390
+
1391
+ <!-- Non-flow modes → 50/50 orchestrate + preview -->
1392
+ <div v-else class="dr-chat-split">
1393
+ <!-- LEFT: config cards -->
1394
+ <section class="dr-config-col">
1395
+ <!-- 提示词 -->
1396
+ <div class="dr-card">
1397
+ <div class="dr-card-head">
1398
+ <span class="dr-card-title">
1399
+ <span class="dr-card-title-dot" />
1400
+ 提示词
1401
+ </span>
1402
+ <button class="dr-link-btn">↗ 生成</button>
1403
+ </div>
1404
+ <textarea
1405
+ v-model="form.instructions"
1406
+ class="dr-prompt-textarea"
1407
+ placeholder="在这里编写你的提示词,输入「{」插入变量,输入「/」插入提示内容块"
1408
+ rows="8"
1409
+ />
1410
+ </div>
1411
+
1412
+ <!-- 变量 —— 与流程设计里 StartNode 的「输入变量」同源,
1413
+ 用弹窗编辑,避免行内输入的信息密度低 / 类型不可选。 -->
1414
+ <div class="dr-card">
1415
+ <div class="dr-card-head">
1416
+ <span class="dr-card-title">
1417
+ <span class="dr-card-title-dot" />
1418
+ 变量
1419
+ </span>
1420
+ <button
1421
+ class="dr-link-btn dr-link-btn-solid"
1422
+ title="添加变量"
1423
+ @click="openVariableEditor()"
1424
+ >
1425
+ + 添加
1426
+ </button>
1427
+ </div>
1428
+ <div v-if="variables.length === 0" class="dr-var-empty">
1429
+ 尚未配置变量,点击右上角 + 添加。变量可通过
1430
+ <code v-pre>{{ name }}</code>
1431
+ 在提示词或开场白中引用。
1432
+ </div>
1433
+ <div v-else class="dr-var-list">
1434
+ <div
1435
+ v-for="(v, i) in variables"
1436
+ :key="v.id ?? i"
1437
+ class="dr-var-item"
1438
+ >
1439
+ <div class="dr-var-left">
1440
+ <span class="dr-var-icon" aria-hidden="true">◈</span>
1441
+ <div class="dr-var-meta">
1442
+ <div class="dr-var-label" :title="v.label || v.name">
1443
+ {{ v.label || v.name || '未命名变量' }}
1444
+ </div>
1445
+ <div class="dr-var-name" :title="v.name">
1446
+ {{ v.name || '—' }}
1447
+ </div>
1448
+ </div>
1449
+ </div>
1450
+ <div class="dr-var-right">
1451
+ <span class="dr-var-tag">
1452
+ {{ variableTypeLabel(v.type) }}
1453
+ </span>
1454
+ <span
1455
+ v-if="v.required"
1456
+ class="dr-var-tag dr-var-tag-warn"
1457
+ >
1458
+ 必填
1459
+ </span>
1460
+ <button
1461
+ class="dr-icon-btn"
1462
+ title="编辑"
1463
+ @click="openVariableEditor(v)"
1464
+ >
1465
+
1466
+ </button>
1467
+ <button
1468
+ class="dr-icon-btn dr-icon-btn-danger"
1469
+ title="删除"
1470
+ @click="removeVariable(i)"
1471
+ >
1472
+
1473
+ </button>
1474
+ </div>
1475
+ </div>
1476
+ </div>
1477
+ </div>
1478
+
1479
+ <!-- 知识库 -->
1480
+ <div class="dr-card">
1481
+ <div class="dr-card-head">
1482
+ <span class="dr-card-title">
1483
+ <span class="dr-card-title-dot" />
1484
+ 知识库
1485
+ </span>
1486
+ <div class="dr-card-actions">
1487
+ <button class="dr-link-btn dr-link-muted">
1488
+ 🔧 召回设置
1489
+ </button>
1490
+ <button class="dr-link-btn" @click="openDatasetPicker">
1491
+ + 添加
1492
+ </button>
1493
+ </div>
1494
+ </div>
1495
+ <div
1496
+ v-if="form.datasetIds.length === 0"
1497
+ class="dr-card-empty"
1498
+ >
1499
+ 您可以导入知识库作为上下文
1500
+ </div>
1501
+ <div v-else class="dr-ds-list">
1502
+ <div
1503
+ v-for="d in selectedDatasets"
1504
+ :key="d.id"
1505
+ class="dr-ds-row"
1506
+ >
1507
+ <span class="dr-ds-row-icon" aria-hidden="true">
1508
+ 📚
1509
+ </span>
1510
+ <span class="dr-ds-row-name" :title="d.name">
1511
+ {{ d.name }}
1512
+ </span>
1513
+ <span class="dr-ds-row-tags">
1514
+ <span
1515
+ v-if="datasetTechniqueLabel(d)"
1516
+ class="dr-ds-tag"
1517
+ >
1518
+ {{ datasetTechniqueLabel(d) }}
1519
+ </span>
1520
+ <span
1521
+ v-if="datasetMethodLabel(d)"
1522
+ class="dr-ds-tag dr-ds-tag-muted"
1523
+ >
1524
+ {{ datasetMethodLabel(d) }}
1525
+ </span>
1526
+ </span>
1527
+ <button
1528
+ class="dr-icon-btn"
1529
+ title="移除"
1530
+ @click="removeDataset(d.id)"
1531
+ >
1532
+
1533
+ </button>
1534
+ </div>
1535
+ </div>
1536
+
1537
+ <!-- 元数据过滤 -->
1538
+ <div class="dr-inline-row">
1539
+ <span class="dr-inline-label">元数据过滤</span>
1540
+ <select v-model="form.metadataFilter" class="dr-mini-select">
1541
+ <option :value="false">禁用</option>
1542
+ <option :value="true">启用</option>
1543
+ </select>
1544
+ </div>
1545
+ </div>
1546
+
1547
+ <!-- 工具 -->
1548
+ <div class="dr-card">
1549
+ <div class="dr-card-head">
1550
+ <span class="dr-card-title">
1551
+ <span class="dr-card-title-dot" />
1552
+ 工具
1553
+ </span>
1554
+ <button class="dr-link-btn">+ 添加</button>
1555
+ </div>
1556
+ <div v-if="tools.length === 0" class="dr-card-empty">
1557
+ 您可以添加工具来扩展智能体能力
1558
+ </div>
1559
+ <div v-else class="dr-chip-list">
1560
+ <label
1561
+ v-for="t in tools"
1562
+ :key="t.name"
1563
+ class="dr-chip"
1564
+ :class="{ on: form.toolNames.includes(t.name) }"
1565
+ >
1566
+ <input
1567
+ type="checkbox"
1568
+ :value="t.name"
1569
+ :checked="form.toolNames.includes(t.name)"
1570
+ @change="
1571
+ (e: Event) => {
1572
+ const on = (e.target as HTMLInputElement).checked;
1573
+ if (on) form.toolNames.push(t.name);
1574
+ else
1575
+ form.toolNames = form.toolNames.filter(
1576
+ (n) => n !== t.name,
1577
+ );
1578
+ }
1579
+ "
1580
+ />
1581
+ <span>{{ t.icon || '🔧' }} {{ t.name }}</span>
1582
+ </label>
1583
+ </div>
1584
+ </div>
1585
+
1586
+ <!-- 视觉 -->
1587
+ <div class="dr-card dr-card-inline">
1588
+ <div class="dr-card-head dr-card-head-inline">
1589
+ <span class="dr-card-title">
1590
+ <span class="dr-card-title-dot" />
1591
+ 视觉
1592
+ </span>
1593
+ <div class="dr-card-actions">
1594
+ <button class="dr-link-btn dr-link-muted">⚙ 设置</button>
1595
+ <label class="dr-switch">
1596
+ <input v-model="form.vision" type="checkbox" />
1597
+ <span class="dr-switch-slider" />
1598
+ </label>
1599
+ </div>
1600
+ </div>
1601
+ </div>
1602
+ </section>
1603
+
1604
+ <!-- RIGHT: preview col
1605
+ 宿主传了 chatConfig → 用 ChatIframePanel 承载 antd-react-chat
1606
+ iframe(变量抽出为可填参数);未传则保留旧的简单预览面板。 -->
1607
+ <section class="dr-preview-col">
1608
+ <ChatIframePanel
1609
+ v-if="chatConfig?.src"
1610
+ :src="chatConfig.src"
1611
+ :params="chatConfig.params"
1612
+ :context="chatConfig.context"
1613
+ :title="chatConfig.title || '调试与预览'"
1614
+ :session-key="chatConfig.sessionKey || `agent-${app?.id || 'draft'}`"
1615
+ :variables="chatDebugVariables"
1616
+ :closable="false"
1617
+ />
1618
+ <template v-else>
1619
+ <div class="dr-preview-head">
1620
+ <span class="dr-card-title">
1621
+ <span class="dr-card-title-dot" />
1622
+ 调试与预览
1623
+ </span>
1624
+ <button
1625
+ class="dr-icon-btn"
1626
+ title="清空"
1627
+ @click="previewMessages = []"
1628
+ >
1629
+
1630
+ </button>
1631
+ </div>
1632
+ <div class="dr-preview-scroll">
1633
+ <div
1634
+ v-if="previewMessages.length === 0"
1635
+ class="dr-preview-empty"
1636
+ >
1637
+ 发一条消息试试 —— 上面的编辑先「发布」后才会作用于预览。
1638
+ </div>
1639
+ <div
1640
+ v-for="(m, i) in previewMessages"
1641
+ :key="i"
1642
+ class="dr-msg"
1643
+ :class="`dr-msg-${m.role}`"
1644
+ >
1645
+ <div class="dr-msg-role">
1646
+ {{ m.role === 'user' ? '你' : '助手' }}
1647
+ </div>
1648
+ <div
1649
+ class="dr-msg-body"
1650
+ :class="{ 'dr-msg-failed': m.failed }"
1651
+ >
1652
+ {{ m.content || (m.role === 'assistant' ? '…' : '') }}
1653
+ </div>
1654
+ </div>
1655
+ </div>
1656
+ <div class="dr-preview-composer">
1657
+ <input
1658
+ v-model="previewInput"
1659
+ class="dr-input"
1660
+ placeholder="和 Bot 聊天"
1661
+ :disabled="previewSending"
1662
+ @keydown.enter.prevent="sendPreview"
1663
+ />
1664
+ <button
1665
+ class="dr-send-btn"
1666
+ :disabled="previewSending || !previewInput.trim()"
1667
+ @click="sendPreview"
1668
+ >
1669
+
1670
+ </button>
1671
+ </div>
1672
+ </template>
1673
+ </section>
1674
+ </div>
1675
+ </div>
1676
+
1677
+ <!-- 访问 API — Dify-parity docs page with API-key management. -->
1678
+ <div v-else-if="nav === 'api'" class="dr-api-slot">
1679
+ <AgentApiDocs
1680
+ :base-url="apiBaseUrl"
1681
+ :endpoints="apiEndpoints"
1682
+ :app-id="app?.id"
1683
+ :api="api"
1684
+ />
1685
+ </div>
1686
+
1687
+ <!-- 日志与标注 — built-in panel wired to props.api. Host can
1688
+ override via the `#logs` slot for full customisation. -->
1689
+ <div v-else-if="nav === 'logs'" class="dr-logs-slot">
1690
+ <slot name="logs" :app="app">
1691
+ <LogAnnotationPanel :app="app" :api="api" />
1692
+ </slot>
1693
+ </div>
1694
+
1695
+ <!-- 监测 — built-in panel wired to props.api. Host can override
1696
+ via the `#monitor` slot. -->
1697
+ <div v-else-if="nav === 'monitor'" class="dr-monitor-slot">
1698
+ <slot name="monitor" :app="app">
1699
+ <MonitorPanel :app="app" :api="api" />
1700
+ </slot>
1701
+ </div>
1702
+ </main>
1703
+ </div>
1704
+
1705
+ <!-- Dataset picker — shared with the workflow KNOWLEDGE_RETRIEVAL
1706
+ node config so both entry points use the same UX. -->
1707
+ <DatasetPickerModal
1708
+ v-model:open="datasetPickerOpen"
1709
+ :initial-selected-ids="form.datasetIds"
1710
+ :datasets="datasetCatalogLoaded ? datasetCatalog : undefined"
1711
+ :tenant-id="tenantId"
1712
+ @submit="onDatasetsPicked"
1713
+ />
1714
+
1715
+ <!-- Variable editor — mirrors the StartNode input-variable modal in
1716
+ the flow designer so the two surfaces feel identical. -->
1717
+ <VariableEditorModal
1718
+ ref="variableEditorRef"
1719
+ @submit="onVariableSubmit"
1720
+ />
1721
+
1722
+ <!-- 调试浮层:浮在抽屉右上,无蒙版 —— 下面的画布/表单仍可交互。
1723
+ 顶部 header 可拖动(避开按钮/输入/iframe),位置写入 localStorage。
1724
+ 变量清单:flow 模式从 designerRef 的 START 节点抽;非 flow 走
1725
+ variables 列表(当前不会走这个分支,保留兼容)。 -->
1726
+ <transition name="dr-debug-pop">
1727
+ <div
1728
+ v-if="debugPanelOpen && chatConfig?.src"
1729
+ class="dr-debug-panel"
1730
+ :class="{ 'dr-debug-panel-dragging': debugDragging }"
1731
+ :style="
1732
+ debugPos
1733
+ ? { left: debugPos.left + 'px', top: debugPos.top + 'px' }
1734
+ : undefined
1735
+ "
1736
+ @pointerdown="onDebugPanelPointerDown"
1737
+ @pointermove="onDebugPanelPointerMove"
1738
+ @pointerup="onDebugPanelPointerUp"
1739
+ @pointercancel="onDebugPanelPointerUp"
1740
+ >
1741
+ <ChatIframePanel
1742
+ :key="debugSessionSuffix"
1743
+ :src="chatConfig.src"
1744
+ :params="chatConfig.params"
1745
+ :context="chatConfig.context"
1746
+ :title="chatConfig.title || `调试:${app?.name || ''}`"
1747
+ :session-key="
1748
+ chatConfig.sessionKey ||
1749
+ `${app?.mode || 'app'}-${app?.id || 'draft'}-${debugSessionSuffix}`
1750
+ "
1751
+ :variables="isFlowMode ? flowDebugVariables : chatDebugVariables"
1752
+ @close="closeDebug"
1753
+ />
1754
+ </div>
1755
+ </transition>
1756
+ </div>
1757
+ </div>
1758
+ </Teleport>
1759
+ </template>
1760
+
1761
+ <style scoped>
1762
+ /* ==== Mask + panel (full-screen) ==== */
1763
+ .dr-mask {
1764
+ position: fixed;
1765
+ inset: 0;
1766
+ z-index: 1050;
1767
+ background: #fff;
1768
+ animation: dr-fade-in 0.14s ease-out;
1769
+ }
1770
+ .dr-panel {
1771
+ width: 100vw;
1772
+ height: 100vh;
1773
+ display: flex;
1774
+ flex-direction: column;
1775
+ background: #f8fafc;
1776
+ overflow: hidden;
1777
+ }
1778
+ @keyframes dr-fade-in {
1779
+ from {
1780
+ opacity: 0;
1781
+ }
1782
+ to {
1783
+ opacity: 1;
1784
+ }
1785
+ }
1786
+
1787
+ /* ==== Top header ==== */
1788
+ .dr-header {
1789
+ flex: none;
1790
+ height: 52px;
1791
+ display: flex;
1792
+ align-items: center;
1793
+ padding: 0 20px;
1794
+ background: #fff;
1795
+ border-bottom: 1px solid #e5e7eb;
1796
+ gap: 16px;
1797
+ }
1798
+ .dr-brand {
1799
+ display: flex;
1800
+ align-items: center;
1801
+ gap: 10px;
1802
+ min-width: 0;
1803
+ }
1804
+ .dr-icon-sm {
1805
+ width: 28px;
1806
+ height: 28px;
1807
+ display: flex;
1808
+ align-items: center;
1809
+ justify-content: center;
1810
+ border-radius: 6px;
1811
+ font-size: 15px;
1812
+ flex-shrink: 0;
1813
+ }
1814
+ .dr-title-inline {
1815
+ display: flex;
1816
+ align-items: center;
1817
+ gap: 8px;
1818
+ min-width: 0;
1819
+ }
1820
+ .dr-title {
1821
+ font-size: 14px;
1822
+ font-weight: 600;
1823
+ color: #0f172a;
1824
+ }
1825
+ .dr-mode-badge {
1826
+ padding: 1px 6px;
1827
+ font-size: 10px;
1828
+ font-weight: 500;
1829
+ color: #4338ca;
1830
+ background: #eef2ff;
1831
+ border-radius: 4px;
1832
+ letter-spacing: 0.05em;
1833
+ }
1834
+
1835
+ /* ----- Info-card brand popover — anchored to the left-rail info card,
1836
+ * pops out to the right so the 380px panel doesn't clip the 224px rail. ----- */
1837
+ /* Panel container — soft floating card with a slight gradient wash and a
1838
+ * more generous shadow so it visually separates from the drawer chrome. */
1839
+ .dr-brand-panel {
1840
+ position: absolute;
1841
+ top: 0;
1842
+ left: calc(100% + 12px);
1843
+ width: 400px;
1844
+ background: linear-gradient(180deg, #ffffff 0%, #fbfbfe 100%);
1845
+ border: 1px solid #e5e7eb;
1846
+ border-radius: 14px;
1847
+ box-shadow:
1848
+ 0 20px 40px -12px rgba(15, 23, 42, 0.16),
1849
+ 0 4px 12px rgba(15, 23, 42, 0.06);
1850
+ padding: 14px;
1851
+ z-index: 60;
1852
+ animation: dr-fade-in 0.14s ease-out;
1853
+ text-align: left;
1854
+ display: flex;
1855
+ flex-direction: column;
1856
+ gap: 12px;
1857
+ }
1858
+
1859
+ /* Panel header ─ app icon + name + mode chip */
1860
+ .dr-bp-head {
1861
+ display: flex;
1862
+ align-items: center;
1863
+ gap: 12px;
1864
+ padding: 2px 2px 12px;
1865
+ border-bottom: 1px solid #f1f5f9;
1866
+ }
1867
+ .dr-bp-icon {
1868
+ width: 40px;
1869
+ height: 40px;
1870
+ display: flex;
1871
+ align-items: center;
1872
+ justify-content: center;
1873
+ border-radius: 10px;
1874
+ font-size: 22px;
1875
+ flex-shrink: 0;
1876
+ box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.04);
1877
+ }
1878
+ .dr-bp-title-wrap {
1879
+ min-width: 0;
1880
+ flex: 1;
1881
+ display: flex;
1882
+ flex-direction: column;
1883
+ gap: 4px;
1884
+ }
1885
+ .dr-bp-title {
1886
+ font-size: 14px;
1887
+ font-weight: 600;
1888
+ color: #0f172a;
1889
+ white-space: nowrap;
1890
+ overflow: hidden;
1891
+ text-overflow: ellipsis;
1892
+ line-height: 1.2;
1893
+ }
1894
+ .dr-bp-mode {
1895
+ align-self: flex-start;
1896
+ padding: 1px 7px;
1897
+ font-size: 10px;
1898
+ font-weight: 600;
1899
+ letter-spacing: 0.06em;
1900
+ color: #4338ca;
1901
+ background: #eef2ff;
1902
+ border-radius: 4px;
1903
+ line-height: 1.5;
1904
+ }
1905
+
1906
+ /* Quick action tiles ─ 4 up, icon-over-label, lifts on hover */
1907
+ .dr-bp-actions {
1908
+ display: grid;
1909
+ grid-template-columns: repeat(4, 1fr);
1910
+ gap: 6px;
1911
+ padding: 0;
1912
+ border-bottom: none;
1913
+ }
1914
+ .dr-bp-action {
1915
+ display: flex;
1916
+ flex-direction: column;
1917
+ align-items: center;
1918
+ justify-content: center;
1919
+ gap: 6px;
1920
+ padding: 10px 4px;
1921
+ border: 1px solid #f1f5f9;
1922
+ background: #fafbfc;
1923
+ border-radius: 10px;
1924
+ color: #475569;
1925
+ font-size: 11px;
1926
+ font-weight: 500;
1927
+ cursor: pointer;
1928
+ transition:
1929
+ background 0.15s ease,
1930
+ border-color 0.15s ease,
1931
+ color 0.15s ease,
1932
+ transform 0.15s ease,
1933
+ box-shadow 0.15s ease;
1934
+ }
1935
+ .dr-bp-action:hover {
1936
+ border-color: #c7d2fe;
1937
+ background: #eef2ff;
1938
+ color: #4338ca;
1939
+ transform: translateY(-1px);
1940
+ box-shadow: 0 3px 8px rgba(99, 102, 241, 0.1);
1941
+ }
1942
+ .dr-bp-action-icon {
1943
+ width: 24px;
1944
+ height: 24px;
1945
+ display: inline-flex;
1946
+ align-items: center;
1947
+ justify-content: center;
1948
+ border-radius: 7px;
1949
+ background: #eef2ff;
1950
+ color: #4338ca;
1951
+ font-size: 12px;
1952
+ line-height: 1;
1953
+ transition:
1954
+ background 0.15s ease,
1955
+ color 0.15s ease;
1956
+ }
1957
+ .dr-bp-action:hover .dr-bp-action-icon {
1958
+ background: #4338ca;
1959
+ color: #fff;
1960
+ }
1961
+
1962
+ /* Section cards — each channel (Web App / API / MCP) sits in its own
1963
+ * light-tinted card with a status chip + toggle in the head row. */
1964
+ .dr-bp-section {
1965
+ padding: 12px;
1966
+ background: #fafbfc;
1967
+ border: 1px solid #f1f5f9;
1968
+ border-radius: 10px;
1969
+ transition:
1970
+ background 0.15s ease,
1971
+ border-color 0.15s ease;
1972
+ }
1973
+ .dr-bp-section:hover {
1974
+ background: #f8fafc;
1975
+ border-color: #e2e8f0;
1976
+ }
1977
+ .dr-bp-section-head {
1978
+ display: flex;
1979
+ align-items: center;
1980
+ gap: 8px;
1981
+ margin-bottom: 10px;
1982
+ }
1983
+ .dr-bp-section-icon {
1984
+ width: 24px;
1985
+ height: 24px;
1986
+ display: inline-flex;
1987
+ align-items: center;
1988
+ justify-content: center;
1989
+ border-radius: 6px;
1990
+ font-size: 12px;
1991
+ flex-shrink: 0;
1992
+ }
1993
+ .dr-bp-section-icon-web {
1994
+ background: #dbeafe;
1995
+ color: #1d4ed8;
1996
+ }
1997
+ .dr-bp-section-icon-api {
1998
+ background: #e0e7ff;
1999
+ color: #4338ca;
2000
+ }
2001
+ .dr-bp-section-icon-mcp {
2002
+ background: #ffedd5;
2003
+ color: #c2410c;
2004
+ }
2005
+ .dr-bp-section-title {
2006
+ font-size: 13px;
2007
+ font-weight: 600;
2008
+ color: #0f172a;
2009
+ }
2010
+ .dr-bp-status {
2011
+ display: inline-flex;
2012
+ align-items: center;
2013
+ gap: 3px;
2014
+ margin-left: 2px;
2015
+ padding: 1px 8px;
2016
+ font-size: 10px;
2017
+ font-weight: 500;
2018
+ color: #64748b;
2019
+ background: #f1f5f9;
2020
+ border-radius: 999px;
2021
+ }
2022
+ .dr-bp-status.on {
2023
+ color: #15803d;
2024
+ background: #dcfce7;
2025
+ }
2026
+ .dr-bp-switch {
2027
+ position: relative;
2028
+ margin-left: auto;
2029
+ width: 32px;
2030
+ height: 18px;
2031
+ cursor: pointer;
2032
+ flex-shrink: 0;
2033
+ }
2034
+ .dr-bp-switch input {
2035
+ position: absolute;
2036
+ opacity: 0;
2037
+ inset: 0;
2038
+ cursor: pointer;
2039
+ z-index: 1;
2040
+ }
2041
+ .dr-bp-switch-track {
2042
+ position: absolute;
2043
+ inset: 0;
2044
+ border-radius: 999px;
2045
+ background: #cbd5e1;
2046
+ transition: background 0.15s ease;
2047
+ }
2048
+ .dr-bp-switch-track::after {
2049
+ content: '';
2050
+ position: absolute;
2051
+ top: 2px;
2052
+ left: 2px;
2053
+ width: 14px;
2054
+ height: 14px;
2055
+ border-radius: 50%;
2056
+ background: #fff;
2057
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.18);
2058
+ transition: transform 0.15s ease;
2059
+ }
2060
+ .dr-bp-switch input:checked + .dr-bp-switch-track {
2061
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
2062
+ }
2063
+ .dr-bp-switch input:checked + .dr-bp-switch-track::after {
2064
+ transform: translateX(14px);
2065
+ }
2066
+
2067
+ /* URL row — reads as an input field with trailing icon buttons */
2068
+ .dr-bp-section-label {
2069
+ margin-bottom: 6px;
2070
+ font-size: 11px;
2071
+ font-weight: 500;
2072
+ color: #64748b;
2073
+ }
2074
+ .dr-bp-url-row {
2075
+ display: flex;
2076
+ align-items: center;
2077
+ gap: 2px;
2078
+ padding: 5px 6px 5px 10px;
2079
+ background: #fff;
2080
+ border: 1px solid #e5e7eb;
2081
+ border-radius: 8px;
2082
+ margin-bottom: 10px;
2083
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
2084
+ }
2085
+ .dr-bp-url-row:hover {
2086
+ border-color: #c7d2fe;
2087
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.06);
2088
+ }
2089
+ .dr-bp-url {
2090
+ flex: 1;
2091
+ min-width: 0;
2092
+ font-size: 12px;
2093
+ color: #0f172a;
2094
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
2095
+ white-space: nowrap;
2096
+ overflow: hidden;
2097
+ text-overflow: ellipsis;
2098
+ }
2099
+ .dr-bp-url-muted {
2100
+ color: #94a3b8;
2101
+ letter-spacing: 0.05em;
2102
+ }
2103
+ .dr-bp-icon-btn {
2104
+ width: 24px;
2105
+ height: 24px;
2106
+ border: none;
2107
+ background: transparent;
2108
+ border-radius: 6px;
2109
+ color: #94a3b8;
2110
+ font-size: 12px;
2111
+ cursor: pointer;
2112
+ display: inline-flex;
2113
+ align-items: center;
2114
+ justify-content: center;
2115
+ flex-shrink: 0;
2116
+ transition: background 0.15s ease, color 0.15s ease;
2117
+ }
2118
+ .dr-bp-icon-btn:hover {
2119
+ background: #eef2ff;
2120
+ color: #4338ca;
2121
+ }
2122
+
2123
+ /* Link pills — rounded chip style, so they visually group as related
2124
+ * shortcuts rather than dissolve into flat text buttons. */
2125
+ .dr-bp-links {
2126
+ display: flex;
2127
+ flex-wrap: wrap;
2128
+ gap: 6px;
2129
+ }
2130
+ .dr-bp-link {
2131
+ padding: 4px 10px;
2132
+ border: 1px solid #e5e7eb;
2133
+ background: #fff;
2134
+ border-radius: 999px;
2135
+ color: #475569;
2136
+ font-size: 11px;
2137
+ font-weight: 500;
2138
+ cursor: pointer;
2139
+ display: inline-flex;
2140
+ align-items: center;
2141
+ gap: 4px;
2142
+ transition:
2143
+ background 0.15s ease,
2144
+ border-color 0.15s ease,
2145
+ color 0.15s ease;
2146
+ }
2147
+ .dr-bp-link:hover {
2148
+ background: #eef2ff;
2149
+ border-color: #c7d2fe;
2150
+ color: #4338ca;
2151
+ }
2152
+
2153
+ /* Right-aligned wrapper for the model picker + action buttons. Grouping them
2154
+ * under a single margin-left: auto keeps both flush right — two separate auto
2155
+ * margins would split the flex free space and push the picker to the middle.
2156
+ * Works uniformly whether or not the picker is rendered. */
2157
+ .dr-header-right {
2158
+ margin-left: auto;
2159
+ display: flex;
2160
+ align-items: center;
2161
+ gap: 12px;
2162
+ }
2163
+ .dr-header-model {
2164
+ display: flex;
2165
+ align-items: center;
2166
+ min-width: 260px;
2167
+ max-width: 320px;
2168
+ }
2169
+ /* The picker's default trigger is a full-width block; keep it compact enough
2170
+ * to sit next to the 发布 / 关闭 buttons without pushing the app title around. */
2171
+ .dr-header-model :deep(.ph-mp-trigger) {
2172
+ width: 100%;
2173
+ min-height: 36px;
2174
+ padding: 4px 10px;
2175
+ }
2176
+ .dr-actions {
2177
+ display: flex;
2178
+ gap: 8px;
2179
+ }
2180
+ .dr-btn {
2181
+ height: 32px;
2182
+ padding: 0 14px;
2183
+ display: inline-flex;
2184
+ align-items: center;
2185
+ justify-content: center;
2186
+ gap: 6px;
2187
+ font-size: 13px;
2188
+ font-weight: 500;
2189
+ line-height: 1;
2190
+ border: 1px solid transparent;
2191
+ border-radius: 8px;
2192
+ cursor: pointer;
2193
+ transition:
2194
+ background 0.15s ease,
2195
+ border-color 0.15s ease,
2196
+ color 0.15s ease,
2197
+ box-shadow 0.15s ease,
2198
+ opacity 0.15s ease;
2199
+ }
2200
+ .dr-btn:focus-visible {
2201
+ outline: none;
2202
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
2203
+ }
2204
+ .dr-btn-icon {
2205
+ font-size: 13px;
2206
+ line-height: 1;
2207
+ display: inline-flex;
2208
+ align-items: center;
2209
+ justify-content: center;
2210
+ }
2211
+ /* 调试 button — bug icon uses success-green so it reads as "run/test",
2212
+ even when the surrounding secondary button is muted. */
2213
+ .dr-btn-icon-debug {
2214
+ color: #16a34a;
2215
+ }
2216
+ .dr-btn:disabled .dr-btn-icon-debug {
2217
+ color: #94a3b8;
2218
+ }
2219
+ .dr-btn-icon-x {
2220
+ font-size: 12px;
2221
+ color: #94a3b8;
2222
+ transition: color 0.15s ease;
2223
+ }
2224
+
2225
+ /* 发布 — primary gradient CTA */
2226
+ .dr-btn-primary {
2227
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
2228
+ color: #fff;
2229
+ box-shadow:
2230
+ 0 1px 2px rgba(79, 70, 229, 0.24),
2231
+ 0 1px 1px rgba(15, 23, 42, 0.06);
2232
+ }
2233
+ .dr-btn-primary:hover:not(:disabled) {
2234
+ box-shadow:
2235
+ 0 3px 8px rgba(79, 70, 229, 0.32),
2236
+ 0 1px 2px rgba(15, 23, 42, 0.08);
2237
+ transform: translateY(-0.5px);
2238
+ }
2239
+ .dr-btn-primary:disabled {
2240
+ background: #cbd5e1;
2241
+ box-shadow: none;
2242
+ cursor: not-allowed;
2243
+ }
2244
+
2245
+ /* 保存 — outlined button, hover picks up an indigo hint to echo the CTA */
2246
+ .dr-btn-secondary {
2247
+ background: #fff;
2248
+ color: #334155;
2249
+ border-color: #e2e8f0;
2250
+ }
2251
+ .dr-btn-secondary:hover:not(:disabled) {
2252
+ background: #eef2ff;
2253
+ border-color: #c7d2fe;
2254
+ color: #4338ca;
2255
+ }
2256
+ .dr-btn-secondary:active:not(:disabled) {
2257
+ background: #e0e7ff;
2258
+ }
2259
+ .dr-btn-secondary:disabled {
2260
+ color: #94a3b8;
2261
+ background: #f8fafc;
2262
+ cursor: not-allowed;
2263
+ }
2264
+ /* 「调试」按钮打开中 —— 用轻微填充 + 边框色标记正在活动 */
2265
+ .dr-btn-active {
2266
+ background: #eef2ff;
2267
+ border-color: #a5b4fc;
2268
+ color: #4338ca;
2269
+ }
2270
+
2271
+ /* 调试浮层:浮在抽屉右上,无蒙版,下面内容仍可点/滚 */
2272
+ .dr-debug-panel {
2273
+ position: fixed; /* 用 fixed 让坐标以视口为准,跨 iframe / scroll 都稳定 */
2274
+ top: 80px;
2275
+ right: 24px;
2276
+ z-index: 60;
2277
+ width: 460px;
2278
+ max-width: 92vw;
2279
+ height: 86vh;
2280
+ max-height: calc(100vh - 96px);
2281
+ box-shadow:
2282
+ 0 20px 50px rgba(15, 23, 42, 0.24),
2283
+ 0 4px 12px rgba(15, 23, 42, 0.1);
2284
+ border-radius: 12px;
2285
+ overflow: hidden;
2286
+ touch-action: none; /* 阻止移动端手势卷动,让 pointermove 走拖动 */
2287
+ }
2288
+ /* 用了 left/top 内联样式时,覆盖默认的 right 定位 */
2289
+ .dr-debug-panel[style*='left'] {
2290
+ right: auto;
2291
+ }
2292
+ /* 顶部 header 区(第一屏 46px)呈 grab 光标,提示可拖 */
2293
+ .dr-debug-panel::before {
2294
+ content: '';
2295
+ position: absolute;
2296
+ top: 0;
2297
+ left: 0;
2298
+ right: 0;
2299
+ height: 46px;
2300
+ cursor: grab;
2301
+ pointer-events: none; /* 只做视觉提示,事件仍由子元素接收 */
2302
+ }
2303
+ .dr-debug-panel-dragging {
2304
+ user-select: none;
2305
+ transition: none !important;
2306
+ }
2307
+ .dr-debug-panel-dragging::before {
2308
+ cursor: grabbing;
2309
+ }
2310
+
2311
+ .dr-debug-pop-enter-active,
2312
+ .dr-debug-pop-leave-active {
2313
+ transition:
2314
+ transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
2315
+ opacity 0.2s ease;
2316
+ transform-origin: top right;
2317
+ }
2318
+ .dr-debug-pop-enter-from,
2319
+ .dr-debug-pop-leave-to {
2320
+ opacity: 0;
2321
+ transform: translateY(-8px) scale(0.98);
2322
+ }
2323
+
2324
+ /* 关闭 — ghost button, quiet by default */
2325
+ .dr-btn-ghost {
2326
+ background: transparent;
2327
+ color: #64748b;
2328
+ }
2329
+ .dr-btn-ghost:hover {
2330
+ background: #f1f5f9;
2331
+ color: #0f172a;
2332
+ }
2333
+ .dr-btn-ghost:hover .dr-btn-icon-x {
2334
+ color: #0f172a;
2335
+ }
2336
+ .dr-btn-ghost:active {
2337
+ background: #e2e8f0;
2338
+ }
2339
+
2340
+ /* ==== Publish split-button + dropdown menu (Dify parity) ==== */
2341
+ .dr-publish-wrap {
2342
+ position: relative;
2343
+ display: inline-flex;
2344
+ }
2345
+ .dr-publish-btn {
2346
+ display: inline-flex;
2347
+ align-items: center;
2348
+ gap: 6px;
2349
+ padding-right: 10px;
2350
+ }
2351
+ .dr-publish-caret {
2352
+ font-size: 10px;
2353
+ opacity: 0.85;
2354
+ transition: transform 0.15s ease;
2355
+ display: inline-block;
2356
+ }
2357
+ .dr-publish-btn-open .dr-publish-caret {
2358
+ transform: rotate(180deg);
2359
+ }
2360
+ .dr-publish-menu {
2361
+ position: absolute;
2362
+ top: calc(100% + 8px);
2363
+ right: 0;
2364
+ z-index: 20;
2365
+ width: 280px;
2366
+ padding: 12px;
2367
+ background: #fff;
2368
+ border: 1px solid #e5e7eb;
2369
+ border-radius: 12px;
2370
+ box-shadow:
2371
+ 0 12px 40px rgba(15, 23, 42, 0.12),
2372
+ 0 2px 6px rgba(15, 23, 42, 0.06);
2373
+ display: flex;
2374
+ flex-direction: column;
2375
+ gap: 6px;
2376
+ animation: dr-publish-in 0.12s ease-out;
2377
+ }
2378
+ @keyframes dr-publish-in {
2379
+ from {
2380
+ opacity: 0;
2381
+ transform: translateY(-4px);
2382
+ }
2383
+ to {
2384
+ opacity: 1;
2385
+ transform: translateY(0);
2386
+ }
2387
+ }
2388
+ .dr-publish-head {
2389
+ padding: 4px 6px 8px;
2390
+ display: flex;
2391
+ flex-direction: column;
2392
+ gap: 6px;
2393
+ }
2394
+ .dr-publish-head-title {
2395
+ font-size: 11px;
2396
+ font-weight: 500;
2397
+ color: #94a3b8;
2398
+ letter-spacing: 0.02em;
2399
+ }
2400
+ .dr-publish-head-row {
2401
+ display: flex;
2402
+ align-items: center;
2403
+ justify-content: space-between;
2404
+ gap: 8px;
2405
+ }
2406
+ .dr-publish-head-time {
2407
+ font-size: 13px;
2408
+ color: #334155;
2409
+ }
2410
+ .dr-publish-restore {
2411
+ padding: 3px 10px;
2412
+ font-size: 12px;
2413
+ color: #4338ca;
2414
+ background: #fff;
2415
+ border: 1px solid #c7d2fe;
2416
+ border-radius: 6px;
2417
+ cursor: pointer;
2418
+ transition: background 0.15s;
2419
+ }
2420
+ .dr-publish-restore:hover {
2421
+ background: #eef2ff;
2422
+ }
2423
+ .dr-publish-primary {
2424
+ margin-top: 2px;
2425
+ padding: 9px 14px;
2426
+ display: flex;
2427
+ align-items: center;
2428
+ justify-content: space-between;
2429
+ gap: 8px;
2430
+ font-size: 13px;
2431
+ font-weight: 500;
2432
+ color: #fff;
2433
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
2434
+ border: none;
2435
+ border-radius: 8px;
2436
+ cursor: pointer;
2437
+ transition: opacity 0.15s;
2438
+ }
2439
+ .dr-publish-primary:hover:not(:disabled) {
2440
+ opacity: 0.92;
2441
+ }
2442
+ .dr-publish-primary:disabled {
2443
+ background: #cbd5e1;
2444
+ cursor: not-allowed;
2445
+ }
2446
+ .dr-publish-shortcut {
2447
+ display: inline-flex;
2448
+ align-items: center;
2449
+ gap: 3px;
2450
+ }
2451
+ .dr-publish-shortcut kbd {
2452
+ min-width: 18px;
2453
+ height: 18px;
2454
+ padding: 0 5px;
2455
+ display: inline-flex;
2456
+ align-items: center;
2457
+ justify-content: center;
2458
+ font-family: inherit;
2459
+ font-size: 10px;
2460
+ font-weight: 500;
2461
+ color: #e0e7ff;
2462
+ background: rgba(255, 255, 255, 0.18);
2463
+ border-radius: 4px;
2464
+ }
2465
+ .dr-publish-divider {
2466
+ height: 1px;
2467
+ margin: 4px 0;
2468
+ background: #f1f5f9;
2469
+ }
2470
+ .dr-publish-item {
2471
+ display: flex;
2472
+ align-items: center;
2473
+ gap: 10px;
2474
+ padding: 8px 10px;
2475
+ font-size: 13px;
2476
+ color: #334155;
2477
+ background: transparent;
2478
+ border: none;
2479
+ border-radius: 8px;
2480
+ cursor: pointer;
2481
+ text-align: left;
2482
+ transition: background 0.12s;
2483
+ }
2484
+ .dr-publish-item:hover {
2485
+ background: #f8fafc;
2486
+ }
2487
+ .dr-publish-item-icon {
2488
+ width: 18px;
2489
+ color: #64748b;
2490
+ font-size: 15px;
2491
+ display: inline-flex;
2492
+ align-items: center;
2493
+ justify-content: center;
2494
+ flex-shrink: 0;
2495
+ }
2496
+ .dr-publish-primary-main {
2497
+ display: inline-flex;
2498
+ align-items: center;
2499
+ gap: 8px;
2500
+ min-width: 0;
2501
+ }
2502
+ .dr-publish-primary-icon {
2503
+ font-size: 14px;
2504
+ display: inline-flex;
2505
+ align-items: center;
2506
+ justify-content: center;
2507
+ flex-shrink: 0;
2508
+ }
2509
+ .dr-publish-item-label {
2510
+ flex: 1;
2511
+ min-width: 0;
2512
+ white-space: nowrap;
2513
+ overflow: hidden;
2514
+ text-overflow: ellipsis;
2515
+ }
2516
+ .dr-publish-item-arrow {
2517
+ color: #cbd5e1;
2518
+ font-size: 12px;
2519
+ flex-shrink: 0;
2520
+ }
2521
+ .dr-publish-item:hover .dr-publish-item-arrow {
2522
+ color: #64748b;
2523
+ }
2524
+
2525
+ /* ==== Body: left rail + main ==== */
2526
+ .dr-body {
2527
+ flex: 1;
2528
+ min-height: 0;
2529
+ display: flex;
2530
+ overflow: hidden;
2531
+ }
2532
+
2533
+ /* -- Left rail -- */
2534
+ .dr-rail {
2535
+ width: 224px;
2536
+ flex-shrink: 0;
2537
+ background: #fff;
2538
+ border-right: 1px solid #e5e7eb;
2539
+ display: flex;
2540
+ flex-direction: column;
2541
+ transition: width 0.25s ease;
2542
+ /* Note: rail must allow visible overflow so the info-card popover can
2543
+ * escape to the right; the nav section owns its own overflow-y scroll. */
2544
+ overflow: visible;
2545
+ }
2546
+ .dr-rail-collapsed {
2547
+ width: 72px;
2548
+ }
2549
+ .dr-info-card {
2550
+ position: relative;
2551
+ margin: 10px 10px 4px;
2552
+ padding: 16px 14px;
2553
+ display: flex;
2554
+ flex-direction: column;
2555
+ align-items: center;
2556
+ gap: 12px;
2557
+ border: 1px solid transparent;
2558
+ border-radius: 10px;
2559
+ transition:
2560
+ background 0.15s ease,
2561
+ border-color 0.15s ease,
2562
+ box-shadow 0.15s ease;
2563
+ }
2564
+ .dr-info-card:hover {
2565
+ background: #f8fafc;
2566
+ border-color: #e2e8f0;
2567
+ }
2568
+ .dr-info-card:hover .dr-info-trigger {
2569
+ opacity: 1;
2570
+ }
2571
+ .dr-info-card-active {
2572
+ background: #eef2ff;
2573
+ border-color: #c7d2fe;
2574
+ box-shadow: 0 1px 2px rgba(99, 102, 241, 0.08);
2575
+ }
2576
+ .dr-info-card-active .dr-info-trigger {
2577
+ opacity: 1;
2578
+ color: #4338ca;
2579
+ background: #e0e7ff;
2580
+ }
2581
+ .dr-info-trigger {
2582
+ position: absolute;
2583
+ top: 8px;
2584
+ right: 8px;
2585
+ width: 22px;
2586
+ height: 22px;
2587
+ padding: 0;
2588
+ border: none;
2589
+ background: transparent;
2590
+ border-radius: 6px;
2591
+ color: #94a3b8;
2592
+ font-size: 16px;
2593
+ line-height: 1;
2594
+ cursor: pointer;
2595
+ display: inline-flex;
2596
+ align-items: center;
2597
+ justify-content: center;
2598
+ opacity: 0;
2599
+ transition:
2600
+ background 0.15s ease,
2601
+ color 0.15s ease,
2602
+ opacity 0.15s ease;
2603
+ z-index: 2;
2604
+ }
2605
+ .dr-info-trigger:hover {
2606
+ background: #eef2ff;
2607
+ color: #4338ca;
2608
+ }
2609
+ .dr-info-trigger-open {
2610
+ opacity: 1;
2611
+ }
2612
+ .dr-rail-collapsed .dr-info-card {
2613
+ margin: 10px 6px 4px;
2614
+ padding: 12px 8px;
2615
+ }
2616
+ .dr-rail-collapsed .dr-info-trigger {
2617
+ display: none;
2618
+ }
2619
+ .dr-info-avatar {
2620
+ width: 48px;
2621
+ height: 48px;
2622
+ display: flex;
2623
+ align-items: center;
2624
+ justify-content: center;
2625
+ border-radius: 50%;
2626
+ font-size: 24px;
2627
+ background: #eef2ff;
2628
+ color: #4338ca;
2629
+ }
2630
+ .dr-info-body {
2631
+ width: 100%;
2632
+ text-align: center;
2633
+ }
2634
+ .dr-info-name {
2635
+ font-size: 13px;
2636
+ font-weight: 600;
2637
+ color: #0f172a;
2638
+ white-space: nowrap;
2639
+ overflow: hidden;
2640
+ text-overflow: ellipsis;
2641
+ }
2642
+ .dr-info-desc {
2643
+ margin-top: 4px;
2644
+ font-size: 11px;
2645
+ color: #64748b;
2646
+ line-height: 1.5;
2647
+ display: -webkit-box;
2648
+ -webkit-line-clamp: 3;
2649
+ -webkit-box-orient: vertical;
2650
+ overflow: hidden;
2651
+ }
2652
+ .dr-info-meta {
2653
+ margin-top: 6px;
2654
+ font-size: 10px;
2655
+ color: #94a3b8;
2656
+ }
2657
+ .dr-info-tags {
2658
+ margin-top: 6px;
2659
+ display: flex;
2660
+ justify-content: center;
2661
+ gap: 4px;
2662
+ flex-wrap: wrap;
2663
+ }
2664
+ .dr-tag {
2665
+ padding: 1px 6px;
2666
+ font-size: 10px;
2667
+ border-radius: 999px;
2668
+ }
2669
+ .dr-tag-live {
2670
+ color: #15803d;
2671
+ background: #dcfce7;
2672
+ }
2673
+ .dr-tag-ver {
2674
+ color: #1d4ed8;
2675
+ background: #dbeafe;
2676
+ }
2677
+ .dr-info-collapsed-dot {
2678
+ width: 6px;
2679
+ height: 6px;
2680
+ background: #22c55e;
2681
+ border-radius: 50%;
2682
+ animation: dr-pulse 1.6s ease-in-out infinite;
2683
+ }
2684
+ @keyframes dr-pulse {
2685
+ 0%,
2686
+ 100% {
2687
+ opacity: 0.6;
2688
+ }
2689
+ 50% {
2690
+ opacity: 1;
2691
+ }
2692
+ }
2693
+
2694
+ .dr-nav {
2695
+ flex: 1;
2696
+ padding: 10px 8px;
2697
+ display: flex;
2698
+ flex-direction: column;
2699
+ gap: 2px;
2700
+ overflow-y: auto;
2701
+ }
2702
+ .dr-nav-item {
2703
+ display: flex;
2704
+ align-items: center;
2705
+ gap: 10px;
2706
+ padding: 8px 12px;
2707
+ border: none;
2708
+ background: transparent;
2709
+ border-radius: 8px;
2710
+ font-size: 13px;
2711
+ color: #475569;
2712
+ cursor: pointer;
2713
+ text-align: left;
2714
+ transition: background 0.15s;
2715
+ }
2716
+ .dr-nav-item:hover {
2717
+ background: #f1f5f9;
2718
+ }
2719
+ .dr-nav-item.on {
2720
+ background: #eef2ff;
2721
+ color: #4338ca;
2722
+ border: 1px solid #c7d2fe;
2723
+ font-weight: 500;
2724
+ }
2725
+ .dr-nav-icon {
2726
+ font-size: 16px;
2727
+ width: 18px;
2728
+ display: inline-flex;
2729
+ align-items: center;
2730
+ justify-content: center;
2731
+ flex-shrink: 0;
2732
+ color: inherit;
2733
+ }
2734
+ .dr-rail-collapsed .dr-nav-item {
2735
+ justify-content: center;
2736
+ padding: 8px;
2737
+ }
2738
+
2739
+ .dr-rail-collapse {
2740
+ flex: none;
2741
+ padding: 8px;
2742
+ border: none;
2743
+ background: #f8fafc;
2744
+ border-top: 1px solid #f1f5f9;
2745
+ color: #64748b;
2746
+ font-size: 14px;
2747
+ cursor: pointer;
2748
+ }
2749
+ .dr-rail-collapse:hover {
2750
+ background: #f1f5f9;
2751
+ color: #0f172a;
2752
+ }
2753
+
2754
+ /* -- Main content -- */
2755
+ .dr-main {
2756
+ flex: 1;
2757
+ min-width: 0;
2758
+ min-height: 0;
2759
+ overflow: hidden;
2760
+ background: #f8fafc;
2761
+ display: flex;
2762
+ flex-direction: column;
2763
+ }
2764
+
2765
+ /* ==== Orchestrate pane ==== */
2766
+ .dr-orch {
2767
+ flex: 1;
2768
+ min-height: 0;
2769
+ overflow: hidden;
2770
+ display: flex;
2771
+ flex-direction: column;
2772
+ }
2773
+ .dr-flow-slot {
2774
+ flex: 1;
2775
+ min-height: 0;
2776
+ overflow: hidden;
2777
+ /* VueFlow needs an *explicit* size on its container or it renders empty —
2778
+ make this a flex container so the slotted FlowDesigner's height:100% has
2779
+ a resolved height context. */
2780
+ display: flex;
2781
+ flex-direction: column;
2782
+ }
2783
+ .dr-flow-slot > * {
2784
+ flex: 1;
2785
+ min-height: 0;
2786
+ }
2787
+ .dr-logs-slot {
2788
+ flex: 1;
2789
+ min-height: 0;
2790
+ overflow: auto;
2791
+ display: flex;
2792
+ flex-direction: column;
2793
+ }
2794
+ .dr-logs-slot > * {
2795
+ flex: 1;
2796
+ min-height: 0;
2797
+ }
2798
+ .dr-monitor-slot {
2799
+ flex: 1;
2800
+ min-height: 0;
2801
+ overflow: auto;
2802
+ display: flex;
2803
+ flex-direction: column;
2804
+ }
2805
+ .dr-monitor-slot > * {
2806
+ flex: 1;
2807
+ min-height: 0;
2808
+ }
2809
+ .dr-api-slot {
2810
+ flex: 1;
2811
+ min-height: 0;
2812
+ display: flex;
2813
+ flex-direction: column;
2814
+ background: #fff;
2815
+ }
2816
+ .dr-api-slot > * {
2817
+ flex: 1;
2818
+ min-height: 0;
2819
+ }
2820
+ .dr-chat-split {
2821
+ flex: 1;
2822
+ min-height: 0;
2823
+ display: flex;
2824
+ gap: 16px;
2825
+ padding: 16px;
2826
+ overflow: hidden;
2827
+ }
2828
+ .dr-config-col {
2829
+ flex: 1;
2830
+ min-width: 0;
2831
+ overflow-y: auto;
2832
+ display: flex;
2833
+ flex-direction: column;
2834
+ gap: 12px;
2835
+ padding-right: 4px;
2836
+ }
2837
+ .dr-preview-col {
2838
+ flex: 1;
2839
+ min-width: 0;
2840
+ display: flex;
2841
+ flex-direction: column;
2842
+ background: #fff;
2843
+ border: 1px solid #e5e7eb;
2844
+ border-radius: 10px;
2845
+ overflow: hidden;
2846
+ }
2847
+
2848
+ /* ==== Config cards ==== */
2849
+ .dr-card {
2850
+ background: #fff;
2851
+ border: 1px solid #e5e7eb;
2852
+ border-radius: 10px;
2853
+ padding: 14px 16px;
2854
+ }
2855
+ .dr-card-inline {
2856
+ padding: 10px 16px;
2857
+ }
2858
+ .dr-card-head {
2859
+ display: flex;
2860
+ justify-content: space-between;
2861
+ align-items: center;
2862
+ margin-bottom: 10px;
2863
+ }
2864
+ .dr-card-head-inline {
2865
+ margin-bottom: 0;
2866
+ }
2867
+ .dr-card-title {
2868
+ display: inline-flex;
2869
+ align-items: center;
2870
+ gap: 6px;
2871
+ font-size: 13px;
2872
+ font-weight: 600;
2873
+ color: #334155;
2874
+ }
2875
+ .dr-card-title-dot {
2876
+ width: 4px;
2877
+ height: 12px;
2878
+ background: #6366f1;
2879
+ border-radius: 2px;
2880
+ }
2881
+ .dr-card-actions {
2882
+ display: flex;
2883
+ gap: 6px;
2884
+ align-items: center;
2885
+ }
2886
+ .dr-card-empty {
2887
+ font-size: 12px;
2888
+ color: #94a3b8;
2889
+ padding: 6px 0;
2890
+ }
2891
+ .dr-card-empty code {
2892
+ padding: 1px 4px;
2893
+ background: #f1f5f9;
2894
+ color: #6366f1;
2895
+ border-radius: 3px;
2896
+ font-size: 11px;
2897
+ }
2898
+ .dr-link-btn {
2899
+ padding: 2px 8px;
2900
+ font-size: 12px;
2901
+ color: #4338ca;
2902
+ background: transparent;
2903
+ border: none;
2904
+ cursor: pointer;
2905
+ border-radius: 4px;
2906
+ }
2907
+ .dr-link-btn:hover {
2908
+ background: #eef2ff;
2909
+ }
2910
+ .dr-link-btn-solid {
2911
+ display: inline-flex;
2912
+ align-items: center;
2913
+ gap: 4px;
2914
+ height: 24px;
2915
+ padding: 0 10px;
2916
+ color: #fff;
2917
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
2918
+ border-radius: 6px;
2919
+ box-shadow: 0 1px 2px rgba(79, 70, 229, 0.24);
2920
+ transition:
2921
+ box-shadow 0.15s ease,
2922
+ transform 0.15s ease;
2923
+ }
2924
+ .dr-link-btn-solid:hover {
2925
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
2926
+ box-shadow: 0 2px 6px rgba(79, 70, 229, 0.32);
2927
+ transform: translateY(-0.5px);
2928
+ }
2929
+ .dr-link-muted {
2930
+ color: #64748b;
2931
+ }
2932
+ .dr-link-muted:hover {
2933
+ background: #f1f5f9;
2934
+ }
2935
+
2936
+ .dr-prompt-textarea {
2937
+ width: 100%;
2938
+ padding: 10px 12px;
2939
+ font-size: 13px;
2940
+ border: 1.5px solid #6366f1;
2941
+ border-radius: 8px;
2942
+ background: #fff;
2943
+ color: #0f172a;
2944
+ font-family: inherit;
2945
+ resize: vertical;
2946
+ min-height: 140px;
2947
+ line-height: 1.6;
2948
+ }
2949
+ .dr-prompt-textarea:focus {
2950
+ outline: none;
2951
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
2952
+ }
2953
+
2954
+ .dr-input {
2955
+ padding: 6px 10px;
2956
+ font-size: 12px;
2957
+ border: 1px solid #d1d5db;
2958
+ border-radius: 6px;
2959
+ background: #fff;
2960
+ color: #0f172a;
2961
+ }
2962
+ .dr-input:focus {
2963
+ outline: none;
2964
+ border-color: #6366f1;
2965
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
2966
+ }
2967
+ .dr-input-sm {
2968
+ padding: 4px 8px;
2969
+ font-size: 12px;
2970
+ }
2971
+
2972
+ /* Variable card list — mirrors the flow designer's StartNode input-variable
2973
+ * rows so the two surfaces feel identical. Empty state + item rows. */
2974
+ .dr-var-empty {
2975
+ padding: 12px;
2976
+ text-align: center;
2977
+ font-size: 12px;
2978
+ color: #94a3b8;
2979
+ background: #f8fafc;
2980
+ border: 1px dashed #e2e8f0;
2981
+ border-radius: 8px;
2982
+ line-height: 1.6;
2983
+ }
2984
+ .dr-var-empty code {
2985
+ padding: 1px 5px;
2986
+ background: #eef2ff;
2987
+ color: #4338ca;
2988
+ border-radius: 4px;
2989
+ font-size: 11px;
2990
+ }
2991
+ .dr-var-list {
2992
+ display: flex;
2993
+ flex-direction: column;
2994
+ gap: 6px;
2995
+ }
2996
+ .dr-var-item {
2997
+ display: flex;
2998
+ align-items: center;
2999
+ justify-content: space-between;
3000
+ gap: 10px;
3001
+ padding: 8px 12px;
3002
+ background: #fff;
3003
+ border: 1px solid #e5e7eb;
3004
+ border-radius: 8px;
3005
+ transition:
3006
+ background 0.15s,
3007
+ border-color 0.15s;
3008
+ }
3009
+ .dr-var-item:hover {
3010
+ background: #f8fafc;
3011
+ border-color: #c7d2fe;
3012
+ }
3013
+ .dr-var-left {
3014
+ display: flex;
3015
+ align-items: center;
3016
+ gap: 10px;
3017
+ min-width: 0;
3018
+ flex: 1;
3019
+ }
3020
+ .dr-var-icon {
3021
+ width: 26px;
3022
+ height: 26px;
3023
+ display: inline-flex;
3024
+ align-items: center;
3025
+ justify-content: center;
3026
+ background: #eef2ff;
3027
+ color: #4338ca;
3028
+ border-radius: 6px;
3029
+ font-size: 13px;
3030
+ flex-shrink: 0;
3031
+ }
3032
+ .dr-var-meta {
3033
+ min-width: 0;
3034
+ display: flex;
3035
+ flex-direction: column;
3036
+ gap: 1px;
3037
+ }
3038
+ .dr-var-label {
3039
+ font-size: 12px;
3040
+ font-weight: 500;
3041
+ color: #0f172a;
3042
+ white-space: nowrap;
3043
+ overflow: hidden;
3044
+ text-overflow: ellipsis;
3045
+ }
3046
+ .dr-var-name {
3047
+ font-size: 11px;
3048
+ color: #94a3b8;
3049
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
3050
+ white-space: nowrap;
3051
+ overflow: hidden;
3052
+ text-overflow: ellipsis;
3053
+ }
3054
+ .dr-var-right {
3055
+ display: flex;
3056
+ align-items: center;
3057
+ gap: 4px;
3058
+ flex-shrink: 0;
3059
+ }
3060
+ .dr-var-tag {
3061
+ padding: 1px 6px;
3062
+ font-size: 10px;
3063
+ color: #4338ca;
3064
+ background: #e0e7ff;
3065
+ border-radius: 4px;
3066
+ white-space: nowrap;
3067
+ }
3068
+ .dr-var-tag-warn {
3069
+ color: #b45309;
3070
+ background: #fef3c7;
3071
+ }
3072
+ .dr-icon-btn-danger:hover {
3073
+ color: #dc2626;
3074
+ background: #fee2e2;
3075
+ }
3076
+
3077
+ .dr-chip-list {
3078
+ display: flex;
3079
+ flex-wrap: wrap;
3080
+ gap: 6px;
3081
+ }
3082
+ .dr-chip {
3083
+ display: inline-flex;
3084
+ align-items: center;
3085
+ gap: 6px;
3086
+ padding: 4px 12px;
3087
+ font-size: 12px;
3088
+ background: #f1f5f9;
3089
+ border: 1px solid transparent;
3090
+ border-radius: 999px;
3091
+ cursor: pointer;
3092
+ color: #334155;
3093
+ }
3094
+ .dr-chip.on {
3095
+ background: #eef2ff;
3096
+ border-color: #6366f1;
3097
+ color: #4338ca;
3098
+ }
3099
+ .dr-chip input {
3100
+ display: none;
3101
+ }
3102
+
3103
+ /* Selected-dataset list (Dify parity — rows of icon + name + badges + ✕) */
3104
+ .dr-ds-list {
3105
+ display: flex;
3106
+ flex-direction: column;
3107
+ gap: 6px;
3108
+ }
3109
+ .dr-ds-row {
3110
+ display: flex;
3111
+ align-items: center;
3112
+ gap: 8px;
3113
+ padding: 8px 10px;
3114
+ background: #fff;
3115
+ border: 1px solid #e5e7eb;
3116
+ border-radius: 8px;
3117
+ transition: border-color 0.15s;
3118
+ }
3119
+ .dr-ds-row:hover {
3120
+ border-color: #c7d2fe;
3121
+ }
3122
+ .dr-ds-row-icon {
3123
+ width: 24px;
3124
+ height: 24px;
3125
+ display: inline-flex;
3126
+ align-items: center;
3127
+ justify-content: center;
3128
+ background: #eef2ff;
3129
+ color: #4338ca;
3130
+ border-radius: 6px;
3131
+ font-size: 13px;
3132
+ flex-shrink: 0;
3133
+ }
3134
+ .dr-ds-row-name {
3135
+ flex: 1;
3136
+ min-width: 0;
3137
+ font-size: 13px;
3138
+ color: #0f172a;
3139
+ font-weight: 500;
3140
+ white-space: nowrap;
3141
+ overflow: hidden;
3142
+ text-overflow: ellipsis;
3143
+ }
3144
+ .dr-ds-row-tags {
3145
+ display: inline-flex;
3146
+ align-items: center;
3147
+ gap: 4px;
3148
+ flex-shrink: 0;
3149
+ }
3150
+ .dr-ds-tag {
3151
+ padding: 1px 6px;
3152
+ font-size: 10px;
3153
+ color: #4338ca;
3154
+ background: #e0e7ff;
3155
+ border-radius: 4px;
3156
+ white-space: nowrap;
3157
+ }
3158
+ .dr-ds-tag-muted {
3159
+ color: #64748b;
3160
+ background: #f1f5f9;
3161
+ }
3162
+
3163
+ .dr-inline-row {
3164
+ margin-top: 10px;
3165
+ padding-top: 10px;
3166
+ border-top: 1px solid #f1f5f9;
3167
+ display: flex;
3168
+ justify-content: space-between;
3169
+ align-items: center;
3170
+ }
3171
+ .dr-inline-label {
3172
+ font-size: 12px;
3173
+ font-weight: 500;
3174
+ color: #334155;
3175
+ }
3176
+ .dr-mini-select {
3177
+ padding: 3px 8px;
3178
+ font-size: 11px;
3179
+ border: 1px solid #e5e7eb;
3180
+ border-radius: 4px;
3181
+ background: #fff;
3182
+ color: #64748b;
3183
+ }
3184
+
3185
+ /* ==== Switch (视觉) ==== */
3186
+ .dr-switch {
3187
+ position: relative;
3188
+ display: inline-flex;
3189
+ align-items: center;
3190
+ cursor: pointer;
3191
+ }
3192
+ .dr-switch input {
3193
+ position: absolute;
3194
+ opacity: 0;
3195
+ width: 100%;
3196
+ height: 100%;
3197
+ cursor: pointer;
3198
+ }
3199
+ .dr-switch-slider {
3200
+ width: 32px;
3201
+ height: 18px;
3202
+ background: #cbd5e1;
3203
+ border-radius: 999px;
3204
+ position: relative;
3205
+ transition: background 0.15s;
3206
+ }
3207
+ .dr-switch-slider::before {
3208
+ content: '';
3209
+ position: absolute;
3210
+ top: 2px;
3211
+ left: 2px;
3212
+ width: 14px;
3213
+ height: 14px;
3214
+ background: #fff;
3215
+ border-radius: 50%;
3216
+ transition: transform 0.15s;
3217
+ }
3218
+ .dr-switch input:checked + .dr-switch-slider {
3219
+ background: #6366f1;
3220
+ }
3221
+ .dr-switch input:checked + .dr-switch-slider::before {
3222
+ transform: translateX(14px);
3223
+ }
3224
+
3225
+ .dr-icon-btn {
3226
+ width: 24px;
3227
+ height: 24px;
3228
+ padding: 0;
3229
+ border: none;
3230
+ background: transparent;
3231
+ color: #94a3b8;
3232
+ cursor: pointer;
3233
+ border-radius: 4px;
3234
+ font-size: 13px;
3235
+ }
3236
+ .dr-icon-btn:hover {
3237
+ background: #f1f5f9;
3238
+ color: #475569;
3239
+ }
3240
+
3241
+ /* ==== Preview col ==== */
3242
+ .dr-preview-head {
3243
+ padding: 12px 16px;
3244
+ display: flex;
3245
+ justify-content: space-between;
3246
+ align-items: center;
3247
+ border-bottom: 1px solid #f1f5f9;
3248
+ }
3249
+ .dr-preview-scroll {
3250
+ flex: 1;
3251
+ min-height: 0;
3252
+ overflow-y: auto;
3253
+ padding: 16px;
3254
+ display: flex;
3255
+ flex-direction: column;
3256
+ gap: 12px;
3257
+ }
3258
+ .dr-preview-empty {
3259
+ margin: auto;
3260
+ text-align: center;
3261
+ font-size: 12px;
3262
+ color: #94a3b8;
3263
+ line-height: 1.6;
3264
+ }
3265
+ .dr-msg {
3266
+ display: flex;
3267
+ flex-direction: column;
3268
+ gap: 3px;
3269
+ }
3270
+ .dr-msg-user {
3271
+ align-items: flex-end;
3272
+ }
3273
+ .dr-msg-role {
3274
+ font-size: 10px;
3275
+ color: #94a3b8;
3276
+ padding: 0 4px;
3277
+ }
3278
+ .dr-msg-body {
3279
+ padding: 8px 12px;
3280
+ font-size: 13px;
3281
+ border-radius: 12px;
3282
+ max-width: 85%;
3283
+ white-space: pre-wrap;
3284
+ word-break: break-word;
3285
+ line-height: 1.55;
3286
+ }
3287
+ .dr-msg-user .dr-msg-body {
3288
+ background: #4f46e5;
3289
+ color: #fff;
3290
+ }
3291
+ .dr-msg-assistant .dr-msg-body {
3292
+ background: #f1f5f9;
3293
+ color: #0f172a;
3294
+ }
3295
+ .dr-msg-failed {
3296
+ background: #fef2f2 !important;
3297
+ color: #b91c1c !important;
3298
+ }
3299
+ .dr-preview-composer {
3300
+ padding: 12px 16px;
3301
+ border-top: 1px solid #f1f5f9;
3302
+ display: flex;
3303
+ gap: 8px;
3304
+ }
3305
+ .dr-preview-composer .dr-input {
3306
+ flex: 1;
3307
+ padding: 8px 12px;
3308
+ font-size: 13px;
3309
+ border-radius: 999px;
3310
+ }
3311
+ .dr-send-btn {
3312
+ width: 36px;
3313
+ height: 36px;
3314
+ padding: 0;
3315
+ border: none;
3316
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
3317
+ color: #fff;
3318
+ border-radius: 50%;
3319
+ cursor: pointer;
3320
+ font-size: 12px;
3321
+ display: flex;
3322
+ align-items: center;
3323
+ justify-content: center;
3324
+ }
3325
+ .dr-send-btn:disabled {
3326
+ background: #cbd5e1;
3327
+ cursor: not-allowed;
3328
+ }
3329
+
3330
+ /* ==== Plain content tabs (API / logs / monitor) ==== */
3331
+ .dr-plain {
3332
+ padding: 32px 40px;
3333
+ overflow-y: auto;
3334
+ }
3335
+ .dr-plain-title {
3336
+ font-size: 18px;
3337
+ font-weight: 600;
3338
+ color: #0f172a;
3339
+ margin-bottom: 12px;
3340
+ }
3341
+ .dr-plain-hint {
3342
+ margin-top: 8px;
3343
+ font-size: 13px;
3344
+ color: #64748b;
3345
+ line-height: 1.7;
3346
+ }
3347
+ .dr-plain-hint code {
3348
+ padding: 1px 6px;
3349
+ background: #f1f5f9;
3350
+ color: #4338ca;
3351
+ border-radius: 4px;
3352
+ font-size: 12px;
3353
+ }
3354
+ .dr-code {
3355
+ margin-top: 10px;
3356
+ padding: 12px 14px;
3357
+ font-size: 12px;
3358
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
3359
+ background: #0f172a;
3360
+ color: #e2e8f0;
3361
+ border-radius: 8px;
3362
+ overflow-x: auto;
3363
+ line-height: 1.6;
3364
+ }
3365
+
3366
+ /* ==== Empty state (flow slot placeholder) ==== */
3367
+ .dr-empty {
3368
+ margin: auto;
3369
+ text-align: center;
3370
+ padding: 40px;
3371
+ color: #94a3b8;
3372
+ }
3373
+ .dr-empty-title {
3374
+ font-size: 14px;
3375
+ font-weight: 500;
3376
+ color: #64748b;
3377
+ margin-bottom: 6px;
3378
+ }
3379
+ .dr-empty-hint {
3380
+ font-size: 12px;
3381
+ line-height: 1.6;
3382
+ color: #94a3b8;
3383
+ }
3384
+ .dr-empty-hint code {
3385
+ padding: 1px 5px;
3386
+ background: #f1f5f9;
3387
+ border-radius: 4px;
3388
+ color: #4338ca;
3389
+ font-size: 11px;
3390
+ }
3391
+ </style>