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 +1 -1
- package/src/scanner/file-scanner.js +3 -104
- package/src/server/api-server.js +59 -18
- package/src/server/views/css/structure.css +122 -0
- package/src/server/views/dashboard.html +44 -5
- package/src/server/views/js/api.js +19 -2
- package/src/server/views/js/features/structure.js +221 -0
- package/src/server/views/js/handlers.js +38 -70
- package/src/server/views/js/utils.js +10 -0
- package/src/tokenizer/token-manager.js +52 -2
- package/vg-coder-cli-2.0.8.tgz +0 -0
- package/vg-coder-cli-2.0.9.tgz +0 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
385
|
-
|
|
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
|
/**
|
package/src/server/api-server.js
CHANGED
|
@@ -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(
|
|
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: ${
|
|
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 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,
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
187
|
-
// Clear input on success
|
|
150
|
+
showToast('Thực thi OK', 'success');
|
|
188
151
|
bashInput.value = '';
|
|
189
152
|
} else {
|
|
190
|
-
|
|
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
|
-
//
|
|
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
|
|
package/vg-coder-cli-2.0.8.tgz
CHANGED
|
Binary file
|
|
Binary file
|