vg-coder-cli 2.0.7 → 2.0.8

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.
@@ -8,435 +8,39 @@
8
8
  <meta name="apple-mobile-web-app-capable" content="yes">
9
9
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
10
10
  <title>VG Coder API Dashboard</title>
11
- <style>
12
- :root {
13
- --ios-bg: #F2F2F7;
14
- --ios-card: #FFFFFF;
15
- --ios-blue: #007AFF;
16
- --ios-green: #34C759;
17
- --ios-red: #FF3B30;
18
- --ios-gray: #8E8E93;
19
- --ios-gray-light: #E5E5EA;
20
- --ios-input-bg: #F2F2F7;
21
- --ios-separator: #C6C6C8;
22
- --safe-area-top: env(safe-area-inset-top);
23
- --safe-area-bottom: env(safe-area-inset-bottom);
24
- }
25
-
26
- * {
27
- margin: 0;
28
- padding: 0;
29
- box-sizing: border-box;
30
- -webkit-tap-highlight-color: transparent;
31
- }
32
-
33
- body {
34
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
35
- background-color: var(--ios-bg);
36
- color: #000;
37
- min-height: 100vh;
38
- padding: 0;
39
- padding-bottom: calc(20px + var(--safe-area-bottom));
40
- -webkit-font-smoothing: antialiased;
41
- }
42
-
43
- .container {
44
- max-width: 800px;
45
- margin: 0 auto;
46
- padding: 20px;
47
- padding-top: calc(20px + var(--safe-area-top));
48
- }
49
-
50
- /* Header iOS Style */
51
- .header {
52
- text-align: left;
53
- margin-bottom: 24px;
54
- padding: 0 4px;
55
- }
56
-
57
- .header h1 {
58
- color: #000;
59
- font-size: 34px;
60
- font-weight: 700;
61
- letter-spacing: -0.5px;
62
- margin-bottom: 8px;
63
- }
64
-
65
- .header p {
66
- color: var(--ios-gray);
67
- font-size: 17px;
68
- line-height: 1.4;
69
- margin-bottom: 12px;
70
- }
71
-
72
- .status {
73
- display: inline-flex;
74
- align-items: center;
75
- padding: 6px 12px;
76
- background: rgba(52, 199, 89, 0.15);
77
- color: var(--ios-green);
78
- border-radius: 20px;
79
- font-size: 13px;
80
- font-weight: 600;
81
- margin-top: 5px;
82
- }
83
-
84
- /* Card Style (Inset Grouped) */
85
- .system-prompt-card,
86
- .endpoint-card {
87
- background: var(--ios-card);
88
- border-radius: 16px;
89
- padding: 20px;
90
- margin-bottom: 20px;
91
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
92
- overflow: hidden;
93
- position: relative;
94
- }
95
-
96
- /* System Prompt */
97
- .system-prompt-header {
98
- display: flex;
99
- justify-content: space-between;
100
- align-items: center;
101
- cursor: pointer;
102
- padding: 4px 0;
103
- }
104
-
105
- .system-prompt-header h2 {
106
- color: #000;
107
- font-size: 20px;
108
- font-weight: 600;
109
- display: flex;
110
- align-items: center;
111
- gap: 10px;
112
- }
113
-
114
- .toggle-icon {
115
- color: var(--ios-gray);
116
- font-size: 14px;
117
- transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
118
- }
119
-
120
- .toggle-icon.open {
121
- transform: rotate(180deg);
122
- }
123
-
124
- .system-prompt-content {
125
- max-height: 0;
126
- overflow: hidden;
127
- transition: max-height 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.3s;
128
- opacity: 0;
129
- }
130
-
131
- .system-prompt-content.open {
132
- max-height: 2000px;
133
- opacity: 1;
134
- margin-top: 16px;
135
- }
136
-
137
- .prompt-text {
138
- background: var(--ios-input-bg);
139
- border: none;
140
- border-radius: 12px;
141
- padding: 16px;
142
- font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
143
- font-size: 13px;
144
- line-height: 1.5;
145
- white-space: pre-wrap;
146
- max-height: 300px;
147
- overflow-y: auto;
148
- margin-bottom: 16px;
149
- color: #333;
150
- -webkit-overflow-scrolling: touch;
151
- }
152
-
153
- /* Endpoints */
154
- .endpoint-header {
155
- display: flex;
156
- align-items: center;
157
- flex-wrap: wrap;
158
- gap: 10px;
159
- margin-bottom: 12px;
160
- border-bottom: 0.5px solid var(--ios-separator);
161
- padding-bottom: 12px;
162
- }
163
-
164
- .method {
165
- padding: 4px 10px;
166
- border-radius: 6px;
167
- font-weight: 700;
168
- font-size: 12px;
169
- text-transform: uppercase;
170
- letter-spacing: 0.5px;
171
- }
172
-
173
- .method.post {
174
- background: var(--ios-green);
175
- color: white;
176
- }
177
-
178
- .endpoint-path {
179
- font-family: 'SF Mono', 'Menlo', monospace;
180
- color: #000;
181
- font-size: 16px;
182
- font-weight: 600;
183
- word-break: break-all;
184
- }
185
-
186
- .endpoint-desc {
187
- color: var(--ios-gray);
188
- margin-bottom: 20px;
189
- font-size: 15px;
190
- line-height: 1.4;
191
- }
192
-
193
- /* Forms */
194
- .form-group {
195
- margin-bottom: 20px;
196
- }
197
-
198
- .form-group label {
199
- display: block;
200
- margin-bottom: 8px;
201
- color: #000;
202
- font-weight: 500;
203
- font-size: 14px;
204
- text-transform: uppercase;
205
- letter-spacing: 0.3px;
206
- opacity: 0.6;
207
- }
208
-
209
- .form-group input,
210
- .form-group textarea {
211
- width: 100%;
212
- padding: 14px;
213
- background: var(--ios-input-bg);
214
- border: none;
215
- border-radius: 12px;
216
- font-size: 17px;
217
- /* Prevent zoom on iOS */
218
- font-family: 'SF Mono', 'Menlo', monospace;
219
- color: #000;
220
- transition: background 0.2s;
221
- appearance: none;
222
- -webkit-appearance: none;
223
- }
224
-
225
- .form-group input:focus,
226
- .form-group textarea:focus {
227
- outline: none;
228
- background: #E5E5EA;
229
- /* Slightly darker on focus */
230
- }
231
-
232
- .form-group textarea {
233
- min-height: 120px;
234
- resize: none;
235
- }
236
-
237
- /* Buttons */
238
- .btn-group {
239
- display: flex;
240
- gap: 12px;
241
- margin-top: 24px;
242
- flex-direction: column;
243
- /* Stack on mobile by default */
244
- }
245
-
246
- @media (min-width: 640px) {
247
- .btn-group {
248
- flex-direction: row;
249
- }
250
- }
251
-
252
- .btn {
253
- background: var(--ios-blue);
254
- color: white;
255
- border: none;
256
- padding: 14px 20px;
257
- border-radius: 12px;
258
- cursor: pointer;
259
- font-size: 17px;
260
- font-weight: 600;
261
- transition: transform 0.1s, opacity 0.2s;
262
- display: flex;
263
- align-items: center;
264
- justify-content: center;
265
- gap: 8px;
266
- width: 100%;
267
- position: relative;
268
- overflow: hidden;
269
- }
270
-
271
- .btn:active {
272
- transform: scale(0.98);
273
- opacity: 0.9;
274
- }
275
-
276
- .btn:disabled {
277
- background: var(--ios-gray-light);
278
- color: var(--ios-gray);
279
- cursor: not-allowed;
280
- transform: none;
281
- }
282
-
283
- .btn-copy {
284
- background: rgba(0, 122, 255, 0.1);
285
- color: var(--ios-blue);
286
- }
287
-
288
- /* Specific override for prompt copy button to look nice */
289
- .system-prompt-content .btn-copy {
290
- margin-top: 8px;
291
- }
292
-
293
- .btn-copy:active {
294
- background: rgba(0, 122, 255, 0.2);
295
- }
296
-
297
- .btn-copy.copied {
298
- background: var(--ios-green);
299
- color: white;
300
- }
301
-
302
- /* Response Area */
303
- .response {
304
- margin-top: 20px;
305
- padding: 16px;
306
- border-radius: 12px;
307
- background: #1C1C1E;
308
- /* Dark background for code contrast */
309
- color: #fff;
310
- display: none;
311
- font-size: 13px;
312
- overflow-x: auto;
313
- border-left: none;
314
- /* Removed the side border style */
315
- }
316
-
317
- .response.show {
318
- display: block;
319
- animation: fadeIn 0.3s ease;
320
- }
321
-
322
- .response.success {
323
- border: 1px solid rgba(52, 199, 89, 0.3);
324
- }
325
-
326
- .response.error {
327
- border: 1px solid rgba(255, 59, 48, 0.3);
328
- background: #2C1515;
329
- }
330
-
331
- .response pre {
332
- margin: 0;
333
- font-family: 'SF Mono', 'Menlo', monospace;
334
- white-space: pre-wrap;
335
- word-wrap: break-word;
336
- }
337
-
338
- @keyframes fadeIn {
339
- from {
340
- opacity: 0;
341
- transform: translateY(5px);
342
- }
343
-
344
- to {
345
- opacity: 1;
346
- transform: translateY(0);
347
- }
348
- }
349
-
350
- /* Loading Spinner */
351
- .loading {
352
- display: inline-block;
353
- width: 18px;
354
- height: 18px;
355
- border: 2px solid rgba(255, 255, 255, 0.3);
356
- border-radius: 50%;
357
- border-top-color: currentColor;
358
- animation: spin 0.8s linear infinite;
359
- }
360
-
361
- @keyframes spin {
362
- to {
363
- transform: rotate(360deg);
364
- }
365
- }
366
-
367
- /* Toast - iOS Notification Style */
368
- .toast {
369
- position: fixed;
370
- top: 10px;
371
- left: 50%;
372
- transform: translateX(-50%) translateY(-100px);
373
- padding: 12px 20px;
374
- border-radius: 25px;
375
- /* Pill shape */
376
- color: #000;
377
- background: rgba(255, 255, 255, 0.85);
378
- backdrop-filter: blur(20px);
379
- -webkit-backdrop-filter: blur(20px);
380
- font-weight: 500;
381
- font-size: 15px;
382
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
383
- transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
384
- z-index: 2000;
385
- display: flex;
386
- align-items: center;
387
- gap: 10px;
388
- width: auto;
389
- max-width: 90%;
390
- white-space: nowrap;
391
- opacity: 0;
392
- pointer-events: none;
393
- }
394
-
395
- .toast.show {
396
- transform: translateX(-50%) translateY(calc(var(--safe-area-top)));
397
- opacity: 1;
398
- }
399
-
400
- /* Icons in toast */
401
- .toast::before {
402
- content: '';
403
- display: block;
404
- width: 20px;
405
- height: 20px;
406
- border-radius: 50%;
407
- flex-shrink: 0;
408
- }
409
-
410
- .toast.success::before {
411
- background: var(--ios-green) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='white'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M5 13l4 4L19 7'/%3E%3C/svg%3E") center/12px no-repeat;
412
- }
413
-
414
- .toast.error::before {
415
- background: var(--ios-red) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='white'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E") center/12px no-repeat;
416
- }
417
-
418
- .toast.info::before {
419
- background: var(--ios-blue);
420
- }
421
- </style>
11
+ <link rel="stylesheet" href="/dashboard.css">
12
+ <script>
13
+ // Pre-load theme to prevent flash
14
+ (function() {
15
+ const savedTheme = localStorage.getItem('theme') || 'light';
16
+ document.documentElement.setAttribute('data-theme', savedTheme);
17
+ })();
18
+ </script>
422
19
  </head>
