@hirohsu/user-web-feedback 2.8.12 → 2.8.13

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 (34) hide show
  1. package/README.md +953 -953
  2. package/dist/cli.cjs +33267 -46031
  3. package/dist/index.cjs +35731 -48497
  4. package/dist/static/app.js +385 -385
  5. package/dist/static/components/navbar.css +406 -406
  6. package/dist/static/components/navbar.html +49 -49
  7. package/dist/static/components/navbar.js +211 -211
  8. package/dist/static/dashboard.css +495 -495
  9. package/dist/static/dashboard.html +95 -95
  10. package/dist/static/dashboard.js +540 -540
  11. package/dist/static/favicon.svg +27 -27
  12. package/dist/static/index.html +541 -541
  13. package/dist/static/logs.html +376 -376
  14. package/dist/static/logs.js +442 -442
  15. package/dist/static/mcp-settings.html +797 -797
  16. package/dist/static/mcp-settings.js +884 -884
  17. package/dist/static/modules/app-core.js +124 -124
  18. package/dist/static/modules/conversation-panel.js +247 -247
  19. package/dist/static/modules/feedback-handler.js +1420 -1420
  20. package/dist/static/modules/image-handler.js +155 -155
  21. package/dist/static/modules/log-viewer.js +296 -296
  22. package/dist/static/modules/mcp-manager.js +474 -474
  23. package/dist/static/modules/prompt-manager.js +364 -364
  24. package/dist/static/modules/settings-manager.js +299 -299
  25. package/dist/static/modules/socket-manager.js +170 -170
  26. package/dist/static/modules/state-manager.js +352 -352
  27. package/dist/static/modules/timer-controller.js +243 -243
  28. package/dist/static/modules/ui-helpers.js +246 -246
  29. package/dist/static/settings.html +426 -426
  30. package/dist/static/settings.js +767 -767
  31. package/dist/static/style.css +2156 -2156
  32. package/dist/static/terminals.html +357 -357
  33. package/dist/static/terminals.js +321 -321
  34. package/package.json +95 -95
