vg-coder-cli 2.0.6 → 2.0.7

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": "2.0.6",
3
+ "version": "2.0.7",
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": {
@@ -7,7 +7,7 @@
7
7
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
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
- <title>VG Coder CLI</title>
10
+ <title>VG Coder API Dashboard</title>
11
11
  <style>
12
12
  :root {
13
13
  --ios-bg: #F2F2F7;
@@ -15,11 +15,12 @@
15
15
  --ios-blue: #007AFF;
16
16
  --ios-green: #34C759;
17
17
  --ios-red: #FF3B30;
18
- --ios-text-primary: #000000;
19
- --ios-text-secondary: #8E8E93;
18
+ --ios-gray: #8E8E93;
19
+ --ios-gray-light: #E5E5EA;
20
+ --ios-input-bg: #F2F2F7;
20
21
  --ios-separator: #C6C6C8;
21
- --ios-input-bg: #E5E5EA;
22
- --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
22
+ --safe-area-top: env(safe-area-inset-top);
23
+ --safe-area-bottom: env(safe-area-inset-bottom);
23
24
  }
24
25
 
25
26
  * {
@@ -30,97 +31,163 @@
30
31
  }
31
32
 
32
33
  body {
33
- font-family: var(--font-stack);
34
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
34
35
  background-color: var(--ios-bg);
35
- color: var(--ios-text-primary);
36
+ color: #000;
36
37
  min-height: 100vh;
37
- padding-bottom: 40px;
38
- font-size: 17px;
39
- line-height: 1.5;
38
+ padding: 0;
39
+ padding-bottom: calc(20px + var(--safe-area-bottom));
40
40
  -webkit-font-smoothing: antialiased;
41
41
  }
42
42
 
43
- /* Glass Header */
44
- .header-nav {
45
- position: sticky;
46
- top: 0;
47
- z-index: 100;
48
- background: rgba(255, 255, 255, 0.85);
49
- backdrop-filter: blur(20px);
50
- -webkit-backdrop-filter: blur(20px);
51
- border-bottom: 0.5px solid rgba(0, 0, 0, 0.1);
52
- padding: 15px 20px;
53
- display: flex;
54
- justify-content: space-between;
55
- align-items: center;
56
- padding-top: max(15px, env(safe-area-inset-top));
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;
57
55
  }
58
56
 
59
- .app-title {
57
+ .header h1 {
58
+ color: #000;
59
+ font-size: 34px;
60
60
  font-weight: 700;
61
- font-size: 1.2rem;
62
61
  letter-spacing: -0.5px;
62
+ margin-bottom: 8px;
63
63
  }
64
64
 
65
- .status-badge {
66
- font-size: 0.75rem;
67
- font-weight: 600;
68
- padding: 4px 10px;
69
- border-radius: 20px;
70
- background: var(--ios-green);
71
- color: white;
72
- box-shadow: 0 2px 4px rgba(52, 199, 89, 0.2);
73
- transition: all 0.3s ease;
65
+ .header p {
66
+ color: var(--ios-gray);
67
+ font-size: 17px;
68
+ line-height: 1.4;
69
+ margin-bottom: 12px;
74
70
  }
75
71
 
76
- .container {
77
- max-width: 800px;
78
- margin: 0 auto;
79
- padding: 20px;
80
- padding-left: max(20px, env(safe-area-inset-left));
81
- padding-right: max(20px, env(safe-area-inset-right));
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
82
  }
83
83
 
84
- /* iOS Cards */
85
- .card {
84
+ /* Card Style (Inset Grouped) */
85
+ .system-prompt-card,
86
+ .endpoint-card {
86
87
  background: var(--ios-card);
87
- border-radius: 20px;
88
- padding: 24px;
89
- margin-bottom: 24px;
90
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
91
- position: relative;
88
+ border-radius: 16px;
89
+ padding: 20px;
90
+ margin-bottom: 20px;
91
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
92
92
  overflow: hidden;
93
+ position: relative;
93
94
  }
94
95
 
95
- .card-header {
96
+ /* System Prompt */
97
+ .system-prompt-header {
96
98
  display: flex;
99
+ justify-content: space-between;
97
100
  align-items: center;
98
- margin-bottom: 16px;
99
- gap: 12px;
101
+ cursor: pointer;
102
+ padding: 4px 0;
100
103
  }
101
104
 
102
- .card-icon {
103
- width: 40px;
104
- height: 40px;
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;
105
140
  border-radius: 12px;
106
- background: var(--ios-bg);
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 {
107
155
  display: flex;
108
156
  align-items: center;
109
- justify-content: center;
110
- font-size: 1.2rem;
111
- color: var(--ios-blue);
157
+ flex-wrap: wrap;
158
+ gap: 10px;
159
+ margin-bottom: 12px;
160
+ border-bottom: 0.5px solid var(--ios-separator);
161
+ padding-bottom: 12px;
112
162
  }
113
163
 
114
- .card-title {
115
- font-size: 1.1rem;
164
+ .method {
165
+ padding: 4px 10px;
166
+ border-radius: 6px;
116
167
  font-weight: 700;
117
- letter-spacing: -0.3px;
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;
118
184
  }
119
185
 
120
- .card-desc {
121
- color: var(--ios-text-secondary);
122
- font-size: 0.9rem;
186
+ .endpoint-desc {
187
+ color: var(--ios-gray);
123
188
  margin-bottom: 20px;
189
+ font-size: 15px;
190
+ line-height: 1.4;
124
191
  }
125
192
 
126
193
  /* Forms */
@@ -128,84 +195,103 @@
128
195
  margin-bottom: 20px;
129
196
  }
130
197
 
131
- .form-label {
198
+ .form-group label {
132
199
  display: block;
133
- font-size: 0.85rem;
134
- font-weight: 600;
135
- color: var(--ios-text-secondary);
136
200
  margin-bottom: 8px;
201
+ color: #000;
202
+ font-weight: 500;
203
+ font-size: 14px;
137
204
  text-transform: uppercase;
138
- letter-spacing: 0.5px;
205
+ letter-spacing: 0.3px;
206
+ opacity: 0.6;
139
207
  }
140
208
 
141
- input[type="text"],
142
- textarea {
209
+ .form-group input,
210
+ .form-group textarea {
143
211
  width: 100%;
212
+ padding: 14px;
144
213
  background: var(--ios-input-bg);
145
214
  border: none;
146
215
  border-radius: 12px;
147
- padding: 14px 16px;
148
- font-size: 1rem;
149
- font-family: inherit;
150
- color: var(--ios-text-primary);
151
- transition: all 0.2s;
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;
152
222
  -webkit-appearance: none;
153
223
  }
154
224
 
155
- textarea {
156
- min-height: 120px;
157
- resize: vertical;
158
- line-height: 1.4;
225
+ .form-group input:focus,
226
+ .form-group textarea:focus {
227
+ outline: none;
228
+ background: #E5E5EA;
229
+ /* Slightly darker on focus */
159
230
  }
160
231
 
161
- input:focus,
162
- textarea:focus {
163
- outline: none;
164
- background: #D1D1D6;
232
+ .form-group textarea {
233
+ min-height: 120px;
234
+ resize: none;
165
235
  }
166
236
 
167
237
  /* Buttons */
168
238
  .btn-group {
169
- display: grid;
170
- grid-template-columns: 1fr 1fr;
239
+ display: flex;
171
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
+ }
172
250
  }
173
251
 
174
252
  .btn {
175
- border: none;
176
253
  background: var(--ios-blue);
177
254
  color: white;
178
- padding: 14px;
179
- border-radius: 14px;
180
- font-size: 1rem;
181
- font-weight: 600;
255
+ border: none;
256
+ padding: 14px 20px;
257
+ border-radius: 12px;
182
258
  cursor: pointer;
259
+ font-size: 17px;
260
+ font-weight: 600;
183
261
  transition: transform 0.1s, opacity 0.2s;
184
262
  display: flex;
185
263
  align-items: center;
186
264
  justify-content: center;
187
265
  gap: 8px;
188
- -webkit-appearance: none;
266
+ width: 100%;
267
+ position: relative;
268
+ overflow: hidden;
189
269
  }
190
270
 
191
271
  .btn:active {
192
272
  transform: scale(0.98);
193
- opacity: 0.8;
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;
194
281
  }
195
282
 
196
- .btn-secondary {
197
- background: rgba(0, 122, 255, 0.15);
283
+ .btn-copy {
284
+ background: rgba(0, 122, 255, 0.1);
198
285
  color: var(--ios-blue);
199
286
  }
200
287
 
201
- /* Full width button */
202
- .btn-full {
203
- grid-column: 1 / -1;
288
+ /* Specific override for prompt copy button to look nice */
289
+ .system-prompt-content .btn-copy {
290
+ margin-top: 8px;
204
291
  }
205
292
 
206
- .btn-copy {
207
- background: #E5E5EA;
208
- color: black;
293
+ .btn-copy:active {
294
+ background: rgba(0, 122, 255, 0.2);
209
295
  }
210
296
 
211
297
  .btn-copy.copied {
@@ -213,376 +299,389 @@
213
299
  color: white;
214
300
  }
215
301
 
216
- /* System Prompt Accordion */
217
- .prompt-accordion {
218
- cursor: pointer;
219
- user-select: none;
220
- }
221
-
222
- .prompt-header {
223
- display: flex;
224
- justify-content: space-between;
225
- align-items: center;
226
- padding: 4px 0;
227
- }
228
-
229
- .chevron {
230
- color: var(--ios-text-secondary);
231
- transition: transform 0.3s ease;
232
- font-size: 0.8rem;
233
- }
234
-
235
- .prompt-content {
236
- max-height: 0;
237
- overflow: hidden;
238
- transition: max-height 0.4s cubic-bezier(0.65, 0, 0.35, 1);
239
- opacity: 0;
240
- }
241
-
242
- .prompt-content.open {
243
- max-height: 500px;
244
- opacity: 1;
245
- margin-top: 15px;
246
- }
247
-
248
- .code-block {
249
- background: #2c2c2e;
250
- color: #fff;
251
- padding: 15px;
252
- border-radius: 12px;
253
- font-family: 'SF Mono', 'Menlo', monospace;
254
- font-size: 0.85rem;
255
- overflow-x: auto;
256
- white-space: pre-wrap;
257
- margin-bottom: 15px;
258
- border: 1px solid rgba(0, 0, 0, 0.1);
259
- }
260
-
261
302
  /* Response Area */
262
303
  .response {
263
304
  margin-top: 20px;
264
- background: #F9F9F9;
305
+ padding: 16px;
265
306
  border-radius: 12px;
266
- padding: 15px;
267
- border-left: 5px solid var(--ios-text-secondary);
307
+ background: #1C1C1E;
308
+ /* Dark background for code contrast */
309
+ color: #fff;
268
310
  display: none;
269
- animation: slideDown 0.3s ease;
311
+ font-size: 13px;
312
+ overflow-x: auto;
313
+ border-left: none;
314
+ /* Removed the side border style */
270
315
  }
271
316
 
272
317
  .response.show {
273
318
  display: block;
319
+ animation: fadeIn 0.3s ease;
274
320
  }
275
321
 
276
322
  .response.success {
277
- border-left-color: var(--ios-green);
278
- background: #F0FFF4;
323
+ border: 1px solid rgba(52, 199, 89, 0.3);
279
324
  }
280
325
 
281
326
  .response.error {
282
- border-left-color: var(--ios-red);
283
- background: #FFF5F5;
327
+ border: 1px solid rgba(255, 59, 48, 0.3);
328
+ background: #2C1515;
284
329
  }
285
330
 
286
331
  .response pre {
332
+ margin: 0;
287
333
  font-family: 'SF Mono', 'Menlo', monospace;
288
- font-size: 0.85rem;
289
334
  white-space: pre-wrap;
290
- word-break: break-all;
291
- color: #333;
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
+ }
292
365
  }
293
366
 
294
- /* Toast */
367
+ /* Toast - iOS Notification Style */
295
368
  .toast {
296
369
  position: fixed;
297
- top: 20px;
370
+ top: 10px;
298
371
  left: 50%;
299
372
  transform: translateX(-50%) translateY(-100px);
300
- background: rgba(0, 0, 0, 0.8);
301
- backdrop-filter: blur(10px);
302
- color: white;
303
- padding: 12px 24px;
304
- border-radius: 50px;
305
- font-weight: 600;
306
- font-size: 0.95rem;
307
- z-index: 1000;
308
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
309
- transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
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;
310
385
  display: flex;
311
386
  align-items: center;
312
- gap: 8px;
387
+ gap: 10px;
388
+ width: auto;
389
+ max-width: 90%;
313
390
  white-space: nowrap;
391
+ opacity: 0;
392
+ pointer-events: none;
314
393
  }
315
394
 
316
395
  .toast.show {
317
- transform: translateX(-50%) translateY(0);
396
+ transform: translateX(-50%) translateY(calc(var(--safe-area-top)));
397
+ opacity: 1;
318
398
  }
319
399
 
320
- .loading-spinner {
321
- width: 18px;
322
- height: 18px;
323
- border: 2px solid rgba(255, 255, 255, 0.3);
400
+ /* Icons in toast */
401
+ .toast::before {
402
+ content: '';
403
+ display: block;
404
+ width: 20px;
405
+ height: 20px;
324
406
  border-radius: 50%;
325
- border-top-color: white;
326
- animation: spin 0.8s linear infinite;
407
+ flex-shrink: 0;
327
408
  }
328
409
 
329
- @keyframes spin {
330
- to {
331
- transform: rotate(360deg);
332
- }
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;
333
412
  }
334
413
 
335
- @keyframes slideDown {
336
- from {
337
- opacity: 0;
338
- transform: translateY(-10px);
339
- }
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
+ }
340
417
 
341
- to {
342
- opacity: 1;
343
- transform: translateY(0);
344
- }
418
+ .toast.info::before {
419
+ background: var(--ios-blue);
345
420
  }
346
421
  </style>
347
422
  </head>
348
423
 
349
424
  <body>
350
- <!-- Glass Header -->
351
- <nav class="header-nav">
352
- <div class="app-title">VG Coder</div>
353
- <div id="status" class="status-badge">● Offline</div>
354
- </nav>
355
-
356
425
  <div class="container">
426
+ <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>
431
+ </div>
357
432
 
358
- <!-- System Prompt Card -->
359
- <div class="card prompt-accordion">
360
- <div class="card-header prompt-header" onclick="toggleSystemPrompt()">
361
- <div style="display: flex; align-items: center; gap: 12px;">
362
- <div class="card-icon" style="background: rgba(255, 59, 48, 0.1); color: var(--ios-red);">⚡</div>
363
- <div class="card-title">System Prompt</div>
364
- </div>
365
- <div class="chevron" id="toggle-icon">▼</div>
433
+ <!-- System Prompt Section -->
434
+ <div class="system-prompt-card">
435
+ <div class="system-prompt-header" onclick="toggleSystemPrompt()">
436
+ <h2>
437
+ <span>📝</span>
438
+ <span>System Prompt</span>
439
+ </h2>
440
+ <span class="toggle-icon" id="toggle-icon">▼</span>
366
441
  </div>
367
- <div class="prompt-content" id="system-prompt-content">
368
- <div class="code-block" id="prompt-text"></div>
369
- <button class="btn btn-secondary btn-full" onclick="copySystemPrompt()">
442
+ <div class="system-prompt-content" id="system-prompt-content">
443
+ <div class="prompt-text" id="prompt-text"></div>
444
+ <button class="btn btn-copy" onclick="copySystemPrompt()">
370
445
  <span id="copy-icon">📋</span>
371
- <span id="copy-text">Copy Prompt</span>
446
+ <span id="copy-text">Copy System Prompt</span>
372
447
  </button>
373
448
  </div>
374
449
  </div>
375
450
 
376
- <!-- Analyze Card -->
377
- <div class="card">
378
- <div class="card-header">
379
- <div class="card-icon">📊</div>
380
- <div>
381
- <div class="card-title">Project Analyze</div>
451
+ <div class="endpoints">
452
+ <!-- Analyze -->
453
+ <div class="endpoint-card">
454
+ <div class="endpoint-header">
455
+ <span class="method post">POST</span>
456
+ <span class="endpoint-path">/api/analyze</span>
382
457
  </div>
383
- </div>
384
- <div class="card-desc">Phân tích dự án và lấy source code</div>
385
-
386
- <div class="form-group">
387
- <label class="form-label">Project Path</label>
388
- <input type="text" id="analyze-path" value="." placeholder="/path/to/project">
389
- </div>
390
-
391
- <div class="btn-group">
392
- <button class="btn" onclick="testAnalyze()">
393
- <span>📥</span> Download
394
- </button>
395
- <button class="btn btn-secondary" onclick="copyAnalyzeResult()">
396
- <span id="analyze-copy-icon">📋</span> Copy
397
- </button>
398
- </div>
399
- <div class="response" id="analyze-response"></div>
400
- </div>
401
-
402
- <!-- Execute Card -->
403
- <div class="card">
404
- <div class="card-header">
405
- <div class="card-icon" style="background: rgba(52, 199, 89, 0.1); color: var(--ios-green);">🚀</div>
406
- <div>
407
- <div class="card-title">Execute Script</div>
458
+ <p class="endpoint-desc">Phân tích dự án và lấy toàn bộ source code.</p>
459
+ <div class="form-group">
460
+ <label>Path</label>
461
+ <input type="text" id="analyze-path" value="." placeholder="Project path (e.g. .)">
408
462
  </div>
409
- </div>
410
- <div class="card-desc">Thực thi bash script an toàn</div>
411
-
412
- <div class="form-group">
413
- <label class="form-label">Bash Script</label>
414
- <textarea id="execute-bash" placeholder="mkdir -p src..."></textarea>
463
+ <div class="btn-group">
464
+ <button class="btn" onclick="testAnalyze()">
465
+ <span>📥</span>
466
+ <span>Download</span>
467
+ </button>
468
+ <button class="btn btn-copy" onclick="copyAnalyzeResult()">
469
+ <span id="analyze-copy-icon">📋</span>
470
+ <span id="analyze-copy-text">Copy Text</span>
471
+ </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
+ </div>
477
+ <div class="response" id="analyze-response"></div>
415
478
  </div>
416
479
 
417
- <div class="btn-group">
418
- <button class="btn" style="background: var(--ios-green);" onclick="testExecute()">
419
- <span>▶️</span> Execute
420
- </button>
421
- <button class="btn btn-secondary" onclick="executeFromClipboard()">
422
- <span>📋</span> Paste & Run
423
- </button>
480
+ <!-- Execute Bash -->
481
+ <div class="endpoint-card">
482
+ <div class="endpoint-header">
483
+ <span class="method post">POST</span>
484
+ <span class="endpoint-path">/api/execute</span>
485
+ </div>
486
+ <p class="endpoint-desc">Thực thi bash script với syntax validation.</p>
487
+ <div class="form-group">
488
+ <label>Bash Script</label>
489
+ <textarea id="execute-bash"
490
+ placeholder="mkdir -p src/test&#10;echo 'Hello' > src/test/hello.txt"></textarea>
491
+ </div>
492
+ <div class="btn-group">
493
+ <button class="btn" onclick="testExecute()">
494
+ <span>▶️</span>
495
+ <span>Run Script</span>
496
+ </button>
497
+ <button class="btn" onclick="executeFromClipboard()">
498
+ <span>📋</span>
499
+ <span>Paste & Run</span>
500
+ </button>
501
+ </div>
502
+ <div class="response" id="execute-response"></div>
424
503
  </div>
425
- <div class="response" id="execute-response"></div>
426
504
  </div>
427
505
  </div>
428
506
 
429
- <!-- iOS Toast -->
430
507
  <div class="toast" id="toast"></div>
431
508
 
432
509
  <script>
433
510
  const API_BASE = window.location.origin;
434
511
  let lastAnalyzeResult = null;
435
512
 
513
+ // System Prompt
436
514
  const SYSTEM_PROMPT = `# VG Coder AI System Prompt
437
515
 
438
516
  ## Command Prefixes
439
- ### /ask - Q&A Mode
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
+
440
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
+
441
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
+
442
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.
443
550
 
444
- ## ⚠️ QUY TẮC BẮT BUỘC /code
551
+ ## ⚠️ QUY TẮC BẮT BUỘC
445
552
 
446
- 1. **Format Script Chuẩn:**
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:**
447
560
  \`\`\`bash
448
561
  mkdir -p $(dirname "path/to/file.ext")
449
562
  cat <<'EOF' > path/to/file.ext
450
- ... content ...
563
+ ... toàn bộ nội dung file sau khi chỉnh sửa ...
451
564
  EOF
452
565
  \`\`\`
453
566
 
454
- 2. **Quy tắc:**
455
- - Luôn có mkdir -p
456
- - Dùng <<'EOF' (có quotes)
457
- - Chỉ include files thay đổi
458
- `;
567
+ ### 3. Chi tiết quan trọng
568
+ - **LUÔN** \`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
459
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
460
632
  document.getElementById('prompt-text').textContent = SYSTEM_PROMPT;
461
633
 
462
634
  function toggleSystemPrompt() {
463
635
  const content = document.getElementById('system-prompt-content');
464
636
  const icon = document.getElementById('toggle-icon');
465
637
  content.classList.toggle('open');
466
- icon.style.transform = content.classList.contains('open') ? 'rotate(180deg)' : 'rotate(0deg)';
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
+ });
467
660
  }
468
661
 
469
662
  function showResponse(elementId, data, isError = false) {
470
663
  const el = document.getElementById(elementId);
471
664
  el.className = 'response show ' + (isError ? 'error' : 'success');
472
-
473
- // Format nice JSON
474
- if (typeof data === 'object') {
475
- if (data.stdout) data.stdout = data.stdout.trim();
476
- if (data.stderr) data.stderr = data.stderr.trim();
477
- }
478
-
479
665
  el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
480
666
  }
481
667
 
482
668
  function showLoading(button, originalText) {
483
669
  button.disabled = true;
484
- button.innerHTML = '<div class="loading-spinner"></div>';
670
+ button.innerHTML = '<span class="loading"></span>';
485
671
  button.dataset.originalText = originalText;
486
672
  }
487
673
 
488
674
  function resetButton(button) {
489
675
  button.disabled = false;
490
- button.innerHTML = button.dataset.originalText;
491
- }
492
-
493
- function showToast(message, type = 'success') {
494
- const toast = document.getElementById('toast');
495
- let icon = type === 'success' ? '✅' : (type === 'error' ? '❌' : 'ℹ️');
496
- toast.innerHTML = `${icon} ${message}`;
497
- toast.classList.add('show');
498
- setTimeout(() => toast.classList.remove('show'), 3000);
499
- }
500
-
501
- // --- ROBUST COPY FUNCTION (MODERN + FALLBACK) ---
502
- function fallbackCopyTextToClipboard(text) {
503
- var textArea = document.createElement("textarea");
504
- textArea.value = text;
505
-
506
- // Ensure textarea is not visible but part of DOM
507
- textArea.style.position = "fixed";
508
- textArea.style.left = "-9999px";
509
- textArea.style.top = "0";
510
- document.body.appendChild(textArea);
511
-
512
- textArea.focus();
513
- textArea.select();
514
-
515
- try {
516
- var successful = document.execCommand('copy');
517
- return successful;
518
- } catch (err) {
519
- console.error('Fallback: Oops, unable to copy', err);
520
- return false;
521
- } finally {
522
- document.body.removeChild(textArea);
523
- }
524
- }
525
-
526
- async function copyToClipboard(text, btnElement, successIconId, successTextId) {
527
- if (!text) return showToast('Nothing to copy', 'error');
528
-
529
- const updateUI = () => {
530
- if (btnElement) {
531
- btnElement.classList.add('copied');
532
- const icon = document.getElementById(successIconId);
533
- const label = document.getElementById(successTextId);
534
-
535
- const originalIcon = icon.textContent;
536
- const originalLabel = label.textContent;
537
-
538
- icon.textContent = '✓';
539
- label.textContent = 'Copied';
540
-
541
- setTimeout(() => {
542
- btnElement.classList.remove('copied');
543
- icon.textContent = originalIcon;
544
- label.textContent = originalLabel;
545
- }, 2000);
546
- }
547
- showToast('Copied to clipboard!');
548
- };
549
-
550
- // Try Modern API first
551
- if (navigator.clipboard && navigator.clipboard.writeText) {
552
- try {
553
- await navigator.clipboard.writeText(text);
554
- updateUI();
555
- } catch (err) {
556
- console.warn('Modern copy failed, trying fallback', err);
557
- // Try fallback if modern API fails
558
- if (fallbackCopyTextToClipboard(text)) {
559
- updateUI();
560
- } else {
561
- showToast('Failed to copy', 'error');
562
- }
563
- }
564
- } else {
565
- // Use fallback immediately if API doesn't exist
566
- if (fallbackCopyTextToClipboard(text)) {
567
- updateUI();
568
- } else {
569
- showToast('Failed to copy', 'error');
570
- }
571
- }
572
- }
573
-
574
- // --- Logic Handlers ---
575
-
576
- function copySystemPrompt() {
577
- copyToClipboard(SYSTEM_PROMPT, event.target.closest('.btn'), 'copy-icon', 'copy-text');
676
+ const originalText = button.dataset.originalText;
677
+ button.innerHTML = originalText;
578
678
  }
579
679
 
580
680
  async function testAnalyze() {
581
681
  const btn = event.target.closest('.btn');
582
682
  const path = document.getElementById('analyze-path').value;
583
- const originalHTML = btn.innerHTML;
584
683
 
585
- showLoading(btn, originalHTML);
684
+ showLoading(btn, btn.innerHTML);
586
685
  try {
587
686
  const res = await fetch(`${API_BASE}/api/analyze`, {
588
687
  method: 'POST',
@@ -594,7 +693,7 @@ EOF
594
693
  const text = await res.text();
595
694
  lastAnalyzeResult = text;
596
695
 
597
- // Trigger download
696
+ // Download file
598
697
  const blob = new Blob([text], { type: 'text/plain' });
599
698
  const url = window.URL.createObjectURL(blob);
600
699
  const a = document.createElement('a');
@@ -602,33 +701,167 @@ EOF
602
701
  a.download = 'project.txt';
603
702
  a.click();
604
703
 
605
- showResponse('analyze-response', { success: true, files: text.split('===== FILE:').length - 1 });
606
- showToast('Analysis completed');
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');
607
711
  } else {
608
712
  const data = await res.json();
609
713
  showResponse('analyze-response', data, true);
610
- showToast('Analysis failed', 'error');
714
+ showToast('Lỗi analyze', 'error');
611
715
  }
612
716
  } catch (err) {
613
717
  showResponse('analyze-response', { error: err.message }, true);
614
- showToast('Connection error', 'error');
718
+ showToast('Lỗi: ' + err.message, 'error');
615
719
  }
616
720
  resetButton(btn);
617
721
  }
618
722
 
619
723
  async function copyAnalyzeResult() {
620
- const btn = event.target.closest('.btn');
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
+
621
728
  if (!lastAnalyzeResult) {
622
- showToast('Please analyze/download first', 'error');
623
- return;
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');
624
854
  }
625
- copyToClipboard(lastAnalyzeResult, btn, 'analyze-copy-icon', 'analyze-copy-text');
626
855
  }
627
856
 
628
857
  async function testExecute() {
629
858
  const btn = event.target.closest('.btn');
630
859
  const bash = document.getElementById('execute-bash').value;
631
- if (!bash.trim()) return showToast('Script is empty', 'error');
860
+
861
+ if (!bash.trim()) {
862
+ showToast('Vui lòng nhập bash script', 'error');
863
+ return;
864
+ }
632
865
 
633
866
  showLoading(btn, btn.innerHTML);
634
867
  try {
@@ -638,41 +871,96 @@ EOF
638
871
  body: JSON.stringify({ bash })
639
872
  });
640
873
  const data = await res.json();
641
- showResponse('execute-response', data, !data.success);
642
- showToast(data.success ? 'Executed successfully' : 'Execution failed', data.success ? 'success' : 'error');
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
+ }
643
881
  } catch (err) {
644
882
  showResponse('execute-response', { error: err.message }, true);
883
+ showToast('Lỗi: ' + err.message, 'error');
645
884
  }
646
885
  resetButton(btn);
647
886
  }
648
887
 
649
888
  async function executeFromClipboard() {
889
+ const btn = event.target.closest('.btn');
890
+
891
+ showLoading(btn, btn.innerHTML);
892
+
650
893
  try {
651
- const text = await navigator.clipboard.readText();
652
- if (!text.trim()) return showToast('Clipboard is empty', 'error');
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
+ }
653
905
 
654
- document.getElementById('execute-bash').value = text;
655
- showToast('Pasted from clipboard');
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
+ }
656
925
  } catch (err) {
657
- showToast('Clipboard permission denied', 'error');
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
+ }
658
932
  }
933
+ resetButton(btn);
659
934
  }
660
935
 
661
- // Init Check
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
662
950
  (async () => {
663
- const statusBadge = document.getElementById('status');
664
951
  try {
665
952
  const res = await fetch(`${API_BASE}/health`);
666
953
  if (res.ok) {
667
- statusBadge.textContent = '● Online';
668
- statusBadge.style.backgroundColor = 'var(--ios-green)';
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)';
669
957
  }
670
958
  } catch {
671
- statusBadge.textContent = '● Offline';
672
- statusBadge.style.backgroundColor = 'var(--ios-text-secondary)';
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)';
673
962
  }
674
963
  })();
675
-
676
964
  </script>
677
965
  </body>
678
966
 
Binary file