vg-coder-cli 1.0.11 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -83,9 +83,8 @@ class ApiServer {
83
83
  const detector = new ProjectDetector(resolvedPath);
84
84
  const projectInfo = await detector.detectAll();
85
85
 
86
- // Scan files
86
+ // Scan files (no token limit, get all files)
87
87
  const scannerOptions = {
88
- maxTokens: parseInt(options.maxTokens || 8000),
89
88
  extensions: options.extensions ? options.extensions.split(',').map(ext => ext.trim()) : undefined,
90
89
  includeHidden: options.includeHidden || false
91
90
  };
@@ -1,5 +1,6 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+
3
4
  <head>
4
5
  <meta charset="UTF-8">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -19,7 +20,7 @@
19
20
  }
20
21
 
21
22
  .container {
22
- max-width: 1200px;
23
+ max-width: 1000px;
23
24
  margin: 0 auto;
24
25
  }
25
26
 
@@ -28,7 +29,7 @@
28
29
  border-radius: 12px;
29
30
  padding: 30px;
30
31
  margin-bottom: 30px;
31
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
32
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
32
33
  }
33
34
 
34
35
  .header h1 {
@@ -52,105 +53,192 @@
52
53
  margin-top: 10px;
53
54
  }
54
55
 
56
+ .system-prompt-card {
57
+ background: white;
58
+ border-radius: 12px;
59
+ padding: 25px;
60
+ margin-bottom: 30px;
61
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
62
+ }
63
+
64
+ .system-prompt-header {
65
+ display: flex;
66
+ justify-content: space-between;
67
+ align-items: center;
68
+ margin-bottom: 15px;
69
+ cursor: pointer;
70
+ user-select: none;
71
+ }
72
+
73
+ .system-prompt-header h2 {
74
+ color: #667eea;
75
+ font-size: 1.5em;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ }
80
+
81
+ .toggle-icon {
82
+ transition: transform 0.3s ease;
83
+ }
84
+
85
+ .toggle-icon.open {
86
+ transform: rotate(180deg);
87
+ }
88
+
89
+ .system-prompt-content {
90
+ max-height: 0;
91
+ overflow: hidden;
92
+ transition: max-height 0.3s ease;
93
+ }
94
+
95
+ .system-prompt-content.open {
96
+ max-height: 2000px;
97
+ }
98
+
99
+ .prompt-text {
100
+ background: #f9fafb;
101
+ border: 2px solid #e5e7eb;
102
+ border-radius: 8px;
103
+ padding: 20px;
104
+ font-family: 'Courier New', monospace;
105
+ font-size: 0.9em;
106
+ line-height: 1.6;
107
+ white-space: pre-wrap;
108
+ max-height: 400px;
109
+ overflow-y: auto;
110
+ margin-bottom: 15px;
111
+ }
112
+
55
113
  .endpoints {
56
114
  display: grid;
57
- grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
58
115
  gap: 20px;
59
116
  }
60
117
 
61
118
  .endpoint-card {
62
119
  background: white;
63
120
  border-radius: 12px;
64
- padding: 25px;
65
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
66
- transition: transform 0.3s ease;
67
- }
68
-
69
- .endpoint-card:hover {
70
- transform: translateY(-5px);
121
+ padding: 30px;
122
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
71
123
  }
72
124
 
73
125
  .endpoint-header {
74
126
  display: flex;
75
127
  align-items: center;
76
128
  gap: 15px;
77
- margin-bottom: 15px;
129
+ margin-bottom: 20px;
78
130
  }
79
131
 
80
132
  .method {
81
- padding: 6px 12px;
133
+ padding: 8px 16px;
82
134
  border-radius: 6px;
83
135
  font-weight: bold;
84
- font-size: 0.85em;
136
+ font-size: 0.9em;
85
137
  }
86
138
 