@@ -1,321 +1,321 @@
1
- /**
2
- * CLI 終端機管理頁面
3
- */
4
-
5
- // 狀態
6
- let terminals = [];
7
-
8
- // 初始化
9
- document.addEventListener('DOMContentLoaded', () => {
10
- loadTerminals();
11
- setupEventListeners();
12
- });
13
-
14
- // 設置事件監聽器
15
- function setupEventListeners() {
16
- document.getElementById('refreshBtn')?.addEventListener('click', loadTerminals);
17
- document.getElementById('closeLogsModal')?.addEventListener('click', closeLogsModal);
18
-
19
- // 點擊 modal 外部關閉
20
- document.getElementById('logsModal')?.addEventListener('click', (e) => {
21
- if (e.target.id === 'logsModal') {
22
- closeLogsModal();
23
- }
24
- });
25
-
26
- // ESC 關閉 modal
27
- document.addEventListener('keydown', (e) => {
28
- if (e.key === 'Escape') {
29
- closeLogsModal();
30
- }
31
- });
32
- }
33
-
34
- // 載入終端機列表
35
- async function loadTerminals() {
36
- try {
37
- const response = await fetch('/api/cli/terminals');
38
- if (!response.ok) {
39
- throw new Error('載入終端機列表失敗');
40
- }
41
-
42
- terminals = await response.json();
43
- renderTerminals();
44
- updateStats();
45
- } catch (error) {
46
- console.error('載入終端機錯誤:', error);
47
- showToast('載入終端機列表失敗', 'error');
48
- }
49
- }
50
-
51
- // 渲染終端機列表
52
- function renderTerminals() {
53
- const container = document.getElementById('terminalsList');
54
-
55
- if (!terminals || terminals.length === 0) {
56
- container.innerHTML = `
57
- <div class="empty-state">
58
- <span class="icon">💻</span>
59
- <h3>尚無 CLI 終端機</h3>
60
- <p>當您使用 CLI 模式與 AI 互動時,終端機將在此顯示</p>
61
- </div>
62
- `;
63
- return;
64
- }
65
-
66
- container.innerHTML = terminals.map(terminal => createTerminalCard(terminal)).join('');
67
-
68
- // 綁定卡片事件
69
- terminals.forEach(terminal => {
70
- const viewLogsBtn = document.querySelector(`[data-view-logs="${terminal.id}"]`);
71
- const deleteBtn = document.querySelector(`[data-delete="${terminal.id}"]`);
72
-
73
- viewLogsBtn?.addEventListener('click', () => viewLogs(terminal.id, terminal.project_name));
74
- deleteBtn?.addEventListener('click', () => deleteTerminal(terminal.id));
75
- });
76
- }
77
-
78
- // 建立終端機卡片 HTML
79
- function createTerminalCard(terminal) {
80
- const statusClass = getStatusClass(terminal.status);
81
- const statusText = getStatusText(terminal.status);
82
- const toolIcon = getToolIcon(terminal.tool_type);
83
- const lastActivity = formatTime(terminal.updated_at || terminal.created_at);
84
-
85
- return `
86
- <div class="terminal-card" id="terminal-${terminal.id}">
87
- <div class="terminal-header">
88
- <div class="terminal-info">
89
- <h3>${escapeHtml(terminal.project_name || '未命名專案')}</h3>
90
- <span class="tool-badge">${toolIcon} ${terminal.tool_type}</span>
91
- </div>
92
- <span class="terminal-status ${statusClass}">${statusText}</span>
93
- </div>
94
-
95
- <div class="terminal-details">
96
- <div class="detail-row">
97
- <span class="detail-label">終端機 ID</span>
98
- <span class="detail-value">${terminal.id.substring(0, 8)}...</span>
99
- </div>
100
- ${terminal.pid ? `
101
- <div class="detail-row">
102
- <span class="detail-label">程序 PID</span>
103
- <span class="detail-value">${terminal.pid}</span>
104
- </div>
105
- ` : ''}
106
- <div class="detail-row">
107
- <span class="detail-label">最後活動</span>
108
- <span class="detail-value">${lastActivity}</span>
109
- </div>
110
- </div>
111
-
112
- <div class="terminal-actions">
113
- <button class="btn btn-secondary btn-sm" data-view-logs="${terminal.id}">
114
- 📋 查看日誌
115
- </button>
116
- <button class="btn btn-danger btn-sm" data-delete="${terminal.id}">
117
- 🗑️ 刪除
118
- </button>
119
- </div>
120
- </div>
121
- `;
122
- }
123
-
124
- // 取得狀態 CSS class
125
- function getStatusClass(status) {
126
- const statusMap = {
127
- 'running': 'running',
128
- 'idle': 'idle',
129
- 'error': 'error',
130
- 'stopped': 'stopped'
131
- };
132
- return statusMap[status] || 'stopped';
133
- }
134
-
135
- // 取得狀態顯示文字
136
- function getStatusText(status) {
137
- const statusMap = {
138
- 'running': '🔄 運行中',
139
- 'idle': '🟢 閒置',
140
- 'error': '❌ 錯誤',
141
- 'stopped': '⏹️ 已停止'
142
- };
143
- return statusMap[status] || '未知';
144
- }
145
-
146
- // 取得工具圖示
147
- function getToolIcon(toolType) {
148
- const iconMap = {
149
- 'gemini': '🌟',
150
- 'claude': '🤖',
151
- 'openai-codex': '🔮'
152
- };
153
- return iconMap[toolType] || '💻';
154
- }
155
-
156
- // 更新統計數據
157
- function updateStats() {
158
- const total = terminals.length;
159
- const active = terminals.filter(t => t.status === 'running' || t.status === 'idle').length;
160
- const errors = terminals.filter(t => t.status === 'error').length;
161
-
162
- document.getElementById('totalTerminals').textContent = total;
163
- document.getElementById('activeTerminals').textContent = active;
164
- document.getElementById('errorTerminals').textContent = errors;
165
- }
166
-
167
- // 查看執行日誌
168
- async function viewLogs(terminalId, projectName) {
169
- try {
170
- const response = await fetch(`/api/cli/terminals/${terminalId}/logs`);
171
- if (!response.ok) {
172
- throw new Error('載入日誌失敗');
173
- }
174
-
175
- const logs = await response.json();
176
- showLogsModal(logs, projectName);
177
- } catch (error) {
178
- console.error('載入日誌錯誤:', error);
179
- showToast('載入執行日誌失敗', 'error');
180
- }
181
- }
182
-
183
- // 顯示日誌 Modal
184
- function showLogsModal(logs, projectName) {
185
- const modal = document.getElementById('logsModal');
186
- const title = document.getElementById('logsModalTitle');
187
- const body = document.getElementById('logsModalBody');
188
-
189
- title.textContent = `執行日誌 - ${projectName || '未命名專案'}`;
190
-
191
- if (!logs || logs.length === 0) {
192
- body.innerHTML = `
193
- <div class="empty-state">
194
- <span class="icon">📋</span>
195
- <h3>尚無執行日誌</h3>
196
- <p>此終端機尚未有任何執行記錄</p>
197
- </div>
198
- `;
199
- } else {
200
- body.innerHTML = logs.map(log => createLogEntry(log)).join('');
201
- }
202
-
203
- modal.classList.add('show');
204
- }
205
-
206
- // 建立日誌條目 HTML
207
- function createLogEntry(log) {
208
- const statusClass = log.success ? 'success' : 'error';
209
- const time = formatTime(log.executed_at);
210
- const duration = log.execution_time ? `${log.execution_time}ms` : 'N/A';
211
-
212
- return `
213
- <div class="log-entry ${statusClass}">
214
- <div class="log-meta">
215
- <span>🕐 ${time}</span>
216
- <span>⏱️ ${duration}</span>
217
- </div>
218
- <div class="log-prompt">
219
- <strong>提示:</strong> ${escapeHtml(truncateText(log.prompt, 200))}
220
- </div>
221
- <div class="log-response">
222
- <strong>回應:</strong> ${escapeHtml(truncateText(log.response || log.error_message || '無回應', 500))}
223
- </div>
224
- </div>
225
- `;
226
- }
227
-
228
- // 關閉日誌 Modal
229
- function closeLogsModal() {
230
- const modal = document.getElementById('logsModal');
231
- modal.classList.remove('show');
232
- }
233
-
234
- // 刪除終端機
235
- async function deleteTerminal(terminalId) {
236
- if (!confirm('確定要刪除此終端機嗎?相關的執行日誌也會被刪除。')) {
237
- return;
238
- }
239
-
240
- try {
241
- const response = await fetch(`/api/cli/terminals/${terminalId}`, {
242
- method: 'DELETE'
243
- });
244
-
245
- if (!response.ok) {
246
- throw new Error('刪除終端機失敗');
247
- }
248
-
249
- showToast('終端機已刪除', 'success');
250
- loadTerminals();
251
- } catch (error) {
252
- console.error('刪除終端機錯誤:', error);
253
- showToast('刪除終端機失敗', 'error');
254
- }
255
- }
256
-
257
- // 格式化時間
258
- function formatTime(timestamp) {
259
- if (!timestamp) return 'N/A';
260
-
261
- const date = new Date(timestamp);
262
- const now = new Date();
263
- const diff = now - date;
264
-
265
- // 1 分鐘內
266
- if (diff < 60000) {
267
- return '剛才';
268
- }
269
-
270
- // 1 小時內
271
- if (diff < 3600000) {
272
- const minutes = Math.floor(diff / 60000);
273
- return `${minutes} 分鐘前`;
274
- }
275
-
276
- // 24 小時內
277
- if (diff < 86400000) {
278
- const hours = Math.floor(diff / 3600000);
279
- return `${hours} 小時前`;
280
- }
281
-
282
- // 超過 24 小時
283
- return date.toLocaleString('zh-TW', {
284
- month: 'short',
285
- day: 'numeric',
286
- hour: '2-digit',
287
- minute: '2-digit'
288
- });
289
- }
290
-
291
- // 截斷文字
292
- function truncateText(text, maxLength) {
293
- if (!text) return '';
294
- if (text.length <= maxLength) return text;
295
- return text.substring(0, maxLength) + '...';
296
- }
297
-
298
- // HTML 轉義
299
- function escapeHtml(text) {
300
- if (!text) return '';
301
- const div = document.createElement('div');
302
- div.textContent = text;
303
- return div.innerHTML;
304
- }
305
-
306
- // 顯示 Toast 通知
307
- function showToast(message, type = 'info') {
308
- const container = document.getElementById('toastContainer');
309
- if (!container) return;
310
-
311
- const toast = document.createElement('div');
312
- toast.className = `toast toast-${type}`;
313
- toast.textContent = message;
314
-
315
- container.appendChild(toast);
316
-
317
- setTimeout(() => {
318
- toast.classList.add('fade-out');
319
- setTimeout(() => toast.remove(), 300);
320
- }, 3000);
321
- }
1
+ /**
2
+ * CLI 終端機管理頁面
3
+ */
4
+
5
+ // 狀態
6
+ let terminals = [];
7
+
8
+ // 初始化
9
+ document.addEventListener('DOMContentLoaded', () => {
10
+ loadTerminals();
11
+ setupEventListeners();
12
+ });
13
+
14
+ // 設置事件監聽器
15
+ function setupEventListeners() {
16
+ document.getElementById('refreshBtn')?.addEventListener('click', loadTerminals);
17
+ document.getElementById('closeLogsModal')?.addEventListener('click', closeLogsModal);
18
+
19
+ // 點擊 modal 外部關閉
20
+ document.getElementById('logsModal')?.addEventListener('click', (e) => {
21
+ if (e.target.id === 'logsModal') {
22
+ closeLogsModal();
23
+ }
24
+ });
25
+
26
+ // ESC 關閉 modal
27
+ document.addEventListener('keydown', (e) => {
28
+ if (e.key === 'Escape') {
29
+ closeLogsModal();
30
+ }
31
+ });
32
+ }
33
+
34
+ // 載入終端機列表
35
+ async function loadTerminals() {
36
+ try {
37
+ const response = await fetch('/api/cli/terminals');
38
+ if (!response.ok) {
39
+ throw new Error('載入終端機列表失敗');
40
+ }
41
+
42
+ terminals = await response.json();
43
+ renderTerminals();
44
+ updateStats();
45
+ } catch (error) {
46
+ console.error('載入終端機錯誤:', error);
47
+ showToast('載入終端機列表失敗', 'error');
48
+ }
49
+ }
50
+
51
+ // 渲染終端機列表
52
+ function renderTerminals() {
53
+ const container = document.getElementById('terminalsList');
54
+
55
+ if (!terminals || terminals.length === 0) {
56
+ container.innerHTML = `
57
+ <div class="empty-state">
58
+ <span class="icon">💻</span>
59
+ <h3>尚無 CLI 終端機</h3>
60
+ <p>當您使用 CLI 模式與 AI 互動時,終端機將在此顯示</p>
61
+ </div>
62
+ `;
63
+ return;
64
+ }
65
+
66
+ container.innerHTML = terminals.map(terminal => createTerminalCard(terminal)).join('');
67
+
68
+ // 綁定卡片事件
69
+ terminals.forEach(terminal => {
70
+ const viewLogsBtn = document.querySelector(`[data-view-logs="${terminal.id}"]`);
71
+ const deleteBtn = document.querySelector(`[data-delete="${terminal.id}"]`);
72
+
73
+ viewLogsBtn?.addEventListener('click', () => viewLogs(terminal.id, terminal.project_name));
74
+ deleteBtn?.addEventListener('click', () => deleteTerminal(terminal.id));
75
+ });
76
+ }
77
+
78
+ // 建立終端機卡片 HTML
79
+ function createTerminalCard(terminal) {
80
+ const statusClass = getStatusClass(terminal.status);
81
+ const statusText = getStatusText(terminal.status);
82
+ const toolIcon = getToolIcon(terminal.tool_type);
83
+ const lastActivity = formatTime(terminal.updated_at || terminal.created_at);
84
+
85
+ return `
86
+ <div class="terminal-card" id="terminal-${terminal.id}">
87
+ <div class="terminal-header">
88
+ <div class="terminal-info">
89
+ <h3>${escapeHtml(terminal.project_name || '未命名專案')}</h3>
90
+ <span class="tool-badge">${toolIcon} ${terminal.tool_type}</span>
91
+ </div>
92
+ <span class="terminal-status ${statusClass}">${statusText}</span>
93
+ </div>
94
+
95
+ <div class="terminal-details">
96
+ <div class="detail-row">
97
+ <span class="detail-label">終端機 ID</span>
98
+ <span class="detail-value">${terminal.id.substring(0, 8)}...</span>
99
+ </div>
100
+ ${terminal.pid ? `
101
+ <div class="detail-row">
102
+ <span class="detail-label">程序 PID</span>
103
+ <span class="detail-value">${terminal.pid}</span>
104
+ </div>
105
+ ` : ''}
106
+ <div class="detail-row">
107
+ <span class="detail-label">最後活動</span>
108
+ <span class="detail-value">${lastActivity}</span>
109
+ </div>
110
+ </div>
111
+
112
+ <div class="terminal-actions">
113
+ <button class="btn btn-secondary btn-sm" data-view-logs="${terminal.id}">
114
+ 📋 查看日誌
115
+ </button>
116
+ <button class="btn btn-danger btn-sm" data-delete="${terminal.id}">
117
+ 🗑️ 刪除
118
+ </button>
119
+ </div>
120
+ </div>
121
+ `;
122
+ }
123
+
124
+ // 取得狀態 CSS class
125
+ function getStatusClass(status) {
126
+ const statusMap = {
127
+ 'running': 'running',
128
+ 'idle': 'idle',
129
+ 'error': 'error',
130
+ 'stopped': 'stopped'
131
+ };
132
+ return statusMap[status] || 'stopped';
133
+ }
134
+
135
+ // 取得狀態顯示文字
136
+ function getStatusText(status) {
137
+ const statusMap = {
138
+ 'running': '🔄 運行中',
139
+ 'idle': '🟢 閒置',
140
+ 'error': '❌ 錯誤',
141
+ 'stopped': '⏹️ 已停止'
142
+ };
143
+ return statusMap[status] || '未知';
144
+ }
145
+
146
+ // 取得工具圖示
147
+ function getToolIcon(toolType) {
148
+ const iconMap = {
149
+ 'gemini': '🌟',
150
+ 'claude': '🤖',
151
+ 'openai-codex': '🔮'
152
+ };
153
+ return iconMap[toolType] || '💻';
154
+ }
155
+
156
+ // 更新統計數據
157
+ function updateStats() {
158
+ const total = terminals.length;
159
+ const active = terminals.filter(t => t.status === 'running' || t.status === 'idle').length;
160
+ const errors = terminals.filter(t => t.status === 'error').length;
161
+
162
+ document.getElementById('totalTerminals').textContent = total;
163
+ document.getElementById('activeTerminals').textContent = active;
164
+ document.getElementById('errorTerminals').textContent = errors;
165
+ }
166
+
167
+ // 查看執行日誌
168
+ async function viewLogs(terminalId, projectName) {
169
+ try {
170
+ const response = await fetch(`/api/cli/terminals/${terminalId}/logs`);
171
+ if (!response.ok) {
172
+ throw new Error('載入日誌失敗');
173
+ }
174
+
175
+ const logs = await response.json();
176
+ showLogsModal(logs, projectName);
177
+ } catch (error) {
178
+ console.error('載入日誌錯誤:', error);
179
+ showToast('載入執行日誌失敗', 'error');
180
+ }
181
+ }
182
+
183
+ // 顯示日誌 Modal
184
+ function showLogsModal(logs, projectName) {
185
+ const modal = document.getElementById('logsModal');
186
+ const title = document.getElementById('logsModalTitle');
187
+ const body = document.getElementById('logsModalBody');
188
+
189
+ title.textContent = `執行日誌 - ${projectName || '未命名專案'}`;
190
+
191
+ if (!logs || logs.length === 0) {
192
+ body.innerHTML = `
193
+ <div class="empty-state">
194
+ <span class="icon">📋</span>
195
+ <h3>尚無執行日誌</h3>
196
+ <p>此終端機尚未有任何執行記錄</p>
197
+ </div>
198
+ `;
199
+ } else {
200
+ body.innerHTML = logs.map(log => createLogEntry(log)).join('');
201
+ }
202
+
203
+ modal.classList.add('show');
204
+ }
205
+
206
+ // 建立日誌條目 HTML
207
+ function createLogEntry(log) {
208
+ const statusClass = log.success ? 'success' : 'error';
209
+ const time = formatTime(log.executed_at);
210
+ const duration = log.execution_time ? `${log.execution_time}ms` : 'N/A';
211
+
212
+ return `
213
+ <div class="log-entry ${statusClass}">
214
+ <div class="log-meta">
215
+ <span>🕐 ${time}</span>
216
+ <span>⏱️ ${duration}</span>
217
+ </div>
218
+ <div class="log-prompt">
219
+ <strong>提示:</strong> ${escapeHtml(truncateText(log.prompt, 200))}
220
+ </div>
221
+ <div class="log-response">
222
+ <strong>回應:</strong> ${escapeHtml(truncateText(log.response || log.error_message || '無回應', 500))}
223
+ </div>
224
+ </div>
225
+ `;
226
+ }
227
+
228
+ // 關閉日誌 Modal
229
+ function closeLogsModal() {
230
+ const modal = document.getElementById('logsModal');
231
+ modal.classList.remove('show');
232
+ }
233
+
234
+ // 刪除終端機
235
+ async function deleteTerminal(terminalId) {
236
+ if (!confirm('確定要刪除此終端機嗎?相關的執行日誌也會被刪除。')) {
237
+ return;
238
+ }
239
+
240
+ try {
241
+ const response = await fetch(`/api/cli/terminals/${terminalId}`, {
242
+ method: 'DELETE'
243
+ });
244
+
245
+ if (!response.ok) {
246
+ throw new Error('刪除終端機失敗');
247
+ }
248
+
249
+ showToast('終端機已刪除', 'success');
250
+ loadTerminals();
251
+ } catch (error) {
252
+ console.error('刪除終端機錯誤:', error);
253
+ showToast('刪除終端機失敗', 'error');
254
+ }
255
+ }
256
+
257
+ // 格式化時間
258
+ function formatTime(timestamp) {
259
+ if (!timestamp) return 'N/A';
260
+
261
+ const date = new Date(timestamp);
262
+ const now = new Date();
263
+ const diff = now - date;
264
+
265
+ // 1 分鐘內
266
+ if (diff < 60000) {
267
+ return '剛才';
268
+ }
269
+
270
+ // 1 小時內
271
+ if (diff < 3600000) {
272
+ const minutes = Math.floor(diff / 60000);
273
+ return `${minutes} 分鐘前`;
274
+ }
275
+
276
+ // 24 小時內
277
+ if (diff < 86400000) {
278
+ const hours = Math.floor(diff / 3600000);
279
+ return `${hours} 小時前`;
280
+ }
281
+
282
+ // 超過 24 小時
283
+ return date.toLocaleString('zh-TW', {
284
+ month: 'short',
285
+ day: 'numeric',
286
+ hour: '2-digit',
287
+ minute: '2-digit'
288
+ });
289
+ }
290
+
291
+ // 截斷文字
292
+ function truncateText(text, maxLength) {
293
+ if (!text) return '';
294
+ if (text.length <= maxLength) return text;
295
+ return text.substring(0, maxLength) + '...';
296
+ }
297
+
298
+ // HTML 轉義
299
+ function escapeHtml(text) {
300
+ if (!text) return '';
301
+ const div = document.createElement('div');
302
+ div.textContent = text;
303
+ return div.innerHTML;
304
+ }
305
+
306
+ // 顯示 Toast 通知
307
+ function showToast(message, type = 'info') {
308
+ const container = document.getElementById('toastContainer');
309
+ if (!container) return;
310
+
311
+ const toast = document.createElement('div');
312
+ toast.className = `toast toast-${type}`;
313
+ toast.textContent = message;
314
+
315
+ container.appendChild(toast);
316
+
317
+ setTimeout(() => {
318
+ toast.classList.add('fade-out');
319
+ setTimeout(() => toast.remove(), 300);
320
+ }, 3000);
321
+ }