vg-coder-cli 1.0.12 → 1.0.14

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.12",
3
+ "version": "1.0.14",
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
  };
@@ -20,7 +20,7 @@
20
20
  }
21
21
 
22
22
  .container {
23
- max-width: 1200px;
23
+ max-width: 1000px;
24
24
  margin: 0 auto;
25
25
  }
26
26
 
@@ -53,41 +53,87 @@
53
53
  margin-top: 10px;
54
54
  }
55
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
+
56
113
  .endpoints {
57
114
  display: grid;
58
- grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
59
115
  gap: 20px;
60
116
  }
61
117
 
62
118
  .endpoint-card {
63
119
  background: white;
64
120
  border-radius: 12px;
65
- padding: 25px;
121
+ padding: 30px;
66
122
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
67
- transition: transform 0.3s ease;
68
- }
69
-
70
- .endpoint-card:hover {
71
- transform: translateY(-5px);
72
123
  }
73
124
 
74
125
  .endpoint-header {
75
126
  display: flex;
76
127
  align-items: center;
77
128
  gap: 15px;
78
- margin-bottom: 15px;
129
+ margin-bottom: 20px;
79
130
  }
80
131
 
81
132
  .method {
82
- padding: 6px 12px;
133
+ padding: 8px 16px;
83
134
  border-radius: 6px;
84
135
  font-weight: bold;
85
- font-size: 0.85em;
86
- }
87
-
88
- .method.get {
89
- background: #3b82f6;
90
- color: white;
136
+ font-size: 0.9em;
91
137
  }
92
138
 
93
139
  .method.post {
@@ -95,74 +141,104 @@
95
141
  color: white;
96
142
  }
97
143
 
98
- .method.delete {
99
- background: #ef4444;
100
- color: white;
101
- }
102
-
103
144
  .endpoint-path {
104
145
  font-family: 'Courier New', monospace;
105
146
  color: #333;
106
- font-size: 1.1em;
147
+ font-size: 1.2em;
148
+ font-weight: 600;
107
149
  }
108
150
 
109
151
  .endpoint-desc {
110
152
  color: #666;
111
- margin-bottom: 15px;
153
+ margin-bottom: 20px;
112
154
  line-height: 1.6;
155
+ font-size: 1.05em;
113
156
  }
114
157
 
115
158
  .form-group {
116
- margin-bottom: 15px;
159
+ margin-bottom: 20px;
117
160
  }
118
161
 
119
162
  .form-group label {
120
163
  display: block;
121
- margin-bottom: 5px;
164
+ margin-bottom: 8px;
122
165
  color: #333;
123
- font-weight: 500;
166
+ font-weight: 600;
167
+ font-size: 1em;
124
168
  }
125
169
 
126
170
  .form-group input,
127
171
  .form-group textarea {
128
172
  width: 100%;
129
- padding: 10px;
173
+ padding: 12px;
130
174
  border: 2px solid #e5e7eb;
131
- border-radius: 6px;
132
- font-size: 0.95em;
175
+ border-radius: 8px;
176
+ font-size: 1em;
133
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;
134
185
  }
135
186
 
136
187
  .form-group textarea {
137
- min-height: 120px;
188
+ min-height: 150px;
138
189
  resize: vertical;
139
190
  }
140
191
 
192
+ .btn-group {
193
+ display: flex;
194
+ gap: 10px;
195
+ margin-top: 20px;
196
+ }
197
+
141
198
  .btn {
142
199
  background: #667eea;
143
200
  color: white;
144
201
  border: none;
145
- padding: 12px 24px;
146
- border-radius: 6px;
202
+ padding: 14px 28px;
203
+ border-radius: 8px;
147
204
  cursor: pointer;
148
205
  font-size: 1em;
149
206
  font-weight: 600;
150
- transition: background 0.3s ease;
207
+ transition: all 0.3s ease;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: 8px;
151
211
  }
152
212
 
153
213
  .btn:hover {
154
214
  background: #5568d3;
215
+ transform: translateY(-2px);
216
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
155
217
  }
156
218
 
157
219
  .btn:disabled {
158
220
  background: #9ca3af;
159
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;
160
236
  }
161
237
 