423
20
 
424
21
  <body>
425
22
  <div class="container">
426
23
  <div class="header">
427
- <span class="status" id="status">● Server Starting...</span>
428
- <div style="height: 10px;"></div>
429
- <h1>VG Coder</h1>
430
- <p>API Dashboard & Automation</p>
24
+ <div class="header-content">
25
+ <span class="status" id="status">● Server Starting...</span>
26
+ <div style="height: 10px;"></div>
27
+ <h1>VG Coder</h1>
28
+ <p>API Dashboard & Automation</p>
29
+ </div>
30
+ <button class="theme-toggle" id="theme-toggle" title="Toggle Dark Mode">
31
+ <span id="theme-icon">🌙</span>
32
+ </button>
431
33
  </div>
432
34
 
433
35
  <!-- System Prompt Section -->
434
36
  <div class="system-prompt-card">
435
37
  <div class="system-prompt-header" onclick="toggleSystemPrompt()">
436
- <h2>
437
- <span>📝</span>
438
- <span>System Prompt</span>
439
- </h2>
38
+ <div class="header-title-group">
39
+ <button class="btn-icon-head" onclick="copySystemPromptFromHeader(event)" title="Copy System Prompt">
40
+ 📋
41
+ </button>
42
+ <h2>System Prompt</h2>
43
+ </div>
440
44
  <span class="toggle-icon" id="toggle-icon">▼</span>
