@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,357 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-TW">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>💻 CLI 終端機 - User Feedback</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<script src="/socket.io.min.js"></script>
|
|
9
|
+
<!-- Navbar Component -->
|
|
10
|
+
<link rel="stylesheet" href="/components/navbar.css">
|
|
11
|
+
<link rel="stylesheet" href="style.css">
|
|
12
|
+
<link rel="stylesheet" href="dashboard.css">
|
|
13
|
+
<style>
|
|
14
|
+
.terminals-container {
|
|
15
|
+
max-width: 1400px;
|
|
16
|
+
margin: 20px auto;
|
|
17
|
+
padding: 0 20px 40px 20px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.page-header {
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
align-items: center;
|
|
24
|
+
margin-bottom: 24px;
|
|
25
|
+
padding-bottom: 16px;
|
|
26
|
+
border-bottom: 1px solid var(--border-color);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.page-title {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
font-size: 1.75rem;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
color: var(--text-primary);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.header-actions {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 12px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.stats-bar {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 20px;
|
|
46
|
+
margin-bottom: 24px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.stat-item {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
padding: 8px 16px;
|
|
54
|
+
background: var(--bg-primary);
|
|
55
|
+
border: 1px solid var(--border-color);
|
|
56
|
+
border-radius: var(--radius-sm);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.stat-item .icon {
|
|
60
|
+
font-size: 1.2rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.stat-item .value {
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
color: var(--text-primary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stat-item .label {
|
|
69
|
+
color: var(--text-secondary);
|
|
70
|
+
font-size: 0.9rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.terminals-grid {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
76
|
+
gap: 20px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.terminal-card {
|
|
80
|
+
background: var(--bg-primary);
|
|
81
|
+
border: 1px solid var(--border-color);
|
|
82
|
+
border-radius: var(--radius-md);
|
|
83
|
+
padding: 20px;
|
|
84
|
+
transition: box-shadow 0.2s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.terminal-card:hover {
|
|
88
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.terminal-header {
|
|
92
|
+
display: flex;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
align-items: flex-start;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.terminal-info h3 {
|
|
99
|
+
font-size: 1.1rem;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
color: var(--text-primary);
|
|
102
|
+
margin-bottom: 4px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.terminal-info .tool-badge {
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 4px;
|
|
109
|
+
padding: 2px 8px;
|
|
110
|
+
background: var(--bg-secondary);
|
|
111
|
+
border-radius: var(--radius-sm);
|
|
112
|
+
font-size: 0.8rem;
|
|
113
|
+
color: var(--text-secondary);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.terminal-status {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 6px;
|
|
120
|
+
padding: 4px 10px;
|
|
121
|
+
border-radius: var(--radius-sm);
|
|
122
|
+
font-size: 0.85rem;
|
|
123
|
+
font-weight: 500;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.terminal-status.running {
|
|
127
|
+
background: rgba(59, 130, 246, 0.1);
|
|
128
|
+
color: var(--accent-blue);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.terminal-status.idle {
|
|
132
|
+
background: rgba(34, 197, 94, 0.1);
|
|
133
|
+
color: var(--accent-green);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.terminal-status.error {
|
|
137
|
+
background: rgba(239, 68, 68, 0.1);
|
|
138
|
+
color: var(--accent-red);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.terminal-status.stopped {
|
|
142
|
+
background: rgba(156, 163, 175, 0.1);
|
|
143
|
+
color: var(--text-muted);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.terminal-details {
|
|
147
|
+
margin-bottom: 16px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.detail-row {
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
padding: 8px 0;
|
|
154
|
+
border-bottom: 1px solid var(--border-color);
|
|
155
|
+
font-size: 0.9rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.detail-row:last-child {
|
|
159
|
+
border-bottom: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.detail-label {
|
|
163
|
+
color: var(--text-secondary);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.detail-value {
|
|
167
|
+
color: var(--text-primary);
|
|
168
|
+
font-family: monospace;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.terminal-actions {
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
justify-content: flex-end;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.btn-sm {
|
|
178
|
+
padding: 6px 12px;
|
|
179
|
+
font-size: 0.85rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.empty-state {
|
|
183
|
+
text-align: center;
|
|
184
|
+
padding: 60px 20px;
|
|
185
|
+
color: var(--text-muted);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.empty-state .icon {
|
|
189
|
+
font-size: 3rem;
|
|
190
|
+
margin-bottom: 16px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.empty-state h3 {
|
|
194
|
+
font-size: 1.25rem;
|
|
195
|
+
margin-bottom: 8px;
|
|
196
|
+
color: var(--text-secondary);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Logs Modal */
|
|
200
|
+
.modal-overlay {
|
|
201
|
+
display: none;
|
|
202
|
+
position: fixed;
|
|
203
|
+
top: 0;
|
|
204
|
+
left: 0;
|
|
205
|
+
right: 0;
|
|
206
|
+
bottom: 0;
|
|
207
|
+
background: rgba(0, 0, 0, 0.5);
|
|
208
|
+
z-index: 1000;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
align-items: center;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.modal-overlay.show {
|
|
214
|
+
display: flex;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.modal-content {
|
|
218
|
+
background: var(--bg-primary);
|
|
219
|
+
border-radius: var(--radius-lg);
|
|
220
|
+
width: 90%;
|
|
221
|
+
max-width: 800px;
|
|
222
|
+
max-height: 80vh;
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.modal-header {
|
|
228
|
+
display: flex;
|
|
229
|
+
justify-content: space-between;
|
|
230
|
+
align-items: center;
|
|
231
|
+
padding: 16px 20px;
|
|
232
|
+
border-bottom: 1px solid var(--border-color);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.modal-header h3 {
|
|
236
|
+
font-size: 1.2rem;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.modal-close {
|
|
241
|
+
background: none;
|
|
242
|
+
border: none;
|
|
243
|
+
font-size: 1.5rem;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
color: var(--text-muted);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.modal-body {
|
|
249
|
+
flex: 1;
|
|
250
|
+
overflow-y: auto;
|
|
251
|
+
padding: 20px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.log-entry {
|
|
255
|
+
padding: 12px;
|
|
256
|
+
margin-bottom: 8px;
|
|
257
|
+
background: var(--bg-secondary);
|
|
258
|
+
border-radius: var(--radius-sm);
|
|
259
|
+
font-family: monospace;
|
|
260
|
+
font-size: 0.85rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.log-entry.success {
|
|
264
|
+
border-left: 3px solid var(--accent-green);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.log-entry.error {
|
|
268
|
+
border-left: 3px solid var(--accent-red);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.log-meta {
|
|
272
|
+
display: flex;
|
|
273
|
+
justify-content: space-between;
|
|
274
|
+
margin-bottom: 8px;
|
|
275
|
+
color: var(--text-muted);
|
|
276
|
+
font-size: 0.8rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.log-prompt {
|
|
280
|
+
color: var(--text-secondary);
|
|
281
|
+
margin-bottom: 8px;
|
|
282
|
+
white-space: pre-wrap;
|
|
283
|
+
word-break: break-all;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.log-response {
|
|
287
|
+
color: var(--text-primary);
|
|
288
|
+
white-space: pre-wrap;
|
|
289
|
+
word-break: break-all;
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<!-- Unified Navigation Bar (injected by navbar.js) -->
|
|
295
|
+
|
|
296
|
+
<div class="terminals-container">
|
|
297
|
+
<header class="page-header">
|
|
298
|
+
<h1 class="page-title">
|
|
299
|
+
<span class="icon">💻</span>
|
|
300
|
+
CLI 終端機
|
|
301
|
+
</h1>
|
|
302
|
+
<div class="header-actions">
|
|
303
|
+
<button id="refreshBtn" class="btn btn-secondary">
|
|
304
|
+
<span class="icon">🔄</span>
|
|
305
|
+
重新整理
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</header>
|
|
309
|
+
|
|
310
|
+
<div class="stats-bar">
|
|
311
|
+
<div class="stat-item">
|
|
312
|
+
<span class="icon">📊</span>
|
|
313
|
+
<span class="value" id="totalTerminals">0</span>
|
|
314
|
+
<span class="label">總終端機</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="stat-item">
|
|
317
|
+
<span class="icon">🟢</span>
|
|
318
|
+
<span class="value" id="activeTerminals">0</span>
|
|
319
|
+
<span class="label">運行中</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="stat-item">
|
|
322
|
+
<span class="icon">⚠️</span>
|
|
323
|
+
<span class="value" id="errorTerminals">0</span>
|
|
324
|
+
<span class="label">錯誤</span>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div id="terminalsList" class="terminals-grid">
|
|
329
|
+
<div class="empty-state">
|
|
330
|
+
<span class="icon">💻</span>
|
|
331
|
+
<h3>尚無 CLI 終端機</h3>
|
|
332
|
+
<p>當您使用 CLI 模式與 AI 互動時,終端機將在此顯示</p>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- Logs Modal -->
|
|
338
|
+
<div id="logsModal" class="modal-overlay">
|
|
339
|
+
<div class="modal-content">
|
|
340
|
+
<div class="modal-header">
|
|
341
|
+
<h3 id="logsModalTitle">執行日誌</h3>
|
|
342
|
+
<button class="modal-close" id="closeLogsModal">×</button>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="modal-body" id="logsModalBody">
|
|
345
|
+
<!-- 日誌將由 JavaScript 動態生成 -->
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<!-- Toast Container -->
|
|
351
|
+
<div id="toastContainer" class="toast-container"></div>
|
|
352
|
+
|
|
353
|
+
<!-- Scripts -->
|
|
354
|
+
<script src="/components/navbar.js"></script>
|
|
355
|
+
<script src="terminals.js"></script>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|
|
@@ -0,0 +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
|
+
}
|