162
238
  .response {
163
- margin-top: 15px;
164
- padding: 15px;
165
- border-radius: 6px;
239
+ margin-top: 20px;
240
+ padding: 20px;
241
+ border-radius: 8px;
166
242
  background: #f9fafb;
167
243
  border-left: 4px solid #667eea;
168
244
  display: none;
@@ -185,7 +261,7 @@
185
261
  .response pre {
186
262
  margin: 0;
187
263
  font-family: 'Courier New', monospace;
188
- font-size: 0.9em;
264
+ font-size: 0.95em;
189
265
  white-space: pre-wrap;
190
266
  word-wrap: break-word;
191
267
  }
@@ -206,76 +282,36 @@
206
282
  }
207
283
  }
208
284
 
209
- .system-prompt-card {
210
- background: white;
211
- border-radius: 12px;
212
- padding: 25px;
213
- margin-bottom: 30px;
214
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
215
- }
216
-
217
- .system-prompt-header {
218
- display: flex;
219
- justify-content: space-between;
220
- align-items: center;
221
- margin-bottom: 15px;
222
- cursor: pointer;
223
- user-select: none;
224
- }
225
-
226
- .system-prompt-header h2 {
227
- color: #667eea;
228
- font-size: 1.5em;
229
- display: flex;
230
- align-items: center;
231
- gap: 10px;
232
- }
233
-
234
- .toggle-icon {
235
- transition: transform 0.3s ease;
236
- }
237
-
238
- .toggle-icon.open {
239
- transform: rotate(180deg);
240
- }
241
-
242
- .system-prompt-content {
243
- max-height: 0;
244
- overflow: hidden;
245
- transition: max-height 0.3s ease;
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;
246
298
  }
247
299
 