441
45
  </div>
442
46
  <div class="system-prompt-content" id="system-prompt-content">
@@ -452,8 +56,15 @@
452
56
  <!-- Analyze -->
453
57
  <div class="endpoint-card">
454
58
  <div class="endpoint-header">
455
- <span class="method post">POST</span>
456
- <span class="endpoint-path">/api/analyze</span>
59
+ <!-- Left side: Method + Path -->
60
+ <div class="endpoint-title-group">
61
+ <span class="method post">POST</span>
62
+ <span class="endpoint-path">/api/analyze</span>
63
+ </div>
64
+ <!-- Right side: Download Icon -->
65
+ <button class="btn-icon-head" onclick="testAnalyze()" title="Download Project Source">
66
+ 📥
67
+ </button>
457
68
  </div>
458
69
  <p class="endpoint-desc">Phân tích dự án và lấy toàn bộ source code.</p>
459
70
  <div class="form-group">
@@ -461,18 +72,11 @@
461
72
  <input type="text" id="analyze-path" value="." placeholder="Project path (e.g. .)">
462
73
  </div>
463
74
  <div class="btn-group">
464
- <button class="btn" onclick="testAnalyze()">
465
- <span>📥</span>
466
- <span>Download</span>
467
- </button>
75
+ <!-- Big Download button removed as it's now in the header -->
468
76
  <button class="btn btn-copy" onclick="copyAnalyzeResult()">
