@hirohsu/user-web-feedback 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +953 -0
- package/dist/cli.cjs +95778 -0
- package/dist/index.cjs +92818 -0
- package/dist/static/app.js +385 -0
- package/dist/static/components/navbar.css +406 -0
- package/dist/static/components/navbar.html +49 -0
- package/dist/static/components/navbar.js +211 -0
- package/dist/static/dashboard.css +495 -0
- package/dist/static/dashboard.html +95 -0
- package/dist/static/dashboard.js +540 -0
- package/dist/static/favicon.svg +27 -0
- package/dist/static/index.html +541 -0
- package/dist/static/logs.html +376 -0
- package/dist/static/logs.js +442 -0
- package/dist/static/mcp-settings.html +797 -0
- package/dist/static/mcp-settings.js +884 -0
- package/dist/static/modules/app-core.js +124 -0
- package/dist/static/modules/conversation-panel.js +247 -0
- package/dist/static/modules/feedback-handler.js +1420 -0
- package/dist/static/modules/image-handler.js +155 -0
- package/dist/static/modules/log-viewer.js +296 -0
- package/dist/static/modules/mcp-manager.js +474 -0
- package/dist/static/modules/prompt-manager.js +364 -0
- package/dist/static/modules/settings-manager.js +299 -0
- package/dist/static/modules/socket-manager.js +170 -0
- package/dist/static/modules/state-manager.js +352 -0
- package/dist/static/modules/timer-controller.js +243 -0
- package/dist/static/modules/ui-helpers.js +246 -0
- package/dist/static/settings.html +355 -0
- package/dist/static/settings.js +425 -0
- package/dist/static/socket.io.min.js +7 -0
- package/dist/static/style.css +2157 -0
- package/dist/static/terminals.html +357 -0
- package/dist/static/terminals.js +321 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feedback-handler.js
|
|
3
|
+
* 反饋處理和 AI 回覆模組
|
|
4
|
+
* 包含反饋提交、AI 回覆生成、MCP 工具調用等功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getSessionId,
|
|
9
|
+
getWorkSummary,
|
|
10
|
+
getCurrentImages,
|
|
11
|
+
getCurrentProjectName,
|
|
12
|
+
getCurrentProjectPath,
|
|
13
|
+
getMaxToolRounds,
|
|
14
|
+
getDebugMode,
|
|
15
|
+
getStreamingAbortController,
|
|
16
|
+
setStreamingAbortController,
|
|
17
|
+
getAutoReplyData,
|
|
18
|
+
setAutoReplyData,
|
|
19
|
+
getAutoReplyConfirmationTimeout,
|
|
20
|
+
setAutoReplyConfirmationTimeout,
|
|
21
|
+
} from "./state-manager.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
showToast,
|
|
25
|
+
showAlertModal,
|
|
26
|
+
hideAlertModal,
|
|
27
|
+
showLoadingOverlay,
|
|
28
|
+
hideLoadingOverlay,
|
|
29
|
+
updateCharCount,
|
|
30
|
+
escapeHtml,
|
|
31
|
+
} from "./ui-helpers.js";
|
|
32
|
+
|
|
33
|
+
import { emitSubmitFeedback, emitUserActivity } from "./socket-manager.js";
|
|
34
|
+
import { clearImages } from "./image-handler.js";
|
|
35
|
+
import { stopAllTimers } from "./timer-controller.js";
|
|
36
|
+
import { getPinnedPromptsContent } from "./prompt-manager.js";
|
|
37
|
+
import {
|
|
38
|
+
showConversationPanel,
|
|
39
|
+
hideConversationPanel,
|
|
40
|
+
addConversationEntry,
|
|
41
|
+
clearConversationPanel,
|
|
42
|
+
updateConversationMode,
|
|
43
|
+
updateConversationTitle,
|
|
44
|
+
addThinkingEntry,
|
|
45
|
+
removeThinkingEntry,
|
|
46
|
+
ConversationEntryType,
|
|
47
|
+
} from "./conversation-panel.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 處理使用者活動
|
|
51
|
+
*/
|
|
52
|
+
export function handleUserActivity() {
|
|
53
|
+
emitUserActivity();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 提交反饋
|
|
58
|
+
*/
|
|
59
|
+
export async function submitFeedback() {
|
|
60
|
+
const text = document.getElementById("feedbackText").value.trim();
|
|
61
|
+
const currentImages = getCurrentImages();
|
|
62
|
+
const sessionId = getSessionId();
|
|
63
|
+
|
|
64
|
+
if (!text && currentImages.length === 0) {
|
|
65
|
+
showToast("error", "錯誤", "請提供文字回應或上傳圖片");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!sessionId) {
|
|
70
|
+
showToast("error", "錯誤", "會話 ID 不存在");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
showAlertModal("提交中", "正在提交反饋,請稍候...");
|
|
75
|
+
|
|
76
|
+
const feedbackData = {
|
|
77
|
+
sessionId: sessionId,
|
|
78
|
+
text: text,
|
|
79
|
+
images: currentImages,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
shouldCloseAfterSubmit: false,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
stopAllTimers();
|
|
85
|
+
emitSubmitFeedback(feedbackData);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 清除所有輸入
|
|
90
|
+
*/
|
|
91
|
+
export function clearInputs() {
|
|
92
|
+
document.getElementById("feedbackText").value = "";
|
|
93
|
+
updateCharCount();
|
|
94
|
+
clearImages();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 選擇性清除提交輸入
|
|
99
|
+
*/
|
|
100
|
+
export function clearSubmissionInputs() {
|
|
101
|
+
document.getElementById("feedbackText").value = "";
|
|
102
|
+
updateCharCount();
|
|
103
|
+
clearImages();
|
|
104
|
+
stopAllTimers();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 生成 AI 回覆 (無 MCP 工具) - 使用新的 Conversation Panel
|
|
109
|
+
*/
|
|
110
|
+
export async function generateAIReply() {
|
|
111
|
+
const workSummary = getWorkSummary();
|
|
112
|
+
if (!workSummary) {
|
|
113
|
+
showToast("error", "錯誤", "無法取得 AI 訊息");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 第一輪不需要 userContext(用戶回覆)
|
|
118
|
+
// 只傳送 AI 工作匯報 + 系統提示詞 + 釘選提示詞
|
|
119
|
+
const userContext = "";
|
|
120
|
+
|
|
121
|
+
showConversationPanel();
|
|
122
|
+
updateConversationTitle("AI 回覆");
|
|
123
|
+
clearConversationPanel();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const requestBody = {
|
|
127
|
+
aiMessage: workSummary,
|
|
128
|
+
userContext: userContext,
|
|
129
|
+
projectName: getCurrentProjectName() || undefined,
|
|
130
|
+
projectPath: getCurrentProjectPath() || undefined,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 先獲取完整提示詞預覽
|
|
134
|
+
let fullPrompt = buildLocalPromptPreview(workSummary, userContext, null);
|
|
135
|
+
let currentMode = "pending";
|
|
136
|
+
let currentCliTool = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const previewResponse = await fetch("/api/prompt-preview", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify(requestBody),
|
|
143
|
+
});
|
|
144
|
+
const previewData = await previewResponse.json();
|
|
145
|
+
if (previewData.success && previewData.prompt) {
|
|
146
|
+
fullPrompt = previewData.prompt;
|
|
147
|
+
currentMode = previewData.mode;
|
|
148
|
+
currentCliTool = previewData.cliTool;
|
|
149
|
+
}
|
|
150
|
+
} catch (previewError) {
|
|
151
|
+
console.warn("無法獲取完整提示詞預覽,使用本地預覽:", previewError);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const modeLabel = currentMode === "cli" ? `CLI (${currentCliTool})` : currentMode === "api" ? "API" : "準備中";
|
|
155
|
+
addConversationEntry(ConversationEntryType.PROMPT, fullPrompt, {
|
|
156
|
+
title: `提示詞 (${modeLabel})`,
|
|
157
|
+
collapsed: false,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
updateConversationMode(currentMode, currentCliTool);
|
|
162
|
+
addThinkingEntry("AI 思考中...");
|
|
163
|
+
|
|
164
|
+
const response = await fetch("/api/ai-reply", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify(requestBody),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const data = await response.json();
|
|
171
|
+
removeThinkingEntry();
|
|
172
|
+
|
|
173
|
+
if (data.success) {
|
|
174
|
+
updateConversationMode(data.mode, data.cliTool);
|
|
175
|
+
|
|
176
|
+
// 如果有 fallback 原因,顯示通知
|
|
177
|
+
if (data.fallbackReason) {
|
|
178
|
+
showToast("warning", "模式切換", data.fallbackReason);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (data.promptSent) {
|
|
182
|
+
const promptEntries = document.querySelectorAll(".entry-prompt");
|
|
183
|
+
if (promptEntries.length > 0) {
|
|
184
|
+
const promptContent = promptEntries[0].querySelector(".entry-content");
|
|
185
|
+
if (promptContent) {
|
|
186
|
+
promptContent.textContent = data.promptSent;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const pinnedPromptsContent = await getPinnedPromptsContent();
|
|
192
|
+
let finalReply = data.reply;
|
|
193
|
+
if (pinnedPromptsContent) {
|
|
194
|
+
finalReply = pinnedPromptsContent + "\n\n以下為我的回覆:\n" + data.reply;
|
|
195
|
+
} else {
|
|
196
|
+
finalReply = "以下為我的回覆:\n" + data.reply;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
document.getElementById("feedbackText").value = finalReply;
|
|
200
|
+
updateCharCount();
|
|
201
|
+
|
|
202
|
+
// 如果是 fallback,badge 顯示不同的樣式
|
|
203
|
+
let badge = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
204
|
+
if (data.fallbackReason) {
|
|
205
|
+
badge = "API (fallback)";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
addConversationEntry(ConversationEntryType.AI, finalReply, {
|
|
209
|
+
title: "AI 回覆",
|
|
210
|
+
collapsed: false,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
badge: badge,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const modeLabel = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
216
|
+
showToast("success", "完成", `AI 回覆完成 (${modeLabel})`);
|
|
217
|
+
} else {
|
|
218
|
+
addConversationEntry(ConversationEntryType.ERROR, data.error || "未知錯誤", {
|
|
219
|
+
title: "錯誤",
|
|
220
|
+
collapsed: false,
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const modeLabel = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
225
|
+
showToast("error", "失敗", `AI 回覆失敗 (${modeLabel})`);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error("生成 AI 回覆失敗:", error);
|
|
229
|
+
removeThinkingEntry();
|
|
230
|
+
addConversationEntry(ConversationEntryType.ERROR, error.message || "無法生成 AI 回覆", {
|
|
231
|
+
title: "錯誤",
|
|
232
|
+
collapsed: false,
|
|
233
|
+
timestamp: Date.now(),
|
|
234
|
+
});
|
|
235
|
+
showToast("error", "錯誤", "無法生成 AI 回覆");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 將 streaming panel 的取消按鈕轉換為確定按鈕
|
|
241
|
+
*/
|
|
242
|
+
function transformToConfirmButton() {
|
|
243
|
+
const cancelBtn = document.getElementById("cancelStreaming");
|
|
244
|
+
if (cancelBtn) {
|
|
245
|
+
cancelBtn.textContent = "確定";
|
|
246
|
+
cancelBtn.classList.remove("btn-secondary");
|
|
247
|
+
cancelBtn.classList.add("btn-primary");
|
|
248
|
+
cancelBtn.onclick = () => {
|
|
249
|
+
hideStreamingPanel();
|
|
250
|
+
cancelBtn.textContent = "取消";
|
|
251
|
+
cancelBtn.classList.remove("btn-primary");
|
|
252
|
+
cancelBtn.classList.add("btn-secondary");
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 顯示 CLI 執行詳情(包含 prompt 和結果)
|
|
259
|
+
* @param {string} cliTool - CLI 工具名稱
|
|
260
|
+
* @param {string} promptSent - 傳送的 prompt
|
|
261
|
+
* @param {string|null} reply - AI 回覆(成功時)
|
|
262
|
+
* @param {string|null} error - 錯誤訊息(失敗時)
|
|
263
|
+
*/
|
|
264
|
+
function showCLIExecutionDetails(
|
|
265
|
+
cliTool,
|
|
266
|
+
promptSent,
|
|
267
|
+
reply = null,
|
|
268
|
+
error = null
|
|
269
|
+
) {
|
|
270
|
+
const container = document.getElementById("streamingOutput");
|
|
271
|
+
if (!container) return;
|
|
272
|
+
|
|
273
|
+
// 清空現有內容
|
|
274
|
+
container.innerHTML = "";
|
|
275
|
+
|
|
276
|
+
// 顯示傳送的 prompt(可折疊)
|
|
277
|
+
if (promptSent) {
|
|
278
|
+
const promptDetails = document.createElement("details");
|
|
279
|
+
promptDetails.className = "cli-prompt-details";
|
|
280
|
+
promptDetails.open = true; // 預設展開讓用戶看到
|
|
281
|
+
|
|
282
|
+
promptDetails.innerHTML = `
|
|
283
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--bg-tertiary); border-radius: 4px; margin-bottom: 8px;">
|
|
284
|
+
📤 傳送給 ${cliTool} CLI 的 Prompt
|
|
285
|
+
</summary>
|
|
286
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 300px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
287
|
+
promptSent
|
|
288
|
+
)}</pre>
|
|
289
|
+
`;
|
|
290
|
+
container.appendChild(promptDetails);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 顯示結果或錯誤
|
|
294
|
+
if (reply) {
|
|
295
|
+
const resultDiv = document.createElement("div");
|
|
296
|
+
resultDiv.className = "cli-result success";
|
|
297
|
+
resultDiv.innerHTML = `
|
|
298
|
+
<details open>
|
|
299
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-green-bg, rgba(34, 197, 94, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-green, #22c55e);">
|
|
300
|
+
✅ CLI 回覆結果
|
|
301
|
+
</summary>
|
|
302
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 300px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
303
|
+
reply
|
|
304
|
+
)}</pre>
|
|
305
|
+
</details>
|
|
306
|
+
`;
|
|
307
|
+
container.appendChild(resultDiv);
|
|
308
|
+
} else if (error) {
|
|
309
|
+
const errorDiv = document.createElement("div");
|
|
310
|
+
errorDiv.className = "cli-result error";
|
|
311
|
+
errorDiv.innerHTML = `
|
|
312
|
+
<details open>
|
|
313
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-red-bg, rgba(239, 68, 68, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-red, #ef4444);">
|
|
314
|
+
❌ CLI 執行錯誤
|
|
315
|
+
</summary>
|
|
316
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--accent-red, #ef4444);">${escapeHtml(
|
|
317
|
+
error
|
|
318
|
+
)}</pre>
|
|
319
|
+
</details>
|
|
320
|
+
`;
|
|
321
|
+
container.appendChild(errorDiv);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
container.scrollTop = container.scrollHeight;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 顯示 API 模式執行詳情
|
|
329
|
+
*/
|
|
330
|
+
function showAPIExecutionDetails(promptSent, reply) {
|
|
331
|
+
const container = document.getElementById("streamingOutput");
|
|
332
|
+
if (!container) return;
|
|
333
|
+
|
|
334
|
+
// 清空現有內容
|
|
335
|
+
container.innerHTML = "";
|
|
336
|
+
|
|
337
|
+
// 顯示發送的提示詞
|
|
338
|
+
if (promptSent) {
|
|
339
|
+
const promptDiv = document.createElement("div");
|
|
340
|
+
promptDiv.className = "cli-prompt-sent";
|
|
341
|
+
promptDiv.innerHTML = `
|
|
342
|
+
<details>
|
|
343
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-blue-bg, rgba(59, 130, 246, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-blue, #3b82f6);">
|
|
344
|
+
📤 發送的提示詞 (API 模式)
|
|
345
|
+
</summary>
|
|
346
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 300px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
347
|
+
promptSent
|
|
348
|
+
)}</pre>
|
|
349
|
+
</details>
|
|
350
|
+
`;
|
|
351
|
+
container.appendChild(promptDiv);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 顯示 API 回覆結果
|
|
355
|
+
const resultDiv = document.createElement("div");
|
|
356
|
+
resultDiv.className = "api-result success";
|
|
357
|
+
resultDiv.innerHTML = `
|
|
358
|
+
<details open>
|
|
359
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-green-bg, rgba(34, 197, 94, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-green, #22c55e);">
|
|
360
|
+
✅ AI 回覆結果 (API 模式)
|
|
361
|
+
</summary>
|
|
362
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 400px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
363
|
+
reply
|
|
364
|
+
)}</pre>
|
|
365
|
+
</details>
|
|
366
|
+
`;
|
|
367
|
+
container.appendChild(resultDiv);
|
|
368
|
+
|
|
369
|
+
container.scrollTop = container.scrollHeight;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 解析 AI 回覆中的 tool_calls JSON
|
|
374
|
+
*/
|
|
375
|
+
export function parseToolCalls(aiResponse) {
|
|
376
|
+
const jsonBlockMatch = aiResponse.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
377
|
+
let jsonContent = null;
|
|
378
|
+
|
|
379
|
+
if (jsonBlockMatch && jsonBlockMatch[1]) {
|
|
380
|
+
jsonContent = jsonBlockMatch[1].trim();
|
|
381
|
+
} else {
|
|
382
|
+
const jsonMatch = aiResponse.match(/\{[\s\S]*"tool_calls"[\s\S]*\}/);
|
|
383
|
+
if (jsonMatch) {
|
|
384
|
+
jsonContent = jsonMatch[0];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!jsonContent) {
|
|
389
|
+
return { hasToolCalls: false, toolCalls: [], message: aiResponse };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(jsonContent);
|
|
394
|
+
|
|
395
|
+
if (!Array.isArray(parsed.tool_calls)) {
|
|
396
|
+
return { hasToolCalls: false, toolCalls: [], message: aiResponse };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const call of parsed.tool_calls) {
|
|
400
|
+
if (typeof call.name !== "string" || typeof call.arguments !== "object") {
|
|
401
|
+
return { hasToolCalls: false, toolCalls: [], message: aiResponse };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
hasToolCalls: parsed.tool_calls.length > 0,
|
|
407
|
+
toolCalls: parsed.tool_calls,
|
|
408
|
+
message: parsed.message || null,
|
|
409
|
+
};
|
|
410
|
+
} catch {
|
|
411
|
+
return { hasToolCalls: false, toolCalls: [], message: aiResponse };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 執行 MCP 工具
|
|
417
|
+
*/
|
|
418
|
+
export async function executeMCPTools(toolCalls) {
|
|
419
|
+
const response = await fetch("/api/mcp/execute-tools", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { "Content-Type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ tools: toolCalls }),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const data = await response.json();
|
|
426
|
+
return data.results || [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 格式化工具執行結果
|
|
431
|
+
*/
|
|
432
|
+
export function formatToolResults(results) {
|
|
433
|
+
const lines = ["Tool execution results:"];
|
|
434
|
+
for (const result of results) {
|
|
435
|
+
if (result.success) {
|
|
436
|
+
lines.push(`- ${result.name}: SUCCESS`);
|
|
437
|
+
if (result.result !== undefined) {
|
|
438
|
+
const resultStr =
|
|
439
|
+
typeof result.result === "string"
|
|
440
|
+
? result.result
|
|
441
|
+
: JSON.stringify(result.result, null, 2);
|
|
442
|
+
lines.push(` Result: ${resultStr}`);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
lines.push(`- ${result.name}: FAILED`);
|
|
446
|
+
if (result.error) {
|
|
447
|
+
lines.push(` Error: ${result.error}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return lines.join("\n");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 顯示 AI Streaming Panel
|
|
456
|
+
*/
|
|
457
|
+
export function showStreamingPanel() {
|
|
458
|
+
const panel = document.getElementById("aiStreamingPanel");
|
|
459
|
+
const progressContainer = document.getElementById("streamingProgress");
|
|
460
|
+
const outputContainer = document.getElementById("streamingOutput");
|
|
461
|
+
|
|
462
|
+
if (panel) {
|
|
463
|
+
panel.style.display = "flex";
|
|
464
|
+
if (progressContainer) progressContainer.innerHTML = "";
|
|
465
|
+
if (outputContainer)
|
|
466
|
+
outputContainer.innerHTML = '<span class="streaming-cursor"></span>';
|
|
467
|
+
updateStreamingStatus("thinking", "準備中...");
|
|
468
|
+
|
|
469
|
+
const cancelBtn = document.getElementById("cancelStreaming");
|
|
470
|
+
if (cancelBtn) {
|
|
471
|
+
cancelBtn.onclick = () => {
|
|
472
|
+
const controller = getStreamingAbortController();
|
|
473
|
+
if (controller) {
|
|
474
|
+
controller.abort();
|
|
475
|
+
}
|
|
476
|
+
hideStreamingPanel();
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 隱藏 AI Streaming Panel
|
|
484
|
+
*/
|
|
485
|
+
export function hideStreamingPanel() {
|
|
486
|
+
const panel = document.getElementById("aiStreamingPanel");
|
|
487
|
+
if (panel) {
|
|
488
|
+
panel.style.display = "none";
|
|
489
|
+
}
|
|
490
|
+
setStreamingAbortController(null);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* 更新 Streaming 狀態
|
|
495
|
+
*/
|
|
496
|
+
export function updateStreamingStatus(status, text) {
|
|
497
|
+
const indicator = document.getElementById("streamingStatusIndicator");
|
|
498
|
+
const statusText = document.getElementById("streamingStatus");
|
|
499
|
+
const title = document.getElementById("streamingTitle");
|
|
500
|
+
|
|
501
|
+
if (indicator) {
|
|
502
|
+
indicator.className = "status-indicator " + status;
|
|
503
|
+
}
|
|
504
|
+
if (statusText) {
|
|
505
|
+
statusText.textContent = text;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const titleMap = {
|
|
509
|
+
thinking: "AI 思考中...",
|
|
510
|
+
executing: "執行工具中...",
|
|
511
|
+
done: "AI 回覆完成",
|
|
512
|
+
error: "發生錯誤",
|
|
513
|
+
};
|
|
514
|
+
if (title && titleMap[status]) {
|
|
515
|
+
title.textContent = titleMap[status];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 添加進度項目到 Streaming Panel
|
|
521
|
+
*/
|
|
522
|
+
export function addStreamingProgress(
|
|
523
|
+
status,
|
|
524
|
+
message,
|
|
525
|
+
toolCalls = [],
|
|
526
|
+
round = 1
|
|
527
|
+
) {
|
|
528
|
+
const container = document.getElementById("streamingProgress");
|
|
529
|
+
if (!container) return;
|
|
530
|
+
|
|
531
|
+
const maxToolRounds = getMaxToolRounds();
|
|
532
|
+
const statusIcons = {
|
|
533
|
+
thinking: "🤔",
|
|
534
|
+
executing: "⏳",
|
|
535
|
+
done: "✅",
|
|
536
|
+
error: "❌",
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const prevItems = container.querySelectorAll(".progress-item.active");
|
|
540
|
+
prevItems.forEach((item) => {
|
|
541
|
+
item.classList.remove("active");
|
|
542
|
+
item.classList.add("completed");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const item = document.createElement("div");
|
|
546
|
+
item.className = `progress-item ${
|
|
547
|
+
status === "done" || status === "error" ? status : "active"
|
|
548
|
+
}`;
|
|
549
|
+
|
|
550
|
+
let toolsHtml = "";
|
|
551
|
+
if (toolCalls.length > 0) {
|
|
552
|
+
toolsHtml = `<div class="progress-tools">${toolCalls
|
|
553
|
+
.map((t) => `<span class="tool-tag">${t.name}</span>`)
|
|
554
|
+
.join("")}</div>`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
item.innerHTML = `
|
|
558
|
+
<span class="progress-icon">${statusIcons[status] || "⏳"}</span>
|
|
559
|
+
<div class="progress-content">
|
|
560
|
+
<div class="progress-message">Round ${round}/${maxToolRounds}: ${message}</div>
|
|
561
|
+
${toolsHtml}
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
|
|
565
|
+
container.appendChild(item);
|
|
566
|
+
container.scrollTop = container.scrollHeight;
|
|
567
|
+
updateStreamingStatus(status, message);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 添加輸出內容到 Streaming Panel
|
|
572
|
+
*/
|
|
573
|
+
export function addStreamingOutput(content, type = "ai-message") {
|
|
574
|
+
const container = document.getElementById("streamingOutput");
|
|
575
|
+
if (!container) return;
|
|
576
|
+
|
|
577
|
+
const cursor = container.querySelector(".streaming-cursor");
|
|
578
|
+
|
|
579
|
+
const typeClasses = {
|
|
580
|
+
"tool-call": "tool-call-display",
|
|
581
|
+
"tool-result": "tool-result-display",
|
|
582
|
+
"ai-message": "ai-message",
|
|
583
|
+
error: "error-message",
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const details = document.createElement("details");
|
|
587
|
+
details.className = typeClasses[type] || "ai-message";
|
|
588
|
+
details.open = true;
|
|
589
|
+
|
|
590
|
+
const summary = document.createElement("summary");
|
|
591
|
+
const contentDiv = document.createElement("div");
|
|
592
|
+
contentDiv.className = "details-content";
|
|
593
|
+
|
|
594
|
+
if (type === "tool-call") {
|
|
595
|
+
summary.innerHTML = `🔧 調用工具`;
|
|
596
|
+
contentDiv.innerHTML = `<pre>${escapeHtml(content)}</pre>`;
|
|
597
|
+
} else if (type === "tool-result") {
|
|
598
|
+
const isSuccess = content.includes("SUCCESS");
|
|
599
|
+
const statusIcon = isSuccess ? "✅" : "❌";
|
|
600
|
+
summary.innerHTML = `📋 工具結果 ${statusIcon}`;
|
|
601
|
+
contentDiv.innerHTML = `<pre>${escapeHtml(content)}</pre>`;
|
|
602
|
+
} else if (type === "error") {
|
|
603
|
+
summary.innerHTML = `❌ 錯誤`;
|
|
604
|
+
contentDiv.innerHTML = escapeHtml(content);
|
|
605
|
+
details.style.color = "var(--accent-red)";
|
|
606
|
+
} else {
|
|
607
|
+
summary.innerHTML = `💬 AI 回應`;
|
|
608
|
+
contentDiv.textContent = content;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
details.appendChild(summary);
|
|
612
|
+
details.appendChild(contentDiv);
|
|
613
|
+
container.appendChild(details);
|
|
614
|
+
|
|
615
|
+
if (cursor) {
|
|
616
|
+
container.appendChild(cursor);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
container.scrollTop = container.scrollHeight;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 顯示第 5 輪確認對話框
|
|
624
|
+
*/
|
|
625
|
+
function showRound5Confirmation() {
|
|
626
|
+
return new Promise((resolve) => {
|
|
627
|
+
showAlertModal(
|
|
628
|
+
"工具呼叫已達最大輪次",
|
|
629
|
+
"AI 已執行 5 輪工具呼叫,是否繼續讓 AI 完成回覆?\n\n點擊「確定」繼續,點擊「取消」停止。",
|
|
630
|
+
() => resolve(true),
|
|
631
|
+
() => resolve(false)
|
|
632
|
+
);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* 更新工具執行進度 UI
|
|
638
|
+
*/
|
|
639
|
+
function updateToolProgressUI(round, status, message, toolCalls = []) {
|
|
640
|
+
addStreamingProgress(status, message, toolCalls, round);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 顯示提示詞預覽
|
|
645
|
+
*/
|
|
646
|
+
function showPromptPreview(prompt, round, mode, cliTool) {
|
|
647
|
+
const container = document.getElementById("streamingOutput");
|
|
648
|
+
if (!container) return;
|
|
649
|
+
|
|
650
|
+
const modeLabel =
|
|
651
|
+
mode === "pending"
|
|
652
|
+
? "準備中..."
|
|
653
|
+
: mode === "cli"
|
|
654
|
+
? `CLI (${cliTool})`
|
|
655
|
+
: "API";
|
|
656
|
+
const promptDiv = document.createElement("div");
|
|
657
|
+
promptDiv.className = "prompt-preview";
|
|
658
|
+
promptDiv.id = `prompt-preview-${round}`;
|
|
659
|
+
promptDiv.innerHTML = `
|
|
660
|
+
<details open>
|
|
661
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-blue-bg, rgba(59, 130, 246, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-blue, #3b82f6);">
|
|
662
|
+
📤 Round ${round}: 發送的提示詞 (${modeLabel})
|
|
663
|
+
</summary>
|
|
664
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 300px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
665
|
+
prompt
|
|
666
|
+
)}</pre>
|
|
667
|
+
</details>
|
|
668
|
+
`;
|
|
669
|
+
container.appendChild(promptDiv);
|
|
670
|
+
container.scrollTop = container.scrollHeight;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* 更新提示詞預覽為完整版本
|
|
675
|
+
*/
|
|
676
|
+
function updatePromptPreview(prompt, round, mode, cliTool) {
|
|
677
|
+
console.log("[feedback-handler] updatePromptPreview called:", {
|
|
678
|
+
round,
|
|
679
|
+
mode,
|
|
680
|
+
cliTool,
|
|
681
|
+
promptLength: prompt?.length,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const promptDiv = document.getElementById(`prompt-preview-${round}`);
|
|
685
|
+
if (!promptDiv) {
|
|
686
|
+
console.warn(
|
|
687
|
+
"[feedback-handler] prompt-preview element not found for round:",
|
|
688
|
+
round
|
|
689
|
+
);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const modeLabel = mode === "cli" ? `CLI (${cliTool})` : "API";
|
|
694
|
+
promptDiv.innerHTML = `
|
|
695
|
+
<details open>
|
|
696
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-blue-bg, rgba(59, 130, 246, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-blue, #3b82f6);">
|
|
697
|
+
📤 Round ${round}: 完整提示詞 (${modeLabel}) - 已更新
|
|
698
|
+
</summary>
|
|
699
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 300px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
700
|
+
prompt
|
|
701
|
+
)}</pre>
|
|
702
|
+
</details>
|
|
703
|
+
`;
|
|
704
|
+
console.log("[feedback-handler] prompt-preview updated successfully");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 顯示 AI 回覆結果
|
|
709
|
+
*/
|
|
710
|
+
function showAIReplyResult(reply, round, mode, cliTool) {
|
|
711
|
+
const container = document.getElementById("streamingOutput");
|
|
712
|
+
if (!container) return;
|
|
713
|
+
|
|
714
|
+
const modeLabel = mode === "cli" ? `CLI (${cliTool})` : "API";
|
|
715
|
+
const resultDiv = document.createElement("div");
|
|
716
|
+
resultDiv.className = "ai-reply-result";
|
|
717
|
+
resultDiv.innerHTML = `
|
|
718
|
+
<details open>
|
|
719
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-green-bg, rgba(34, 197, 94, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-green, #22c55e);">
|
|
720
|
+
✅ Round ${round}: AI 回覆 (${modeLabel})
|
|
721
|
+
</summary>
|
|
722
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 400px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
723
|
+
reply
|
|
724
|
+
)}</pre>
|
|
725
|
+
</details>
|
|
726
|
+
`;
|
|
727
|
+
container.appendChild(resultDiv);
|
|
728
|
+
container.scrollTop = container.scrollHeight;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* 顯示工具呼叫
|
|
733
|
+
*/
|
|
734
|
+
function showToolCalls(toolCalls, round) {
|
|
735
|
+
const container = document.getElementById("streamingOutput");
|
|
736
|
+
if (!container) return;
|
|
737
|
+
|
|
738
|
+
const toolCallsDisplay = toolCalls
|
|
739
|
+
.map((t) => `${t.name}(${JSON.stringify(t.arguments, null, 2)})`)
|
|
740
|
+
.join("\n\n");
|
|
741
|
+
|
|
742
|
+
const toolDiv = document.createElement("div");
|
|
743
|
+
toolDiv.className = "tool-calls";
|
|
744
|
+
toolDiv.innerHTML = `
|
|
745
|
+
<details open>
|
|
746
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-orange-bg, rgba(249, 115, 22, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-orange, #f97316);">
|
|
747
|
+
🔧 Round ${round}: 工具呼叫
|
|
748
|
+
</summary>
|
|
749
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 200px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
750
|
+
toolCallsDisplay
|
|
751
|
+
)}</pre>
|
|
752
|
+
</details>
|
|
753
|
+
`;
|
|
754
|
+
container.appendChild(toolDiv);
|
|
755
|
+
container.scrollTop = container.scrollHeight;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 顯示工具執行結果
|
|
760
|
+
*/
|
|
761
|
+
function showToolResults(results, round) {
|
|
762
|
+
const container = document.getElementById("streamingOutput");
|
|
763
|
+
if (!container) return;
|
|
764
|
+
|
|
765
|
+
const resultDiv = document.createElement("div");
|
|
766
|
+
resultDiv.className = "tool-results";
|
|
767
|
+
resultDiv.innerHTML = `
|
|
768
|
+
<details>
|
|
769
|
+
<summary style="cursor: pointer; padding: 8px; background: var(--accent-purple-bg, rgba(168, 85, 247, 0.1)); border-radius: 4px; margin-bottom: 8px; color: var(--accent-purple, #a855f7);">
|
|
770
|
+
📋 Round ${round}: 工具執行結果
|
|
771
|
+
</summary>
|
|
772
|
+
<pre style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin: 8px 0; max-height: 200px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid var(--border-color);">${escapeHtml(
|
|
773
|
+
results
|
|
774
|
+
)}</pre>
|
|
775
|
+
</details>
|
|
776
|
+
`;
|
|
777
|
+
container.appendChild(resultDiv);
|
|
778
|
+
container.scrollTop = container.scrollHeight;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 構建前端提示詞預覽(簡化版 - 用於 API 獲取失敗時的後備方案)
|
|
783
|
+
*/
|
|
784
|
+
function buildLocalPromptPreview(workSummary, userContext, toolResults) {
|
|
785
|
+
let preview = "";
|
|
786
|
+
|
|
787
|
+
preview += "## AI 工作匯報\n";
|
|
788
|
+
preview += workSummary + "\n\n";
|
|
789
|
+
|
|
790
|
+
if (userContext) {
|
|
791
|
+
preview += "## 使用者上下文\n";
|
|
792
|
+
preview += userContext + "\n\n";
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (toolResults) {
|
|
796
|
+
preview += "## 工具執行結果\n";
|
|
797
|
+
preview += toolResults + "\n\n";
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return preview;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* 帶 MCP 工具呼叫支援的 AI 回覆生成
|
|
805
|
+
*/
|
|
806
|
+
export async function generateAIReplyWithTools() {
|
|
807
|
+
const workSummary = getWorkSummary();
|
|
808
|
+
if (!workSummary) {
|
|
809
|
+
showToast("error", "錯誤", "無法取得 AI 訊息");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 第一輪不需要 userContext(用戶回覆),只有後續輪次才需要
|
|
814
|
+
// userContext 會在工具執行後由用戶填入回覆
|
|
815
|
+
let userContext = "";
|
|
816
|
+
const maxToolRounds = getMaxToolRounds();
|
|
817
|
+
|
|
818
|
+
let hasMCPTools = false;
|
|
819
|
+
try {
|
|
820
|
+
const toolsResponse = await fetch("/api/mcp-tools");
|
|
821
|
+
const toolsData = await toolsResponse.json();
|
|
822
|
+
hasMCPTools =
|
|
823
|
+
toolsData.success && toolsData.tools && toolsData.tools.length > 0;
|
|
824
|
+
} catch {
|
|
825
|
+
hasMCPTools = false;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!hasMCPTools) {
|
|
829
|
+
return generateAIReply();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
showConversationPanel();
|
|
833
|
+
updateConversationTitle("AI 回覆 (含工具)");
|
|
834
|
+
clearConversationPanel();
|
|
835
|
+
|
|
836
|
+
const controller = new AbortController();
|
|
837
|
+
setStreamingAbortController(controller);
|
|
838
|
+
|
|
839
|
+
let round = 0;
|
|
840
|
+
let toolResults = "";
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
while (round < maxToolRounds) {
|
|
844
|
+
if (controller.signal.aborted) {
|
|
845
|
+
throw new Error("使用者取消操作");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
round++;
|
|
849
|
+
|
|
850
|
+
const requestBody = {
|
|
851
|
+
aiMessage: workSummary,
|
|
852
|
+
userContext: userContext,
|
|
853
|
+
includeMCPTools: true,
|
|
854
|
+
toolResults: toolResults || undefined,
|
|
855
|
+
projectName: getCurrentProjectName() || undefined,
|
|
856
|
+
projectPath: getCurrentProjectPath() || undefined,
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// 先獲取完整提示詞預覽
|
|
860
|
+
let fullPrompt = buildLocalPromptPreview(workSummary, userContext, toolResults);
|
|
861
|
+
let currentMode = "pending";
|
|
862
|
+
let currentCliTool = null;
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
const previewResponse = await fetch("/api/prompt-preview", {
|
|
866
|
+
method: "POST",
|
|
867
|
+
headers: { "Content-Type": "application/json" },
|
|
868
|
+
body: JSON.stringify(requestBody),
|
|
869
|
+
});
|
|
870
|
+
const previewData = await previewResponse.json();
|
|
871
|
+
if (previewData.success && previewData.prompt) {
|
|
872
|
+
fullPrompt = previewData.prompt;
|
|
873
|
+
currentMode = previewData.mode;
|
|
874
|
+
currentCliTool = previewData.cliTool;
|
|
875
|
+
}
|
|
876
|
+
} catch (previewError) {
|
|
877
|
+
console.warn("無法獲取完整提示詞預覽,使用本地預覽:", previewError);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const modeLabel = currentMode === "cli" ? `CLI (${currentCliTool})` : currentMode === "api" ? "API" : "準備中";
|
|
881
|
+
addConversationEntry(ConversationEntryType.PROMPT, fullPrompt, {
|
|
882
|
+
title: `提示詞 (第 ${round} 輪) - ${modeLabel}`,
|
|
883
|
+
collapsed: false,
|
|
884
|
+
timestamp: Date.now(),
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
updateConversationMode(currentMode, currentCliTool);
|
|
888
|
+
addThinkingEntry(`AI 思考中 (第 ${round} 輪)...`);
|
|
889
|
+
|
|
890
|
+
const timeoutController = new AbortController();
|
|
891
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), 180000);
|
|
892
|
+
|
|
893
|
+
let response;
|
|
894
|
+
try {
|
|
895
|
+
response = await fetch("/api/ai-reply", {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers: { "Content-Type": "application/json" },
|
|
898
|
+
body: JSON.stringify(requestBody),
|
|
899
|
+
signal: timeoutController.signal,
|
|
900
|
+
});
|
|
901
|
+
} finally {
|
|
902
|
+
clearTimeout(timeoutId);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const data = await response.json();
|
|
906
|
+
removeThinkingEntry();
|
|
907
|
+
|
|
908
|
+
if (!data.success) {
|
|
909
|
+
addConversationEntry(ConversationEntryType.ERROR, data.error || "AI 回覆失敗", {
|
|
910
|
+
title: "錯誤",
|
|
911
|
+
collapsed: false,
|
|
912
|
+
timestamp: Date.now(),
|
|
913
|
+
});
|
|
914
|
+
showToast("error", "AI 回覆失敗", data.error);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
updateConversationMode(data.mode, data.cliTool);
|
|
919
|
+
|
|
920
|
+
// 如果有 fallback 原因,顯示通知
|
|
921
|
+
if (data.fallbackReason) {
|
|
922
|
+
showToast("warning", "模式切換", data.fallbackReason);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// 如果是 fallback,badge 顯示不同的樣式
|
|
926
|
+
let badgeTools1 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
927
|
+
if (data.fallbackReason) {
|
|
928
|
+
badgeTools1 = "API (fallback)";
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
addConversationEntry(ConversationEntryType.AI, data.reply, {
|
|
932
|
+
title: `AI 回覆 (第 ${round} 輪)`,
|
|
933
|
+
collapsed: false,
|
|
934
|
+
timestamp: Date.now(),
|
|
935
|
+
badge: badgeTools1,
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const parsed = parseToolCalls(data.reply);
|
|
939
|
+
|
|
940
|
+
if (!parsed.hasToolCalls) {
|
|
941
|
+
const pinnedPromptsContent = await getPinnedPromptsContent();
|
|
942
|
+
let finalReply = parsed.message || data.reply;
|
|
943
|
+
if (pinnedPromptsContent) {
|
|
944
|
+
finalReply = pinnedPromptsContent + "\n\n以下為我的回覆:\n" + finalReply;
|
|
945
|
+
} else {
|
|
946
|
+
finalReply = "以下為我的回覆:\n" + finalReply;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
document.getElementById("feedbackText").value = finalReply;
|
|
950
|
+
updateCharCount();
|
|
951
|
+
|
|
952
|
+
const modeLabel = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
953
|
+
showToast("success", "完成", `AI 回覆完成 (${modeLabel})`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const toolCallsInfo = parsed.toolCalls.map(t => `${t.name}: ${JSON.stringify(t.arguments)}`).join("\n");
|
|
958
|
+
addConversationEntry(ConversationEntryType.TOOL, toolCallsInfo, {
|
|
959
|
+
title: `工具呼叫 (${parsed.toolCalls.length} 個)`,
|
|
960
|
+
collapsed: false,
|
|
961
|
+
timestamp: Date.now(),
|
|
962
|
+
badge: `第 ${round} 輪`,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const results = await executeMCPTools(parsed.toolCalls);
|
|
966
|
+
toolResults = formatToolResults(results);
|
|
967
|
+
|
|
968
|
+
addConversationEntry(ConversationEntryType.RESULT, toolResults, {
|
|
969
|
+
title: "工具執行結果",
|
|
970
|
+
collapsed: true,
|
|
971
|
+
timestamp: Date.now(),
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
if (round === maxToolRounds) {
|
|
975
|
+
const shouldContinue = await showRound5Confirmation();
|
|
976
|
+
if (!shouldContinue) {
|
|
977
|
+
const pinnedPromptsContent = await getPinnedPromptsContent();
|
|
978
|
+
let finalReply =
|
|
979
|
+
parsed.message ||
|
|
980
|
+
"AI 工具呼叫已達最大輪次,請手動完成回覆。\n\n" + toolResults;
|
|
981
|
+
if (pinnedPromptsContent) {
|
|
982
|
+
finalReply = pinnedPromptsContent + "\n\n以下為我的回覆:\n" + finalReply;
|
|
983
|
+
} else {
|
|
984
|
+
finalReply = "以下為我的回覆:\n" + finalReply;
|
|
985
|
+
}
|
|
986
|
+
document.getElementById("feedbackText").value = finalReply;
|
|
987
|
+
updateCharCount();
|
|
988
|
+
showToast("warning", "提示", "已達最大輪次,用戶選擇停止");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
round = 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.error("MCP AI 回覆失敗:", error);
|
|
996
|
+
removeThinkingEntry();
|
|
997
|
+
if (error.message !== "使用者取消操作") {
|
|
998
|
+
addConversationEntry(ConversationEntryType.ERROR, error.message || "無法生成 AI 回覆", {
|
|
999
|
+
title: "錯誤",
|
|
1000
|
+
collapsed: false,
|
|
1001
|
+
timestamp: Date.now(),
|
|
1002
|
+
});
|
|
1003
|
+
showToast("error", "錯誤", "無法生成 AI 回覆");
|
|
1004
|
+
} else {
|
|
1005
|
+
showToast("warning", "提示", "使用者取消操作");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* 觸發自動 AI 回應
|
|
1012
|
+
*/
|
|
1013
|
+
export async function triggerAutoAIReply() {
|
|
1014
|
+
console.log("觸發自動 AI 回應...");
|
|
1015
|
+
const maxToolRounds = getMaxToolRounds();
|
|
1016
|
+
const debugMode = getDebugMode();
|
|
1017
|
+
|
|
1018
|
+
const timerEl = document.getElementById("auto-reply-timer");
|
|
1019
|
+
if (timerEl) {
|
|
1020
|
+
timerEl.classList.remove("active");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const workSummary = getWorkSummary();
|
|
1024
|
+
if (!workSummary) {
|
|
1025
|
+
console.error("無法取得 AI 訊息");
|
|
1026
|
+
showToast("error", "錯誤", "無法取得 AI 訊息,自動回覆失敗");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const userContext = document.getElementById("feedbackText").value;
|
|
1031
|
+
|
|
1032
|
+
let hasMCPTools = false;
|
|
1033
|
+
try {
|
|
1034
|
+
const toolsResponse = await fetch("/api/mcp-tools");
|
|
1035
|
+
const toolsData = await toolsResponse.json();
|
|
1036
|
+
hasMCPTools =
|
|
1037
|
+
toolsData.success && toolsData.tools && toolsData.tools.length > 0;
|
|
1038
|
+
} catch {
|
|
1039
|
+
hasMCPTools = false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!hasMCPTools) {
|
|
1043
|
+
showConversationPanel();
|
|
1044
|
+
updateConversationTitle("自動 AI 回覆");
|
|
1045
|
+
clearConversationPanel();
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
const requestBody = {
|
|
1049
|
+
aiMessage: workSummary,
|
|
1050
|
+
userContext: userContext,
|
|
1051
|
+
projectName: getCurrentProjectName() || undefined,
|
|
1052
|
+
projectPath: getCurrentProjectPath() || undefined,
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// 先獲取完整提示詞預覽
|
|
1056
|
+
let fullPrompt = buildLocalPromptPreview(workSummary, userContext, null);
|
|
1057
|
+
let currentMode = "pending";
|
|
1058
|
+
let currentCliTool = null;
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
const previewResponse = await fetch("/api/prompt-preview", {
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
headers: { "Content-Type": "application/json" },
|
|
1064
|
+
body: JSON.stringify(requestBody),
|
|
1065
|
+
});
|
|
1066
|
+
const previewData = await previewResponse.json();
|
|
1067
|
+
if (previewData.success && previewData.prompt) {
|
|
1068
|
+
fullPrompt = previewData.prompt;
|
|
1069
|
+
currentMode = previewData.mode;
|
|
1070
|
+
currentCliTool = previewData.cliTool;
|
|
1071
|
+
}
|
|
1072
|
+
} catch (previewError) {
|
|
1073
|
+
console.warn("無法獲取完整提示詞預覽,使用本地預覽:", previewError);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const modeLabel = currentMode === "cli" ? `CLI (${currentCliTool})` : currentMode === "api" ? "API" : "準備中";
|
|
1077
|
+
addConversationEntry(ConversationEntryType.PROMPT, fullPrompt, {
|
|
1078
|
+
title: `提示詞 (${modeLabel})`,
|
|
1079
|
+
collapsed: false,
|
|
1080
|
+
timestamp: Date.now(),
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
updateConversationMode(currentMode, currentCliTool);
|
|
1084
|
+
addThinkingEntry("自動 AI 回覆中...");
|
|
1085
|
+
|
|
1086
|
+
const response = await fetch("/api/ai-reply", {
|
|
1087
|
+
method: "POST",
|
|
1088
|
+
headers: { "Content-Type": "application/json" },
|
|
1089
|
+
body: JSON.stringify(requestBody),
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
const data = await response.json();
|
|
1093
|
+
removeThinkingEntry();
|
|
1094
|
+
|
|
1095
|
+
if (data.success) {
|
|
1096
|
+
updateConversationMode(data.mode, data.cliTool);
|
|
1097
|
+
|
|
1098
|
+
// 如果有 fallback 原因,顯示通知
|
|
1099
|
+
if (data.fallbackReason) {
|
|
1100
|
+
showToast("warning", "模式切換", data.fallbackReason);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const pinnedPromptsContent = await getPinnedPromptsContent();
|
|
1104
|
+
let finalReply = data.reply;
|
|
1105
|
+
if (pinnedPromptsContent) {
|
|
1106
|
+
finalReply = pinnedPromptsContent + "\n\n以下為我的回覆:\n" + data.reply;
|
|
1107
|
+
} else {
|
|
1108
|
+
finalReply = "以下為我的回覆:\n" + data.reply;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 如果是 fallback,badge 顯示不同的樣式
|
|
1112
|
+
let badgeAuto1 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
1113
|
+
if (data.fallbackReason) {
|
|
1114
|
+
badgeAuto1 = "API (fallback)";
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
addConversationEntry(ConversationEntryType.AI, finalReply, {
|
|
1118
|
+
title: "AI 回覆",
|
|
1119
|
+
collapsed: false,
|
|
1120
|
+
timestamp: Date.now(),
|
|
1121
|
+
badge: badgeAuto1,
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
document.getElementById("feedbackText").value = finalReply;
|
|
1125
|
+
updateCharCount();
|
|
1126
|
+
|
|
1127
|
+
if (!debugMode) hideConversationPanel();
|
|
1128
|
+
showAutoReplyConfirmModal(finalReply);
|
|
1129
|
+
} else {
|
|
1130
|
+
addConversationEntry(ConversationEntryType.ERROR, data.error || "AI 回覆失敗", {
|
|
1131
|
+
title: "錯誤",
|
|
1132
|
+
collapsed: false,
|
|
1133
|
+
timestamp: Date.now(),
|
|
1134
|
+
});
|
|
1135
|
+
showToast("error", "AI 回覆失敗", data.error);
|
|
1136
|
+
}
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
console.error("自動生成 AI 回覆失敗:", error);
|
|
1139
|
+
removeThinkingEntry();
|
|
1140
|
+
addConversationEntry(ConversationEntryType.ERROR, error.message || "無法自動生成 AI 回覆", {
|
|
1141
|
+
title: "錯誤",
|
|
1142
|
+
collapsed: false,
|
|
1143
|
+
timestamp: Date.now(),
|
|
1144
|
+
});
|
|
1145
|
+
showToast("error", "錯誤", "無法自動生成 AI 回覆");
|
|
1146
|
+
}
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
showConversationPanel();
|
|
1151
|
+
updateConversationTitle("自動 AI 回覆 (含工具)");
|
|
1152
|
+
clearConversationPanel();
|
|
1153
|
+
|
|
1154
|
+
const controller = new AbortController();
|
|
1155
|
+
setStreamingAbortController(controller);
|
|
1156
|
+
|
|
1157
|
+
let round = 0;
|
|
1158
|
+
let toolResults = "";
|
|
1159
|
+
let finalReply = "";
|
|
1160
|
+
|
|
1161
|
+
try {
|
|
1162
|
+
while (round < maxToolRounds) {
|
|
1163
|
+
if (controller.signal.aborted) {
|
|
1164
|
+
throw new Error("使用者取消操作");
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
round++;
|
|
1168
|
+
|
|
1169
|
+
const requestBody = {
|
|
1170
|
+
aiMessage: workSummary,
|
|
1171
|
+
userContext: userContext,
|
|
1172
|
+
includeMCPTools: true,
|
|
1173
|
+
toolResults: toolResults || undefined,
|
|
1174
|
+
projectName: getCurrentProjectName() || undefined,
|
|
1175
|
+
projectPath: getCurrentProjectPath() || undefined,
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// 先獲取完整提示詞預覽
|
|
1179
|
+
let fullPrompt = buildLocalPromptPreview(workSummary, userContext, toolResults);
|
|
1180
|
+
let currentMode = "pending";
|
|
1181
|
+
let currentCliTool = null;
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
const previewResponse = await fetch("/api/prompt-preview", {
|
|
1185
|
+
method: "POST",
|
|
1186
|
+
headers: { "Content-Type": "application/json" },
|
|
1187
|
+
body: JSON.stringify(requestBody),
|
|
1188
|
+
});
|
|
1189
|
+
const previewData = await previewResponse.json();
|
|
1190
|
+
if (previewData.success && previewData.prompt) {
|
|
1191
|
+
fullPrompt = previewData.prompt;
|
|
1192
|
+
currentMode = previewData.mode;
|
|
1193
|
+
currentCliTool = previewData.cliTool;
|
|
1194
|
+
}
|
|
1195
|
+
} catch (previewError) {
|
|
1196
|
+
console.warn("無法獲取完整提示詞預覽,使用本地預覽:", previewError);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const modeLabel = currentMode === "cli" ? `CLI (${currentCliTool})` : currentMode === "api" ? "API" : "準備中";
|
|
1200
|
+
addConversationEntry(ConversationEntryType.PROMPT, fullPrompt, {
|
|
1201
|
+
title: `提示詞 (第 ${round} 輪) - ${modeLabel}`,
|
|
1202
|
+
collapsed: false,
|
|
1203
|
+
timestamp: Date.now(),
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
updateConversationMode(currentMode, currentCliTool);
|
|
1207
|
+
addThinkingEntry(`自動 AI 思考中 (第 ${round} 輪)...`);
|
|
1208
|
+
|
|
1209
|
+
const timeoutController = new AbortController();
|
|
1210
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), 180000);
|
|
1211
|
+
|
|
1212
|
+
let response;
|
|
1213
|
+
try {
|
|
1214
|
+
response = await fetch("/api/ai-reply", {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
headers: { "Content-Type": "application/json" },
|
|
1217
|
+
body: JSON.stringify(requestBody),
|
|
1218
|
+
signal: timeoutController.signal,
|
|
1219
|
+
});
|
|
1220
|
+
} finally {
|
|
1221
|
+
clearTimeout(timeoutId);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const data = await response.json();
|
|
1225
|
+
removeThinkingEntry();
|
|
1226
|
+
|
|
1227
|
+
if (!data.success) {
|
|
1228
|
+
addConversationEntry(ConversationEntryType.ERROR, data.error || "AI 回覆失敗", {
|
|
1229
|
+
title: "錯誤",
|
|
1230
|
+
collapsed: false,
|
|
1231
|
+
timestamp: Date.now(),
|
|
1232
|
+
});
|
|
1233
|
+
showToast("error", "AI 回覆失敗", data.error);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
updateConversationMode(data.mode, data.cliTool);
|
|
1238
|
+
|
|
1239
|
+
// 如果有 fallback 原因,顯示通知
|
|
1240
|
+
if (data.fallbackReason) {
|
|
1241
|
+
showToast("warning", "模式切換", data.fallbackReason);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// 如果是 fallback,badge 顯示不同的樣式
|
|
1245
|
+
let badgeAuto2 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
|
|
1246
|
+
if (data.fallbackReason) {
|
|
1247
|
+
badgeAuto2 = "API (fallback)";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
addConversationEntry(ConversationEntryType.AI, data.reply, {
|
|
1251
|
+
title: `AI 回覆 (第 ${round} 輪)`,
|
|
1252
|
+
collapsed: false,
|
|
1253
|
+
timestamp: Date.now(),
|
|
1254
|
+
badge: badgeAuto2,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const parsed = parseToolCalls(data.reply);
|
|
1258
|
+
|
|
1259
|
+
if (!parsed.hasToolCalls) {
|
|
1260
|
+
finalReply = parsed.message || data.reply;
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const toolCallsInfo = parsed.toolCalls.map(t => `${t.name}: ${JSON.stringify(t.arguments)}`).join("\n");
|
|
1265
|
+
addConversationEntry(ConversationEntryType.TOOL, toolCallsInfo, {
|
|
1266
|
+
title: `工具呼叫 (${parsed.toolCalls.length} 個)`,
|
|
1267
|
+
collapsed: false,
|
|
1268
|
+
timestamp: Date.now(),
|
|
1269
|
+
badge: `第 ${round} 輪`,
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
const results = await executeMCPTools(parsed.toolCalls);
|
|
1273
|
+
toolResults = formatToolResults(results);
|
|
1274
|
+
|
|
1275
|
+
addConversationEntry(ConversationEntryType.RESULT, toolResults, {
|
|
1276
|
+
title: "工具執行結果",
|
|
1277
|
+
collapsed: true,
|
|
1278
|
+
timestamp: Date.now(),
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
if (round === maxToolRounds) {
|
|
1282
|
+
finalReply =
|
|
1283
|
+
parsed.message || "AI 工具呼叫已達最大輪次。\n\n" + toolResults;
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const pinnedPromptsContent = await getPinnedPromptsContent();
|
|
1289
|
+
if (pinnedPromptsContent) {
|
|
1290
|
+
finalReply = pinnedPromptsContent + "\n\n以下為我的回覆:\n" + finalReply;
|
|
1291
|
+
} else {
|
|
1292
|
+
finalReply = "以下為我的回覆:\n" + finalReply;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
document.getElementById("feedbackText").value = finalReply;
|
|
1296
|
+
updateCharCount();
|
|
1297
|
+
|
|
1298
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
1299
|
+
if (!debugMode) hideConversationPanel();
|
|
1300
|
+
|
|
1301
|
+
showAutoReplyConfirmModal(finalReply);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
console.error("自動生成 AI 回覆失敗:", error);
|
|
1304
|
+
removeThinkingEntry();
|
|
1305
|
+
if (error.message !== "使用者取消操作") {
|
|
1306
|
+
addConversationEntry(ConversationEntryType.ERROR, error.message || "無法自動生成 AI 回覆", {
|
|
1307
|
+
title: "錯誤",
|
|
1308
|
+
collapsed: false,
|
|
1309
|
+
timestamp: Date.now(),
|
|
1310
|
+
});
|
|
1311
|
+
showToast("error", "錯誤", "無法自動生成 AI 回覆");
|
|
1312
|
+
}
|
|
1313
|
+
} finally {
|
|
1314
|
+
if (!debugMode) hideConversationPanel();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* 顯示自動回覆確認模態框
|
|
1320
|
+
*/
|
|
1321
|
+
export function showAutoReplyConfirmModal(replyContent) {
|
|
1322
|
+
const modal = document.getElementById("autoReplyConfirmModal");
|
|
1323
|
+
const preview = document.getElementById("autoReplyPreview");
|
|
1324
|
+
const countdown = document.getElementById("autoReplyCountdown");
|
|
1325
|
+
|
|
1326
|
+
if (!modal) {
|
|
1327
|
+
console.warn("自動回覆確認模態框未找到");
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
preview.textContent = replyContent;
|
|
1332
|
+
modal.style.display = "flex";
|
|
1333
|
+
setAutoReplyData(replyContent);
|
|
1334
|
+
|
|
1335
|
+
const totalSeconds = 10;
|
|
1336
|
+
countdown.textContent = totalSeconds;
|
|
1337
|
+
|
|
1338
|
+
let remainingSeconds = totalSeconds;
|
|
1339
|
+
const existingTimeout = getAutoReplyConfirmationTimeout();
|
|
1340
|
+
if (existingTimeout) {
|
|
1341
|
+
clearInterval(existingTimeout);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const intervalId = setInterval(() => {
|
|
1345
|
+
remainingSeconds--;
|
|
1346
|
+
countdown.textContent = remainingSeconds;
|
|
1347
|
+
|
|
1348
|
+
if (remainingSeconds <= 0) {
|
|
1349
|
+
clearInterval(intervalId);
|
|
1350
|
+
setAutoReplyConfirmationTimeout(null);
|
|
1351
|
+
console.log("10 秒倒數結束,自動提交回應");
|
|
1352
|
+
confirmAutoReplySubmit();
|
|
1353
|
+
}
|
|
1354
|
+
}, 1000);
|
|
1355
|
+
|
|
1356
|
+
setAutoReplyConfirmationTimeout(intervalId);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* 隱藏自動回覆確認模態框
|
|
1361
|
+
*/
|
|
1362
|
+
export function hideAutoReplyConfirmModal() {
|
|
1363
|
+
const modal = document.getElementById("autoReplyConfirmModal");
|
|
1364
|
+
if (modal) {
|
|
1365
|
+
modal.style.display = "none";
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const timeout = getAutoReplyConfirmationTimeout();
|
|
1369
|
+
if (timeout) {
|
|
1370
|
+
clearInterval(timeout);
|
|
1371
|
+
setAutoReplyConfirmationTimeout(null);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* 確認自動回覆提交
|
|
1377
|
+
*/
|
|
1378
|
+
export function confirmAutoReplySubmit() {
|
|
1379
|
+
hideAutoReplyConfirmModal();
|
|
1380
|
+
|
|
1381
|
+
const autoReplyData = getAutoReplyData();
|
|
1382
|
+
if (autoReplyData) {
|
|
1383
|
+
document.getElementById("feedbackText").value = autoReplyData;
|
|
1384
|
+
updateCharCount();
|
|
1385
|
+
setAutoReplyData(null);
|
|
1386
|
+
console.log("確認自動回覆,提交反饋");
|
|
1387
|
+
submitFeedback();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* 取消自動回覆
|
|
1393
|
+
*/
|
|
1394
|
+
export function cancelAutoReplyConfirm() {
|
|
1395
|
+
hideAutoReplyConfirmModal();
|
|
1396
|
+
setAutoReplyData(null);
|
|
1397
|
+
console.log("已取消自動回覆");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export default {
|
|
1401
|
+
handleUserActivity,
|
|
1402
|
+
submitFeedback,
|
|
1403
|
+
clearInputs,
|
|
1404
|
+
clearSubmissionInputs,
|
|
1405
|
+
generateAIReply,
|
|
1406
|
+
generateAIReplyWithTools,
|
|
1407
|
+
parseToolCalls,
|
|
1408
|
+
executeMCPTools,
|
|
1409
|
+
formatToolResults,
|
|
1410
|
+
showStreamingPanel,
|
|
1411
|
+
hideStreamingPanel,
|
|
1412
|
+
updateStreamingStatus,
|
|
1413
|
+
addStreamingProgress,
|
|
1414
|
+
addStreamingOutput,
|
|
1415
|
+
triggerAutoAIReply,
|
|
1416
|
+
showAutoReplyConfirmModal,
|
|
1417
|
+
hideAutoReplyConfirmModal,
|
|
1418
|
+
confirmAutoReplySubmit,
|
|
1419
|
+
cancelAutoReplyConfirm,
|
|
1420
|
+
};
|