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,1601 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * AgentAppsPage — 一站式「应用列表」页面组件。
4
+ *
5
+ * 目标:宿主只需要 `<AgentAppsPage />` 就能获得完整的 Dify 风格应用管理页 ——
6
+ * 列表 / 过滤 / 创建(类型选择器)/ 编辑(设计抽屉)/ 分享嵌入 / 发布 / 删除,
7
+ * 全部内置。api-base 默认 `/api`(宿主代理前缀),/agent-start 命名空间由组件
8
+ * 内部自动拼上。带鉴权的宿主用 :headers 注入 Authorization。
9
+ *
10
+ * 设计约束:
11
+ * - 组件只依赖 vue / vue-router / ant-design-vue / @ant-design/icons-vue,
12
+ * 以及本模块内的 AppDesignDrawer / CreateAppModal,绝不依赖 Vben 生态
13
+ * (@vben/*);
14
+ * - 所有后端调用都是通过 props.apiBase 拼出的 fetch,共用一个 {code, data}
15
+ * 信封的解包器 —— 与 useAgentStudio / useKnowledge / useProviderHub 一致;
16
+ * - 导航(独立对话、去配置模型)通过 useRouter() 内部完成,捕获异常防止
17
+ * 宿主没配对应路由时报错。
18
+ */
19
+ import { computed, onDeactivated, onMounted, reactive, ref } from 'vue';
20
+ import { useRouter } from 'vue-router';
21
+
22
+ import {
23
+ Button,
24
+ Card,
25
+ Dropdown,
26
+ Empty,
27
+ Form,
28
+ FormItem,
29
+ Input,
30
+ Menu,
31
+ MenuItem,
32
+ message,
33
+ Modal,
34
+ Select,
35
+ Skeleton,
36
+ Spin,
37
+ Tag,
38
+ Textarea,
39
+ } from 'ant-design-vue';
40
+
41
+ import type { ChatIframeConfig } from '../../agent-flow/components/chat-iframe-types';
42
+
43
+ import type { AppStudioApi } from '../api';
44
+ import type {
45
+ AgentEntity,
46
+ AgentStrategy,
47
+ AppType,
48
+ } from '../types';
49
+
50
+ import AppDesignDrawer from './AppDesignDrawer.vue';
51
+ import CreateAppModal from './CreateAppModal.vue';
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Props
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Header 值可以是静态对象,也可以是同步/异步函数 —— 每次请求前都会重新
58
+ * 求值,方便宿主接入自家 access-token store(token 轮换时无须重挂组件)。
59
+ */
60
+ type HeadersLike =
61
+ | Record<string, string>
62
+ | (() => Promise<Record<string, string>> | Record<string, string>);
63
+
64
+ interface Props {
65
+ /**
66
+ * 宿主的代理前缀。默认 `/api`:与宿主 vite `/api` 代理约定一致,代理
67
+ * 剥掉 `/api` 后剩下的路径就是后端。组件内部会自动拼上 spring-agent-web
68
+ * 给控制器加的固定 `/agent-start` 命名空间 —— 那是本组件与后端的约定,
69
+ * 宿主不用关心。所以最终请求 URL 是 `${apiBase}/agent-start/xxx`。
70
+ *
71
+ * 若宿主没走 `/api` 代理而是同源直连后端,传 `''`(空字符串),最终
72
+ * 就是 `/agent-start/xxx`。
73
+ */
74
+ apiBase?: string;
75
+ /**
76
+ * 追加到每个请求上的 header。宿主用 vben 等业务框架时,把
77
+ * `Authorization: Bearer xxx` 通过这里注入即可,不用改组件内部代码。
78
+ *
79
+ * 传函数就每次请求前重新求值 —— 推荐用于依赖 pinia store 的 token
80
+ * (登录/退出时不用重挂组件):
81
+ * :headers="() => ({ Authorization: `Bearer ${accessStore.accessToken}` })"
82
+ *
83
+ * 传对象就是静态 header —— 适合固定 API-key:
84
+ * :headers="{ 'X-Api-Key': 'xxx' }"
85
+ */
86
+ headers?: HeadersLike;
87
+ }
88
+
89
+ const props = withDefaults(defineProps<Props>(), {
90
+ apiBase: '/api',
91
+ headers: () => ({}),
92
+ });
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Backend client — small fetch wrapper that unwraps {code, message, data}.
96
+ // Kept local (not exported to useAgentStudio) so this component stays a true
97
+ // drop-in with zero shared state.
98
+ // ---------------------------------------------------------------------------
99
+ const AGENT_START_NAMESPACE = '/agent-start';
100
+
101
+ interface Envelope<T> {
102
+ code: string;
103
+ message?: string;
104
+ data: T;
105
+ }
106
+
107
+ function apiUrl(path: string): string {
108
+ const base = props.apiBase.replace(/\/+$/, '');
109
+ return `${base}${AGENT_START_NAMESPACE}${path.startsWith('/') ? '' : '/'}${path}`;
110
+ }
111
+
112
+ async function resolveHeaders(): Promise<Record<string, string>> {
113
+ const h = props.headers;
114
+ const out = typeof h === 'function' ? await h() : h;
115
+ return out ?? {};
116
+ }
117
+
118
+ async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
119
+ const injected = await resolveHeaders();
120
+ const res = await fetch(apiUrl(path), {
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ ...injected,
124
+ ...(init.headers ?? {}),
125
+ },
126
+ ...init,
127
+ });
128
+ if (!res.ok) {
129
+ throw new Error(`${res.status} ${res.statusText}`);
130
+ }
131
+ const env = (await res.json()) as Envelope<T>;
132
+ if (env.code !== 'ok') {
133
+ throw new Error(env.message ?? env.code);
134
+ }
135
+ return env.data;
136
+ }
137
+
138
+ // ---- CreateAgent / UpdateAgent request shape (mirrors #/api/agent) ----
139
+ interface CreateAgentRequestFull {
140
+ tenantId?: string;
141
+ name: string;
142
+ description?: string;
143
+ icon?: string;
144
+ iconBackground?: string;
145
+ mode?: string;
146
+ instructions?: string;
147
+ openingStatement?: string;
148
+ suggestedQuestions?: string[];
149
+ datasetIds?: string[];
150
+ retrievalConfig?: Record<string, unknown>;
151
+ modelName?: string;
152
+ modelProvider?: string;
153
+ strategy?: AgentStrategy;
154
+ toolNames?: string[];
155
+ approvalRequiredTools?: string[];
156
+ delegateAgentIds?: string[];
157
+ maxIterations?: number;
158
+ memoryEnabled?: boolean;
159
+ memoryWindow?: number;
160
+ published?: boolean;
161
+ modelSettings?: Record<string, unknown>;
162
+ }
163
+
164
+ // ---- Agent CRUD ----
165
+ const listAgents = () => call<AgentEntity[]>('/agents');
166
+ const createAgentReq = (req: CreateAgentRequestFull) =>
167
+ call<AgentEntity>('/agents', { method: 'POST', body: JSON.stringify(req) });
168
+ const updateAgentReq = (id: string, req: CreateAgentRequestFull) =>
169
+ call<AgentEntity>(`/agents/${id}`, { method: 'PUT', body: JSON.stringify(req) });
170
+ const deleteAgentReq = (id: string) =>
171
+ call<void>(`/agents/${id}`, { method: 'DELETE' });
172
+
173
+ // ---- Chat stream (raw fetch — SSE) ----
174
+ async function chatStreamReq(id: string, body: { query: string }) {
175
+ const injected = await resolveHeaders();
176
+ return fetch(apiUrl(`/agents/${id}/chat/stream`), {
177
+ method: 'POST',
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ Accept: 'text/event-stream',
181
+ ...injected,
182
+ },
183
+ body: JSON.stringify(body),
184
+ });
185
+ }
186
+
187
+ // ---- Conversations + history (called by AppStudioApi bag) ----
188
+ interface ConversationSummary {
189
+ conversationId: string;
190
+ name?: string;
191
+ userId?: null | string;
192
+ firstMessage?: string;
193
+ updatedAt?: string;
194
+ pinned?: boolean;
195
+ }
196
+ interface HistoryMessage {
197
+ role: 'ASSISTANT' | 'SYSTEM' | 'USER';
198
+ content: string;
199
+ }
200
+ const listConversationsReq = (appId: string, limit = 100) =>
201
+ call<ConversationSummary[]>(`/chat/conversations/${appId}`, {
202
+ method: 'POST',
203
+ body: JSON.stringify({ limit }),
204
+ });
205
+ const fetchHistoryReq = (appId: string, conversationId: string, limit = 500) =>
206
+ call<HistoryMessage[]>(
207
+ `/chat/conversations/${appId}/${conversationId}/messages`,
208
+ { method: 'POST', body: JSON.stringify({ limit }) },
209
+ );
210
+
211
+ // ---- Annotations ----
212
+ interface AppAnnotation {
213
+ id: string;
214
+ appId: string;
215
+ question?: string;
216
+ content?: string;
217
+ enabled?: boolean;
218
+ hitCount?: number;
219
+ createdAt?: string;
220
+ updatedAt?: string;
221
+ }
222
+ interface AppAnnotationRequest {
223
+ question: string;
224
+ content: string;
225
+ enabled?: boolean;
226
+ }
227
+ const listAnnotationsReq = (appId: string) =>
228
+ call<AppAnnotation[]>(`/apps/${appId}/annotations`);
229
+ const createAnnotationReq = (appId: string, req: AppAnnotationRequest) =>
230
+ call<AppAnnotation>(`/apps/${appId}/annotations`, {
231
+ method: 'POST',
232
+ body: JSON.stringify(req),
233
+ });
234
+ const updateAnnotationReq = (
235
+ appId: string,
236
+ id: string,
237
+ req: AppAnnotationRequest,
238
+ ) =>
239
+ call<AppAnnotation>(`/apps/${appId}/annotations/${id}`, {
240
+ method: 'PUT',
241
+ body: JSON.stringify(req),
242
+ });
243
+ const deleteAnnotationReq = (appId: string, id: string) =>
244
+ call<void>(`/apps/${appId}/annotations/${id}`, { method: 'DELETE' });
245
+
246
+ // ---- API keys ----
247
+ interface AppApiKey {
248
+ id: string;
249
+ appId: string;
250
+ name?: string;
251
+ type?: string;
252
+ token: string;
253
+ createdAt?: string;
254
+ lastUsedAt?: string;
255
+ }
256
+ const listApiKeysReq = (appId: string) =>
257
+ call<AppApiKey[]>(`/apps/${appId}/api-tokens`);
258
+ const createApiKeyReq = (appId: string, name?: string) =>
259
+ call<AppApiKey>(`/apps/${appId}/api-tokens`, {
260
+ method: 'POST',
261
+ body: JSON.stringify({ name }),
262
+ });
263
+ const renameApiKeyReq = (appId: string, id: string, name: string) =>
264
+ call<AppApiKey>(`/apps/${appId}/api-tokens/${id}/rename`, {
265
+ method: 'POST',
266
+ body: JSON.stringify({ name }),
267
+ });
268
+ const deleteApiKeyReq = (appId: string, id: string) =>
269
+ call<void>(`/apps/${appId}/api-tokens/${id}`, { method: 'DELETE' });
270
+
271
+ // ---- Metrics / LLM Ops ----
272
+ interface AppMetrics {
273
+ appId: string;
274
+ totalConversations: number;
275
+ totalMessages: number;
276
+ userMessages: number;
277
+ assistantMessages: number;
278
+ avgInteractionsPerConversation: number;
279
+ lastActivityAt?: string;
280
+ }
281
+ interface LlmUsageStats {
282
+ model?: string;
283
+ calls: number;
284
+ errors: number;
285
+ promptTokens: number;
286
+ completionTokens: number;
287
+ totalTokens: number;
288
+ costMicros: number;
289
+ avgLatencyMs: number;
290
+ }
291
+ interface LlmCallRecord {
292
+ id: string;
293
+ provider: string;
294
+ model: string;
295
+ promptTokens?: number;
296
+ completionTokens?: number;
297
+ totalTokens?: number;
298
+ costMicros?: number;
299
+ latencyMs?: number;
300
+ success: boolean;
301
+ errorType?: string;
302
+ createdAt?: string;
303
+ }
304
+ const fetchAppMetricsReq = (appId: string) =>
305
+ call<AppMetrics>(`/apps/${appId}/metrics`);
306
+ const fetchTotalReq = () => call<LlmUsageStats>('/llmops/total');
307
+ const fetchRecentReq = (limit = 50) =>
308
+ call<LlmCallRecord[]>(`/llmops/recent?limit=${limit}`);
309
+
310
+ // ---- Models / Tools / Datasets ----
311
+ interface ModelEntity {
312
+ id: string;
313
+ tenantId?: string;
314
+ providerName: string;
315
+ modelName: string;
316
+ modelType: string;
317
+ enabled: boolean;
318
+ isDefault: boolean;
319
+ }
320
+ interface ToolView {
321
+ name: string;
322
+ description: string;
323
+ inputSchema?: string;
324
+ }
325
+ interface DatasetEntity {
326
+ id: string;
327
+ name: string;
328
+ description?: string;
329
+ }
330
+ const listModelsReq = (type?: string) =>
331
+ call<ModelEntity[]>(`/models${type ? `?type=${encodeURIComponent(type)}` : ''}`);
332
+ const listToolsReq = () => call<ToolView[]>('/tools');
333
+ const listDatasetsReq = () => call<DatasetEntity[]>('/datasets');
334
+
335
+ // ---- Workflow draft ----
336
+ interface WorkflowEntity {
337
+ id: string;
338
+ appId?: string;
339
+ graph?: unknown;
340
+ version?: string;
341
+ published?: boolean;
342
+ }
343
+ const getWorkflowDraftReq = (appId: string) =>
344
+ call<WorkflowEntity>(`/apps/${appId}/workflow/draft`);
345
+ const saveWorkflowDraftReq = (appId: string, graph: unknown) =>
346
+ call<WorkflowEntity>(`/apps/${appId}/workflow/draft`, {
347
+ method: 'PUT',
348
+ body: JSON.stringify({ graph }),
349
+ });
350
+ const publishWorkflowDraftReq = (appId: string) =>
351
+ call<WorkflowEntity>(`/apps/${appId}/workflow/publish`, {
352
+ method: 'POST',
353
+ body: JSON.stringify({}),
354
+ });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // AppStudioApi callback bag — the drawer's built-in panels call these.
358
+ // ---------------------------------------------------------------------------
359
+ const studioApi: AppStudioApi = {
360
+ listConversations: (appId: string, limit?: number) =>
361
+ listConversationsReq(appId, limit),
362
+ fetchHistory: (appId: string, conversationId: string, limit?: number) =>
363
+ fetchHistoryReq(appId, conversationId, limit),
364
+ listAnnotations: (appId: string) => listAnnotationsReq(appId),
365
+ createAnnotation: (appId: string, req: any) =>
366
+ createAnnotationReq(appId, req),
367
+ updateAnnotation: (appId: string, id: string, req: any) =>
368
+ updateAnnotationReq(appId, id, req),
369
+ deleteAnnotation: (appId: string, id: string) =>
370
+ deleteAnnotationReq(appId, id),
371
+ fetchAppMetrics: (appId: string) => fetchAppMetricsReq(appId),
372
+ fetchLlmUsage: () => fetchTotalReq() as any,
373
+ fetchRecentLlmCalls: (limit?: number) => fetchRecentReq(limit) as any,
374
+ listApiKeys: (appId: string) => listApiKeysReq(appId) as any,
375
+ createApiKey: (appId: string, name?: string) =>
376
+ createApiKeyReq(appId, name) as any,
377
+ renameApiKey: (appId: string, id: string, name: string) =>
378
+ renameApiKeyReq(appId, id, name) as any,
379
+ deleteApiKey: (appId: string, id: string) => deleteApiKeyReq(appId, id),
380
+ };
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Navigation helpers — routed via useRouter(); wrapped in try/catch so hosts
384
+ // that lack the expected routes (AgentChat, ModelList) don't blow up.
385
+ // ---------------------------------------------------------------------------
386
+ const router = useRouter();
387
+
388
+ function goToModel() {
389
+ try {
390
+ router?.push({ name: 'ModelList' });
391
+ } catch {
392
+ // Host doesn't have a ModelList route — silently no-op.
393
+ }
394
+ }
395
+ function openChat(a: AgentEntity) {
396
+ try {
397
+ router?.push({ name: 'AgentChat', params: { id: a.id } });
398
+ } catch {
399
+ // Host doesn't have an AgentChat route — silently no-op.
400
+ }
401
+ }
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Chat iframe config — minimal port of #/adapter/chat-iframe-config.ts, sans
405
+ // any @vben/preferences / @vben/stores dependency. `context.user` stays null
406
+ // (hosts can wrap the iframe in their own theme provider if needed).
407
+ // ---------------------------------------------------------------------------
408
+ interface AgentLike {
409
+ id?: string;
410
+ name?: string;
411
+ mode?: string;
412
+ workflowId?: string;
413
+ difyApiKey?: string;
414
+ }
415
+
416
+ function resolveIframeBase(): string {
417
+ const raw = (import.meta.env.VITE_CHAT_IFRAME_BASE as string | undefined)?.trim();
418
+ const base = raw && raw.length > 0 ? raw : '/chat/';
419
+ return base.endsWith('/') ? base : `${base}/`;
420
+ }
421
+
422
+ function resolveApiKey(agent?: AgentLike | null): string | undefined {
423
+ const fromAgent = agent?.difyApiKey?.trim();
424
+ if (fromAgent) return fromAgent;
425
+ const fromEnv = (
426
+ import.meta.env.VITE_DIFY_APP_KEY as string | undefined
427
+ )?.trim();
428
+ if (fromEnv) return fromEnv;
429
+ return agent?.id?.trim() || undefined;
430
+ }
431
+
432
+ function buildChatIframeConfig(
433
+ agent: AgentLike | null,
434
+ options: { debug?: boolean } = {},
435
+ ): ChatIframeConfig {
436
+ const src = resolveIframeBase();
437
+ const difyApiKey = resolveApiKey(agent);
438
+ const label = agent?.name || agent?.id || 'spring-agent';
439
+ const workflowId = agent?.workflowId;
440
+ return {
441
+ src,
442
+ title: agent?.name ? `调试:${agent.name}` : '调试与预览',
443
+ sessionKey: `${agent?.mode ?? 'agent'}-${agent?.id ?? 'draft'}`,
444
+ params: {
445
+ mode: 'Copilot',
446
+ difyApiKey,
447
+ label,
448
+ appId: agent?.id,
449
+ workflowId,
450
+ debug: options.debug ? 'true' : undefined,
451
+ },
452
+ context: {
453
+ appId: agent?.id,
454
+ appMode: agent?.mode,
455
+ appName: agent?.name,
456
+ // Vben-specific user store lookup removed — hosts that want to plumb
457
+ // theme/user info through can post-message it themselves after mount.
458
+ user: null,
459
+ theme: 'light',
460
+ },
461
+ };
462
+ }
463
+
464
+ // ---------------------------------------------------------------------------
465
+ // Reactive state — direct port of the original list.vue.
466
+ // ---------------------------------------------------------------------------
467
+ const agents = ref<AgentEntity[]>([]);
468
+ const llmModels = ref<ModelEntity[]>([]);
469
+ const tools = ref<ToolView[]>([]);
470
+ const datasets = ref<DatasetEntity[]>([]);
471
+ const loading = ref(false);
472
+ const llmLoaded = ref(false);
473
+
474
+ const keyword = ref('');
475
+ const filterStrategy = ref<'ALL' | AgentStrategy>('ALL');
476
+ /** Draft vs published filter — Dify's "只看已发布 / 只看草稿" toggle. */
477
+ const filterPublished = ref<'all' | 'draft' | 'published'>('all');
478
+
479
+ const showCreate = ref(false);
480
+ const submitting = ref(false);
481
+ /**
482
+ * Dify-style two-step create: first pick an "app type" from CreateAppModal,
483
+ * then land in the config form with defaults seeded from that choice.
484
+ */
485
+ const showTypePicker = ref(false);
486
+ const editingId = ref<null | string>(null);
487
+ /**
488
+ * Basic-info edit form — the "编辑基本信息" surface intentionally only carries
489
+ * the four presentation fields (name / icon / iconBackground / description).
490
+ */
491
+ const form = reactive({
492
+ name: '',
493
+ description: '',
494
+ icon: '',
495
+ iconBackground: '',
496
+ });
497
+
498
+ // Dify's app-card mode icons — colored square with an emoji
499
+ const ICONS = ['🤖', '💬', '🧠', '🎯', '🛠', '📊', '💡', '⚡', '🔎', '📎'];
500
+ const BGS = [
501
+ '#FEF3F2',
502
+ '#EEF4FF',
503
+ '#EFFDF4',
504
+ '#FFF4ED',
505
+ '#F0F9FF',
506
+ '#FEF6EE',
507
+ '#FDF2FA',
508
+ '#F0FDF9',
509
+ ];
510
+ function hashCode(s: string): number {
511
+ let h = 0;
512
+ for (let i = 0; i < s.length; i++) {
513
+ h = Math.trunc((h << 5) - h + s.charCodeAt(i));
514
+ }
515
+ return Math.abs(h);
516
+ }
517
+ function iconOf(a: AgentEntity) {
518
+ return a.icon || ICONS[hashCode(a.id || a.name) % ICONS.length];
519
+ }
520
+ function bgOf(a: AgentEntity) {
521
+ return (
522
+ a.iconBackground || BGS[hashCode((a.id || a.name) + '.bg') % BGS.length]
523
+ );
524
+ }
525
+ function modeLabel(m?: string): string {
526
+ if (m === 'chat') return 'Chat';
527
+ if (m === 'chatflow') return 'Chatflow';
528
+ if (m === 'workflow') return 'Workflow';
529
+ if (m === 'completion') return 'Completion';
530
+ return 'Agent';
531
+ }
532
+ function modeColor(m?: string): string {
533
+ if (m === 'chat') return 'blue';
534
+ if (m === 'chatflow') return 'cyan';
535
+ if (m === 'workflow') return 'purple';
536
+ if (m === 'completion') return 'orange';
537
+ return 'geekblue';
538
+ }
539
+
540
+ function fromNow(iso?: string): string {
541
+ if (!iso) return '';
542
+ const t = new Date(iso).getTime();
543
+ if (Number.isNaN(t)) return '';
544
+ const diff = (Date.now() - t) / 1000;
545
+ if (diff < 60) return '刚刚';
546
+ if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
547
+ if (diff < 86_400) return `${Math.floor(diff / 3600)} 小时前`;
548
+ if (diff < 86_400 * 30) return `${Math.floor(diff / 86_400)} 天前`;
549
+ return new Date(iso).toLocaleDateString();
550
+ }
551
+
552
+ function strategyLabel(s?: string): string {
553
+ if (s === 'REACT') return 'ReAct';
554
+ if (s === 'FUNCTION_CALLING') return 'Function Calling';
555
+ if (s === 'PLAN_EXECUTE') return 'Plan & Execute';
556
+ return s ?? '';
557
+ }
558
+ function strategyColor(s?: string): string {
559
+ if (s === 'FUNCTION_CALLING') return 'green';
560
+ if (s === 'PLAN_EXECUTE') return 'purple';
561
+ return 'blue';
562
+ }
563
+
564
+ function modelLabel(a: AgentEntity): string {
565
+ if (a.modelProvider && a.modelName) {
566
+ return `${a.modelProvider} · ${a.modelName}`;
567
+ }
568
+ if (a.modelName) return a.modelName;
569
+ return '未选择模型';
570
+ }
571
+
572
+ const filtered = computed(() => {
573
+ const q = keyword.value.trim().toLowerCase();
574
+ return agents.value.filter((a) => {
575
+ if (
576
+ filterStrategy.value !== 'ALL' &&
577
+ a.strategy !== filterStrategy.value
578
+ ) {
579
+ return false;
580
+ }
581
+ if (filterPublished.value === 'published' && a.published === false)
582
+ return false;
583
+ if (filterPublished.value === 'draft' && a.published !== false)
584
+ return false;
585
+ if (!q) return true;
586
+ return (
587
+ a.name.toLowerCase().includes(q) ||
588
+ (a.description ?? '').toLowerCase().includes(q) ||
589
+ (a.instructions ?? '').toLowerCase().includes(q)
590
+ );
591
+ });
592
+ });
593
+
594
+ /** Convenience count helpers for the filter tabs. */
595
+ const publishedCount = computed(
596
+ () => agents.value.filter((a) => a.published !== false).length,
597
+ );
598
+ const draftCount = computed(
599
+ () => agents.value.filter((a) => a.published === false).length,
600
+ );
601
+
602
+ async function refresh() {
603
+ loading.value = true;
604
+ try {
605
+ agents.value = await listAgents();
606
+ } catch (e: any) {
607
+ // 后端 401/404/网络断开等都在这里兜住 —— 不能让异常冒到
608
+ // onMounted 里变成 unhandledrejection,那会触发 vite 错误覆盖层,
609
+ // 视觉上像 <KeepAlive> 里的路由切换全部白屏。
610
+ agents.value = [];
611
+ message.error(`加载应用列表失败:${e?.message ?? e}`);
612
+ } finally {
613
+ loading.value = false;
614
+ }
615
+ }
616
+
617
+ async function loadDeps() {
618
+ try {
619
+ llmModels.value = await listModelsReq('LLM');
620
+ } catch {
621
+ llmModels.value = [];
622
+ } finally {
623
+ llmLoaded.value = true;
624
+ }
625
+ try {
626
+ tools.value = await listToolsReq();
627
+ } catch {
628
+ tools.value = [];
629
+ }
630
+ try {
631
+ datasets.value = await listDatasetsReq();
632
+ } catch {
633
+ datasets.value = [];
634
+ }
635
+ }
636
+
637
+ function openTypePicker() {
638
+ showTypePicker.value = true;
639
+ }
640
+
641
+ /**
642
+ * Dify-parity create flow — always creates an `apps` row and refreshes the list
643
+ * in place. Creating an app must never be gated on model availability.
644
+ *
645
+ * The 4 app types map to Dify's `mode` field on the `apps` row so a single
646
+ * table backs the whole family:
647
+ * chatbot → mode='chat' · REACT
648
+ * agent → mode='agent' · FUNCTION_CALLING
649
+ * workflow → mode='workflow' · FUNCTION_CALLING + empty graph seed
650
+ * text-generator → mode='completion' · PLAN_EXECUTE, memory off
651
+ */
652
+ async function onCreateApp(payload: {
653
+ appType: AppType;
654
+ name: string;
655
+ description: string;
656
+ icon: { emoji: string; background: string };
657
+ }) {
658
+ if (!llmLoaded.value) {
659
+ await loadDeps();
660
+ }
661
+ const defaultModel =
662
+ llmModels.value.find((m) => m.isDefault) ?? llmModels.value[0];
663
+
664
+ const modeByType: Record<string, string> = {
665
+ agent: 'agent',
666
+ chatbot: 'chat',
667
+ chatflow: 'chatflow',
668
+ 'text-generator': 'completion',
669
+ workflow: 'workflow',
670
+ };
671
+ const strategyByType: Record<string, AgentStrategy> = {
672
+ agent: 'FUNCTION_CALLING',
673
+ chatbot: 'REACT',
674
+ chatflow: 'REACT',
675
+ 'text-generator': 'PLAN_EXECUTE',
676
+ workflow: 'FUNCTION_CALLING',
677
+ };
678
+ const mode = modeByType[payload.appType] ?? 'agent';
679
+ const strategy = strategyByType[payload.appType] ?? 'REACT';
680
+ const maxIterations = payload.appType === 'text-generator' ? 3 : 6;
681
+ const memoryWindow = payload.appType === 'text-generator' ? 0 : 20;
682
+
683
+ submitting.value = true;
684
+ try {
685
+ await createAgentReq({
686
+ name: payload.name,
687
+ description: payload.description || undefined,
688
+ icon: payload.icon.emoji,
689
+ iconBackground: payload.icon.background,
690
+ mode,
691
+ instructions: payload.description || undefined,
692
+ modelName: defaultModel?.modelName ?? undefined,
693
+ modelProvider: defaultModel?.providerName ?? undefined,
694
+ strategy,
695
+ toolNames: [],
696
+ maxIterations,
697
+ memoryEnabled: memoryWindow > 0,
698
+ memoryWindow,
699
+ });
700
+ message.success(`已创建「${payload.name}」`);
701
+ await refresh();
702
+ } catch (e: any) {
703
+ message.error(e?.message ?? '创建失败');
704
+ } finally {
705
+ submitting.value = false;
706
+ }
707
+ }
708
+
709
+ // ---------------------------------------------------------------------------
710
+ // Design drawer — click a card to open a Dify-style drawer where the user can
711
+ // design the app. Workflow-mode apps see the DAG canvas; others see the
712
+ // orchestrate/preview two-pane. Persisted via updateAgent().
713
+ // ---------------------------------------------------------------------------
714
+ const drawerOpen = ref(false);
715
+ const drawerApp = ref<AgentEntity | null>(null);
716
+ /**
717
+ * Draft-workflow graph JSON prefetched for the currently open drawer. Only
718
+ * populated for workflow / chatflow apps.
719
+ */
720
+ const drawerGraphJson = ref<string>('');
721
+
722
+ /**
723
+ * 抽屉里挂 ChatIframePanel / FlowDesigner 的调试 iframe 配置。每次打开抽屉时
724
+ * 按当前 app 重新构造 —— session key 走 mode+id,切换 app 不会串会话。
725
+ */
726
+ const drawerChatConfig = computed<ChatIframeConfig | null>(() =>
727
+ drawerApp.value
728
+ ? buildChatIframeConfig(drawerApp.value as AgentLike, { debug: true })
729
+ : null,
730
+ );
731
+
732
+ async function openDesigner(a: AgentEntity) {
733
+ drawerApp.value = a;
734
+ drawerGraphJson.value = '';
735
+ // Prefetch the draft BEFORE opening the drawer for flow-mode apps so
736
+ // FlowDesigner mounts with the real payload already on `props` (avoids a
737
+ // race with initGraph()'s setTimeout(10) seed).
738
+ if (a.mode === 'workflow' || a.mode === 'chatflow') {
739
+ try {
740
+ const draft = await getWorkflowDraftReq(a.id);
741
+ if (draft?.graph) drawerGraphJson.value = JSON.stringify(draft.graph);
742
+ } catch {
743
+ drawerGraphJson.value = '';
744
+ }
745
+ }
746
+ drawerOpen.value = true;
747
+ if (!llmLoaded.value) loadDeps();
748
+ }
749
+
750
+ /**
751
+ * Safely parse the drawer's serialised retrieval config so we can hand the
752
+ * backend a plain object. Empty / malformed input returns undefined.
753
+ */
754
+ function parseRetrievalConfig(
755
+ json?: string,
756
+ ): Record<string, unknown> | undefined {
757
+ if (!json) return undefined;
758
+ try {
759
+ const v = JSON.parse(json);
760
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
761
+ } catch {
762
+ return undefined;
763
+ }
764
+ }
765
+
766
+ async function onDrawerSave(payload: {
767
+ appId: string;
768
+ mode: string;
769
+ graphJson?: string;
770
+ name?: string;
771
+ instructions?: string;
772
+ openingStatement?: string;
773
+ modelName?: string;
774
+ modelProvider?: string;
775
+ modelSettings?: Record<string, unknown>;
776
+ toolNames?: string[];
777
+ datasetIds?: string[];
778
+ retrievalConfigJson?: string;
779
+ }) {
780
+ const existing = agents.value.find((x) => x.id === payload.appId);
781
+ submitting.value = true;
782
+ try {
783
+ await updateAgentReq(payload.appId, {
784
+ name: payload.name ?? existing?.name ?? '',
785
+ mode: payload.mode,
786
+ instructions: payload.instructions,
787
+ openingStatement: payload.openingStatement,
788
+ modelName: payload.modelName ?? existing?.modelName,
789
+ modelProvider: payload.modelProvider ?? existing?.modelProvider,
790
+ modelSettings: payload.modelSettings,
791
+ strategy: (existing?.strategy as AgentStrategy) ?? 'REACT',
792
+ toolNames: payload.toolNames ?? [],
793
+ datasetIds: payload.datasetIds ?? [],
794
+ retrievalConfig: parseRetrievalConfig(payload.retrievalConfigJson),
795
+ maxIterations: existing?.maxIterations,
796
+ memoryEnabled: existing?.memoryEnabled,
797
+ memoryWindow: existing?.memoryWindow,
798
+ published: existing?.published,
799
+ });
800
+ if (
801
+ (payload.mode === 'workflow' || payload.mode === 'chatflow') &&
802
+ payload.graphJson
803
+ ) {
804
+ try {
805
+ const parsed = JSON.parse(payload.graphJson);
806
+ await saveWorkflowDraftReq(payload.appId, parsed);
807
+ } catch {
808
+ message.warning('画布数据无法解析,草稿未保存');
809
+ }
810
+ }
811
+ message.success('已保存');
812
+ await refresh();
813
+ const fresh = agents.value.find((x) => x.id === payload.appId);
814
+ if (fresh) drawerApp.value = fresh;
815
+ } catch (e: any) {
816
+ message.error(e?.message ?? '保存失败');
817
+ } finally {
818
+ submitting.value = false;
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Brand popover → 编辑信息 shortcut. Reuses the same basic-info modal the
824
+ * card ⋯ menu opens; the drawer stays open so the user can drop back into
825
+ * design after saving.
826
+ */
827
+ function onDrawerEditInfo(payload: { appId: string }) {
828
+ const a = agents.value.find((x) => x.id === payload.appId);
829
+ if (a) openEdit(a);
830
+ }
831
+ function onDrawerExportDsl(_payload: { appId: string }) {
832
+ message.info('导出 DSL 功能开发中');
833
+ }
834
+ function onDrawerDuplicate(_payload: { appId: string }) {
835
+ message.info('复制应用功能开发中');
836
+ }
837
+
838
+ async function onDrawerPublish(payload: { appId: string }) {
839
+ try {
840
+ await publishWorkflowDraftReq(payload.appId);
841
+ message.success('已发布新版本');
842
+ await refresh();
843
+ } catch (e: any) {
844
+ message.error(e?.message ?? '发布失败');
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Handle the drawer's `preview` event — pipe the SSE chat stream into the
850
+ * preview pane. Only used for non-workflow modes.
851
+ */
852
+ async function onDrawerPreview(payload: {
853
+ appId: string;
854
+ query: string;
855
+ onChunk: (t: string) => void;
856
+ onDone: () => void;
857
+ onError: (m: string) => void;
858
+ }) {
859
+ try {
860
+ const res = await chatStreamReq(payload.appId, { query: payload.query });
861
+ if (!res.ok || !res.body) {
862
+ payload.onError(`预览失败:${res.status}`);
863
+ return;
864
+ }
865
+ const reader = res.body.getReader();
866
+ const decoder = new TextDecoder('utf-8');
867
+ let buffer = '';
868
+ while (true) {
869
+ const { value, done } = await reader.read();
870
+ if (done) break;
871
+ buffer += decoder.decode(value, { stream: true });
872
+ let idx = buffer.indexOf('\n\n');
873
+ while (idx >= 0) {
874
+ const raw = buffer.slice(0, idx);
875
+ buffer = buffer.slice(idx + 2);
876
+ let eventName = 'message';
877
+ let dataStr = '';
878
+ for (const line of raw.split('\n')) {
879
+ if (line.startsWith('event:')) eventName = line.slice(6).trim();
880
+ else if (line.startsWith('data:')) dataStr += line.slice(5).trim();
881
+ }
882
+ if (eventName === 'result' && dataStr) {
883
+ try {
884
+ const r = JSON.parse(dataStr);
885
+ if (r.text) payload.onChunk(String(r.text));
886
+ } catch {
887
+ // ignore malformed frame
888
+ }
889
+ }
890
+ idx = buffer.indexOf('\n\n');
891
+ }
892
+ }
893
+ payload.onDone();
894
+ } catch (e: any) {
895
+ payload.onError(String(e?.message ?? e));
896
+ }
897
+ }
898
+
899
+ function openEdit(a: AgentEntity) {
900
+ editingId.value = a.id;
901
+ form.name = a.name;
902
+ form.description = a.description ?? '';
903
+ form.icon = a.icon ?? '';
904
+ form.iconBackground = a.iconBackground ?? '';
905
+ showCreate.value = true;
906
+ }
907
+
908
+ /**
909
+ * Save the trimmed basic-info form. Carries over every other field from the
910
+ * existing entity so the PUT doesn't clobber model / strategy / tools /
911
+ * instructions / memory / published state — those live in the design drawer.
912
+ */
913
+ async function submitCreate() {
914
+ if (!form.name) {
915
+ message.warning('请填写名称');
916
+ return;
917
+ }
918
+ if (!editingId.value) return;
919
+ const existing = agents.value.find((x) => x.id === editingId.value);
920
+ if (!existing) {
921
+ message.error('未找到应用');
922
+ return;
923
+ }
924
+ submitting.value = true;
925
+ try {
926
+ const body: CreateAgentRequestFull = {
927
+ name: form.name,
928
+ description: form.description || undefined,
929
+ icon: form.icon || undefined,
930
+ iconBackground: form.iconBackground || undefined,
931
+ mode: existing.mode,
932
+ instructions: existing.instructions,
933
+ openingStatement: existing.openingStatement,
934
+ modelName: existing.modelName,
935
+ modelProvider: existing.modelProvider,
936
+ strategy: (existing.strategy as AgentStrategy) ?? 'REACT',
937
+ toolNames: existing.toolNamesJson
938
+ ? safeParseArray(existing.toolNamesJson)
939
+ : [],
940
+ datasetIds: existing.datasetIdsJson
941
+ ? safeParseArray(existing.datasetIdsJson)
942
+ : [],
943
+ maxIterations: existing.maxIterations,
944
+ memoryEnabled: existing.memoryEnabled,
945
+ memoryWindow: existing.memoryWindow,
946
+ published: existing.published,
947
+ };
948
+ await updateAgentReq(editingId.value, body);
949
+ message.success('已更新');
950
+ showCreate.value = false;
951
+ editingId.value = null;
952
+ await refresh();
953
+ } finally {
954
+ submitting.value = false;
955
+ }
956
+ }
957
+
958
+ function safeParseArray(json?: string): string[] {
959
+ if (!json) return [];
960
+ try {
961
+ const v = JSON.parse(json);
962
+ return Array.isArray(v) ? v.map(String) : [];
963
+ } catch {
964
+ return [];
965
+ }
966
+ }
967
+
968
+ function remove(a: AgentEntity) {
969
+ Modal.confirm({
970
+ title: '删除智能体',
971
+ content: `确认删除「${a.name}」?相关对话历史仍会保留在数据库中。`,
972
+ okText: '删除',
973
+ okType: 'danger',
974
+ onOk: async () => {
975
+ await deleteAgentReq(a.id);
976
+ message.success('已删除');
977
+ await refresh();
978
+ },
979
+ });
980
+ }
981
+
982
+ // ---- share embed modal ----
983
+ const showShare = ref(false);
984
+ const shareAgent = ref<AgentEntity | null>(null);
985
+
986
+ function openShare(a: AgentEntity) {
987
+ shareAgent.value = a;
988
+ showShare.value = true;
989
+ }
990
+
991
+ const shareUrl = computed(() => {
992
+ if (!shareAgent.value) return '';
993
+ return `${window.location.origin}/embed/agent/${shareAgent.value.id}`;
994
+ });
995
+
996
+ const iframeSnippet = computed(() =>
997
+ shareUrl.value
998
+ ? `<iframe src="${shareUrl.value}" width="420" height="620" style="border:1px solid #eee;border-radius:8px" allow="microphone"></iframe>`
999
+ : '',
1000
+ );
1001
+
1002
+ async function copyText(txt: string) {
1003
+ try {
1004
+ await navigator.clipboard.writeText(txt);
1005
+ message.success('已复制');
1006
+ } catch {
1007
+ message.error('复制失败,请手动选中');
1008
+ }
1009
+ }
1010
+
1011
+ onMounted(() => {
1012
+ // 两个都是 async fn,但不 await —— 显式 .catch 兜住 unhandledrejection,
1013
+ // 避免任何一次网络失败把 <KeepAlive> 缓存里的组件挂坏。
1014
+ refresh().catch(() => {});
1015
+ loadDeps().catch(() => {});
1016
+ });
1017
+
1018
+ // 路由切走时(<KeepAlive> 挂起本组件),强制把抽屉状态关掉。抽屉用
1019
+ // <Teleport to="body"> 挂全屏白色遮罩,若切走时 drawerOpen 还是 true,
1020
+ // 遮罩会漏在 body 上盖住后续路由,表现为「切页面就空白,只能 F5 」。
1021
+ // 配合 AppDesignDrawer 里的 :disabled="!open" 是双保险。
1022
+ onDeactivated(() => {
1023
+ drawerOpen.value = false;
1024
+ });
1025
+ </script>
1026
+
1027
+ <template>
1028
+ <div class="agent-apps-page">
1029
+ <div class="agent-apps-header">
1030
+ <div class="agent-apps-title">应用</div>
1031
+ <div class="agent-apps-subtitle">
1032
+ 应用列表:对话 · 智能体 · 工作流 · 文本生成
1033
+ </div>
1034
+ </div>
1035
+
1036
+ <!-- Soft nudge only — creating an app never requires a model configured
1037
+ upfront. Real gating happens when the user tries to *run* the app. -->
1038
+ <Card
1039
+ v-if="llmLoaded && llmModels.length === 0"
1040
+ class="mb-4"
1041
+ :body-style="{ padding: '12px 16px' }"
1042
+ >
1043
+ <div class="flex items-center gap-3">
1044
+ <div class="text-xl">💡</div>
1045
+ <div class="flex-1 text-sm text-gray-600">
1046
+ 还没有配置 LLM 模型。你可以先创建应用占位,等到想跑起来的时候再去
1047
+ <a class="text-indigo-600 cursor-pointer" @click="goToModel">
1048
+ 配置模型
1049
+ </a>
1050
+ 即可。
1051
+ </div>
1052
+ </div>
1053
+ </Card>
1054
+
1055
+ <!-- 顶部搜索 + 过滤 -->
1056
+ <div
1057
+ class="mb-4 flex flex-wrap items-center justify-between gap-2 rounded-lg bg-white p-3 shadow-sm dark:bg-neutral-900"
1058
+ >
1059
+ <div class="flex flex-wrap items-center gap-2">
1060
+ <Input
1061
+ v-model:value="keyword"
1062
+ placeholder="搜索智能体..."
1063
+ allow-clear
1064
+ style="width: 240px"
1065
+ />
1066
+ <Select
1067
+ v-model:value="filterStrategy"
1068
+ :options="[
1069
+ { label: '全部策略', value: 'ALL' },
1070
+ { label: 'ReAct', value: 'REACT' },
1071
+ { label: 'Function Calling', value: 'FUNCTION_CALLING' },
1072
+ { label: 'Plan & Execute', value: 'PLAN_EXECUTE' },
1073
+ ]"
1074
+ style="width: 180px"
1075
+ />
1076
+ <div class="dify-pub-tabs">
1077
+ <button
1078
+ class="dify-pub-tab"
1079
+ :class="{ 'dify-pub-tab--on': filterPublished === 'all' }"
1080
+ @click="filterPublished = 'all'"
1081
+ >
1082
+ 全部 <span class="dify-pub-count">{{ agents.length }}</span>
1083
+ </button>
1084
+ <button
1085
+ class="dify-pub-tab"
1086
+ :class="{ 'dify-pub-tab--on': filterPublished === 'published' }"
1087
+ @click="filterPublished = 'published'"
1088
+ >
1089
+ 已发布
1090
+ <span class="dify-pub-count">{{ publishedCount }}</span>
1091
+ </button>
1092
+ <button
1093
+ class="dify-pub-tab"
1094
+ :class="{ 'dify-pub-tab--on': filterPublished === 'draft' }"
1095
+ @click="filterPublished = 'draft'"
1096
+ >
1097
+ 草稿 <span class="dify-pub-count">{{ draftCount }}</span>
1098
+ </button>
1099
+ </div>
1100
+ </div>
1101
+ <div class="text-xs text-gray-500">
1102
+ 共 {{ filtered.length }} / {{ agents.length }} 个
1103
+ </div>
1104
+ </div>
1105
+
1106
+ <div v-if="loading && agents.length === 0" class="dify-grid">
1107
+ <div v-for="n in 6" :key="n" class="dify-card">
1108
+ <Skeleton :active="true" :paragraph="{ rows: 3 }" />
1109
+ </div>
1110
+ </div>
1111
+
1112
+ <Spin :spinning="loading && agents.length > 0">
1113
+ <div v-if="!loading || agents.length > 0" class="dify-grid">
1114
+ <!-- "新建" 占位卡 —— 点开走 Dify 风格的 CreateAppModal 选类型。
1115
+ Never disabled: apps can be created without a model configured. -->
1116
+ <div class="dify-card dify-card-new" @click="openTypePicker">
1117
+ <div class="dify-new-inner">
1118
+ <div class="dify-new-plus">+</div>
1119
+ <div class="dify-new-text">新建应用</div>
1120
+ <div class="dify-new-sub">
1121
+ 对话 / Agent / 工作流 / Chatflow / 文本生成
1122
+ </div>
1123
+ </div>
1124
+ </div>
1125
+
1126
+ <!-- 智能体卡 -->
1127
+ <div
1128
+ v-for="a in filtered"
1129
+ :key="a.id"
1130
+ class="dify-card"
1131
+ @click="openDesigner(a)"
1132
+ >
1133
+ <div class="dify-header">
1134
+ <div class="dify-icon" :style="{ background: bgOf(a) }">
1135
+ {{ iconOf(a) }}
1136
+ </div>
1137
+ <div class="dify-title-wrap">
1138
+ <div class="dify-title" :title="a.name">
1139
+ {{ a.name }}
1140
+ <Tag
1141
+ v-if="a.published === false"
1142
+ color="orange"
1143
+ size="small"
1144
+ style="margin-left: 4px; font-size: 10px"
1145
+ >
1146
+ 草稿
1147
+ </Tag>
1148
+ </div>
1149
+ <div class="dify-meta">
1150
+ <Tag :color="modeColor(a.mode)" style="margin-right: 4px">
1151
+ {{ modeLabel(a.mode) }}
1152
+ </Tag>
1153
+ <Tag :color="strategyColor(a.strategy)" style="margin-right: 4px">
1154
+ {{ strategyLabel(a.strategy) }}
1155
+ </Tag>
1156
+ <span>· {{ fromNow(a.updatedAt) || '刚刚' }}</span>
1157
+ </div>
1158
+ </div>
1159
+ </div>
1160
+
1161
+ <div
1162
+ class="dify-desc"
1163
+ :title="a.description || a.instructions || ''"
1164
+ >
1165
+ {{ a.description || a.instructions || '暂无描述' }}
1166
+ </div>
1167
+
1168
+ <div class="dify-spacer" />
1169
+
1170
+ <div class="dify-footer">
1171
+ <span class="dify-footer-item" :title="modelLabel(a)">
1172
+ 🧠 {{ modelLabel(a) }}
1173
+ </span>
1174
+ <span
1175
+ v-if="a.maxIterations"
1176
+ class="dify-footer-item"
1177
+ :title="`最多 ${a.maxIterations} 轮`"
1178
+ >
1179
+ 🔁 {{ a.maxIterations }}
1180
+ </span>
1181
+ </div>
1182
+
1183
+ <div class="dify-more" @click.stop>
1184
+ <Dropdown :trigger="['click']" placement="bottomRight">
1185
+ <Button type="text" size="small" class="dify-more-btn">
1186
+
1187
+ </Button>
1188
+ <template #overlay>
1189
+ <Menu>
1190
+ <MenuItem key="design" @click="openDesigner(a)">
1191
+ 设计
1192
+ </MenuItem>
1193
+ <MenuItem key="chat" @click="openChat(a)">
1194
+ 独立对话
1195
+ </MenuItem>
1196
+ <MenuItem key="edit" @click="openEdit(a)">
1197
+ 编辑基本信息
1198
+ </MenuItem>
1199
+ <MenuItem key="share" @click="openShare(a)">
1200
+ 分享嵌入
1201
+ </MenuItem>
1202
+ <MenuItem key="delete" danger @click="remove(a)">
1203
+ 删除
1204
+ </MenuItem>
1205
+ </Menu>
1206
+ </template>
1207
+ </Dropdown>
1208
+ </div>
1209
+ </div>
1210
+ </div>
1211
+
1212
+ <Empty
1213
+ v-if="!loading && filtered.length === 0"
1214
+ class="mt-8"
1215
+ :description="
1216
+ keyword
1217
+ ? '没有找到匹配的智能体'
1218
+ : '还没有智能体,点上面的“新建智能体”开始吧'
1219
+ "
1220
+ />
1221
+ </Spin>
1222
+
1223
+ <Modal
1224
+ v-model:open="showCreate"
1225
+ title="编辑基本信息"
1226
+ width="520px"
1227
+ :confirm-loading="submitting"
1228
+ ok-text="保存"
1229
+ @ok="submitCreate"
1230
+ >
1231
+ <Form :model="form" layout="vertical">
1232
+ <FormItem label="图标">
1233
+ <div class="flex flex-wrap gap-1">
1234
+ <button
1235
+ v-for="ic in ICONS"
1236
+ :key="ic"
1237
+ type="button"
1238
+ class="dify-emoji-btn"
1239
+ :class="{ 'dify-emoji-btn--on': form.icon === ic }"
1240
+ :style="{ background: form.iconBackground || '#F3F4F6' }"
1241
+ @click="form.icon = ic"
1242
+ >
1243
+ {{ ic }}
1244
+ </button>
1245
+ </div>
1246
+ </FormItem>
1247
+ <FormItem label="背景颜色">
1248
+ <div class="flex flex-wrap gap-1">
1249
+ <button
1250
+ v-for="bg in BGS"
1251
+ :key="bg"
1252
+ type="button"
1253
+ class="dify-bg-btn"
1254
+ :class="{ 'dify-bg-btn--on': form.iconBackground === bg }"
1255
+ :style="{ background: bg }"
1256
+ @click="form.iconBackground = bg"
1257
+ />
1258
+ </div>
1259
+ </FormItem>
1260
+ <FormItem label="名称" required>
1261
+ <Input v-model:value="form.name" placeholder="例如 report-writer" />
1262
+ </FormItem>
1263
+ <FormItem label="描述">
1264
+ <Textarea
1265
+ v-model:value="form.description"
1266
+ :rows="3"
1267
+ placeholder="一句话说明这个智能体在做什么 —— 显示在卡片上"
1268
+ />
1269
+ </FormItem>
1270
+ </Form>
1271
+ </Modal>
1272
+
1273
+ <Modal
1274
+ v-model:open="showShare"
1275
+ :title="shareAgent ? `分享 · ${shareAgent.name}` : '分享'"
1276
+ width="640px"
1277
+ :footer="null"
1278
+ >
1279
+ <div class="mb-2 text-xs text-gray-500">
1280
+ 无登录嵌入。iframe 到你的站点即可用,会话会自动保存到浏览器 localStorage。
1281
+ </div>
1282
+ <div class="mb-1 text-xs font-medium">直链</div>
1283
+ <div class="mb-3 flex gap-2">
1284
+ <Input :value="shareUrl" readonly class="flex-1" />
1285
+ <Button @click="copyText(shareUrl)">复制</Button>
1286
+ </div>
1287
+ <div class="mb-1 text-xs font-medium">iframe 嵌入代码</div>
1288
+ <div class="flex gap-2">
1289
+ <Textarea
1290
+ :value="iframeSnippet"
1291
+ :rows="4"
1292
+ readonly
1293
+ class="flex-1"
1294
+ style="font-family: monospace; font-size: 12px"
1295
+ />
1296
+ <Button @click="copyText(iframeSnippet)">复制</Button>
1297
+ </div>
1298
+ <div class="mt-3 text-xs text-gray-400">
1299
+ 提示:URL 支持 <code>?title=xxx</code> 覆盖标题、
1300
+ <code>?conversation=xxx</code> 恢复到指定会话。
1301
+ </div>
1302
+ </Modal>
1303
+
1304
+ <!-- Dify-style app-type picker. Single-page: type tiles + form on the
1305
+ left, big preview on the right. Chatflow is a first-class primary
1306
+ tile alongside workflow — same DAG canvas, but the runtime keeps
1307
+ per-conversation memory + streams via ANSWER nodes. -->
1308
+ <CreateAppModal
1309
+ v-model:open="showTypePicker"
1310
+ :allow="['chatbot', 'agent', 'workflow', 'chatflow', 'text-generator']"
1311
+ @create="onCreateApp"
1312
+ />
1313
+
1314
+ <!-- Click-a-card design drawer. Drawer's built-in DrawerFlowDesigner /
1315
+ LogAnnotationPanel / MonitorPanel handle everything internally now —
1316
+ we just wire them via :api to the local fetch bag. -->
1317
+ <AppDesignDrawer
1318
+ v-model:open="drawerOpen"
1319
+ :app="drawerApp"
1320
+ :initial-graph-json="drawerGraphJson"
1321
+ :api="studioApi"
1322
+ :api-base="apiBase"
1323
+ :chat-config="drawerChatConfig"
1324
+ :models="
1325
+ llmModels.map((m) => ({
1326
+ id: m.id,
1327
+ label: `${m.providerName} · ${m.modelName}`,
1328
+ provider: m.providerName,
1329
+ }))
1330
+ "
1331
+ :tools="tools.map((t) => ({ name: t.name, description: t.description }))"
1332
+ :knowledge-bases="datasets.map((d) => ({ id: d.id, name: d.name }))"
1333
+ @save="onDrawerSave"
1334
+ @preview="onDrawerPreview"
1335
+ @publish="onDrawerPublish"
1336
+ @edit-info="onDrawerEditInfo"
1337
+ @export-dsl="onDrawerExportDsl"
1338
+ @duplicate="onDrawerDuplicate"
1339
+ />
1340
+ </div>
1341
+ </template>
1342
+
1343
+ <style scoped>
1344
+ .agent-apps-page {
1345
+ padding: 16px;
1346
+ }
1347
+ .agent-apps-header {
1348
+ margin-bottom: 16px;
1349
+ }
1350
+ .agent-apps-title {
1351
+ font-size: 18px;
1352
+ font-weight: 600;
1353
+ color: #111827;
1354
+ }
1355
+ .agent-apps-subtitle {
1356
+ margin-top: 4px;
1357
+ font-size: 12px;
1358
+ color: #6b7280;
1359
+ }
1360
+ :global(.dark) .agent-apps-title {
1361
+ color: #f3f4f6;
1362
+ }
1363
+ :global(.dark) .agent-apps-subtitle {
1364
+ color: #9ca3af;
1365
+ }
1366
+
1367
+ .dify-grid {
1368
+ display: grid;
1369
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1370
+ gap: 16px;
1371
+ }
1372
+ .dify-card {
1373
+ position: relative;
1374
+ height: 200px;
1375
+ padding: 14px 16px 12px;
1376
+ background: #fff;
1377
+ border: 1px solid #e5e7eb;
1378
+ border-radius: 12px;
1379
+ cursor: pointer;
1380
+ transition:
1381
+ box-shadow 0.15s ease,
1382
+ transform 0.15s ease,
1383
+ background 0.15s ease;
1384
+ display: flex;
1385
+ flex-direction: column;
1386
+ overflow: hidden;
1387
+ }
1388
+ :global(.dark) .dify-card {
1389
+ background: #1f1f1f;
1390
+ border-color: #2d2d2d;
1391
+ }
1392
+ .dify-card:hover {
1393
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
1394
+ transform: translateY(-1px);
1395
+ }
1396
+ .dify-card-new {
1397
+ border: 1.5px dashed #c7d2fe;
1398
+ background: linear-gradient(135deg, #f5f9ff 0%, #f0f5ff 100%);
1399
+ }
1400
+ .dify-card-new:hover {
1401
+ border-color: #6366f1;
1402
+ background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
1403
+ }
1404
+ .dify-card-disabled {
1405
+ opacity: 0.6;
1406
+ cursor: not-allowed;
1407
+ }
1408
+ .dify-new-inner {
1409
+ margin: auto 0;
1410
+ text-align: center;
1411
+ color: #6366f1;
1412
+ }
1413
+ .dify-new-plus {
1414
+ font-size: 42px;
1415
+ line-height: 1;
1416
+ font-weight: 200;
1417
+ }
1418
+ .dify-new-text {
1419
+ margin-top: 6px;
1420
+ font-size: 15px;
1421
+ font-weight: 500;
1422
+ }
1423
+ .dify-new-sub {
1424
+ margin-top: 4px;
1425
+ font-size: 12px;
1426
+ color: #94a3b8;
1427
+ }
1428
+ .dify-header {
1429
+ display: flex;
1430
+ gap: 12px;
1431
+ align-items: flex-start;
1432
+ }
1433
+ .dify-icon {
1434
+ width: 40px;
1435
+ height: 40px;
1436
+ flex-shrink: 0;
1437
+ display: flex;
1438
+ align-items: center;
1439
+ justify-content: center;
1440
+ border-radius: 8px;
1441
+ font-size: 22px;
1442
+ }
1443
+ .dify-title-wrap {
1444
+ min-width: 0;
1445
+ flex: 1;
1446
+ }
1447
+ .dify-title {
1448
+ font-size: 15px;
1449
+ font-weight: 600;
1450
+ color: #111827;
1451
+ white-space: nowrap;
1452
+ overflow: hidden;
1453
+ text-overflow: ellipsis;
1454
+ }
1455
+ :global(.dark) .dify-title {
1456
+ color: #f3f4f6;
1457
+ }
1458
+ .dify-meta {
1459
+ margin-top: 4px;
1460
+ font-size: 12px;
1461
+ color: #6b7280;
1462
+ display: flex;
1463
+ align-items: center;
1464
+ gap: 4px;
1465
+ }
1466
+ .dify-desc {
1467
+ margin-top: 10px;
1468
+ font-size: 13px;
1469
+ color: #6b7280;
1470
+ line-height: 1.5;
1471
+ display: -webkit-box;
1472
+ -webkit-line-clamp: 2;
1473
+ -webkit-box-orient: vertical;
1474
+ overflow: hidden;
1475
+ }
1476
+ :global(.dark) .dify-desc {
1477
+ color: #9ca3af;
1478
+ }
1479
+ .dify-spacer {
1480
+ flex: 1;
1481
+ }
1482
+ .dify-footer {
1483
+ display: flex;
1484
+ align-items: center;
1485
+ gap: 12px;
1486
+ font-size: 12px;
1487
+ color: #6b7280;
1488
+ border-top: 1px solid #f3f4f6;
1489
+ padding-top: 8px;
1490
+ overflow: hidden;
1491
+ white-space: nowrap;
1492
+ }
1493
+ :global(.dark) .dify-footer {
1494
+ border-top-color: #2d2d2d;
1495
+ }
1496
+ .dify-footer-item {
1497
+ display: inline-flex;
1498
+ align-items: center;
1499
+ gap: 4px;
1500
+ overflow: hidden;
1501
+ text-overflow: ellipsis;
1502
+ max-width: 200px;
1503
+ }
1504
+ .dify-more {
1505
+ position: absolute;
1506
+ top: 8px;
1507
+ right: 8px;
1508
+ opacity: 0;
1509
+ transition: opacity 0.15s ease;
1510
+ }
1511
+ .dify-card:hover .dify-more {
1512
+ opacity: 1;
1513
+ }
1514
+ .dify-more-btn {
1515
+ font-size: 18px;
1516
+ line-height: 1;
1517
+ padding: 0 6px;
1518
+ height: 26px;
1519
+ }
1520
+
1521
+ /* Emoji picker for the agent icon */
1522
+ .dify-emoji-btn {
1523
+ width: 32px;
1524
+ height: 32px;
1525
+ font-size: 18px;
1526
+ border: 1px solid transparent;
1527
+ border-radius: 8px;
1528
+ cursor: pointer;
1529
+ display: flex;
1530
+ align-items: center;
1531
+ justify-content: center;
1532
+ transition:
1533
+ border-color 0.15s,
1534
+ transform 0.1s;
1535
+ }
1536
+ .dify-emoji-btn:hover {
1537
+ border-color: #a5b4fc;
1538
+ transform: scale(1.05);
1539
+ }
1540
+ .dify-emoji-btn--on {
1541
+ border-color: #6366f1;
1542
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
1543
+ }
1544
+
1545
+ /* Published filter tabs */
1546
+ .dify-pub-tabs {
1547
+ display: inline-flex;
1548
+ gap: 2px;
1549
+ padding: 2px;
1550
+ background: #f3f4f6;
1551
+ border-radius: 6px;
1552
+ }
1553
+ .dify-pub-tab {
1554
+ background: transparent;
1555
+ border: none;
1556
+ border-radius: 4px;
1557
+ padding: 4px 10px;
1558
+ font-size: 12px;
1559
+ color: #6b7280;
1560
+ cursor: pointer;
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 4px;
1564
+ }
1565
+ .dify-pub-tab:hover {
1566
+ color: #111827;
1567
+ }
1568
+ .dify-pub-tab--on {
1569
+ background: #fff;
1570
+ color: #111827;
1571
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1572
+ }
1573
+ .dify-pub-count {
1574
+ padding: 0 4px;
1575
+ font-size: 10px;
1576
+ border-radius: 999px;
1577
+ background: rgba(0, 0, 0, 0.05);
1578
+ min-width: 16px;
1579
+ text-align: center;
1580
+ }
1581
+ .dify-pub-tab--on .dify-pub-count {
1582
+ background: #eef2ff;
1583
+ color: #4338ca;
1584
+ }
1585
+
1586
+ /* Background color picker */
1587
+ .dify-bg-btn {
1588
+ width: 28px;
1589
+ height: 28px;
1590
+ border: 2px solid transparent;
1591
+ border-radius: 6px;
1592
+ cursor: pointer;
1593
+ transition: border-color 0.15s;
1594
+ }
1595
+ .dify-bg-btn:hover {
1596
+ border-color: #9ca3af;
1597
+ }
1598
+ .dify-bg-btn--on {
1599
+ border-color: #6366f1;
1600
+ }
1601
+ </style>