469
77
  <span id="analyze-copy-icon">📋</span>
470
78
  <span id="analyze-copy-text">Copy Text</span>
471
79
  </button>
472
- <button class="btn btn-copy" onclick="copyAnalyzeAsFile()">
473
- <span id="analyze-file-icon">📄</span>
474
- <span id="analyze-file-text">Copy as File</span>
475
- </button>
476
80
  </div>
477
81
  <div class="response" id="analyze-response"></div>
478
82
  </div>
@@ -480,8 +84,11 @@
480
84
  <!-- Execute Bash -->
481
85
  <div class="endpoint-card">
482
86
  <div class="endpoint-header">
483
- <span class="method post">POST</span>
484
- <span class="endpoint-path">/api/execute</span>
87
+ <div class="endpoint-title-group">
88
+ <span class="method post">POST</span>
89
+ <span class="endpoint-path">/api/execute</span>
90
+ </div>
91
+ <!-- Could add an execute icon here later if needed -->
485
92
  </div>
486
93
  <p class="endpoint-desc">Thực thi bash script với syntax validation.</p>
487
94
  <div class="form-group">
@@ -506,462 +113,7 @@
506
113
 
507
114
  <div class="toast" id="toast"></div>
508
115
 
509
- <script>
510
- const API_BASE = window.location.origin;
511
- let lastAnalyzeResult = null;
512
-
513
- // System Prompt
514
- const SYSTEM_PROMPT = `# VG Coder AI System Prompt
515
-
516
- ## Command Prefixes
517
-
518
- ### /ask - Question & Answer Mode
519
- 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 đề.
520
-
521
- **Response Format:** Markdown
522
- - Trả lời chi tiết, rõ ràng
523
- - Sử dụng code blocks, lists, tables khi cần
524
- - Cung cấp ví dụ minh họa
525
-
526
- ---
527
-
528
- ### /plan - Planning Mode
529
- 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.
530
-
531
- **Response Format:** Markdown checklist với bash commands
532
- - Chia nhỏ thành các bước cụ thể
533
- - Mỗi bước có bash command tương ứng
534
- - Sắp xếp theo thứ tự logic
535
-
536
- ---
537
-
538
- ### /fix - Bug Fix Mode
539
- 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.
540
-
541
- **Response Format:** Markdown + Bash script
542
- 1. **Phân tích lỗi:** Giải thích nguyên nhân
543
- 2. **Giải pháp:** Mô tả cách fix
544
- 3. **Bash script:** Code để fix (nếu cần)
545
-
546
- ---
547
-
548
- ### /code - Code Generation Mode
549
- 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.
550
-
551
- ## ⚠️ QUY TẮC BẮT BUỘC
552
-
553
- ### 1. Chỉ bao gồm files có thay đổi
554
- - ❌ **KHÔNG** bao gồm files không có sự thay đổi nội dung
555
- - ✅ Nếu nội dung file sau chỉnh sửa giống 100% bản cũ → **BỎ QUA**
556
-
557
- ### 2. Format Script Chuẩn
558
-
559
- **Mỗi file PHẢI theo cú pháp:**
560
- \`\`\`bash
561
- mkdir -p $(dirname "path/to/file.ext")
562
- cat <<'EOF' > path/to/file.ext
563
- ... toàn bộ nội dung file sau khi chỉnh sửa ...
564
- EOF
565
- \`\`\`
566
-
567
- ### 3. Chi tiết quan trọng
568
- - ✅ **LUÔN** có \`mkdir -p $(dirname "...")\` trước mỗi file
569
- - ✅ Sử dụng \`<<'EOF'\` (có dấu nháy đơn) để tránh bash expansion
570
- - ✅ Ghi đè hoàn toàn file bằng nội dung mới
571
- - ✅ Tự động tạo file và thư mục cha nếu chưa tồn tại
572
- - ✅ Đường dẫn giống với file mẫu đính kèm
573
-
574
- ### 4. Example Output
575
-
576
- \`\`\`bash
577
- # Create/Update component file
578
- mkdir -p $(dirname "src/components/Button/index.tsx")
579
- cat <<'EOF' > src/components/Button/index.tsx
580
- import React from 'react';
581
-
582
- export const Button = () => {
583
- return <button>Click me</button>;
584
- };
585
- EOF
586
-
587
- # Create/Update styles
588
- mkdir -p $(dirname "src/components/Button/styles.css")
589
- cat <<'EOF' > src/components/Button/styles.css
590
- .button {
591
- padding: 10px 20px;
592
- background: blue;
593
- }
594
- EOF
595
- \`\`\`
596
-
597
- ---
598
-
599
- ## Integration với VG Coder CLI
600
-
601
- Bash scripts được generate sẽ được thực thi qua:
602
- \`\`\`bash
603
- POST http://localhost:6868/api/execute
604
- {
605
- "bash": "mkdir -p $(dirname \\"src/...\\")\\\\ncat <<'EOF' > ..."
606
- }
607
- \`\`\`
608
-
609
- API sẽ:
610
- 1. ✅ Validate bash syntax trong \`.vg/temp-execute\`
611
- 2. ✅ Execute tại working directory nếu syntax OK
612
- 3. ✅ Trả về stdout/stderr/exitCode
613
- 4. ✅ Auto cleanup temp directory
614
-
615
- ---
616
-
617
- ## Best Practices
618
-
619
- ### DO ✅
620
- - Luôn dùng \`mkdir -p $(dirname "...")\` trước mỗi file
621
- - Sử dụng \`<<'EOF'\` để tránh variable expansion
622
- - Ghi đè toàn bộ nội dung file
623
- - Chỉ include files có thay đổi thực sự
624
-
625
- ### DON'T ❌
626
- - Không tạo file mà không tạo thư mục cha
627
- - Không dùng \`<<EOF\` (thiếu quotes) nếu có \`$\` trong content
628
- - Không include files không thay đổi
629
- - Không dùng relative paths phức tạp`;
630
-
631
- // Load system prompt on page load
632
- document.getElementById('prompt-text').textContent = SYSTEM_PROMPT;
633
-
634
- function toggleSystemPrompt() {
635
- const content = document.getElementById('system-prompt-content');
636
- const icon = document.getElementById('toggle-icon');
637
- content.classList.toggle('open');
638
- icon.classList.toggle('open');
639
- }
640
-
641
- function copySystemPrompt() {
642
- const copyBtn = event.target.closest('.btn-copy');
643
- const copyIcon = document.getElementById('copy-icon');
644
- const copyText = document.getElementById('copy-text');
645
-
646
- navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
647
- copyBtn.classList.add('copied');
648
- copyIcon.textContent = '✓';
649
- copyText.textContent = 'Copied';
650
- showToast('Đã copy System Prompt', 'success');
651
-
652
- setTimeout(() => {
653
- copyBtn.classList.remove('copied');
654
- copyIcon.textContent = '📋';
655
- copyText.textContent = 'Copy System Prompt';
656
- }, 2000);
657
- }).catch(err => {
658
- showToast('Lỗi copy: ' + err.message, 'error');
659
- });
660
- }
661
-
662
- function showResponse(elementId, data, isError = false) {
663
- const el = document.getElementById(elementId);
664
- el.className = 'response show ' + (isError ? 'error' : 'success');
665
- el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
666
- }
667
-
668
- function showLoading(button, originalText) {
669
- button.disabled = true;
670
- button.innerHTML = '<span class="loading"></span>';
671
- button.dataset.originalText = originalText;
672
- }
673
-
674
- function resetButton(button) {
675
- button.disabled = false;
676
- const originalText = button.dataset.originalText;
677
- button.innerHTML = originalText;
678
- }
679
-
680
- async function testAnalyze() {
681
- const btn = event.target.closest('.btn');
682
- const path = document.getElementById('analyze-path').value;
683
-
684
- showLoading(btn, btn.innerHTML);
685
- try {
686
- const res = await fetch(`${API_BASE}/api/analyze`, {
687
- method: 'POST',
688
- headers: { 'Content-Type': 'application/json' },
689
- body: JSON.stringify({ path })
690
- });
691
-
692
- if (res.ok) {
693
- const text = await res.text();
694
- lastAnalyzeResult = text;
695
-
696
- // Download file
697
- const blob = new Blob([text], { type: 'text/plain' });
698
- const url = window.URL.createObjectURL(blob);
699
- const a = document.createElement('a');
700
- a.href = url;
701
- a.download = 'project.txt';
702
- a.click();
703
-
704
- showResponse('analyze-response', {
705
- success: true,
706
- message: 'File downloaded!',
707
- files: text.split('\n').filter(l => l.includes('===== FILE:')).length,
708
- size: (text.length / 1024).toFixed(2) + ' KB'
709
- });
710
- showToast('Đã download file', 'success');
711
- } else {
712
- const data = await res.json();
713
- showResponse('analyze-response', data, true);
714
- showToast('Lỗi analyze', 'error');
715
- }
716
- } catch (err) {
717
- showResponse('analyze-response', { error: err.message }, true);
718
- showToast('Lỗi: ' + err.message, 'error');
719
- }
720
- resetButton(btn);
721
- }
722
-
723
- async function copyAnalyzeResult() {
724
- const copyBtn = event.target.closest('.btn-copy');
725
- const copyIcon = document.getElementById('analyze-copy-icon');
726
- const copyText = document.getElementById('analyze-copy-text');
727
-
728
- if (!lastAnalyzeResult) {
729
- // Fetch if not already analyzed
730
- const path = document.getElementById('analyze-path').value;
731
- showLoading(copyBtn, copyBtn.innerHTML);
732
-
733
- try {
734
- const res = await fetch(`${API_BASE}/api/analyze`, {
735
- method: 'POST',
736
- headers: { 'Content-Type': 'application/json' },
737
- body: JSON.stringify({ path })
738
- });
739
-
740
- if (res.ok) {
741
- lastAnalyzeResult = await res.text();
742
- } else {
743
- showToast('Lỗi analyze', 'error');
744
- resetButton(copyBtn);
745
- return;
746
- }
747
- } catch (err) {
748
- showToast('Lỗi: ' + err.message, 'error');
749
- resetButton(copyBtn);
750
- return;
751
- }
752
- resetButton(copyBtn);
753
- }
754
-
755
- // Copy to clipboard using ClipboardItem
756
- try {
757
- const blob = new Blob([lastAnalyzeResult], { type: 'text/plain' });
758
- const item = new ClipboardItem({ 'text/plain': blob });
759
- await navigator.clipboard.write([item]);
760
-
761
- copyBtn.classList.add('copied');
762
- copyIcon.textContent = '✓';
763
- copyText.textContent = 'Copied';
764
- showToast('Đã copy project.txt', 'success');
765
-
766
- setTimeout(() => {
767
- copyBtn.classList.remove('copied');
768
- copyIcon.textContent = '📋';
769
- copyText.textContent = 'Copy Text';
770
- }, 2000);
771
- } catch (err) {
772
- try {
773
- await navigator.clipboard.writeText(lastAnalyzeResult);
774
- copyBtn.classList.add('copied');
775
- copyIcon.textContent = '✓';
776
- copyText.textContent = 'Copied';
777
- showToast('Đã copy project.txt', 'success');
778
-
779
- setTimeout(() => {
780
- copyBtn.classList.remove('copied');
781
- copyIcon.textContent = '📋';
782
- copyText.textContent = 'Copy Text';
783
- }, 2000);
784
- } catch (fallbackErr) {
785
- showToast('Lỗi copy: ' + fallbackErr.message, 'error');
786
- }
787
- }
788
- }
789
-
790
- async function copyAsFile(filename, content) {
791
- const blob = new Blob([content], {
792
- type: "application/octet-stream"
793
- });
794
-
795
- const item = new ClipboardItem(
796
- { [blob.type]: blob },
797
- {
798
- type: "application/octet-stream",
799
- presentationStyle: "attachment",
800
- name: filename
801
- }
802
- );
803
-
804
- await navigator.clipboard.write([item]);
805
- }
806
-
807
- async function copyAnalyzeAsFile() {
808
- const copyBtn = event.target.closest('.btn-copy');
809
- const copyIcon = document.getElementById('analyze-file-icon');
810
- const copyText = document.getElementById('analyze-file-text');
811
-
812
- if (!lastAnalyzeResult) {
813
- // Fetch if not already analyzed
814
- const path = document.getElementById('analyze-path').value;
815
- showLoading(copyBtn, copyBtn.innerHTML);
816
-
817
- try {
818
- const res = await fetch(`${API_BASE}/api/analyze`, {
819
- method: 'POST',
820
- headers: { 'Content-Type': 'application/json' },
821
- body: JSON.stringify({ path })
822
- });
823
-
824
- if (res.ok) {
825
- lastAnalyzeResult = await res.text();
826
- } else {
827
- showToast('Lỗi analyze', 'error');
828
- resetButton(copyBtn);
829
- return;
830
- }
831
- } catch (err) {
832
- showToast('Lỗi: ' + err.message, 'error');
833
- resetButton(copyBtn);
834
- return;
835
- }
836
- resetButton(copyBtn);
837
- }
838
-
839
- try {
840
- await copyAsFile("project.txt", lastAnalyzeResult);
841
-
842
- copyBtn.classList.add('copied');
843
- copyIcon.textContent = '✓';
844
- copyText.textContent = 'Copied';
845
- showToast('Đã copy file', 'success');
846
-
847
- setTimeout(() => {
848
- copyBtn.classList.remove('copied');
849
- copyIcon.textContent = '📄';
850
- copyText.textContent = 'Copy as File';
851
- }, 2000);
852
- } catch (err) {
853
- showToast('Lỗi copy: ' + err.message, 'error');
854
- }
855
- }
856
-
857
- async function testExecute() {
858
- const btn = event.target.closest('.btn');
859
- const bash = document.getElementById('execute-bash').value;
860
-
861
- if (!bash.trim()) {
862
- showToast('Vui lòng nhập bash script', 'error');
863
- return;
864
- }
865
-
866
- showLoading(btn, btn.innerHTML);
867
- try {
868
- const res = await fetch(`${API_BASE}/api/execute`, {
869
- method: 'POST',
870
- headers: { 'Content-Type': 'application/json' },
871
- body: JSON.stringify({ bash })
872
- });
873
- const data = await res.json();
874
- showResponse('execute-response', data, !res.ok || !data.success);
875
-
876
- if (data.success) {
877
- showToast('Thực thi thành công', 'success');
878
- } else {
879
- showToast('Thực thi thất bại', 'error');
880
- }
881
- } catch (err) {
882
- showResponse('execute-response', { error: err.message }, true);
883
- showToast('Lỗi: ' + err.message, 'error');
884
- }
885
- resetButton(btn);
886
- }
887
-
888
- async function executeFromClipboard() {
889
- const btn = event.target.closest('.btn');
890
-
891
- showLoading(btn, btn.innerHTML);
892
-
893
- try {
894
- const clipboardText = await navigator.clipboard.readText();
895
-
896
- if (!clipboardText || !clipboardText.trim()) {
897
- showToast('Clipboard trống!', 'error');
898
- showResponse('execute-response', {
899
- error: 'Clipboard is empty',
900
- message: 'Please copy a bash script to clipboard first'
901
- }, true);
902
- resetButton(btn);
903
- return;
904
- }
905
-
906
- document.getElementById('execute-bash').value = clipboardText;
907
-
908
- const res = await fetch(`${API_BASE}/api/execute`, {
909
- method: 'POST',
910
- headers: { 'Content-Type': 'application/json' },
911
- body: JSON.stringify({ bash: clipboardText })
912
- });
913
- const data = await res.json();
914
- showResponse('execute-response', data, !res.ok || !data.success);
915
-
916
- if (data.success) {
917
- showToast('Thực thi từ clipboard OK', 'success');
918
- } else {
919
- if (data.syntaxError) {
920
- showToast('Lỗi syntax script', 'error');
921
- } else {
922
- showToast('Thực thi thất bại', 'error');
923
- }
924
- }
925
- } catch (err) {
926
- if (err.name === 'NotAllowedError') {
927
- showToast('Không có quyền clipboard', 'error');
928
- } else {
929
- showResponse('execute-response', { error: err.message }, true);
930
- showToast('Lỗi: ' + err.message, 'error');
931
- }
932
- }
933
- resetButton(btn);
934
- }
935
-
936
- function showToast(message, type = 'success') {
937
- const toast = document.getElementById('toast');
938
- // Reset text content to remove potential icon junk
939
- toast.textContent = message;
940
- toast.className = `toast ${type}`;
941
- toast.classList.add('show');
942
-
943
- // Clear previous timeout if exists
944
- if (toast.timeoutId) clearTimeout(toast.timeoutId);
945
-
946
- toast.timeoutId = setTimeout(() => toast.classList.remove('show'), 3000);
947
- }
948
-
949
- // Check server status
950
- (async () => {
951
- try {
952
- const res = await fetch(`${API_BASE}/health`);
953
- if (res.ok) {
954
- document.getElementById('status').textContent = '● Online';
955
- document.getElementById('status').style.background = 'rgba(52, 199, 89, 0.15)';
956
- document.getElementById('status').style.color = 'var(--ios-green)';
957
- }
958
- } catch {
959
- document.getElementById('status').textContent = '● Offline';
960
- document.getElementById('status').style.background = 'rgba(255, 59, 48, 0.15)';
961
- document.getElementById('status').style.color = 'var(--ios-red)';
962
- }
963
- })();
964
- </script>
116
+ <script type="module" src="/js/main.js"></script>
965
117
  </body>
966
118
 
967
- </html>
119
+ </html>