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,553 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * AgentChatPage — 独立会话页。
4
+ *
5
+ * 用法:
6
+ * <AgentChatPage :agent-id="id" api-base="/api" />
7
+ *
8
+ * 组件内部完成:查询 agent 元数据、拉工具列表、加载/切换会话、SSE 流式发送、
9
+ * 变量输入侧栏、curl 快捷复制、失败重试。宿主只需给 agentId + apiBase。
10
+ */
11
+ import { computed, nextTick, onMounted, ref } from 'vue';
12
+
13
+ import {
14
+ Button,
15
+ Card,
16
+ Dropdown,
17
+ Empty,
18
+ Input,
19
+ Menu,
20
+ MenuItem,
21
+ message,
22
+ Tag,
23
+ } from 'ant-design-vue';
24
+
25
+ import {
26
+ type AgentEntity,
27
+ type AgentStudioApi,
28
+ type AgentToolView,
29
+ createAgentStudioSpringBackend,
30
+ } from '../adapters/springAgentStart';
31
+
32
+ interface Props {
33
+ /** 应用 id — 必传 (路由参数直接传进来即可)。 */
34
+ agentId: string;
35
+ /** 后端 base URL, 默认 `/api`, 忽略 `api` 覆写. */
36
+ apiBase?: string;
37
+ /** 完整 AgentStudioApi 覆写内建 adapter。 */
38
+ api?: AgentStudioApi;
39
+ }
40
+ const props = withDefaults(defineProps<Props>(), {
41
+ apiBase: '/api',
42
+ api: undefined,
43
+ });
44
+
45
+ const backend = computed<AgentStudioApi>(() =>
46
+ props.api ?? createAgentStudioSpringBackend({ baseUrl: props.apiBase }),
47
+ );
48
+ // Shim for the copy-curl button — still hard-codes localhost:18090 in the
49
+ // original view; keep the same behaviour but expose apiBase for override.
50
+ const curlBaseUrl = computed(() => {
51
+ try {
52
+ const u = new URL(props.apiBase, window.location.origin);
53
+ return u.origin + u.pathname.replace(/\/+$/, '');
54
+ } catch {
55
+ return `http://localhost:18090${props.apiBase}`;
56
+ }
57
+ });
58
+
59
+ const agentId = computed(() => props.agentId);
60
+ const getAgent = (id: string) => backend.value.getAgent(id);
61
+ const fetchAgentTools = (id: string) => backend.value.fetchAgentTools(id);
62
+ const fetchConversations = (id: string) => backend.value.fetchConversations(id);
63
+ const fetchHistory = (id: string, cid: string) =>
64
+ backend.value.fetchHistoryMessages(id, cid);
65
+ const chatStream = (id: string, req: any) => backend.value.chatStream(id, req);
66
+
67
+ /** localStorage key for "which conversation was this agent last using". */
68
+ const storageKey = computed(() => `spring-agent:chat:${agentId.value}:conversation`);
69
+ /** localStorage key for the list of known conversations for this agent. */
70
+ const listKey = computed(() => `spring-agent:chat:${agentId.value}:conversations`);
71
+
72
+ /** Loaded list of past conversations (id + first user query as preview). */
73
+ const conversations = ref<Array<{ id: string; preview: string }>>([]);
74
+
75
+ async function loadKnownConversations() {
76
+ // Prefer the server list (survives cache clears + different browsers); fall back
77
+ // to localStorage when the backend has never seen this agent's conversations.
78
+ try {
79
+ const server = await fetchConversations(agentId.value);
80
+ if (server.length > 0) {
81
+ conversations.value = server.map((c) => ({
82
+ id: c.conversationId,
83
+ preview: c.firstMessage,
84
+ }));
85
+ return;
86
+ }
87
+ } catch {
88
+ // fall through to localStorage
89
+ }
90
+ try {
91
+ const raw = localStorage.getItem(listKey.value);
92
+ conversations.value = raw ? JSON.parse(raw) : [];
93
+ } catch {
94
+ conversations.value = [];
95
+ }
96
+ }
97
+
98
+ function rememberConversation(id: string, firstMessage: string) {
99
+ const preview = firstMessage.slice(0, 40) || id.slice(0, 8);
100
+ const existing = conversations.value.find((c) => c.id === id);
101
+ if (existing) {
102
+ existing.preview = existing.preview || preview;
103
+ } else {
104
+ conversations.value = [{ id, preview }, ...conversations.value].slice(0, 20);
105
+ }
106
+ localStorage.setItem(listKey.value, JSON.stringify(conversations.value));
107
+ }
108
+
109
+ const agent = ref<AgentEntity | null>(null);
110
+
111
+ /** Parsed Dify-parity suggested-question chips shown on the empty conversation. */
112
+ const suggestedQuestions = computed<string[]>(() => {
113
+ const raw = agent.value?.suggestedQuestionsJson;
114
+ if (!raw) return [];
115
+ try {
116
+ const arr = JSON.parse(raw);
117
+ return Array.isArray(arr) ? arr.filter((q) => typeof q === 'string' && q.trim().length > 0) : [];
118
+ } catch {
119
+ return [];
120
+ }
121
+ });
122
+ const agentTools = ref<AgentToolView[]>([]);
123
+ const messages = ref<
124
+ Array<{
125
+ role: 'user' | 'assistant';
126
+ content: string;
127
+ steps?: Array<Record<string, unknown>>;
128
+ failed?: boolean;
129
+ /** For retry: the user query that produced this failed assistant reply. */
130
+ sourceQuery?: string;
131
+ }>
132
+ >([]);
133
+
134
+ const input = ref('');
135
+ const sending = ref(false);
136
+ const conversationId = ref<string | undefined>(undefined);
137
+ const scroller = ref<HTMLElement | null>(null);
138
+
139
+ // -------- variables sidebar (for {{#user.xxx#}} style templates) --------
140
+ const variables = ref<Array<{ key: string; value: string }>>([
141
+ { key: '', value: '' },
142
+ ]);
143
+ const variablesKey = computed(
144
+ () => `spring-agent:chat:${agentId.value}:variables`,
145
+ );
146
+ // Persist per-agent so switching agents remembers each one's inputs.
147
+ function saveVariables() {
148
+ const map: Record<string, string> = {};
149
+ for (const v of variables.value) {
150
+ if (v.key) map[v.key] = v.value;
151
+ }
152
+ localStorage.setItem(variablesKey.value, JSON.stringify(map));
153
+ }
154
+ function loadVariables() {
155
+ try {
156
+ const raw = localStorage.getItem(variablesKey.value);
157
+ if (raw) {
158
+ const map = JSON.parse(raw) as Record<string, string>;
159
+ const arr = Object.entries(map).map(([k, v]) => ({ key: k, value: v }));
160
+ variables.value = arr.length > 0 ? arr : [{ key: '', value: '' }];
161
+ }
162
+ } catch {
163
+ // ignore malformed storage
164
+ }
165
+ }
166
+ function addVariable() {
167
+ variables.value.push({ key: '', value: '' });
168
+ }
169
+ function removeVariable(idx: number) {
170
+ variables.value.splice(idx, 1);
171
+ if (variables.value.length === 0) addVariable();
172
+ saveVariables();
173
+ }
174
+ function variablesMap(): Record<string, string> {
175
+ const map: Record<string, string> = {};
176
+ for (const v of variables.value) {
177
+ if (v.key) map[v.key] = v.value;
178
+ }
179
+ return map;
180
+ }
181
+ function copyCurl() {
182
+ const body = {
183
+ query: input.value || '<your query here>',
184
+ conversationId: conversationId.value,
185
+ variables: variablesMap(),
186
+ };
187
+ const cmd =
188
+ `curl -X POST ${curlBaseUrl.value}/agents/${agentId.value}/chat \\\n` +
189
+ ` -H "Content-Type: application/json" \\\n` +
190
+ ` -d '${JSON.stringify(body).replace(/'/g, `'\\''`)}'`;
191
+ navigator.clipboard.writeText(cmd).then(
192
+ () => message.success('已复制 curl 命令'),
193
+ () => message.error('复制失败,请手动选择'),
194
+ );
195
+ }
196
+
197
+ onMounted(async () => {
198
+ agent.value = await getAgent(agentId.value);
199
+ fetchAgentTools(agentId.value)
200
+ .then((t) => {
201
+ agentTools.value = t;
202
+ })
203
+ .catch(() => {
204
+ agentTools.value = [];
205
+ });
206
+ await loadKnownConversations();
207
+ loadVariables();
208
+ // Recover the last conversation from storage, then load its history.
209
+ const stored = localStorage.getItem(storageKey.value);
210
+ if (stored) {
211
+ conversationId.value = stored;
212
+ await loadHistory();
213
+ }
214
+ });
215
+
216
+ async function switchTo(id: string) {
217
+ conversationId.value = id;
218
+ localStorage.setItem(storageKey.value, id);
219
+ messages.value = [];
220
+ await loadHistory();
221
+ }
222
+
223
+ async function loadHistory() {
224
+ if (!conversationId.value) return;
225
+ try {
226
+ const history = await fetchHistory(agentId.value, conversationId.value);
227
+ messages.value = history.map((h) => ({
228
+ role: h.role === 'USER' ? 'user' : 'assistant',
229
+ content: h.content,
230
+ }));
231
+ scrollBottom();
232
+ } catch {
233
+ // History fetch is best-effort — a fresh conversation should also work.
234
+ }
235
+ }
236
+
237
+ async function scrollBottom() {
238
+ await nextTick();
239
+ scroller.value?.scrollTo({ top: scroller.value.scrollHeight });
240
+ }
241
+
242
+ async function send(overrideText?: string) {
243
+ const text = overrideText ?? input.value.trim();
244
+ if (!text) return;
245
+ if (!overrideText) input.value = '';
246
+ messages.value.push({ role: 'user', content: text });
247
+ const asst = {
248
+ role: 'assistant' as const,
249
+ content: '',
250
+ steps: [] as any[],
251
+ sourceQuery: text,
252
+ failed: false,
253
+ };
254
+ messages.value.push(asst);
255
+ scrollBottom();
256
+
257
+ sending.value = true;
258
+ try {
259
+ saveVariables();
260
+ const res = await chatStream(agentId.value, {
261
+ query: text,
262
+ conversationId: conversationId.value,
263
+ variables: variablesMap(),
264
+ });
265
+ if (!res.ok || !res.body) {
266
+ throw new Error(`HTTP ${res.status}`);
267
+ }
268
+ const reader = res.body.getReader();
269
+ const decoder = new TextDecoder();
270
+ let buffer = '';
271
+ let currentEvent = 'message';
272
+ while (true) {
273
+ const { value, done } = await reader.read();
274
+ if (done) break;
275
+ buffer += decoder.decode(value, { stream: true });
276
+ const parts = buffer.split(/\r?\n\r?\n/);
277
+ buffer = parts.pop() ?? '';
278
+ for (const chunk of parts) {
279
+ const lines = chunk.split(/\r?\n/);
280
+ let data = '';
281
+ for (const line of lines) {
282
+ if (line.startsWith('event:')) currentEvent = line.slice(6).trim();
283
+ else if (line.startsWith('data:')) data += line.slice(5).trim();
284
+ }
285
+ if (!data) continue;
286
+ try {
287
+ const parsed = JSON.parse(data);
288
+ if (currentEvent === 'step') {
289
+ asst.steps!.push(parsed);
290
+ asst.content = livePreview(asst.steps!);
291
+ } else if (currentEvent === 'result') {
292
+ asst.content = parsed.text ?? '(无内容)';
293
+ asst.steps = parsed.steps ?? asst.steps;
294
+ conversationId.value = parsed.conversationId;
295
+ if (parsed.conversationId) {
296
+ localStorage.setItem(storageKey.value, parsed.conversationId);
297
+ rememberConversation(parsed.conversationId, text);
298
+ }
299
+ } else if (currentEvent === 'error') {
300
+ asst.content = `❌ ${parsed.message ?? '未知错误'}`;
301
+ asst.failed = true;
302
+ }
303
+ } catch {
304
+ // ignore parse error
305
+ }
306
+ scrollBottom();
307
+ }
308
+ }
309
+ } catch (e: any) {
310
+ asst.content = `❌ ${e?.message ?? e}`;
311
+ asst.failed = true;
312
+ message.error(String(e));
313
+ } finally {
314
+ sending.value = false;
315
+ scrollBottom();
316
+ }
317
+ }
318
+
319
+ async function retry(m: { sourceQuery?: string }) {
320
+ if (!m.sourceQuery) return;
321
+ // Drop the failed assistant reply + its user pair so we don't leave stale bubbles.
322
+ // The user query is always the message just before the assistant one.
323
+ const failedIdx = messages.value.findIndex((x) => x === (m as any));
324
+ if (failedIdx >= 1) {
325
+ messages.value.splice(failedIdx - 1, 2);
326
+ }
327
+ await send(m.sourceQuery);
328
+ }
329
+
330
+ function newConversation() {
331
+ conversationId.value = undefined;
332
+ messages.value = [];
333
+ localStorage.removeItem(storageKey.value);
334
+ }
335
+
336
+ /**
337
+ * 边跑边显示最新一步的实际内容,而不是"思考中... N 步"。
338
+ * 优先级:最后一步的 thought → observation → action → 兜底 kind。
339
+ */
340
+ function livePreview(steps: Array<Record<string, any>>): string {
341
+ if (steps.length === 0) return '思考中...';
342
+ const last = steps[steps.length - 1] ?? {};
343
+ const kind = String(last.kind ?? '').toUpperCase();
344
+ const truncate = (s: string) => (s.length > 200 ? s.slice(0, 200) + '…' : s);
345
+ if (last.thought) return `💭 ${truncate(String(last.thought))}`;
346
+ if (last.observation) return `👀 ${truncate(String(last.observation))}`;
347
+ if (last.action) return `🔧 调用工具 ${last.action}(${truncate(String(last.actionInput ?? ''))})`;
348
+ return `${kind || 'STEP'} · 第 ${steps.length} 步...`;
349
+ }
350
+
351
+ // Sample placeholder token shown in the variables sidebar. Assembled here so
352
+ // the template's `{{ }}` compiler doesn't try to eat the inner `{{`.
353
+ const SAMPLE_VAR_TOKEN = '{' + '{#user.xxx#}' + '}';
354
+ </script>
355
+
356
+ <template>
357
+ <div class="agent-start-chat-page">
358
+ <div class="agent-start-chat-page__header">
359
+ <div class="agent-start-chat-page__title">{{ agent?.name ?? '对话' }}</div>
360
+ <div v-if="agent?.instructions" class="agent-start-chat-page__desc">
361
+ {{ agent.instructions }}
362
+ </div>
363
+ </div>
364
+ <div class="grid grid-cols-1 gap-4 xl:grid-cols-4">
365
+ <Card class="xl:col-span-3">
366
+ <!-- Tools this agent can call — helps the user know what to ask -->
367
+ <div
368
+ v-if="agentTools.length > 0"
369
+ class="mb-2 flex flex-wrap items-center gap-1 text-xs"
370
+ >
371
+ <span class="text-gray-500">🔧 可用工具:</span>
372
+ <Tag
373
+ v-for="t in agentTools"
374
+ :key="t.name"
375
+ color="processing"
376
+ :title="t.description"
377
+ style="cursor: help"
378
+ >
379
+ {{ t.name }}
380
+ </Tag>
381
+ </div>
382
+ <div class="mb-3 flex items-center justify-between border-b pb-2">
383
+ <div class="flex items-center gap-2 text-sm text-gray-500">
384
+ conversation:
385
+ <Tag v-if="conversationId">{{ conversationId.slice(0, 8) }}</Tag>
386
+ <Tag v-else color="default">新会话</Tag>
387
+ </div>
388
+ <div class="flex items-center gap-2">
389
+ <Button size="small" @click="copyCurl">复制 curl</Button>
390
+ <Dropdown v-if="conversations.length > 0" trigger="click">
391
+ <Button size="small">
392
+ 切换会话 ({{ conversations.length }})
393
+ </Button>
394
+ <template #overlay>
395
+ <Menu>
396
+ <MenuItem
397
+ v-for="c in conversations"
398
+ :key="c.id"
399
+ @click="switchTo(c.id)"
400
+ >
401
+ <div class="text-xs text-gray-500">
402
+ {{ c.id.slice(0, 8) }}
403
+ </div>
404
+ <div class="max-w-[300px] truncate text-sm">
405
+ {{ c.preview }}
406
+ </div>
407
+ </MenuItem>
408
+ </Menu>
409
+ </template>
410
+ </Dropdown>
411
+ <Button size="small" @click="newConversation">新会话</Button>
412
+ </div>
413
+ </div>
414
+
415
+ <div
416
+ ref="scroller"
417
+ class="mb-3 h-[520px] overflow-y-auto rounded border border-gray-100 p-3"
418
+ >
419
+ <!-- Dify-parity welcome: opening statement + suggested question chips -->
420
+ <div
421
+ v-if="messages.length === 0 && (agent?.openingStatement || suggestedQuestions.length > 0)"
422
+ class="mb-4"
423
+ >
424
+ <div
425
+ v-if="agent?.openingStatement"
426
+ class="mb-3 rounded-lg border border-indigo-100 bg-indigo-50 p-3 text-sm"
427
+ >
428
+ <div class="mb-1 flex items-center gap-2 text-xs font-medium text-indigo-700">
429
+ <Tag color="green" style="margin: 0">Agent</Tag>
430
+ <span>开场白</span>
431
+ </div>
432
+ <div class="whitespace-pre-wrap text-gray-800">
433
+ {{ agent.openingStatement }}
434
+ </div>
435
+ </div>
436
+ <div
437
+ v-if="suggestedQuestions.length > 0"
438
+ class="flex flex-wrap gap-2"
439
+ >
440
+ <button
441
+ v-for="q in suggestedQuestions"
442
+ :key="q"
443
+ type="button"
444
+ class="rounded-full border border-gray-200 bg-white px-3 py-1 text-sm text-gray-700 hover:border-indigo-400 hover:bg-indigo-50"
445
+ @click="input = q; send();"
446
+ >
447
+ 💬 {{ q }}
448
+ </button>
449
+ </div>
450
+ </div>
451
+ <Empty
452
+ v-if="messages.length === 0 && !agent?.openingStatement && suggestedQuestions.length === 0"
453
+ description="发第一条消息试试"
454
+ />
455
+ <div v-for="(m, i) in messages" :key="i" class="mb-4">
456
+ <div class="mb-1 text-xs font-medium">
457
+ <Tag v-if="m.role === 'user'" color="blue">用户</Tag>
458
+ <Tag v-else color="green">Agent</Tag>
459
+ </div>
460
+ <div class="whitespace-pre-wrap text-sm">{{ m.content }}</div>
461
+ <div v-if="m.failed" class="mt-1">
462
+ <Button size="small" @click="retry(m)">重试</Button>
463
+ </div>
464
+ <details v-if="m.role === 'assistant' && m.steps && m.steps.length > 0" class="mt-2">
465
+ <summary class="cursor-pointer text-xs text-gray-500">
466
+ 思维链 · {{ m.steps.length }} 步
467
+ </summary>
468
+ <div
469
+ v-for="(s, si) in m.steps"
470
+ :key="si"
471
+ class="mt-1 rounded bg-gray-50 p-2 text-xs"
472
+ >
473
+ <div v-if="(s as any).thought"><b>Thought:</b> {{ (s as any).thought }}</div>
474
+ <div v-if="(s as any).action"><b>Action:</b> {{ (s as any).action }}</div>
475
+ <div v-if="(s as any).toolName"><b>Tool:</b> {{ (s as any).toolName }}({{ (s as any).toolArgsJson }})</div>
476
+ <div v-if="(s as any).observation"><b>Observation:</b> {{ (s as any).observation }}</div>
477
+ </div>
478
+ </details>
479
+ </div>
480
+ </div>
481
+
482
+ <div class="flex gap-2">
483
+ <Input.TextArea
484
+ v-model:value="input"
485
+ :rows="2"
486
+ placeholder="输入问题,Ctrl+Enter 发送"
487
+ @press-enter.ctrl="send()"
488
+ />
489
+ <Button type="primary" :loading="sending" @click="send()">
490
+ 发送
491
+ </Button>
492
+ </div>
493
+ </Card>
494
+
495
+ <!-- Variables sidebar: for prompts that use {{#user.xxx#}} placeholders -->
496
+ <Card title="变量" :body-style="{ padding: '12px 12px 8px' }">
497
+ <div class="mb-2 text-xs text-gray-500">
498
+ Instructions 里的 <code>{{ SAMPLE_VAR_TOKEN }}</code>
499
+ 占位符会用这里的键值渲染。持久化到本地。
500
+ </div>
501
+ <div
502
+ v-for="(v, idx) in variables"
503
+ :key="idx"
504
+ class="mb-2 flex items-center gap-1"
505
+ >
506
+ <Input
507
+ v-model:value="v.key"
508
+ placeholder="key"
509
+ size="small"
510
+ style="width: 40%"
511
+ @blur="saveVariables"
512
+ />
513
+ <Input
514
+ v-model:value="v.value"
515
+ placeholder="value"
516
+ size="small"
517
+ style="flex: 1"
518
+ @blur="saveVariables"
519
+ />
520
+ <Button type="text" size="small" danger @click="removeVariable(idx)">
521
+ ×
522
+ </Button>
523
+ </div>
524
+ <Button size="small" block @click="addVariable">+ 添加变量</Button>
525
+ </Card>
526
+ </div>
527
+ </div>
528
+ </template>
529
+
530
+ <style scoped>
531
+ .agent-start-chat-page {
532
+ padding: 16px;
533
+ }
534
+ .agent-start-chat-page__header {
535
+ margin-bottom: 16px;
536
+ }
537
+ .agent-start-chat-page__title {
538
+ font-size: 18px;
539
+ font-weight: 600;
540
+ color: #111827;
541
+ }
542
+ .agent-start-chat-page__desc {
543
+ margin-top: 4px;
544
+ font-size: 12px;
545
+ color: #6b7280;
546
+ }
547
+ :global(.dark) .agent-start-chat-page__title {
548
+ color: #f3f4f6;
549
+ }
550
+ :global(.dark) .agent-start-chat-page__desc {
551
+ color: #9ca3af;
552
+ }
553
+ </style>