vg-coder-cli 2.0.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
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": {
@@ -106,6 +106,7 @@ class FileScanner {
106
106
 
107
107
  const node = {
108
108
  path: dirPath,
109
+ relativePath: relativePath,
109
110
  name: name,
110
111
  size: stats.size,
111
112
  type: stats.isDirectory() ? 'directory' : 'file',
@@ -148,47 +149,6 @@ class FileScanner {
148
149
  return node;
149
150
  }
150
151
 
151
- /**
152
- * Get regex for build directories and hidden files to ignore
153
- */
154
- getBuildIgnoreRegex() {
155
- // Create regex pattern for directories to ignore
156
- const ignorePatterns = [
157
- // Hidden directories (starting with .)
158
- '^\\..*',
159
-
160
- // Build directories
161
- '^build$',
162
- '^target$',
163
- '^dist$',
164
- '^out$',
165
- '^bin$',
166
-
167
- // Dependencies
168
- '^node_modules$',
169
- '^vendor$',
170
-
171
- // Temporary files
172
- '^tmp$',
173
- '^temp$',
174
-
175
- // Logs
176
- '^logs$',
177
- '^log$',
178
-
179
- // Coverage reports
180
- '^coverage$'
181
- ];
182
-
183
- // Combine all patterns into one regex
184
- const combinedPattern = ignorePatterns.join('|');
185
- return new RegExp(`(${combinedPattern})`);
186
- }
187
-
188
-
189
-
190
-
191
-
192
152
  /**
193
153
  * Trích xuất danh sách files từ tree
194
154
  */
@@ -381,69 +341,8 @@ class FileScanner {
381
341
  * Tạo nội dung kết hợp từ tất cả files
382
342
  */
383
343
  async createCombinedContent(files, options = {}) {
384
- const {
385
- includeStats = true,
386
- includeTree = true,
387
- headerTemplate = this.getDefaultHeaderTemplate(),
388
- separatorTemplate = this.getDefaultSeparatorTemplate()
389
- } = options;
390
-
391
- let content = '';
392
-
393
- // Header với thông tin project
394
- if (includeStats) {
395
- content += this.generateProjectHeader(files);
396
- content += '\n\n';
397
- }
398
-
399
- // Cấu trúc thư mục
400
- if (includeTree) {
401
- content += this.generateTreeStructure(files);
402
- content += '\n\n';
403
- }
404
-
405
- // Nội dung từng file
406
- for (let i = 0; i < files.length; i++) {
407
- const file = files[i];
408
-
409
- // Header cho file
410
- content += headerTemplate
411
- .replace('{path}', file.relativePath)
412
- .replace('{name}', file.name)
413
- .replace('{extension}', file.extension || '')
414
- .replace('{size}', file.size)
415
- .replace('{lines}', file.lines);
416
-
417
- content += '\n';
418
- content += file.content;
419
- content += '\n';
420
-
421
- // Separator giữa các files
422
- if (i < files.length - 1) {
423
- content += separatorTemplate;
424
- content += '\n';
425
- }
426
- }
427
-
428
- return content;
429
- }
430
-
431
- /**
432
- * Template header mặc định cho file
433
- */
434
- getDefaultHeaderTemplate() {
435
- return `
436
- ================================================================================
437
- File: {path}
438
- Size: {size} bytes | Lines: {lines}
439
- ================================================================================`;
440
- }
441
-
442
- /**
443
- * Template separator mặc định
444
- */
445
- getDefaultSeparatorTemplate() {
446
- return '\n\n';
344
+ // ... (unchanged)
345
+ return this.createCombinedContentForAI(files, options);
447
346
  }
448
347
 
449
348
  /**
@@ -29,8 +29,8 @@ class ApiServer {
29
29
  */
30
30
  setupMiddleware() {
31
31
  this.app.use(cors());
32
- this.app.use(bodyParser.json());
33
- this.app.use(bodyParser.urlencoded({ extended: true }));
32
+ this.app.use(bodyParser.json({ limit: '50mb' })); // Increase limit for large file lists
33
+ this.app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
34
34
 
35
35
  // Serve static files from views directory (CSS, JS)
36
36
  this.app.use(express.static(path.join(__dirname, 'views')));
@@ -63,7 +63,7 @@ class ApiServer {
63
63
  // Analyze endpoint - returns project.txt file
64
64
  this.app.post('/api/analyze', async (req, res) => {
65
65
  try {
66
- const { path: projectPath, options = {} } = req.body;
66
+ const { path: projectPath, options = {}, specificFiles } = req.body;
67
67
 
68
68
  if (!projectPath) {
69
69
  return res.status(400).json({
@@ -81,6 +81,9 @@ class ApiServer {
81
81
  }
82
82
 
83
83
  console.log(chalk.yellow(`Analyzing project: ${resolvedPath}`));
84
+ if (specificFiles) {
85
+ console.log(chalk.yellow(`Filtering for ${specificFiles.length} specific files`));
86
+ }
84
87
 
85
88
  // Detect project type
86
89
  const detector = new ProjectDetector(resolvedPath);
@@ -95,8 +98,15 @@ class ApiServer {
95
98
  const scanner = new FileScanner(resolvedPath, scannerOptions);
96
99
  const scanResult = await scanner.scanProject();
97
100
 
101
+ let filesToProcess = scanResult.files;
102
+
103
+ // Filter specific files if requested
104
+ if (specificFiles && Array.isArray(specificFiles) && specificFiles.length > 0) {
105
+ filesToProcess = filesToProcess.filter(file => specificFiles.includes(file.relativePath));
106
+ }
107
+
98
108
  // Create AI-friendly content
99
- const aiContent = await scanner.createCombinedContentForAI(scanResult.files, {
109
+ const aiContent = await scanner.createCombinedContentForAI(filesToProcess, {
100
110
  includeStats: true,
101
111
  includeTree: true,
102
112
  preserveLineNumbers: true
@@ -107,7 +117,7 @@ class ApiServer {
107
117
  res.setHeader('Content-Disposition', 'attachment; filename="project.txt"');
108
118
  res.send(aiContent);
109
119
 
110
- console.log(chalk.green(`✓ Analysis completed: ${scanResult.files.length} files`));
120
+ console.log(chalk.green(`✓ Analysis completed: ${filesToProcess.length} files`));
111
121
 
112
122
  } catch (error) {
113
123
  console.error(chalk.red('Error during analysis:'), error);
@@ -181,6 +191,50 @@ class ApiServer {
181
191
  }
182
192
  });
183
193
 
194
+ // Structure endpoint
195
+ this.app.get('/api/structure', async (req, res) => {
196
+ try {
197
+ const projectPath = req.query.path || '.';
198
+ const resolvedPath = path.resolve(projectPath);
199
+
200
+ // Validate path
201
+ if (!await fs.pathExists(resolvedPath)) {
202
+ return res.status(404).json({
203
+ error: `Project path does not exist: ${projectPath}`
204
+ });
205
+ }
206
+
207
+ console.log(chalk.yellow(`Analyzing structure for: ${resolvedPath}`));
208
+
209
+ // 1. Scan files
210
+ const scanner = new FileScanner(resolvedPath);
211
+ const scanResult = await scanner.scanProject();
212
+
213
+ // 2. Tokenize tree
214
+ const tokenManager = new TokenManager();
215
+ const enrichedTree = tokenManager.analyzeTree(scanResult.tree, scanResult.files);
216
+
217
+ tokenManager.cleanup();
218
+
219
+ // 3. Return result
220
+ res.json({
221
+ path: resolvedPath,
222
+ totalFiles: scanResult.files.length,
223
+ rootTokens: enrichedTree.tokens,
224
+ structure: enrichedTree
225
+ });
226
+
227
+ console.log(chalk.green(`✓ Structure analysis completed: ${scanResult.files.length} files`));
228
+
229
+ } catch (error) {
230
+ console.error(chalk.red('Error getting structure:'), error);
231
+ res.status(500).json({
232
+ error: 'Failed to get structure',
233
+ message: error.message
234
+ });
235
+ }
236
+ });
237
+
184
238
  // Clean endpoint
185
239
  this.app.delete('/api/clean', async (req, res) => {
186
240
  try {
@@ -285,23 +339,10 @@ class ApiServer {
285
339
  console.log(chalk.green(`\n🚀 VG Coder API Server started!`));
286
340
  console.log(chalk.blue(`📡 Listening on: http://localhost:${this.port}`));
287
341
  console.log(chalk.cyan(`\n🎨 Dashboard: http://localhost:${this.port}`));
288
- console.log(chalk.yellow(`\n📚 Available endpoints:`));
289
- console.log(` GET /health - Health check`);
290
- console.log(` POST /api/analyze - Analyze project (returns project.txt)`);
291
- console.log(` GET /api/info?path=. - Get project info`);
292
- console.log(` DELETE /api/clean - Clean output directory`);
293
- console.log(` POST /api/execute - Execute bash script`);
294
- console.log(chalk.gray(`\n💡 Press Ctrl+C to stop the server\n`));
295
342
  resolve();
296
343
  });
297
344
 
298
345
  this.server.on('error', (err) => {
299
- if (err.code === 'EADDRINUSE') {
300
- console.error(chalk.red(`\n❌ Port ${this.port} is already in use!`));
301
- console.log(chalk.yellow(`Try using a different port with: vg start -p <port>\n`));
302
- } else {
303
- console.error(chalk.red('\n❌ Server error:'), err.message);
304
- }
305
346
  reject(err);
306
347
  });
307
348
  });
@@ -0,0 +1,122 @@
1
+ /* Tree View Styles */
2
+ .tree-container {
3
+ margin-top: 20px;
4
+ background: var(--ios-input-bg);
5
+ border-radius: 12px;
6
+ overflow: hidden;
7
+ border: 1px solid rgba(0,0,0,0.05);
8
+ }
9
+
10
+ .tree-header {
11
+ padding: 12px 16px;
12
+ background: rgba(0,0,0,0.03);
13
+ border-bottom: 1px solid rgba(0,0,0,0.05);
14
+ font-weight: 600;
15
+ display: flex;
16
+ justify-content: space-between;
17
+ align-items: center;
18
+ color: var(--text-primary);
19
+ }
20
+
21
+ .tree-content {
22
+ padding: 10px;
23
+ font-family: 'SF Mono', 'Menlo', monospace;
24
+ font-size: 13px;
25
+ overflow-x: auto;
26
+ }
27
+
28
+ .tree-ul {
29
+ list-style: none;
30
+ padding-left: 20px;
31
+ margin: 0;
32
+ border-left: 1px solid var(--ios-separator);
33
+ }
34
+
35
+ /* Root level shouldn't have border */
36
+ .tree-content > .tree-ul {
37
+ border-left: none;
38
+ padding-left: 0;
39
+ }
40
+
41
+ .tree-li {
42
+ margin: 4px 0;
43
+ position: relative;
44
+ }
45
+
46
+ .tree-item-row {
47
+ display: flex;
48
+ align-items: center;
49
+ padding: 4px 8px;
50
+ border-radius: 6px;
51
+ cursor: pointer;
52
+ transition: background 0.1s;
53
+ }
54
+
55
+ .tree-item-row:hover {
56
+ background: rgba(0,0,0,0.05);
57
+ }
58
+
59
+ /* Checkbox Style */
60
+ .tree-checkbox {
61
+ margin-right: 8px;
62
+ cursor: pointer;
63
+ width: 16px;
64
+ height: 16px;
65
+ accent-color: var(--ios-blue);
66
+ }
67
+
68
+ .tree-icon {
69
+ margin-right: 8px;
70
+ font-size: 14px;
71
+ width: 16px;
72
+ text-align: center;
73
+ }
74
+
75
+ .tree-name {
76
+ flex: 1;
77
+ white-space: nowrap;
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ margin-right: 10px;
81
+ color: var(--text-primary);
82
+ }
83
+
84
+ /* Token Badges */
85
+ .token-badge {
86
+ font-size: 11px;
87
+ padding: 2px 6px;
88
+ border-radius: 4px;
89
+ font-weight: 600;
90
+ min-width: 40px;
91
+ text-align: center;
92
+ }
93
+
94
+ .token-low { background: rgba(52, 199, 89, 0.15); color: var(--ios-green); } /* < 2k */
95
+ .token-med { background: rgba(255, 149, 0, 0.15); color: #FF9500; } /* 2k - 5k */
96
+ .token-high { background: rgba(255, 59, 48, 0.15); color: var(--ios-red); } /* > 5k */
97
+
98
+ /* Folder toggle states */
99
+ .tree-li > .tree-ul {
100
+ display: block; /* Default expanded */
101
+ }
102
+
103
+ .tree-li.collapsed > .tree-ul {
104
+ display: none;
105
+ }
106
+
107
+ .arrow {
108
+ display: inline-block;
109
+ width: 12px;
110
+ font-size: 10px;
111
+ color: var(--text-secondary);
112
+ transition: transform 0.2s;
113
+ margin-right: 4px;
114
+ }
115
+
116
+ .tree-li.collapsed > .tree-item-row .arrow {
117
+ transform: rotate(-90deg);
118
+ }
119
+
120
+ .tree-li:not(.has-children) .arrow {
121
+ visibility: hidden;
122
+ }
@@ -9,6 +9,7 @@
9
9
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
10
10
  <title>VG Coder API Dashboard</title>
11
11
  <link rel="stylesheet" href="/dashboard.css">
12
+ <link rel="stylesheet" href="/css/structure.css">
12
13
  <script>
13
14
  // Pre-load theme to prevent flash
14
15
  (function() {
@@ -45,7 +46,7 @@
45
46
  </div>
46
47
  <div class="system-prompt-content" id="system-prompt-content">
47
48
  <div class="prompt-text" id="prompt-text"></div>
48
- <button class="btn btn-copy" onclick="copySystemPrompt()">
49
+ <button class="btn btn-copy" onclick="copySystemPrompt(event)">
49
50
  <span id="copy-icon">📋</span>
50
51
  <span id="copy-text">Copy System Prompt</span>
51
52
  </button>
@@ -62,7 +63,7 @@
62
63
  <span class="endpoint-path">/api/analyze</span>
63
64
  </div>
64
65
  <!-- Right side: Download Icon -->
65
- <button class="btn-icon-head" onclick="testAnalyze()" title="Download Project Source">
66
+ <button class="btn-icon-head" onclick="testAnalyze(event)" title="Download Project Source">
66
67
  📥
67
68
  </button>
68
69
  </div>
@@ -73,7 +74,7 @@
73
74
  </div>
74
75
  <div class="btn-group">
75
76
  <!-- Big Download button removed as it's now in the header -->
76
- <button class="btn btn-copy" onclick="copyAnalyzeResult()">
77
+ <button class="btn btn-copy" onclick="copyAnalyzeResult(event)">
77
78
  <span id="analyze-copy-icon">📋</span>
78
79
  <span id="analyze-copy-text">Copy Text</span>
79
80
  </button>
@@ -97,17 +98,55 @@
97
98
  placeholder="mkdir -p src/test&#10;echo 'Hello' > src/test/hello.txt"></textarea>
98
99
  </div>
99
100
  <div class="btn-group">
100
- <button class="btn" onclick="testExecute()">
101
+ <button class="btn" onclick="testExecute(event)">
101
102
  <span>▶️</span>
102
103
  <span>Run Script</span>
103
104
  </button>
104
- <button class="btn" onclick="executeFromClipboard()">
105
+ <button class="btn" onclick="executeFromClipboard(event)">
105
106
  <span>📋</span>
106
107
  <span>Paste & Run</span>
107
108
  </button>
108
109
  </div>
109
110
  <div class="response" id="execute-response"></div>
110
111
  </div>
112
+
113
+ <!-- Structure & Tokens (NEW) -->
114
+ <div class="endpoint-card">
115
+ <div class="endpoint-header">
116
+ <div class="endpoint-title-group">
117
+ <span class="method get" style="background: var(--ios-blue); color: white;">GET</span>
118
+ <span class="endpoint-path">/api/structure</span>
119
+ </div>
120
+ <!-- Button Copy Selected -->
121
+ <button class="btn-icon-head" onclick="copySelectedStructure(event)" title="Copy Selected Tree">
122
+ 📋
123
+ </button>
124
+ </div>
125
+ <p class="endpoint-desc">Xem cây thư mục và phân tích số lượng Token.</p>
126
+ <div class="form-group">
127
+ <label>Path</label>
128
+ <input type="text" id="structure-path" value="." placeholder="Project path">
129
+ </div>
130
+ <div class="btn-group">
131
+ <button class="btn" onclick="testStructure(event)">
132
+ <span>🌳</span>
133
+ <span>View Structure</span>
134
+ </button>
135
+ <button class="btn btn-copy" onclick="copySelectedStructure(event)">
136
+ <span id="copy-structure-icon">📋</span>
137
+ <span id="copy-structure-text">Copy Selected</span>
138
+ </button>
139
+ </div>
140
+ <!-- Tree Container -->
141
+ <div class="tree-container" id="structure-tree" style="display: none;">
142
+ <div class="tree-header">
143
+ <span>Project Tree</span>
144
+ <span class="tree-total-tokens" id="total-tokens-badge">0 tokens</span>
145
+ </div>
146
+ <div class="tree-content" id="tree-content"></div>
147
+ </div>
148
+ <div class="response" id="structure-response" style="display: none;"></div>
149
+ </div>
111
150
  </div>
112
151
  </div>
113
152
 
@@ -4,13 +4,14 @@ import { API_BASE } from './config.js';
4
4
  /**
5
5
  * Analyze project and get source code
6
6
  * @param {string} path - Project path to analyze
7
+ * @param {Array<string>} specificFiles - Optional list of relative paths to filter
7
8
  * @returns {Promise<string>} - Project content as text
8
9
  */
9
- export async function analyzeProject(path) {
10
+ export async function analyzeProject(path, specificFiles = null) {
10
11
  const res = await fetch(`${API_BASE}/api/analyze`, {
11
12
  method: 'POST',
12
13
  headers: { 'Content-Type': 'application/json' },
13
- body: JSON.stringify({ path })
14
+ body: JSON.stringify({ path, specificFiles })
14
15
  });
15
16
 
16
17
  if (!res.ok) {
@@ -55,6 +56,22 @@ export async function checkHealth() {
55
56
  }
56
57
  }
57
58
 
59
+ /**
60
+ * Get project structure with tokens
61
+ * @param {string} path - Project path
62
+ * @returns {Promise<Object>} - Structure data
63
+ */
64
+ export async function getStructure(path) {
65
+ const res = await fetch(`${API_BASE}/api/structure?path=${encodeURIComponent(path)}`);
66
+
67
+ if (!res.ok) {
68
+ const data = await res.json();
69
+ throw new Error(data.error || 'Failed to get structure');
70
+ }
71
+
72
+ return await res.json();
73
+ }
74
+
58
75
  /**
59
76
  * Copy content as file to clipboard
60
77
  * @param {string} filename - Filename for the clipboard item
@@ -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
+ }
@@ -1,14 +1,16 @@
1
1
  // Event Handlers & Business Logic
2
2
  import { SYSTEM_PROMPT } from './config.js';
3
- import { analyzeProject, executeScript, copyAsFile, copyToClipboard, readFromClipboard } from './api.js';
3
+ import { analyzeProject, executeScript, copyToClipboard, readFromClipboard } from './api.js';
4
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';
5
7
 
6
- // State management
7
8
  let lastAnalyzeResult = null;
8
9
 
9
- /**
10
- * Toggle system prompt section
11
- */
10
+ // ==========================================
11
+ // SYSTEM PROMPT HANDLERS
12
+ // ==========================================
13
+
12
14
  export function toggleSystemPrompt() {
13
15
  const content = document.getElementById('system-prompt-content');
14
16
  const icon = document.getElementById('toggle-icon');
@@ -16,32 +18,20 @@ export function toggleSystemPrompt() {
16
18
  icon.classList.toggle('open');
17
19
  }
18
20
 
19
- /**
20
- * Copy system prompt from the Header Button
21
- * Stops propagation so the accordion doesn't toggle
22
- */
23
21
  export function copySystemPromptFromHeader(event) {
24
- event.stopPropagation(); // Stop accordion from toggling
25
-
26
- // Animate button
22
+ event.stopPropagation();
27
23
  const btn = event.currentTarget;
28
24
  btn.textContent = '✓';
29
-
30
25
  navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
31
26
  showToast('Đã copy System Prompt', 'success');
32
- setTimeout(() => {
33
- btn.textContent = '📋';
34
- }, 2000);
27
+ setTimeout(() => btn.textContent = '📋', 2000);
35
28
  }).catch(err => {
36
29
  showToast('Lỗi copy: ' + err.message, 'error');
37
30
  btn.textContent = '📋';
38
31
  });
39
32
  }
40
33
 
41
- /**
42
- * Copy system prompt to clipboard (Main content button)
43
- */
44
- export function copySystemPrompt() {
34
+ export function copySystemPrompt(event) {
45
35
  const copyBtn = event.target.closest('.btn-copy');
46
36
  const copyIcon = document.getElementById('copy-icon');
47
37
  const copyText = document.getElementById('copy-text');
@@ -49,16 +39,14 @@ export function copySystemPrompt() {
49
39
  navigator.clipboard.writeText(SYSTEM_PROMPT).then(() => {
50
40
  showCopiedState(copyBtn, copyIcon, copyText, '📋', 'Copy System Prompt');
51
41
  showToast('Đã copy System Prompt', 'success');
52
- }).catch(err => {
53
- showToast('Lỗi copy: ' + err.message, 'error');
54
- });
42
+ }).catch(err => showToast('Lỗi copy: ' + err.message, 'error'));
55
43
  }
56
44
 
57
- /**
58
- * Handle analyze button click
59
- */
60
- export async function testAnalyze() {
61
- // Support both the big button .btn and the header icon .btn-icon-head
45
+ // ==========================================
46
+ // ANALYZE HANDLERS
47
+ // ==========================================
48
+
49
+ export async function testAnalyze(event) {
62
50
  const btn = event.target.closest('.btn') || event.target.closest('.btn-icon-head');
63
51
  const path = document.getElementById('analyze-path').value;
64
52
 
@@ -67,7 +55,6 @@ export async function testAnalyze() {
67
55
  const text = await analyzeProject(path);
68
56
  lastAnalyzeResult = text;
69
57
 
70
- // Download file
71
58
  const blob = new Blob([text], { type: 'text/plain' });
72
59
  const url = window.URL.createObjectURL(blob);
73
60
  const a = document.createElement('a');
@@ -89,19 +76,14 @@ export async function testAnalyze() {
89
76
  resetButton(btn);
90
77
  }
91
78
 
92
- /**
93
- * Copy analyze result as text
94
- */
95
- export async function copyAnalyzeResult() {
79
+ export async function copyAnalyzeResult(event) {
96
80
  const copyBtn = event.target.closest('.btn-copy');
97
81
  const copyIcon = document.getElementById('analyze-copy-icon');
98
82
  const copyText = document.getElementById('analyze-copy-text');
99
83
 
100
84
  if (!lastAnalyzeResult) {
101
- // Fetch if not already analyzed
102
85
  const path = document.getElementById('analyze-path').value;
103
86
  showLoading(copyBtn, copyBtn.innerHTML);
104
-
105
87
  try {
106
88
  lastAnalyzeResult = await analyzeProject(path);
107
89
  } catch (err) {
@@ -112,7 +94,6 @@ export async function copyAnalyzeResult() {
112
94
  resetButton(copyBtn);
113
95
  }
114
96
 
115
- // Copy to clipboard
116
97
  try {
117
98
  await copyToClipboard(lastAnalyzeResult);
118
99
  showCopiedState(copyBtn, copyIcon, copyText, '📋', 'Copy Text');
@@ -122,10 +103,11 @@ export async function copyAnalyzeResult() {
122
103
  }
123
104
  }
124
105
 
125
- /**
126
- * Handle execute button click
127
- */
128
- export async function testExecute() {
106
+ // ==========================================
107
+ // EXECUTE HANDLERS
108
+ // ==========================================
109
+
110
+ export async function testExecute(event) {
129
111
  const btn = event.target.closest('.btn');
130
112
  const bashInput = document.getElementById('execute-bash');
131
113
  const bash = bashInput.value;
@@ -139,14 +121,8 @@ export async function testExecute() {
139
121
  try {
140
122
  const data = await executeScript(bash);
141
123
  showResponse('execute-response', data, !data.success);
142
-
143
- if (data.success) {
144
- showToast('Thực thi thành công', 'success');
145
- // Clear input on success
146
- bashInput.value = '';
147
- } else {
148
- showToast('Thực thi thất bại', 'error');
149
- }
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 = '';
150
126
  } catch (err) {
151
127
  showResponse('execute-response', { error: err.message }, true);
152
128
  showToast('Lỗi: ' + err.message, 'error');
@@ -154,44 +130,27 @@ export async function testExecute() {
154
130
  resetButton(btn);
155
131
  }
156
132
 
157
- /**
158
- * Execute script from clipboard
159
- */
160
- export async function executeFromClipboard() {
133
+ export async function executeFromClipboard(event) {
161
134
  const btn = event.target.closest('.btn');
162
135
  const bashInput = document.getElementById('execute-bash');
163
136
 
164
137
  showLoading(btn, btn.innerHTML);
165
-
166
138
  try {
167
139
  const clipboardText = await readFromClipboard();
168
-
169
140
  if (!clipboardText || !clipboardText.trim()) {
170
141
  showToast('Clipboard trống!', 'error');
171
- showResponse('execute-response', {
172
- error: 'Clipboard is empty',
173
- message: 'Please copy a bash script to clipboard first'
174
- }, true);
175
142
  resetButton(btn);
176
143
  return;
177
144
  }
178
-
179
- // Show what we are running
180
145
  bashInput.value = clipboardText;
181
-
182
146
  const data = await executeScript(clipboardText);
183
147
  showResponse('execute-response', data, !data.success);
184
-
148
+
185
149
  if (data.success) {
186
- showToast('Thực thi từ clipboard OK', 'success');
187
- // Clear input on success
150
+ showToast('Thực thi OK', 'success');
188
151
  bashInput.value = '';
189
152
  } else {
190
- if (data.syntaxError) {
191
- showToast('Lỗi syntax script', 'error');
192
- } else {
193
- showToast('Thực thi thất bại', 'error');
194
- }
153
+ data.syntaxError ? showToast('Lỗi syntax script', 'error') : showToast('Thực thi thất bại', 'error');
195
154
  }
196
155
  } catch (err) {
197
156
  if (err.name === 'NotAllowedError') {
@@ -204,7 +163,10 @@ export async function executeFromClipboard() {
204
163
  resetButton(btn);
205
164
  }
206
165
 
207
- // Make functions globally available for onclick handlers
166
+ // ==========================================
167
+ // EXPORT TO WINDOW (GLOBAL)
168
+ // ==========================================
169
+
208
170
  window.toggleSystemPrompt = toggleSystemPrompt;
209
171
  window.copySystemPrompt = copySystemPrompt;
210
172
  window.copySystemPromptFromHeader = copySystemPromptFromHeader;
@@ -212,3 +174,9 @@ window.testAnalyze = testAnalyze;
212
174
  window.copyAnalyzeResult = copyAnalyzeResult;
213
175
  window.testExecute = testExecute;
214
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;
@@ -70,3 +70,13 @@ export function showCopiedState(button, icon, text, originalIcon, originalText)
70
70
  text.textContent = originalText;
71
71
  }, 2000);
72
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