87
- .method.get { background: #3b82f6; color: white; }
88
- .method.post { background: #10b981; color: white; }
89
- .method.delete { background: #ef4444; color: white; }
139
+ .method.post {
140
+ background: #10b981;
141
+ color: white;
142
+ }
90
143
 
91
144
  .endpoint-path {
92
145
  font-family: 'Courier New', monospace;
93
146
  color: #333;
94
- font-size: 1.1em;
147
+ font-size: 1.2em;
148
+ font-weight: 600;
95
149
  }
96
150
 
97
151
  .endpoint-desc {
98
152
  color: #666;
99
- margin-bottom: 15px;
153
+ margin-bottom: 20px;
100
154
  line-height: 1.6;
155
+ font-size: 1.05em;
101
156
  }
102
157
 
103
158
  .form-group {
104
- margin-bottom: 15px;
159
+ margin-bottom: 20px;
105
160
  }
106
161
 
107
162
  .form-group label {
108
163
  display: block;
109
- margin-bottom: 5px;
164
+ margin-bottom: 8px;
110
165
  color: #333;
111
- font-weight: 500;
166
+ font-weight: 600;
167
+ font-size: 1em;
112
168
  }
113
169
 
114
170
  .form-group input,
115
171
  .form-group textarea {
116
172
  width: 100%;
117
- padding: 10px;
173
+ padding: 12px;
118
174
  border: 2px solid #e5e7eb;
119
- border-radius: 6px;
120
- font-size: 0.95em;
175
+ border-radius: 8px;
176
+ font-size: 1em;
121
177
  font-family: 'Courier New', monospace;
178
+ transition: border-color 0.3s ease;
179
+ }
180
+
181
+ .form-group input:focus,
182
+ .form-group textarea:focus {
183
+ outline: none;
184
+ border-color: #667eea;
122
185
  }
123
186
 
124
187
  .form-group textarea {
125
- min-height: 120px;
188
+ min-height: 150px;
126
189
  resize: vertical;
127
190
  }
128
191
 
192
+ .btn-group {
193
+ display: flex;
194
+ gap: 10px;
195
+ margin-top: 20px;
196
+ }
197
+
129
198
  .btn {
130
199
  background: #667eea;
131
200
  color: white;
132
201
  border: none;
133
- padding: 12px 24px;
134
- border-radius: 6px;
202
+ padding: 14px 28px;
203
+ border-radius: 8px;
135
204
  cursor: pointer;
136
205
  font-size: 1em;
137
206
  font-weight: 600;
138
- transition: background 0.3s ease;
207
+ transition: all 0.3s ease;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: 8px;
139
211
  }
140
212
 
141
213
  .btn:hover {
142
214
  background: #5568d3;
215
+ transform: translateY(-2px);
216
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
143
217
  }
144
218
 
145
219
  .btn:disabled {
146
220
  background: #9ca3af;
147
221
  cursor: not-allowed;
222
+ transform: none;
223
+ }
224
+
225
+ .btn-copy {
226
+ background: #10b981;
227
+ }
228
+
229
+ .btn-copy:hover {
230
+ background: #059669;
231
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
232
+ }
233
+
234
+ .btn-copy.copied {
235
+ background: #6366f1;
148
236
  }
149
237
 
150
238
  .response {
151
- margin-top: 15px;
152
- padding: 15px;
153
- border-radius: 6px;
239
+ margin-top: 20px;
240
+ padding: 20px;
241
+ border-radius: 8px;
154
242
  background: #f9fafb;
155
243
  border-left: 4px solid #667eea;
156
244
  display: none;
@@ -173,7 +261,7 @@
173
261
  .response pre {
174
262
  margin: 0;
175
263
  font-family: 'Courier New', monospace;
176
- font-size: 0.9em;
264
+ font-size: 0.95em;
177
265
  white-space: pre-wrap;
178
266
  word-wrap: break-word;
179
267
  }
@@ -182,105 +270,278 @@
182
270
  display: inline-block;
183
271
  width: 16px;
184
272
  height: 16px;
185
- border: 3px solid rgba(255,255,255,.3);
273
+ border: 3px solid rgba(255, 255, 255, .3);
186
274
  border-radius: 50%;
187
275
  border-top-color: white;
188
276
  animation: spin 1s ease-in-out infinite;
189
277
  }
190
278
 
191
279
  @keyframes spin {
192
- to { transform: rotate(360deg); }
280
+ to {
281
+ transform: rotate(360deg);
282
+ }
283
+ }
284
+
285
+ .toast {
286
+ position: fixed;
287
+ top: 20px;
288
+ right: 20px;
289
+ padding: 16px 24px;
290
+ border-radius: 8px;
291
+ color: white;
292
+ font-weight: 600;
293
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
294
+ opacity: 0;
295
+ transform: translateY(-20px);
296
+ transition: all 0.3s ease;
297
+ z-index: 1000;
298
+ }
299
+
300
+ .toast.show {
301
+ opacity: 1;
302
+ transform: translateY(0);
303
+ }
304
+
305
+ .toast.success {
306
+ background: #10b981;
307
+ }
308
+
309
+ .toast.error {
310
+ background: #ef4444;
311
+ }
312
+
313
+ .toast.info {
314
+ background: #3b82f6;
193
315
  }
194
316
  </style>
195
317
  </head>
318
+
196
319
  <body>
197
320
  <div class="container">
198
321
  <div class="header">
199
322
  <h1>🚀 VG Coder API Dashboard</h1>
200
- <p>Test your API endpoints directly from the browser</p>
323
+ <p>Phân tích dự án thực thi bash scripts</p>
201
324
  <span class="status" id="status">● Server Running</span>
202
325
  </div>
203
326
 
204
- <div class="endpoints">
205
- <!-- Health Check -->
206
- <div class="endpoint-card">
207
- <div class="endpoint-header">
208
- <span class="method get">GET</span>
209
- <span class="endpoint-path">/health</span>
210
- </div>
211
- <p class="endpoint-desc">Check server health status</p>
212
- <button class="btn" onclick="testHealth()">Test Health Check</button>
213
- <div class="response" id="health-response"></div>
327
+ <!-- System Prompt Section -->
328
+ <div class="system-prompt-card">
329
+ <div class="system-prompt-header" onclick="toggleSystemPrompt()">
330
+ <h2>
331
+ <span>📝</span>
332
+ <span>AI System Prompt</span>
333
+ </h2>
334
+ <span class="toggle-icon" id="toggle-icon">▼</span>
214
335
  </div>
336
+ <div class="system-prompt-content" id="system-prompt-content">
337
+ <div class="prompt-text" id="prompt-text"></div>
338
+ <button class="btn btn-copy" onclick="copySystemPrompt()">
339
+ <span id="copy-icon">📋</span>
340
+ <span id="copy-text">Copy System Prompt</span>
341
+ </button>
342
+ </div>
343
+ </div>
215
344
 
345
+ <div class="endpoints">
216
346
  <!-- Analyze -->
217
347
  <div class="endpoint-card">
218
348
  <div class="endpoint-header">
219
349
  <span class="method post">POST</span>
220
350
  <span class="endpoint-path">/api/analyze</span>
221
351
  </div>
222
- <p class="endpoint-desc">Analyze project and download project.txt</p>
352
+ <p class="endpoint-desc">📊 Phân tích dự án và lấy toàn bộ source code</p>
223
353
  <div class="form-group">
224
- <label>Project Path:</label>
354
+ <label>📁 Project Path:</label>
225
355
  <input type="text" id="analyze-path" value="." placeholder=".">
226
356
  </div>
227
- <div class="form-group">
228
- <label>Max Tokens:</label>
229
- <input type="number" id="analyze-tokens" value="8000" placeholder="8000">
357
+ <div class="btn-group">
358
+ <button class="btn" onclick="testAnalyze()">
359
+ <span>📥</span>
360
+ <span>Download File</span>
361
+ </button>
362
+ <button class="btn btn-copy" onclick="copyAnalyzeResult()">
363
+ <span id="analyze-copy-icon">📋</span>
364
+ <span id="analyze-copy-text">Copy to Clipboard</span>
365
+ </button>
230
366
  </div>
231
- <button class="btn" onclick="testAnalyze()">Analyze & Download</button>
232
367
  <div class="response" id="analyze-response"></div>
233
368
  </div>
234
369
 
235
- <!-- Info -->
236
- <div class="endpoint-card">
237
- <div class="endpoint-header">
238
- <span class="method get">GET</span>
239
- <span class="endpoint-path">/api/info</span>
240
- </div>
241
- <p class="endpoint-desc">Get project information and statistics</p>
242
- <div class="form-group">
243
- <label>Project Path:</label>
244
- <input type="text" id="info-path" value="." placeholder=".">
245
- </div>
246
- <button class="btn" onclick="testInfo()">Get Info</button>
247
- <div class="response" id="info-response"></div>
248
- </div>
249
-
250
370
  <!-- Execute Bash -->
251
371
  <div class="endpoint-card">
252
372
  <div class="endpoint-header">
253
373
  <span class="method post">POST</span>
254
374
  <span class="endpoint-path">/api/execute</span>
255
375
  </div>
256
- <p class="endpoint-desc">Execute bash script with syntax validation</p>
376
+ <p class="endpoint-desc">⚡ Thực thi bash script với syntax validation</p>
257
377
  <div class="form-group">
258
- <label>Bash Script:</label>
259
- <textarea id="execute-bash" placeholder="echo 'Hello World'&#10;date"></textarea>
378
+ <label>💻 Bash Script:</label>
379
+ <textarea id="execute-bash"
380
+ placeholder="mkdir -p $(dirname &quot;src/test.js&quot;)&#10;cat <<'EOF' > src/test.js&#10;console.log('Hello World');&#10;EOF"></textarea>
260
381
  </div>
261
- <button class="btn" onclick="testExecute()">Execute Script</button>
382
+ <button class="btn" onclick="testExecute()">
383
+ <span>▶️</span>
384
+ <span>Execute Script</span>
385
+ </button>
262
386
  <div class="response" id="execute-response"></div>
263
387
  </div>
264
-
265
- <!-- Clean -->
266
- <div class="endpoint-card">
267
- <div class="endpoint-header">
268
- <span class="method delete">DELETE</span>
269
- <span class="endpoint-path">/api/clean</span>
270
- </div>
271
- <p class="endpoint-desc">Clean output directory</p>
272
- <div class="form-group">
273
- <label>Output Path:</label>
274
- <input type="text" id="clean-output" value="./vg-output" placeholder="./vg-output">
275
- </div>
276
- <button class="btn" onclick="testClean()">Clean Directory</button>
277
- <div class="response" id="clean-response"></div>
278
- </div>
279
388
  </div>
280
389
  </div>
281
390
 
391
+ <div class="toast" id="toast"></div>
392
+
282
393
  <script>
283
394
  const API_BASE = window.location.origin;
395
+ let lastAnalyzeResult = null;
396
+
397
+ // System Prompt
398
+ const SYSTEM_PROMPT = `# VG Coder AI System Prompt
399
+
400
+ ## Command Prefixes
401
+
402
+ ### /ask - Question & Answer Mode
403
+ Khi người dùng hỏi với prefix /ask, họ đang muốn tìm hiểu hoặc được giải thích về một vấn đề.
404
+
405
+ **Response Format:** Markdown
406
+ - Trả lời chi tiết, rõ ràng
407
+ - Sử dụng code blocks, lists, tables khi cần
408
+ - Cung cấp ví dụ minh họa
409
+
410
+ ---
411
+
412
+ ### /plan - Planning Mode
413
+ Khi người dùng muốn lên kế hoạch với prefix /plan, tạo một implementation plan chi tiết.
414
+
415
+ **Response Format:** Markdown checklist với bash commands
416
+ - Chia nhỏ thành các bước cụ thể
417
+ - Mỗi bước có bash command tương ứng
418
+ - Sắp xếp theo thứ tự logic
419
+
420
+ ---
421
+
422
+ ### /fix - Bug Fix Mode
423
+ Khi người dùng cần fix bug với prefix /fix, phân tích lỗi và đưa ra giải pháp.
424
+
425
+ **Response Format:** Markdown + Bash script
426
+ 1. **Phân tích lỗi:** Giải thích nguyên nhân
427
+ 2. **Giải pháp:** Mô tả cách fix
428
+ 3. **Bash script:** Code để fix (nếu cần)
429
+
430
+ ---
431
+
432
+ ### /code - Code Generation Mode
433
+ Khi người dùng hỏi với prefix /code, trả về **BASH SCRIPT DUY NHẤT** để tạo/cập nhật files.
434
+
435
+ ## ⚠️ QUY TẮC BẮT BUỘC
436
+
437
+ ### 1. Chỉ bao gồm files có thay đổi
438
+ - ❌ **KHÔNG** bao gồm files không có sự thay đổi nội dung
439
+ - ✅ Nếu nội dung file sau chỉnh sửa giống 100% bản cũ → **BỎ QUA**
440
+
441
+ ### 2. Format Script Chuẩn
442
+
443
+ **Mỗi file PHẢI theo cú pháp:**
444
+ \`\`\`bash
445
+ mkdir -p $(dirname "path/to/file.ext")
446
+ cat <<'EOF' > path/to/file.ext
447
+ ... toàn bộ nội dung file sau khi chỉnh sửa ...
448
+ EOF
449
+ \`\`\`
450
+
451
+ ### 3. Chi tiết quan trọng
452
+ - ✅ **LUÔN** có \`mkdir -p $(dirname "...")\` trước mỗi file
453
+ - ✅ Sử dụng \`<<'EOF'\` (có dấu nháy đơn) để tránh bash expansion
454
+ - ✅ Ghi đè hoàn toàn file bằng nội dung mới
455
+ - ✅ Tự động tạo file và thư mục cha nếu chưa tồn tại
456
+ - ✅ Đường dẫn giống với file mẫu đính kèm
457
+
458
+ ### 4. Example Output
459
+
460
+ \`\`\`bash
461
+ # Create/Update component file
462
+ mkdir -p $(dirname "src/components/Button/index.tsx")
463
+ cat <<'EOF' > src/components/Button/index.tsx
464
+ import React from 'react';
465
+
466
+ export const Button = () => {
467
+ return <button>Click me</button>;
468
+ };
469
+ EOF
470
+
471
+ # Create/Update styles
472
+ mkdir -p $(dirname "src/components/Button/styles.css")
473
+ cat <<'EOF' > src/components/Button/styles.css
474
+ .button {
475
+ padding: 10px 20px;
476
+ background: blue;
477
+ }
478
+ EOF
479
+ \`\`\`
480
+
481
+ ---
482
+
483
+ ## Integration với VG Coder CLI
484
+
485
+ Bash scripts được generate sẽ được thực thi qua:
486
+ \`\`\`bash
487
+ POST http://localhost:6868/api/execute
488
+ {
489
+ "bash": "mkdir -p $(dirname \\"src/...\\")\\\\ncat <<'EOF' > ..."
490
+ }
491
+ \`\`\`
492
+
493
+ API sẽ:
494
+ 1. ✅ Validate bash syntax trong \`.vg/temp-execute\`
495
+ 2. ✅ Execute tại working directory nếu syntax OK
496
+ 3. ✅ Trả về stdout/stderr/exitCode
497
+ 4. ✅ Auto cleanup temp directory
498
+
499
+ ---
500
+
501
+ ## Best Practices
502
+
503
+ ### DO ✅
504
+ - Luôn dùng \`mkdir -p $(dirname "...")\` trước mỗi file
505
+ - Sử dụng \`<<'EOF'\` để tránh variable expansion
506
+ - Ghi đè toàn bộ nội dung file
507
+ - Chỉ include files có thay đổi thực sự
508
+
509
+ ### DON'T ❌
510
+ - Không tạo file mà không tạo thư mục cha
511
+ - Không dùng \`<<EOF\` (thiếu quotes) nếu có \`$\` trong content
512
+ - Không include files không thay đổi
513
+ - Không dùng relative paths phức tạp`;
514
+
515
+ // Load system prompt on page load
516
+ document.getElementById('prompt-text').textContent = SYSTEM_PROMPT;
517
+
518
+ function toggleSystemPrompt() {
519
+ const content = document.getElementById('system-prompt-content');
520
+ const icon = document.getElementById('toggle-icon');
521
+ content.classList.toggle('open');
522
+ icon.classList.toggle('open');
523
+ }
524
+
525
+ function copySystemPrompt() {
526
+ const copyBtn = event.target.closest('.btn-copy');
527
+ const copyIcon = document.getElementById('copy-icon');
528
+ const copyText = document.getElementById('copy-text');
529
+
530
+ navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
531
+ copyBtn.classList.add('copied');
532
+ copyIcon.textContent = '✓';
533
+ copyText.textContent = 'Copied!';
534
+ showToast('✅ Đã copy System Prompt!', 'success');
535
+
536
+ setTimeout(() => {
537
+ copyBtn.classList.remove('copied');
538
+ copyIcon.textContent = '📋';
539
+ copyText.textContent = 'Copy System Prompt';
540
+ }, 2000);
541
+ }).catch(err => {
542
+ showToast('❌ Lỗi copy: ' + err.message, 'error');
543
+ });
544
+ }
284
545
 
285
546
  function showResponse(elementId, data, isError = false) {
286
547
  const el = document.getElementById(elementId);
@@ -288,88 +549,121 @@
288
549
  el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
289
550
  }
290
551
 
291
- function showLoading(button) {
552
+ function showLoading(button, originalText) {
292
553
  button.disabled = true;
293
554
  button.innerHTML = '<span class="loading"></span> Loading...';
555
+ button.dataset.originalText = originalText;
294
556
  }
295
557
 
296
- function resetButton(button, text) {
558
+ function resetButton(button) {
297
559
  button.disabled = false;
298
- button.textContent = text;
299
- }
300
-
301
- async function testHealth() {
302
- const btn = event.target;
303
- showLoading(btn);
304
- try {
305
- const res = await fetch(`${API_BASE}/health`);
306
- const data = await res.json();
307
- showResponse('health-response', data);
308
- } catch (err) {
309
- showResponse('health-response', { error: err.message }, true);
310
- }
311
- resetButton(btn, 'Test Health Check');
560
+ const originalText = button.dataset.originalText;
561
+ button.innerHTML = originalText;
312
562
  }
313
563
 
314
564
  async function testAnalyze() {
315
- const btn = event.target;
565
+ const btn = event.target.closest('.btn');
316
566
  const path = document.getElementById('analyze-path').value;
317
- const maxTokens = document.getElementById('analyze-tokens').value;
318
-
319
- showLoading(btn);
567
+
568
+ showLoading(btn, '<span>📥</span><span>Download File</span>');
320
569
  try {
321
570
  const res = await fetch(`${API_BASE}/api/analyze`, {
322
571
  method: 'POST',
323
572
  headers: { 'Content-Type': 'application/json' },
324
- body: JSON.stringify({
325
- path,
326
- options: { maxTokens: parseInt(maxTokens) }
327
- })
573
+ body: JSON.stringify({ path })
328
574
  });
329
575
 
330
576
  if (res.ok) {
331
- const blob = await res.blob();
577
+ const text = await res.text();
578
+ lastAnalyzeResult = text;
579
+
580
+ // Download file
581
+ const blob = new Blob([text], { type: 'text/plain' });
332
582
  const url = window.URL.createObjectURL(blob);
333
583
  const a = document.createElement('a');
334
584
  a.href = url;
335
585
  a.download = 'project.txt';
336
586
  a.click();
337
- showResponse('analyze-response', { success: true, message: 'File downloaded!' });
587
+
588
+ showResponse('analyze-response', {
589
+ success: true,
590
+ message: 'File downloaded!',
591
+ files: text.split('\n').filter(l => l.includes('===== FILE:')).length,
592
+ size: (text.length / 1024).toFixed(2) + ' KB'
593
+ });
594
+ showToast('✅ Đã download file!', 'success');
338
595
  } else {
339
596
  const data = await res.json();
340
597
  showResponse('analyze-response', data, true);
598
+ showToast('❌ Lỗi analyze!', 'error');
341
599
  }
342
600
  } catch (err) {
343
601
  showResponse('analyze-response', { error: err.message }, true);
602
+ showToast('❌ Lỗi: ' + err.message, 'error');
603
+ }
604
+ resetButton(btn);
605
+ }
606
+
607
+ async function copyAnalyzeResult() {
608
+ const copyBtn = event.target.closest('.btn-copy');
609
+ const copyIcon = document.getElementById('analyze-copy-icon');
610
+ const copyText = document.getElementById('analyze-copy-text');
611
+
612
+ if (!lastAnalyzeResult) {
613
+ // Fetch if not already analyzed
614
+ const path = document.getElementById('analyze-path').value;
615
+ showLoading(copyBtn, '<span id="analyze-copy-icon">📋</span><span id="analyze-copy-text">Copy to Clipboard</span>');
616
+
617
+ try {
618
+ const res = await fetch(`${API_BASE}/api/analyze`, {
619
+ method: 'POST',
620
+ headers: { 'Content-Type': 'application/json' },
621
+ body: JSON.stringify({ path })
622
+ });
623
+
624
+ if (res.ok) {
625
+ lastAnalyzeResult = await res.text();
626
+ } else {
627
+ showToast('❌ Lỗi analyze!', 'error');
628
+ resetButton(copyBtn);
629
+ return;
630
+ }
631
+ } catch (err) {
632
+ showToast('❌ Lỗi: ' + err.message, 'error');
633
+ resetButton(copyBtn);
634
+ return;
635
+ }
636
+ resetButton(copyBtn);
344
637
  }
345
- resetButton(btn, 'Analyze & Download');
346
- }
347
638
 
348
- async function testInfo() {
349
- const btn = event.target;
350
- const path = document.getElementById('info-path').value;
351
-
352
- showLoading(btn);
639
+ // Copy to clipboard
353
640
  try {
354
- const res = await fetch(`${API_BASE}/api/info?path=${encodeURIComponent(path)}`);
355
- const data = await res.json();
356
- showResponse('info-response', data, !res.ok);
641
+ await navigator.clipboard.writeText(lastAnalyzeResult);
642
+ copyBtn.classList.add('copied');
643
+ copyIcon.textContent = '';
644
+ copyText.textContent = 'Copied!';
645
+ showToast('✅ Đã copy vào clipboard!', 'success');
646
+
647
+ setTimeout(() => {
648
+ copyBtn.classList.remove('copied');
649
+ copyIcon.textContent = '📋';
650
+ copyText.textContent = 'Copy to Clipboard';
651
+ }, 2000);
357
652
  } catch (err) {
358
- showResponse('info-response', { error: err.message }, true);
653
+ showToast(' Lỗi copy: ' + err.message, 'error');
359
654
  }
360
- resetButton(btn, 'Get Info');
361
655
  }
362
656
 
363
657
  async function testExecute() {
364
- const btn = event.target;
658
+ const btn = event.target.closest('.btn');
365
659
  const bash = document.getElementById('execute-bash').value;
366
-
660
+
367
661
  if (!bash.trim()) {
368
- showResponse('execute-response', { error: 'Bash script is required' }, true);
662
+ showToast('⚠️ Vui lòng nhập bash script!', 'error');
369
663
  return;
370
664
  }
371
665
 
372
- showLoading(btn);
666
+ showLoading(btn, '<span>▶️</span><span>Execute Script</span>');
373
667
  try {
374
668
  const res = await fetch(`${API_BASE}/api/execute`, {
375
669
  method: 'POST',
@@ -378,33 +672,29 @@
378
672
  });
379
673
  const data = await res.json();
380
674
  showResponse('execute-response', data, !res.ok || !data.success);
675
+
676
+ if (data.success) {
677
+ showToast('✅ Thực thi thành công!', 'success');
678
+ } else {
679
+ showToast('❌ Thực thi thất bại!', 'error');
680
+ }
381
681
  } catch (err) {
382
682
  showResponse('execute-response', { error: err.message }, true);
683
+ showToast('❌ Lỗi: ' + err.message, 'error');
383
684
  }
384
- resetButton(btn, 'Execute Script');
685
+ resetButton(btn);
385
686
  }
386
687
 
387
- async function testClean() {
388
- const btn = event.target;
389
- const output = document.getElementById('clean-output').value;
390
-
391
- showLoading(btn);
392
- try {
393
- const res = await fetch(`${API_BASE}/api/clean`, {
394
- method: 'DELETE',
395
- headers: { 'Content-Type': 'application/json' },
396
- body: JSON.stringify({ output })
397
- });
398
- const data = await res.json();
399
- showResponse('clean-response', data, !res.ok);
400
- } catch (err) {
401
- showResponse('clean-response', { error: err.message }, true);
402
- }
403
- resetButton(btn, 'Clean Directory');
688
+ function showToast(message, type = 'success') {
689
+ const toast = document.getElementById('toast');
690
+ toast.textContent = message;
691
+ toast.className = `toast ${type}`;
692
+ toast.classList.add('show');
693
+ setTimeout(() => toast.classList.remove('show'), 3000);
404
694
  }
405
695
 
406
- // Check server status periodically
407
- setInterval(async () => {
696
+ // Check server status ONCE on page load
697
+ (async () => {
408
698
  try {
409
699
  const res = await fetch(`${API_BASE}/health`);
410
700
  if (res.ok) {
@@ -415,7 +705,8 @@
415
705
  document.getElementById('status').textContent = '● Server Offline';
416
706
  document.getElementById('status').style.background = '#ef4444';
417
707
  }
418
- }, 5000);
708
+ })();
419
709
  </script>
420
710
  </body>
421
- </html>
711
+
712
+ </html>