248
- .system-prompt-content.open {
249
- max-height: 2000px;
300
+ .toast.show {
301
+ opacity: 1;
302
+ transform: translateY(0);
250
303
  }
251
304
 
252
- .prompt-text {
253
- background: #f9fafb;
254
- border: 2px solid #e5e7eb;
255
- border-radius: 8px;
256
- padding: 20px;
257
- font-family: 'Courier New', monospace;
258
- font-size: 0.9em;
259
- line-height: 1.6;
260
- white-space: pre-wrap;
261
- max-height: 400px;
262
- overflow-y: auto;
263
- margin-bottom: 15px;
264
- }
265
-
266
- .btn-copy {
305
+ .toast.success {
267
306
  background: #10b981;
268
- display: inline-flex;
269
- align-items: center;
270
- gap: 8px;
271
307
  }
272
308
 
273
- .btn-copy:hover {
274
- background: #059669;
309
+ .toast.error {
310
+ background: #ef4444;
275
311
  }
276
312
 
277
- .btn-copy.copied {
278
- background: #6366f1;
313
+ .toast.info {
314
+ background: #3b82f6;
279
315
  }
280
316
  </style>
281
317
  </head>
@@ -284,7 +320,7 @@
284
320
  <div class="container">
285
321
  <div class="header">
286
322
  <h1>🚀 VG Coder API Dashboard</h1>
287
- <p>Test your API endpoints directly from the browser</p>
323
+ <p>Phân tích dự án thực thi bash scripts</p>
288
324
  <span class="status" id="status">● Server Running</span>
289
325
  </div>
290
326
 
@@ -307,206 +343,56 @@
307
343
  </div>
308
344
 
309
345
  <div class="endpoints">
310
- <!-- Health Check -->
311
- <div class="endpoint-card">
312
- <div class="endpoint-header">
313
- <span class="method get">GET</span>
314
- <span class="endpoint-path">/health</span>
315
- </div>
316
- <p class="endpoint-desc">Check server health status</p>
317
- <button class="btn" onclick="testHealth()">Test Health Check</button>
318
- <div class="response" id="health-response"></div>
319
- </div>
320
-
321
346
  <!-- Analyze -->
322
347
  <div class="endpoint-card">
323
348
  <div class="endpoint-header">
324
349
  <span class="method post">POST</span>
325
350
  <span class="endpoint-path">/api/analyze</span>
326
351
  </div>
327
- <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>
328
353
  <div class="form-group">
329
- <label>Project Path:</label>
354
+ <label>📁 Project Path:</label>
330
355
  <input type="text" id="analyze-path" value="." placeholder=".">
331
356
  </div>
332
- <div class="form-group">
333
- <label>Max Tokens:</label>
334
- <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>
335
366
  </div>
336
- <button class="btn" onclick="testAnalyze()">Analyze & Download</button>
337
367
  <div class="response" id="analyze-response"></div>
338
368
  </div>
339
369
 
340
- <!-- Info -->
341
- <div class="endpoint-card">
342
- <div class="endpoint-header">
343
- <span class="method get">GET</span>
344
- <span class="endpoint-path">/api/info</span>
345
- </div>
346
- <p class="endpoint-desc">Get project information and statistics</p>
347
- <div class="form-group">
348
- <label>Project Path:</label>
349
- <input type="text" id="info-path" value="." placeholder=".">
350
- </div>
351
- <button class="btn" onclick="testInfo()">Get Info</button>
352
- <div class="response" id="info-response"></div>
353
- </div>
354
-
355
370
  <!-- Execute Bash -->
356
371
  <div class="endpoint-card">
357
372
  <div class="endpoint-header">
358
373
  <span class="method post">POST</span>
359
374
  <span class="endpoint-path">/api/execute</span>
360
375
  </div>
361
- <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>
362
377
  <div class="form-group">
363
- <label>Bash Script:</label>
364
- <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>
365
381
  </div>
366
- <button class="btn" onclick="testExecute()">Execute Script</button>
382
+ <button class="btn" onclick="testExecute()">
383
+ <span>▶️</span>
384
+ <span>Execute Script</span>
385
+ </button>
367
386
  <div class="response" id="execute-response"></div>
368
387
  </div>
369
-
370
- <!-- Clean -->
371
- <div class="endpoint-card">
372
- <div class="endpoint-header">
373
- <span class="method delete">DELETE</span>
374
- <span class="endpoint-path">/api/clean</span>
375
- </div>
376
- <p class="endpoint-desc">Clean output directory</p>
377
- <div class="form-group">
378
- <label>Output Path:</label>
379
- <input type="text" id="clean-output" value="./vg-output" placeholder="./vg-output">
380
- </div>
381
- <button class="btn" onclick="testClean()">Clean Directory</button>
382
- <div class="response" id="clean-response"></div>
383
- </div>
384
388
  </div>
385
389
  </div>
386
390
 
391
+ <div class="toast" id="toast"></div>
392
+
387
393
  <script>
388
394
  const API_BASE = window.location.origin;
389
-
390
- function showResponse(elementId, data, isError = false) {
391
- const el = document.getElementById(elementId);
392
- el.className = 'response show ' + (isError ? 'error' : 'success');
393
- el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
394
- }
395
-
396
- function showLoading(button) {
397
- button.disabled = true;
398
- button.innerHTML = '<span class="loading"></span> Loading...';
399
- }
400
-
401
- function resetButton(button, text) {
402
- button.disabled = false;
403
- button.textContent = text;
404
- }
405
-
406
- async function testHealth() {
407
- const btn = event.target;
408
- showLoading(btn);
409
- try {
410
- const res = await fetch(`${API_BASE}/health`);
411
- const data = await res.json();
412
- showResponse('health-response', data);
413
- } catch (err) {
414
- showResponse('health-response', { error: err.message }, true);
415
- }
416
- resetButton(btn, 'Test Health Check');
417
- }
418
-
419
- async function testAnalyze() {
420
- const btn = event.target;
421
- const path = document.getElementById('analyze-path').value;
422
- const maxTokens = document.getElementById('analyze-tokens').value;
423
-
424
- showLoading(btn);
425
- try {
426
- const res = await fetch(`${API_BASE}/api/analyze`, {
427
- method: 'POST',
428
- headers: { 'Content-Type': 'application/json' },
429
- body: JSON.stringify({
430
- path,
431
- options: { maxTokens: parseInt(maxTokens) }
432
- })
433
- });
434
-
435
- if (res.ok) {
436
- const blob = await res.blob();
437
- const url = window.URL.createObjectURL(blob);
438
- const a = document.createElement('a');
439
- a.href = url;
440
- a.download = 'project.txt';
441
- a.click();
442
- showResponse('analyze-response', { success: true, message: 'File downloaded!' });
443
- } else {
444
- const data = await res.json();
445
- showResponse('analyze-response', data, true);
446
- }
447
- } catch (err) {
448
- showResponse('analyze-response', { error: err.message }, true);
449
- }
450
- resetButton(btn, 'Analyze & Download');
451
- }
452
-
453
- async function testInfo() {
454
- const btn = event.target;
455
- const path = document.getElementById('info-path').value;
456
-
457
- showLoading(btn);
458
- try {
459
- const res = await fetch(`${API_BASE}/api/info?path=${encodeURIComponent(path)}`);
460
- const data = await res.json();
461
- showResponse('info-response', data, !res.ok);
462
- } catch (err) {
463
- showResponse('info-response', { error: err.message }, true);
464
- }
465
- resetButton(btn, 'Get Info');
466
- }
467
-
468
- async function testExecute() {
469
- const btn = event.target;
470
- const bash = document.getElementById('execute-bash').value;
471
-
472
- if (!bash.trim()) {
473
- showResponse('execute-response', { error: 'Bash script is required' }, true);
474
- return;
475
- }
476
-
477
- showLoading(btn);
478
- try {
479
- const res = await fetch(`${API_BASE}/api/execute`, {
480
- method: 'POST',
481
- headers: { 'Content-Type': 'application/json' },
482
- body: JSON.stringify({ bash })
483
- });
484
- const data = await res.json();
485
- showResponse('execute-response', data, !res.ok || !data.success);
486
- } catch (err) {
487
- showResponse('execute-response', { error: err.message }, true);
488
- }
489
- resetButton(btn, 'Execute Script');
490
- }
491
-
492
- async function testClean() {
493
- const btn = event.target;
494
- const output = document.getElementById('clean-output').value;
495
-
496
- showLoading(btn);
497
- try {
498
- const res = await fetch(`${API_BASE}/api/clean`, {
499
- method: 'DELETE',
500
- headers: { 'Content-Type': 'application/json' },
501
- body: JSON.stringify({ output })
502
- });
503
- const data = await res.json();
504
- showResponse('clean-response', data, !res.ok);
505
- } catch (err) {
506
- showResponse('clean-response', { error: err.message }, true);
507
- }
508
- resetButton(btn, 'Clean Directory');
509
- }
395
+ let lastAnalyzeResult = null;
510
396
 
511
397
  // System Prompt
512
398
  const SYSTEM_PROMPT = `# VG Coder AI System Prompt
@@ -645,6 +531,7 @@ API sẽ:
645
531
  copyBtn.classList.add('copied');
646
532
  copyIcon.textContent = '✓';
647
533
  copyText.textContent = 'Copied!';
534
+ showToast('✅ Đã copy System Prompt!', 'success');
648
535
 
649
536
  setTimeout(() => {
650
537
  copyBtn.classList.remove('copied');
@@ -652,20 +539,185 @@ API sẽ:
652
539
  copyText.textContent = 'Copy System Prompt';
653
540
  }, 2000);
654
541
  }).catch(err => {
655
- alert('Failed to copy: ' + err.message);
542
+ showToast(' Lỗi copy: ' + err.message, 'error');
656
543
  });
657
544
  }
658
545
 
546
+ function showResponse(elementId, data, isError = false) {
547
+ const el = document.getElementById(elementId);
548
+ el.className = 'response show ' + (isError ? 'error' : 'success');
549
+ el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
550
+ }
551
+
552
+ function showLoading(button, originalText) {
553
+ button.disabled = true;
554
+ button.innerHTML = '<span class="loading"></span> Loading...';
555
+ button.dataset.originalText = originalText;
556
+ }
557
+
558
+ function resetButton(button) {
559
+ button.disabled = false;
560
+ const originalText = button.dataset.originalText;
561
+ button.innerHTML = originalText;
562
+ }
563
+
564
+ async function testAnalyze() {
565
+ const btn = event.target.closest('.btn');
566
+ const path = document.getElementById('analyze-path').value;
567
+
568
+ showLoading(btn, '<span>📥</span><span>Download File</span>');
569
+ try {
570
+ const res = await fetch(`${API_BASE}/api/analyze`, {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify({ path })
574
+ });
575
+
576
+ if (res.ok) {
577
+ const text = await res.text();
578
+ lastAnalyzeResult = text;
579
+
580
+ // Download file
581
+ const blob = new Blob([text], { type: 'text/plain' });
582
+ const url = window.URL.createObjectURL(blob);
583
+ const a = document.createElement('a');
584
+ a.href = url;
585
+ a.download = 'project.txt';
586
+ a.click();
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');
595
+ } else {
596
+ const data = await res.json();
597
+ showResponse('analyze-response', data, true);
598
+ showToast('❌ Lỗi analyze!', 'error');
599
+ }
600
+ } catch (err) {
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);
637
+ }
638
+
639
+ // Copy to clipboard using ClipboardItem (like reference code)
640
+ try {
641
+ const blob = new Blob([lastAnalyzeResult], { type: 'text/plain' });
642
+ const item = new ClipboardItem({ 'text/plain': blob });
643
+ await navigator.clipboard.write([item]);
644
+
645
+ copyBtn.classList.add('copied');
646
+ copyIcon.textContent = '✓';
647
+ copyText.textContent = 'Copied!';
648
+ showToast('✅ Đã copy project.txt vào clipboard!', 'success');
649
+
650
+ setTimeout(() => {
651
+ copyBtn.classList.remove('copied');
652
+ copyIcon.textContent = '📋';
653
+ copyText.textContent = 'Copy to Clipboard';
654
+ }, 2000);
655
+ } catch (err) {
656
+ // Fallback to writeText if ClipboardItem fails
657
+ try {
658
+ await navigator.clipboard.writeText(lastAnalyzeResult);
659
+ copyBtn.classList.add('copied');
660
+ copyIcon.textContent = '✓';
661
+ copyText.textContent = 'Copied!';
662
+ showToast('✅ Đã copy project.txt vào clipboard!', 'success');
663
+
664
+ setTimeout(() => {
665
+ copyBtn.classList.remove('copied');
666
+ copyIcon.textContent = '📋';
667
+ copyText.textContent = 'Copy to Clipboard';
668
+ }, 2000);
669
+ } catch (fallbackErr) {
670
+ showToast('❌ Lỗi copy: ' + fallbackErr.message, 'error');
671
+ }
672
+ }
673
+ }
674
+
675
+ async function testExecute() {
676
+ const btn = event.target.closest('.btn');
677
+ const bash = document.getElementById('execute-bash').value;
678
+
679
+ if (!bash.trim()) {
680
+ showToast('⚠️ Vui lòng nhập bash script!', 'error');
681
+ return;
682
+ }
683
+
684
+ showLoading(btn, '<span>▶️</span><span>Execute Script</span>');
685
+ try {
686
+ const res = await fetch(`${API_BASE}/api/execute`, {
687
+ method: 'POST',
688
+ headers: { 'Content-Type': 'application/json' },
689
+ body: JSON.stringify({ bash })
690
+ });
691
+ const data = await res.json();
692
+ showResponse('execute-response', data, !res.ok || !data.success);
693
+
694
+ if (data.success) {
695
+ showToast('✅ Thực thi thành công!', 'success');
696
+ } else {
697
+ showToast('❌ Thực thi thất bại!', 'error');
698
+ }
699
+ } catch (err) {
700
+ showResponse('execute-response', { error: err.message }, true);
701
+ showToast('❌ Lỗi: ' + err.message, 'error');
702
+ }
703
+ resetButton(btn);
704
+ }
705
+
706
+ function showToast(message, type = 'success') {
707
+ const toast = document.getElementById('toast');
708
+ toast.textContent = message;
709
+ toast.className = `toast ${type}`;
710
+ toast.classList.add('show');
711
+ setTimeout(() => toast.classList.remove('show'), 3000);
712
+ }
713
+
659
714
  // Check server status ONCE on page load
660
715
  (async () => {
661
716
  try {
662
717
  const res = await fetch(`${API_BASE}/health`);
663
718
  if (res.ok) {
664
- const data = await res.json();
665
719
  document.getElementById('status').textContent = '● Server Running';
666
720
  document.getElementById('status').style.background = '#10b981';
667
- // Auto-show health check result
668
- showResponse('health-response', data);
669
721
  }
670
722
  } catch {
671
723
  document.getElementById('status').textContent = '● Server Offline';