@hirohsu/user-web-feedback 2.8.17 → 2.8.19

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.
@@ -1,26 +1,21 @@
1
1
  /**
2
2
  * conversation-panel.js
3
3
  * 對話面板元件 - 顯示 AI 對話流程
4
- * 支援 6 種對話條目類型: prompt, thinking, tool, result, ai, error
4
+ * 支援 7 種對話條目類型: prompt, thinking, tool, result, ai, error, debug
5
5
  */
6
6
 
7
7
  import { escapeHtml } from './ui-helpers.js';
8
8
 
9
- /**
10
- * 對話條目類型
11
- */
12
9
  export const ConversationEntryType = {
13
10
  PROMPT: 'prompt',
14
11
  THINKING: 'thinking',
15
12
  TOOL: 'tool',
16
13
  RESULT: 'result',
17
14
  AI: 'ai',
18
- ERROR: 'error'
15
+ ERROR: 'error',
16
+ DEBUG: 'debug'
19
17
  };
20
18
 
21
- /**
22
- * 對話條目視覺配置
23
- */
24
19
  const entryConfig = {
25
20
  prompt: {
26
21
  icon: '📤',
@@ -57,12 +52,15 @@ const entryConfig = {
57
52
  title: '錯誤',
58
53
  className: 'entry-error',
59
54
  borderColor: 'var(--accent-red)'
55
+ },
56
+ debug: {
57
+ icon: '🐛',
58
+ title: 'Debug',
59
+ className: 'entry-debug',
60
+ borderColor: 'var(--accent-orange, #f97316)'
60
61
  }
61
62
  };
62
63
 
63
- /**
64
- * 建立對話面板容器
65
- */
66
64
  export function createConversationPanel() {
67
65
  const panel = document.createElement('div');
68
66
  panel.id = 'conversationPanel';
@@ -73,14 +71,18 @@ export function createConversationPanel() {
73
71
  <span class="icon">💬</span>
74
72
  <span id="conversationTitle">AI 對話</span>
75
73
  </div>
76
- <div class="conversation-mode">
77
- <span class="mode-indicator" id="conversationModeIndicator"></span>
78
- <span id="conversationMode">準備中</span>
74
+ <div class="conversation-header-right">
75
+ <div class="conversation-mode">
76
+ <span class="mode-indicator" id="conversationModeIndicator"></span>
77
+ <span id="conversationMode">準備中</span>
78
+ </div>
79
+ <label class="debug-toggle" title="顯示 Debug 資訊">
80
+ <input type="checkbox" id="debugToggle">
81
+ <span>🐛</span>
82
+ </label>
79
83
  </div>
80
84
  </div>
81
- <div class="conversation-body" id="conversationBody">
82
- <!-- 對話條目會動態添加 -->
83
- </div>
85
+ <div class="conversation-body" id="conversationBody"></div>
84
86
  <div class="conversation-footer">
85
87
  <button type="button" id="closeConversation" class="btn btn-secondary">關閉</button>
86
88
  </div>
@@ -88,9 +90,6 @@ export function createConversationPanel() {
88
90
  return panel;
89
91
  }
90
92
 
91
- /**
92
- * 建立對話條目元素
93
- */
94
93
  export function createConversationEntry(type, content, options = {}) {
95
94
  const config = entryConfig[type] || entryConfig.ai;
96
95
  const entry = document.createElement('div');
@@ -98,7 +97,7 @@ export function createConversationEntry(type, content, options = {}) {
98
97
  entry.style.borderLeftColor = config.borderColor;
99
98
 
100
99
  const titleText = options.title || config.title;
101
- const collapsed = options.collapsed ?? (type === 'prompt' || type === 'tool');
100
+ const collapsed = options.collapsed ?? (type === 'prompt' || type === 'tool' || type === 'debug');
102
101
  const timestamp = options.timestamp ? formatTimestamp(options.timestamp) : '';
103
102
 
104
103
  let contentHtml = '';
@@ -126,8 +125,93 @@ export function createConversationEntry(type, content, options = {}) {
126
125
  }
127
126
 
128
127
  /**
129
- * 新增對話條目到面板
128
+ * 建立 Debug 資訊條目(結構化表格)
130
129
  */
130
+ export function createDebugEntry(debugInfo, options = {}) {
131
+ const config = entryConfig.debug;
132
+ const entry = document.createElement('div');
133
+ entry.className = `conversation-entry ${config.className}`;
134
+ entry.style.borderLeftColor = config.borderColor;
135
+
136
+ const titleText = options.title || config.title;
137
+ const collapsed = options.collapsed ?? true;
138
+ const timestamp = options.timestamp ? formatTimestamp(options.timestamp) : '';
139
+
140
+ let bodyHtml = '<div class="debug-info-grid">';
141
+
142
+ if (debugInfo.elapsedMs !== undefined) {
143
+ bodyHtml += debugRow('⏱️ 耗時', `${debugInfo.elapsedMs} ms`);
144
+ }
145
+ if (debugInfo.model) {
146
+ bodyHtml += debugRow('🧠 模型', debugInfo.model);
147
+ }
148
+ if (debugInfo.temperature !== undefined) {
149
+ bodyHtml += debugRow('🌡️ Temperature', debugInfo.temperature);
150
+ }
151
+ if (debugInfo.maxTokens !== undefined) {
152
+ bodyHtml += debugRow('📏 Max Tokens', debugInfo.maxTokens);
153
+ }
154
+ if (debugInfo.tokenEstimate !== undefined) {
155
+ bodyHtml += debugRow('🔢 Token 預估', `~${debugInfo.tokenEstimate}`);
156
+ }
157
+ if (debugInfo.totalPromptLength !== undefined) {
158
+ bodyHtml += debugRow('📐 Prompt 長度', `${debugInfo.totalPromptLength} chars`);
159
+ }
160
+ if (debugInfo.componentCount !== undefined) {
161
+ bodyHtml += debugRow('🧩 組件數量', debugInfo.componentCount);
162
+ }
163
+ if (debugInfo.mcpToolsCount !== undefined) {
164
+ bodyHtml += debugRow('🔧 MCP 工具數', debugInfo.mcpToolsCount);
165
+ }
166
+
167
+ bodyHtml += '</div>';
168
+
169
+ if (debugInfo.sections && debugInfo.sections.length > 0) {
170
+ bodyHtml += '<div class="debug-sections">';
171
+ bodyHtml += '<div class="debug-section-title">📋 提示詞區段順序(實際送出)</div>';
172
+ bodyHtml += '<table class="debug-table"><thead><tr><th>#</th><th>區段名稱</th><th>順序</th><th>長度</th></tr></thead><tbody>';
173
+ debugInfo.sections.forEach((s, i) => {
174
+ bodyHtml += `<tr><td>${i + 1}</td><td>${escapeHtml(s.name)}</td><td>${s.order}</td><td>${s.length} chars</td></tr>`;
175
+ });
176
+ bodyHtml += '</tbody></table></div>';
177
+ }
178
+
179
+ if (debugInfo.promptConfigs && debugInfo.promptConfigs.length > 0) {
180
+ bodyHtml += '<div class="debug-sections">';
181
+ bodyHtml += '<div class="debug-section-title">⚙️ 提示詞配置(設定值)</div>';
182
+ bodyHtml += '<table class="debug-table"><thead><tr><th>ID</th><th>啟用</th><th>第一次</th><th>第二次</th></tr></thead><tbody>';
183
+ debugInfo.promptConfigs.forEach(c => {
184
+ const enabledIcon = c.enabled ? '✅' : '❌';
185
+ bodyHtml += `<tr><td>${escapeHtml(c.id)}</td><td>${enabledIcon}</td><td>${c.firstOrder}</td><td>${c.secondOrder}</td></tr>`;
186
+ });
187
+ bodyHtml += '</tbody></table></div>';
188
+ }
189
+
190
+ if (debugInfo.error) {
191
+ bodyHtml += `<div class="debug-error">❌ 錯誤: ${escapeHtml(debugInfo.error)}</div>`;
192
+ }
193
+
194
+ entry.innerHTML = `
195
+ <details ${collapsed ? '' : 'open'}>
196
+ <summary class="entry-summary">
197
+ <span class="entry-icon">${config.icon}</span>
198
+ <span class="entry-title">${titleText}</span>
199
+ ${timestamp ? `<span class="entry-timestamp">${timestamp}</span>` : ''}
200
+ ${options.badge ? `<span class="entry-badge">${options.badge}</span>` : ''}
201
+ </summary>
202
+ <div class="entry-body debug-body">
203
+ ${bodyHtml}
204
+ </div>
205
+ </details>
206
+ `;
207
+
208
+ return entry;
209
+ }
210
+
211
+ function debugRow(label, value) {
212
+ return `<div class="debug-row"><span class="debug-label">${label}</span><span class="debug-value">${escapeHtml(String(value))}</span></div>`;
213
+ }
214
+
131
215
  export function addConversationEntry(type, content, options = {}) {
132
216
  const body = document.getElementById('conversationBody');
133
217
  if (!body) return null;
@@ -135,13 +219,25 @@ export function addConversationEntry(type, content, options = {}) {
135
219
  const entry = createConversationEntry(type, content, options);
136
220
  body.appendChild(entry);
137
221
  body.scrollTop = body.scrollHeight;
222
+ return entry;
223
+ }
224
+
225
+ export function addDebugEntry(debugInfo, options = {}) {
226
+ const body = document.getElementById('conversationBody');
227
+ if (!body) return null;
228
+
229
+ const entry = createDebugEntry(debugInfo, options);
230
+ body.appendChild(entry);
231
+ body.scrollTop = body.scrollHeight;
232
+
233
+ const debugToggle = document.getElementById('debugToggle');
234
+ if (debugToggle && !debugToggle.checked) {
235
+ entry.style.display = 'none';
236
+ }
138
237
 
139
238
  return entry;
140
239
  }
141
240
 
142
- /**
143
- * 清空對話面板
144
- */
145
241
  export function clearConversationPanel() {
146
242
  const body = document.getElementById('conversationBody');
147
243
  if (body) {
@@ -149,9 +245,6 @@ export function clearConversationPanel() {
149
245
  }
150
246
  }
151
247
 
152
- /**
153
- * 更新對話面板模式顯示
154
- */
155
248
  export function updateConversationMode(mode, cliTool = null) {
156
249
  const modeElement = document.getElementById('conversationMode');
157
250
  const indicator = document.getElementById('conversationModeIndicator');
@@ -176,9 +269,6 @@ export function updateConversationMode(mode, cliTool = null) {
176
269
  }
177
270
  }
178
271
 
179
- /**
180
- * 更新對話面板標題
181
- */
182
272
  export function updateConversationTitle(title) {
183
273
  const titleElement = document.getElementById('conversationTitle');
184
274
  if (titleElement) {
@@ -186,9 +276,6 @@ export function updateConversationTitle(title) {
186
276
  }
187
277
  }
188
278
 
189
- /**
190
- * 顯示對話面板
191
- */
192
279
  export function showConversationPanel() {
193
280
  let panel = document.getElementById('aiConversationPanel');
194
281
  if (!panel) {
@@ -202,14 +289,21 @@ export function showConversationPanel() {
202
289
  if (closeBtn) {
203
290
  closeBtn.onclick = hideConversationPanel;
204
291
  }
292
+
293
+ const debugToggle = panel.querySelector('#debugToggle');
294
+ if (debugToggle) {
295
+ debugToggle.addEventListener('change', (e) => {
296
+ const show = e.target.checked;
297
+ panel.querySelectorAll('.entry-debug').forEach(el => {
298
+ el.style.display = show ? '' : 'none';
299
+ });
300
+ });
301
+ }
205
302
  }
206
303
  panel.style.display = 'flex';
207
304
  clearConversationPanel();
208
305
  }
209
306
 
210
- /**
211
- * 隱藏對話面板
212
- */
213
307
  export function hideConversationPanel() {
214
308
  const panel = document.getElementById('aiConversationPanel');
215
309
  if (panel) {
@@ -217,17 +311,11 @@ export function hideConversationPanel() {
217
311
  }
218
312
  }
219
313
 
220
- /**
221
- * 格式化時間戳記
222
- */
223
314
  function formatTimestamp(timestamp) {
224
315
  const date = new Date(timestamp);
225
316
  return date.toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
226
317
  }
227
318
 
228
- /**
229
- * 新增思考中動畫條目
230
- */
231
319
  export function addThinkingEntry(message = 'AI 思考中...') {
232
320
  return addConversationEntry(ConversationEntryType.THINKING, message, {
233
321
  collapsed: false,
@@ -235,13 +323,10 @@ export function addThinkingEntry(message = 'AI 思考中...') {
235
323
  });
236
324
  }
237
325
 
238
- /**
239
- * 移除思考中條目
240
- */
241
326
  export function removeThinkingEntry() {
242
327
  const body = document.getElementById('conversationBody');
243
328
  if (!body) return;
244
-
329
+
245
330
  const thinkingEntries = body.querySelectorAll('.entry-thinking');
246
331
  thinkingEntries.forEach(entry => entry.remove());
247
332
  }
@@ -38,6 +38,7 @@ import {
38
38
  showConversationPanel,
39
39
  hideConversationPanel,
40
40
  addConversationEntry,
41
+ addDebugEntry,
41
42
  clearConversationPanel,
42
43
  updateConversationMode,
43
44
  updateConversationTitle,
@@ -170,10 +171,18 @@ export async function generateAIReply() {
170
171
  const data = await response.json();
171
172
  removeThinkingEntry();
172
173
 
174
+ if (data.debug) {
175
+ addDebugEntry(data.debug, {
176
+ title: "Debug 資訊",
177
+ collapsed: true,
178
+ timestamp: Date.now(),
179
+ badge: data.debug.elapsedMs ? `${data.debug.elapsedMs}ms` : undefined,
180
+ });
181
+ }
182
+
173
183
  if (data.success) {
174
184
  updateConversationMode(data.mode, data.cliTool);
175
185
 
176
- // 如果有 fallback 原因,顯示通知
177
186
  if (data.fallbackReason) {
178
187
  showToast("warning", "模式切換", data.fallbackReason);
179
188
  }
@@ -199,7 +208,6 @@ export async function generateAIReply() {
199
208
  document.getElementById("feedbackText").value = finalReply;
200
209
  updateCharCount();
201
210
 
202
- // 如果是 fallback,badge 顯示不同的樣式
203
211
  let badge = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
204
212
  if (data.fallbackReason) {
205
213
  badge = "API (fallback)";
@@ -905,6 +913,15 @@ export async function generateAIReplyWithTools() {
905
913
  const data = await response.json();
906
914
  removeThinkingEntry();
907
915
 
916
+ if (data.debug) {
917
+ addDebugEntry(data.debug, {
918
+ title: `Debug (第 ${round} 輪)`,
919
+ collapsed: true,
920
+ timestamp: Date.now(),
921
+ badge: data.debug.elapsedMs ? `${data.debug.elapsedMs}ms` : undefined,
922
+ });
923
+ }
924
+
908
925
  if (!data.success) {
909
926
  addConversationEntry(ConversationEntryType.ERROR, data.error || "AI 回覆失敗", {
910
927
  title: "錯誤",
@@ -917,12 +934,10 @@ export async function generateAIReplyWithTools() {
917
934
 
918
935
  updateConversationMode(data.mode, data.cliTool);
919
936
 
920
- // 如果有 fallback 原因,顯示通知
921
937
  if (data.fallbackReason) {
922
938
  showToast("warning", "模式切換", data.fallbackReason);
923
939
  }
924
940
 
925
- // 如果是 fallback,badge 顯示不同的樣式
926
941
  let badgeTools1 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
927
942
  if (data.fallbackReason) {
928
943
  badgeTools1 = "API (fallback)";
@@ -1092,10 +1107,18 @@ export async function triggerAutoAIReply() {
1092
1107
  const data = await response.json();
1093
1108
  removeThinkingEntry();
1094
1109
 
1110
+ if (data.debug) {
1111
+ addDebugEntry(data.debug, {
1112
+ title: "Debug 資訊 (自動回覆)",
1113
+ collapsed: true,
1114
+ timestamp: Date.now(),
1115
+ badge: data.debug.elapsedMs ? `${data.debug.elapsedMs}ms` : undefined,
1116
+ });
1117
+ }
1118
+
1095
1119
  if (data.success) {
1096
1120
  updateConversationMode(data.mode, data.cliTool);
1097
1121
 
1098
- // 如果有 fallback 原因,顯示通知
1099
1122
  if (data.fallbackReason) {
1100
1123
  showToast("warning", "模式切換", data.fallbackReason);
1101
1124
  }
@@ -1108,7 +1131,6 @@ export async function triggerAutoAIReply() {
1108
1131
  finalReply = "以下為我的回覆:\n" + data.reply;
1109
1132
  }
1110
1133
 
1111
- // 如果是 fallback,badge 顯示不同的樣式
1112
1134
  let badgeAuto1 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
1113
1135
  if (data.fallbackReason) {
1114
1136
  badgeAuto1 = "API (fallback)";
@@ -1224,6 +1246,15 @@ export async function triggerAutoAIReply() {
1224
1246
  const data = await response.json();
1225
1247
  removeThinkingEntry();
1226
1248
 
1249
+ if (data.debug) {
1250
+ addDebugEntry(data.debug, {
1251
+ title: `Debug (自動第 ${round} 輪)`,
1252
+ collapsed: true,
1253
+ timestamp: Date.now(),
1254
+ badge: data.debug.elapsedMs ? `${data.debug.elapsedMs}ms` : undefined,
1255
+ });
1256
+ }
1257
+
1227
1258
  if (!data.success) {
1228
1259
  addConversationEntry(ConversationEntryType.ERROR, data.error || "AI 回覆失敗", {
1229
1260
  title: "錯誤",
@@ -1236,12 +1267,10 @@ export async function triggerAutoAIReply() {
1236
1267
 
1237
1268
  updateConversationMode(data.mode, data.cliTool);
1238
1269
 
1239
- // 如果有 fallback 原因,顯示通知
1240
1270
  if (data.fallbackReason) {
1241
1271
  showToast("warning", "模式切換", data.fallbackReason);
1242
1272
  }
1243
1273
 
1244
- // 如果是 fallback,badge 顯示不同的樣式
1245
1274
  let badgeAuto2 = data.mode === "cli" ? `CLI (${data.cliTool})` : "API";
1246
1275
  if (data.fallbackReason) {
1247
1276
  badgeAuto2 = "API (fallback)";
@@ -108,6 +108,12 @@ export function startCloseCountdown() {
108
108
  setCloseCountdownInterval(null);
109
109
  console.log("倒數結束,關閉頁面");
110
110
  window.close();
111
+ setTimeout(() => {
112
+ document.body.innerHTML = `
113
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:system-ui;color:#888;">
114
+ <p style="font-size:1.2rem;">✅ 回應已送出,您可以關閉此分頁。</p>
115
+ </div>`;
116
+ }, 300);
111
117
  }
112
118
  }, 1000);
113
119
 
@@ -2075,6 +2075,112 @@ textarea.form-control {
2075
2075
  border-left-color: var(--accent-red);
2076
2076
  }
2077
2077
 
2078
+ .conversation-entry.entry-debug {
2079
+ border-left-color: var(--accent-orange, #f97316);
2080
+ }
2081
+
2082
+ .conversation-header-right {
2083
+ display: flex;
2084
+ align-items: center;
2085
+ gap: var(--spacing-sm);
2086
+ }
2087
+
2088
+ .debug-toggle {
2089
+ display: flex;
2090
+ align-items: center;
2091
+ gap: 4px;
2092
+ cursor: pointer;
2093
+ font-size: 14px;
2094
+ user-select: none;
2095
+ opacity: 0.6;
2096
+ transition: opacity 0.2s;
2097
+ }
2098
+
2099
+ .debug-toggle:hover {
2100
+ opacity: 1;
2101
+ }
2102
+
2103
+ .debug-toggle input[type="checkbox"] {
2104
+ width: 14px;
2105
+ height: 14px;
2106
+ cursor: pointer;
2107
+ }
2108
+
2109
+ .debug-info-grid {
2110
+ display: grid;
2111
+ grid-template-columns: 1fr 1fr;
2112
+ gap: 4px 16px;
2113
+ margin-bottom: 8px;
2114
+ }
2115
+
2116
+ .debug-row {
2117
+ display: flex;
2118
+ justify-content: space-between;
2119
+ padding: 3px 8px;
2120
+ border-radius: 4px;
2121
+ background: var(--bg-primary);
2122
+ font-size: 12px;
2123
+ }
2124
+
2125
+ .debug-label {
2126
+ color: var(--text-secondary);
2127
+ font-weight: 500;
2128
+ }
2129
+
2130
+ .debug-value {
2131
+ color: var(--text-primary);
2132
+ font-family: "Consolas", "Monaco", monospace;
2133
+ }
2134
+
2135
+ .debug-sections {
2136
+ margin-top: 8px;
2137
+ }
2138
+
2139
+ .debug-section-title {
2140
+ font-size: 12px;
2141
+ font-weight: 600;
2142
+ color: var(--text-secondary);
2143
+ margin-bottom: 4px;
2144
+ }
2145
+
2146
+ .debug-table {
2147
+ width: 100%;
2148
+ border-collapse: collapse;
2149
+ font-size: 12px;
2150
+ font-family: "Consolas", "Monaco", monospace;
2151
+ }
2152
+
2153
+ .debug-table th,
2154
+ .debug-table td {
2155
+ padding: 4px 8px;
2156
+ border: 1px solid var(--border-color);
2157
+ text-align: left;
2158
+ }
2159
+
2160
+ .debug-table th {
2161
+ background: var(--bg-hover);
2162
+ color: var(--text-secondary);
2163
+ font-weight: 600;
2164
+ }
2165
+
2166
+ .debug-table td {
2167
+ color: var(--text-primary);
2168
+ }
2169
+
2170
+ .debug-error {
2171
+ margin-top: 8px;
2172
+ padding: 6px 10px;
2173
+ background: rgba(239, 68, 68, 0.1);
2174
+ border: 1px solid rgba(239, 68, 68, 0.3);
2175
+ border-radius: 4px;
2176
+ font-size: 12px;
2177
+ color: var(--accent-red);
2178
+ }
2179
+
2180
+ .debug-body {
2181
+ font-size: 12px;
2182
+ }
2183
+
2078
2184
  .entry-summary {
2079
2185
  padding: var(--spacing-sm) var(--spacing-md);
2080
2186
  cursor: pointer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirohsu/user-web-feedback",
3
- "version": "2.8.17",
3
+ "version": "2.8.19",
4
4
  "description": "基於Node.js的MCP回饋收集器 - 支持AI工作彙報和用戶回饋收集",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {