vg-coder-cli 2.0.7 → 2.0.9

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.
@@ -0,0 +1,221 @@
1
+ import { getStructure, analyzeProject, copyToClipboard } from '../api.js';
2
+ import { showToast, showLoading, resetButton, showResponse, formatNumber, showCopiedState } from '../utils.js';
3
+
4
+ // Global variable to store current structure data
5
+ let currentStructureData = null;
6
+
7
+ /**
8
+ * Handle Structure button click logic
9
+ */
10
+ export async function handleStructureView(event) {
11
+ const btn = event.target.closest('.btn');
12
+ const pathInput = document.getElementById('structure-path');
13
+ const treeContainer = document.getElementById('structure-tree');
14
+ const treeContent = document.getElementById('tree-content');
15
+ const errorContainer = document.getElementById('structure-response');
16
+
17
+ const path = pathInput.value;
18
+
19
+ showLoading(btn, btn.innerHTML);
20
+ treeContainer.style.display = 'none';
21
+ errorContainer.style.display = 'none';
22
+
23
+ try {
24
+ const data = await getStructure(path);
25
+ currentStructureData = data.structure;
26
+
27
+ // Render Tree HTML using recursive function
28
+ treeContent.innerHTML = generateTreeHtml(data.structure);
29
+
30
+ // Initial token update
31
+ updateTotalTokens();
32
+
33
+ treeContainer.style.display = 'block';
34
+ showToast('Tải cấu trúc thành công', 'success');
35
+
36
+ } catch (err) {
37
+ showResponse('structure-response', { error: err.message }, true);
38
+ showToast('Lỗi: ' + err.message, 'error');
39
+ }
40
+
41
+ resetButton(btn);
42
+ }
43
+
44
+ /**
45
+ * Toggle folder collapse/expand
46
+ * Only triggers if clicked on row but NOT on checkbox
47
+ */
48
+ export function handleToggleFolder(event) {
49
+ if (event.target.type === 'checkbox') return;
50
+
51
+ // Find closest parent LI
52
+ const li = event.currentTarget.closest('.tree-li');
53
+ if (li && li.classList.contains('has-children')) {
54
+ li.classList.toggle('collapsed');
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Handle Checkbox Logic (Parent <-> Child sync) & Update Token Total
60
+ */
61
+ export function handleCheckboxChange(event) {
62
+ event.stopPropagation();
63
+ const checkbox = event.target;
64
+ const isChecked = checkbox.checked;
65
+
66
+ // 1. Sync Children: If this is a folder, update all children checkboxes
67
+ const li = checkbox.closest('.tree-li');
68
+ if (li) {
69
+ const childrenCheckboxes = li.querySelectorAll('.tree-checkbox');
70
+ childrenCheckboxes.forEach(child => {
71
+ child.checked = isChecked;
72
+ });
73
+ }
74
+
75
+ // 2. Recalculate Tokens
76
+ updateTotalTokens();
77
+ }
78
+
79
+ /**
80
+ * Calculate total tokens of CHECKED files only
81
+ */
82
+ function updateTotalTokens() {
83
+ // Select all checked checkboxes that are FILES (have data-tokens)
84
+ const checkedFiles = document.querySelectorAll('.tree-checkbox[data-type="file"]:checked');
85
+
86
+ let total = 0;
87
+ checkedFiles.forEach(box => {
88
+ const tokens = parseInt(box.dataset.tokens || '0');
89
+ total += tokens;
90
+ });
91
+
92
+ // Update Badge
93
+ const badge = document.getElementById('total-tokens-badge');
94
+ badge.textContent = `${formatNumber(total)} tokens`;
95
+
96
+ // Optional: Visual styling if 0
97
+ if (total === 0) {
98
+ badge.style.color = 'var(--ios-gray)';
99
+ } else {
100
+ badge.style.color = ''; // reset to default
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Copy Content of Selected Files (via API)
106
+ */
107
+ export async function handleCopySelected(event) {
108
+ const btn = event.target.closest('.btn-copy') || event.target.closest('.btn-icon-head');
109
+ const icon = document.getElementById('copy-structure-icon') || btn;
110
+ const text = document.getElementById('copy-structure-text') || { textContent: '' };
111
+
112
+ // 1. Get all checked FILE paths
113
+ const checkedBoxes = document.querySelectorAll('.tree-checkbox[data-type="file"]:checked');
114
+ const checkedPaths = [];
115
+
116
+ checkedBoxes.forEach(box => {
117
+ if (box.dataset.path) {
118
+ checkedPaths.push(box.dataset.path);
119
+ }
120
+ });
121
+
122
+ if (checkedPaths.length === 0) {
123
+ showToast('Chưa chọn file nào', 'error');
124
+ return;
125
+ }
126
+
127
+ // Save original button state
128
+ const originalText = btn.innerHTML;
129
+ if (btn.classList.contains('btn-copy')) {
130
+ showLoading(btn, btn.innerHTML);
131
+ } else {
132
+ // For header icon, just show visual feedback
133
+ btn.style.opacity = '0.5';
134
+ }
135
+
136
+ try {
137
+ const path = document.getElementById('structure-path').value;
138
+
139
+ // 2. Call Analyze API with specific files
140
+ const content = await analyzeProject(path, checkedPaths);
141
+
142
+ // 3. Copy to clipboard
143
+ await copyToClipboard(content);
144
+
145
+ // UI Feedback
146
+ if (btn.classList.contains('btn-copy')) {
147
+ showCopiedState(btn, icon, text, '📋', 'Copy Selected');
148
+ resetButton(btn);
149
+ } else {
150
+ // Header icon feedback
151
+ btn.style.opacity = '1';
152
+ const originalIcon = btn.textContent;
153
+ btn.textContent = '✓';
154
+ setTimeout(() => btn.textContent = originalIcon, 2000);
155
+ }
156
+
157
+ showToast(`Đã copy nội dung ${checkedPaths.length} file!`, 'success');
158
+
159
+ } catch (err) {
160
+ resetButton(btn);
161
+ if (!btn.classList.contains('btn-copy')) btn.style.opacity = '1';
162
+ showToast('Lỗi copy: ' + err.message, 'error');
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Recursive function to generate Tree HTML with Checkboxes
168
+ */
169
+ function generateTreeHtml(node) {
170
+ if (!node) return '';
171
+
172
+ const isDir = node.type === 'directory';
173
+ const hasChildren = isDir && node.children && node.children.length > 0;
174
+
175
+ // Determine Token Color
176
+ const tokens = node.tokens || 0;
177
+ let tokenClass = 'token-low';
178
+ if (tokens > 5000) tokenClass = 'token-high';
179
+ else if (tokens > 2000) tokenClass = 'token-med';
180
+
181
+ // Icon
182
+ const icon = isDir ? (hasChildren ? '📁' : '📂') : '📄';
183
+ const arrow = hasChildren ? '▼' : '';
184
+ const liClass = `tree-li ${hasChildren ? 'has-children' : ''}`;
185
+
186
+ // Build HTML
187
+ let html = `<li class="${liClass}">`;
188
+
189
+ const clickAttr = hasChildren ? 'onclick="toggleFolder(event)"' : '';
190
+
191
+ // Add data-tokens and data-type for client-side calculation
192
+
193
+ html += `
194
+ <div class="tree-item-row" ${clickAttr}>
195
+ <span class="arrow">${arrow}</span>
196
+ <input type="checkbox" class="tree-checkbox"
197
+ data-path="${node.relativePath || node.path}"
198
+ data-tokens="${tokens}"
199
+ data-type="${node.type}"
200
+ checked
201
+ onclick="handleCheckboxChange(event)">
202
+ <span class="tree-icon">${icon}</span>
203
+ <span class="tree-name">${node.name}</span>
204
+ <span class="token-badge ${tokenClass}">${formatNumber(tokens)}</span>
205
+ </div>
206
+ `;
207
+
208
+ // Children recursion
209
+ if (hasChildren) {
210
+ html += '<ul class="tree-ul">';
211
+ // Sort: Folders first, then files
212
+ node.children.forEach(child => {
213
+ html += generateTreeHtml(child);
214
+ });
215
+ html += '</ul>';
216
+ }
217
+
218
+ html += '</li>';
219
+
220
+ return isDir ? `<ul class="tree-ul">${html}</ul>` : html;
221
+ }
@@ -0,0 +1,182 @@
1
+ // Event Handlers & Business Logic
2
+ import { SYSTEM_PROMPT } from './config.js';
3
+ import { analyzeProject, executeScript, copyToClipboard, readFromClipboard } from './api.js';
4
+ import { showToast, showLoading, resetButton, showResponse, showCopiedState } from './utils.js';
5
+ // Import dedicated feature handlers
6
+ import { handleStructureView, handleToggleFolder, handleCheckboxChange, handleCopySelected } from './features/structure.js';
7
+
8
+ let lastAnalyzeResult = null;
9
+
10
+ // ==========================================
11
+ // SYSTEM PROMPT HANDLERS
12
+ // ==========================================
13
+
14
+ export function toggleSystemPrompt() {
15
+ const content = document.getElementById('system-prompt-content');
16
+ const icon = document.getElementById('toggle-icon');
17
+ content.classList.toggle('open');
18
+ icon.classList.toggle('open');
19
+ }
20
+
21
+ export function copySystemPromptFromHeader(event) {
22
+ event.stopPropagation();
23
+ const btn = event.currentTarget;
24
+ btn.textContent = '✓';
25
+ navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
26
+ showToast('Đã copy System Prompt', 'success');
27
+ setTimeout(() => btn.textContent = '📋', 2000);
28
+ }).catch(err => {
29
+ showToast('Lỗi copy: ' + err.message, 'error');
30
+ btn.textContent = '📋';
31
+ });
32
+ }
33
+
34
+ export function copySystemPrompt(event) {
35
+ const copyBtn = event.target.closest('.btn-copy');
36
+ const copyIcon = document.getElementById('copy-icon');
37
+ const copyText = document.getElementById('copy-text');
38
+
39
+ navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
40
+ showCopiedState(copyBtn, copyIcon, copyText, '📋', 'Copy System Prompt');
41
+ showToast('Đã copy System Prompt', 'success');
42
+ }).catch(err => showToast('Lỗi copy: ' + err.message, 'error'));
43
+ }
44
+
45
+ // ==========================================
46
+ // ANALYZE HANDLERS
47
+ // ==========================================
48
+
49
+ export async function testAnalyze(event) {
50
+ const btn = event.target.closest('.btn') || event.target.closest('.btn-icon-head');
51
+ const path = document.getElementById('analyze-path').value;
52
+
53
+ showLoading(btn, btn.innerHTML);
54
+ try {
55
+ const text = await analyzeProject(path);
56
+ lastAnalyzeResult = text;
57
+
58
+ const blob = new Blob([text], { type: 'text/plain' });
59
+ const url = window.URL.createObjectURL(blob);
60
+ const a = document.createElement('a');
61
+ a.href = url;
62
+ a.download = 'project.txt';
63
+ a.click();
64
+
65
+ showResponse('analyze-response', {
66
+ success: true,
67
+ message: 'File downloaded!',
68
+ files: text.split('\n').filter(l => l.includes('===== FILE:')).length,
69
+ size: (text.length / 1024).toFixed(2) + ' KB'
70
+ });
71
+ showToast('Đã download file', 'success');
72
+ } catch (err) {
73
+ showResponse('analyze-response', { error: err.message }, true);
74
+ showToast('Lỗi: ' + err.message, 'error');
75
+ }
76
+ resetButton(btn);
77
+ }
78
+
79
+ export async function copyAnalyzeResult(event) {
80
+ const copyBtn = event.target.closest('.btn-copy');
81
+ const copyIcon = document.getElementById('analyze-copy-icon');
82
+ const copyText = document.getElementById('analyze-copy-text');
83
+
84
+ if (!lastAnalyzeResult) {
85
+ const path = document.getElementById('analyze-path').value;
86
+ showLoading(copyBtn, copyBtn.innerHTML);
87
+ try {
88
+ lastAnalyzeResult = await analyzeProject(path);
89
+ } catch (err) {
90
+ showToast('Lỗi: ' + err.message, 'error');
91
+ resetButton(copyBtn);
92
+ return;
93
+ }
94
+ resetButton(copyBtn);
95
+ }
96
+
97
+ try {
98
+ await copyToClipboard(lastAnalyzeResult);
99
+ showCopiedState(copyBtn, copyIcon, copyText, '📋', 'Copy Text');
100
+ showToast('Đã copy project.txt', 'success');
101
+ } catch (err) {
102
+ showToast('Lỗi copy: ' + err.message, 'error');
103
+ }
104
+ }
105
+
106
+ // ==========================================
107
+ // EXECUTE HANDLERS
108
+ // ==========================================
109
+
110
+ export async function testExecute(event) {
111
+ const btn = event.target.closest('.btn');
112
+ const bashInput = document.getElementById('execute-bash');
113
+ const bash = bashInput.value;
114
+
115
+ if (!bash.trim()) {
116
+ showToast('Vui lòng nhập bash script', 'error');
117
+ return;
118
+ }
119
+
120
+ showLoading(btn, btn.innerHTML);
121
+ try {
122
+ const data = await executeScript(bash);
123
+ showResponse('execute-response', data, !data.success);
124
+ data.success ? showToast('Thực thi thành công', 'success') : showToast('Thực thi thất bại', 'error');
125
+ if (data.success) bashInput.value = '';
126
+ } catch (err) {
127
+ showResponse('execute-response', { error: err.message }, true);
128
+ showToast('Lỗi: ' + err.message, 'error');
129
+ }
130
+ resetButton(btn);
131
+ }
132
+
133
+ export async function executeFromClipboard(event) {
134
+ const btn = event.target.closest('.btn');
135
+ const bashInput = document.getElementById('execute-bash');
136
+
137
+ showLoading(btn, btn.innerHTML);
138
+ try {
139
+ const clipboardText = await readFromClipboard();
140
+ if (!clipboardText || !clipboardText.trim()) {
141
+ showToast('Clipboard trống!', 'error');
142
+ resetButton(btn);
143
+ return;
144
+ }
145
+ bashInput.value = clipboardText;
146
+ const data = await executeScript(clipboardText);
147
+ showResponse('execute-response', data, !data.success);
148
+
149
+ if (data.success) {
150
+ showToast('Thực thi OK', 'success');
151
+ bashInput.value = '';
152
+ } else {
153
+ data.syntaxError ? showToast('Lỗi syntax script', 'error') : showToast('Thực thi thất bại', 'error');
154
+ }
155
+ } catch (err) {
156
+ if (err.name === 'NotAllowedError') {
157
+ showToast('Không có quyền clipboard', 'error');
158
+ } else {
159
+ showResponse('execute-response', { error: err.message }, true);
160
+ showToast('Lỗi: ' + err.message, 'error');
161
+ }
162
+ }
163
+ resetButton(btn);
164
+ }
165
+
166
+ // ==========================================
167
+ // EXPORT TO WINDOW (GLOBAL)
168
+ // ==========================================
169
+
170
+ window.toggleSystemPrompt = toggleSystemPrompt;
171
+ window.copySystemPrompt = copySystemPrompt;
172
+ window.copySystemPromptFromHeader = copySystemPromptFromHeader;
173
+ window.testAnalyze = testAnalyze;
174
+ window.copyAnalyzeResult = copyAnalyzeResult;
175
+ window.testExecute = testExecute;
176
+ window.executeFromClipboard = executeFromClipboard;
177
+
178
+ // Map Structure handlers from feature module to window
179
+ window.testStructure = handleStructureView;
180
+ window.toggleFolder = handleToggleFolder;
181
+ window.handleCheckboxChange = handleCheckboxChange;
182
+ window.copySelectedStructure = handleCopySelected;
@@ -0,0 +1,72 @@
1
+ // Main entry point - Initialize application
2
+ import { SYSTEM_PROMPT } from './config.js';
3
+ import { checkHealth } from './api.js';
4
+ import './handlers.js'; // Import to register global functions
5
+
6
+ /**
7
+ * Initialize application on DOM ready
8
+ */
9
+ document.addEventListener('DOMContentLoaded', async () => {
10
+ // Load system prompt text
11
+ document.getElementById('prompt-text').textContent = SYSTEM_PROMPT;
12
+
13
+ // Check server status
14
+ await checkServerStatus();
15
+
16
+ // Initialize Theme
17
+ initTheme();
18
+ });
19
+
20
+ /**
21
+ * Check and update server status
22
+ */
23
+ async function checkServerStatus() {
24
+ const statusEl = document.getElementById('status');
25
+ const isHealthy = await checkHealth();
26
+
27
+ if (isHealthy) {
28
+ statusEl.textContent = '● Online';
29
+ statusEl.style.background = 'rgba(52, 199, 89, 0.15)';
30
+ statusEl.style.color = 'var(--ios-green)';
31
+ } else {
32
+ statusEl.textContent = '● Offline';
33
+ statusEl.style.background = 'rgba(255, 59, 48, 0.15)';
34
+ statusEl.style.color = 'var(--ios-red)';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Initialize Theme Logic
40
+ */
41
+ function initTheme() {
42
+ const themeBtn = document.getElementById('theme-toggle');
43
+ const themeIcon = document.getElementById('theme-icon');
44
+
45
+ // Get current theme from DOM (set by inline script) or localStorage
46
+ let currentTheme = localStorage.getItem('theme') || 'light';
47
+
48
+ // Update icon initially
49
+ updateThemeIcon(currentTheme);
50
+
51
+ themeBtn.addEventListener('click', () => {
52
+ // Toggle theme
53
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
54
+
55
+ // Update DOM
56
+ document.documentElement.setAttribute('data-theme', newTheme);
57
+ localStorage.setItem('theme', newTheme);
58
+
59
+ // Update local state
60
+ currentTheme = newTheme;
61
+ updateThemeIcon(newTheme);
62
+ });
63
+ }
64
+
65
+ function updateThemeIcon(theme) {
66
+ const themeIcon = document.getElementById('theme-icon');
67
+ if (theme === 'dark') {
68
+ themeIcon.textContent = '☀️';
69
+ } else {
70
+ themeIcon.textContent = '🌙';
71
+ }
72
+ }
@@ -0,0 +1,82 @@
1
+ // UI Utility Functions
2
+
3
+ /**
4
+ * Display a toast notification
5
+ * @param {string} message - Message to display
6
+ * @param {string} type - Type of toast: 'success', 'error', 'info'
7
+ */
8
+ export function showToast(message, type = 'success') {
9
+ const toast = document.getElementById('toast');
10
+ // Reset text content to remove potential icon junk
11
+ toast.textContent = message;
12
+ toast.className = `toast ${type}`;
13
+ toast.classList.add('show');
14
+
15
+ // Clear previous timeout if exists
16
+ if (toast.timeoutId) clearTimeout(toast.timeoutId);
17
+
18
+ toast.timeoutId = setTimeout(() => toast.classList.remove('show'), 3000);
19
+ }
20
+
21
+ /**
22
+ * Show loading state on button
23
+ * @param {HTMLElement} button - Button element
24
+ * @param {string} originalText - Original button HTML
25
+ */
26
+ export function showLoading(button, originalText) {
27
+ button.disabled = true;
28
+ button.innerHTML = '<span class="loading"></span>';
29
+ button.dataset.originalText = originalText;
30
+ }
31
+
32
+ /**
33
+ * Reset button to original state
34
+ * @param {HTMLElement} button - Button element
35
+ */
36
+ export function resetButton(button) {
37
+ button.disabled = false;
38
+ const originalText = button.dataset.originalText;
39
+ button.innerHTML = originalText;
40
+ }
41
+
42
+ /**
43
+ * Display API response in response area
44
+ * @param {string} elementId - ID of response element
45
+ * @param {Object} data - Response data
46
+ * @param {boolean} isError - Whether this is an error response
47
+ */
48
+ export function showResponse(elementId, data, isError = false) {
49
+ const el = document.getElementById(elementId);
50
+ el.className = 'response show ' + (isError ? 'error' : 'success');
51
+ el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
52
+ }
53
+
54
+ /**
55
+ * Update button state to show copied status
56
+ * @param {HTMLElement} button - Button element
57
+ * @param {HTMLElement} icon - Icon element
58
+ * @param {HTMLElement} text - Text element
59
+ * @param {string} originalIcon - Original icon text
60
+ * @param {string} originalText - Original button text
61
+ */
62
+ export function showCopiedState(button, icon, text, originalIcon, originalText) {
63
+ button.classList.add('copied');
64
+ icon.textContent = '✓';
65
+ text.textContent = 'Copied';
66
+
67
+ setTimeout(() => {
68
+ button.classList.remove('copied');
69
+ icon.textContent = originalIcon;
70
+ text.textContent = originalText;
71
+ }, 2000);
72
+ }
73
+
74
+ /**
75
+ * Format number with commas (handles undefined/null)
76
+ * @param {number} num - Number to format
77
+ * @returns {string} Formatted string
78
+ */
79
+ export function formatNumber(num) {
80
+ if (num === undefined || num === null) return '0';
81
+ return num.toLocaleString('en-US');
82
+ }
@@ -93,6 +93,58 @@ class TokenManager {
93
93
  };
94
94
  }
95
95
 
96
+ /**
97
+ * Phân tích và điền thông tin token vào cấu trúc cây thư mục
98
+ * Tính toán token cho từng file và tổng hợp cho folder
99
+ */
100
+ analyzeTree(tree, files) {
101
+ // Tạo map để lookup content file nhanh hơn
102
+ const fileMap = new Map();
103
+ files.forEach(f => {
104
+ if (f.content) {
105
+ fileMap.set(f.path, f.content);
106
+ }
107
+ });
108
+
109
+ const traverse = (node) => {
110
+ if (!node) return 0;
111
+
112
+ // Nếu là file
113
+ if (node.type === 'file') {
114
+ const content = fileMap.get(node.path);
115
+ // Nếu không có content (binary hoặc error), token là 0
116
+ node.tokens = content ? this.countTokens(content) : 0;
117
+ return node.tokens;
118
+ }
119
+
120
+ // Nếu là directory có con
121
+ if (node.children) {
122
+ let sum = 0;
123
+
124
+ // Sắp xếp: Folder trước, File sau
125
+ node.children.sort((a, b) => {
126
+ if (a.type === b.type) return a.name.localeCompare(b.name);
127
+ return a.type === 'directory' ? -1 : 1;
128
+ });
129
+
130
+ // Đệ quy tính tổng token của con
131
+ node.children.forEach(child => {
132
+ sum += traverse(child);
133
+ });
134
+
135
+ node.tokens = sum;
136
+ return sum;
137
+ }
138
+
139
+ // Nếu là node rỗng hoặc loại khác
140
+ node.tokens = 0;
141
+ return 0;
142
+ };
143
+
144
+ traverse(tree);
145
+ return tree;
146
+ }
147
+
96
148
  /**
97
149
  * Chia nhỏ content thành chunks
98
150
  */
@@ -247,8 +299,6 @@ class TokenManager {
247
299
  let currentTokens = 0;
248
300
  let chunkIndex = 0;
249
301
 
250
- // Không thêm header "Large File" nữa để UI sạch hơn
251
-
252
302
  for (const line of lines) {
253
303
  const lineTokens = this.countTokens(line + '\n');
254
304
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file