vg-coder-cli 2.0.25 → 2.0.27
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/.vg/tree-state.json +1 -7
- package/package.json +1 -1
- package/src/index.js +36 -0
- package/src/server/api-server.js +167 -31
- package/src/server/project-manager.js +353 -0
- package/src/server/terminal-manager.js +31 -3
- package/src/server/views/css/iframe.css +1 -0
- package/src/server/views/dashboard.css +138 -30
- package/src/server/views/dashboard.html +94 -11
- package/src/server/views/js/features/commands.js +3 -1
- package/src/server/views/js/features/iframe-manager.js +6 -2
- package/src/server/views/js/features/project-switcher.js +153 -0
- package/src/server/views/js/features/terminal.js +44 -6
- package/src/server/views/js/main.js +61 -0
- package/src/server/views/vg-coder/controller.js +397 -2
- package/vetgo-auto/chrome/src/controller.ts +75 -1
- package/vetgo-auto/chrome/src/utils/ai-domains.ts +33 -0
- package/vetgo-auto/chrome/src/utils/injector-script.ts +272 -0
- package/vetgo-auto/vg-coder.zip +0 -0
- package/vg-coder-cli-2.0.26.tgz +0 -0
- package/vg-coder-cli-2.0.27.tgz +0 -0
- package/vg-coder-cli-2.0.23.tgz +0 -0
- package/vg-coder-cli-2.0.24.tgz +0 -0
- package/vg-coder-cli-2.0.25.tgz +0 -0
|
@@ -60,20 +60,39 @@
|
|
|
60
60
|
<!-- CỘT TRÁI: Giao diện VG Coder -->
|
|
61
61
|
<div class="left-panel">
|
|
62
62
|
<div class="container">
|
|
63
|
+
<!-- Header Layout Redesign -->
|
|
63
64
|
<div class="header">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div class="project-
|
|
71
|
-
|
|
65
|
+
<!-- Top Row: Status, Switcher (Title), Actions -->
|
|
66
|
+
<div class="header-top-row">
|
|
67
|
+
<div class="header-left-group">
|
|
68
|
+
<span class="status" id="status" style="font-size: 14px;">●</span>
|
|
69
|
+
|
|
70
|
+
<!-- Project Switcher takes primary spot -->
|
|
71
|
+
<div class="project-switcher">
|
|
72
|
+
<select id="project-selector" class="project-selector" onchange="window.switchProject(this.value)" title="Switch Project">
|
|
73
|
+
<option value="">Loading...</option>
|
|
74
|
+
</select>
|
|
75
|
+
<span class="project-count" id="project-count">1 project</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Right Actions -->
|
|
80
|
+
<div class="header-actions">
|
|
81
|
+
<button class="stop-server-btn" id="stop-server-btn" onclick="window.stopServer()" title="Stop Server">
|
|
82
|
+
🛑
|
|
83
|
+
</button>
|
|
84
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle Dark Mode">
|
|
85
|
+
<span id="theme-icon">🌙</span>
|
|
86
|
+
</button>
|
|
72
87
|
</div>
|
|
73
88
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
|
|
90
|
+
<!-- Bottom Row: Meta Info (Path/Type) -->
|
|
91
|
+
<div class="header-bottom-row">
|
|
92
|
+
<div class="project-meta" id="project-meta">...</div>
|
|
93
|
+
<!-- Hidden element to keep JS happy -->
|
|
94
|
+
<div id="project-name" style="display: none;"></div>
|
|
95
|
+
</div>
|
|
77
96
|
</div>
|
|
78
97
|
|
|
79
98
|
<!-- Saved Commands Panel -->
|
|
@@ -324,6 +343,70 @@
|
|
|
324
343
|
</div>
|
|
325
344
|
</div>
|
|
326
345
|
|
|
346
|
+
<!-- VG Coder Parent Context Detector -->
|
|
347
|
+
<script>
|
|
348
|
+
// Listener for nested iframe detection
|
|
349
|
+
// When extension's controller.ts sends VG_CODER_PING, we reply with VG_CODER_PARENT
|
|
350
|
+
window.addEventListener('message', (event) => {
|
|
351
|
+
if (event.data?.type === 'VG_CODER_PING') {
|
|
352
|
+
// Confirm this is VG Coder parent context
|
|
353
|
+
event.source.postMessage({ type: 'VG_CODER_PARENT' }, '*');
|
|
354
|
+
console.log('📡 Responded to VG_CODER_PING from iframe:', event.origin);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
</script>
|
|
358
|
+
|
|
359
|
+
<!-- Embedded Mode Detection -->
|
|
360
|
+
<script>
|
|
361
|
+
// When dashboard is loaded with ?embedded=true (inside AI chat page),
|
|
362
|
+
// hide the AI tab and iframe to prevent nested structure
|
|
363
|
+
(function() {
|
|
364
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
365
|
+
const isEmbedded = urlParams.has('embedded');
|
|
366
|
+
|
|
367
|
+
if (isEmbedded) {
|
|
368
|
+
console.log('🎯 Embedded mode detected - hiding AI iframe tab');
|
|
369
|
+
|
|
370
|
+
// Wait for DOM to be ready
|
|
371
|
+
if (document.readyState === 'loading') {
|
|
372
|
+
document.addEventListener('DOMContentLoaded', hideAITab);
|
|
373
|
+
} else {
|
|
374
|
+
hideAITab();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function hideAITab() {
|
|
378
|
+
// Hide AI tab
|
|
379
|
+
const aiTab = document.getElementById('ai-tab');
|
|
380
|
+
if (aiTab) {
|
|
381
|
+
aiTab.style.display = 'none';
|
|
382
|
+
console.log('✅ AI tab hidden');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Hide AI iframe container
|
|
386
|
+
const aiContainer = document.querySelector('.ai-iframe-container');
|
|
387
|
+
if (aiContainer) {
|
|
388
|
+
aiContainer.style.display = 'none';
|
|
389
|
+
console.log('✅ AI iframe container hidden');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Switch to first editor tab if exists, or show monaco by default
|
|
393
|
+
const fileTabsContainer = document.getElementById('file-tabs-container');
|
|
394
|
+
if (fileTabsContainer && fileTabsContainer.children.length > 0) {
|
|
395
|
+
// Click first file tab
|
|
396
|
+
const firstTab = fileTabsContainer.children[0];
|
|
397
|
+
if (firstTab) firstTab.click();
|
|
398
|
+
} else {
|
|
399
|
+
// Show monaco editor as default
|
|
400
|
+
const monacoContainer = document.getElementById('monaco-container');
|
|
401
|
+
if (monacoContainer) {
|
|
402
|
+
monacoContainer.classList.remove('view-mode-hidden');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
})();
|
|
408
|
+
</script>
|
|
409
|
+
|
|
327
410
|
<!-- Smart Log Copy Utilities -->
|
|
328
411
|
<script src="/js/utils/log-utils.js"></script>
|
|
329
412
|
<script src="/js/utils/smart-copy-engine.js"></script>
|
|
@@ -22,6 +22,7 @@ async function loadSavedCommands() {
|
|
|
22
22
|
const response = await fetch('/api/commands/load');
|
|
23
23
|
const data = await response.json();
|
|
24
24
|
savedCommands = data.commands || [];
|
|
25
|
+
renderCommands(); // Re-render after loading
|
|
25
26
|
} catch (error) {
|
|
26
27
|
console.error('Failed to load commands:', error);
|
|
27
28
|
savedCommands = [];
|
|
@@ -214,6 +215,7 @@ window.editCommand = editCommand;
|
|
|
214
215
|
window.closeCommandModal = closeCommandModal;
|
|
215
216
|
window.deleteCommand = deleteCommand;
|
|
216
217
|
window.runSavedCommand = runSavedCommand;
|
|
218
|
+
window.loadSavedCommands = loadSavedCommands; // Export for project switching
|
|
217
219
|
|
|
218
220
|
// Setup form submit handler
|
|
219
221
|
if (typeof document !== 'undefined') {
|
|
@@ -225,4 +227,4 @@ if (typeof document !== 'undefined') {
|
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
export { openAddCommandModal, editCommand, deleteCommand, runSavedCommand };
|
|
230
|
+
export { openAddCommandModal, editCommand, deleteCommand, runSavedCommand, loadSavedCommands };
|
|
@@ -37,13 +37,17 @@ export function initIframeManager() {
|
|
|
37
37
|
const updateProvider = (providerId) => {
|
|
38
38
|
const provider = AI_PROVIDERS.find(p => p.id === providerId) || AI_PROVIDERS[0];
|
|
39
39
|
|
|
40
|
+
// Add VG Coder context parameter to prevent nested iframe injection
|
|
41
|
+
const urlWithParam = new URL(provider.url);
|
|
42
|
+
urlWithParam.searchParams.set('vg_coder_context', 'true');
|
|
43
|
+
|
|
40
44
|
// Reset iframe source to trigger reload
|
|
41
45
|
iframe.src = 'about:blank';
|
|
42
46
|
setTimeout(() => {
|
|
43
|
-
iframe.src =
|
|
47
|
+
iframe.src = urlWithParam.toString();
|
|
44
48
|
}, 50);
|
|
45
49
|
|
|
46
|
-
// Update placeholder link
|
|
50
|
+
// Update placeholder link (without the parameter for direct access)
|
|
47
51
|
if (placeholderLink) {
|
|
48
52
|
placeholderLink.href = provider.url;
|
|
49
53
|
placeholderLink.textContent = `Mở ${provider.name} tab mới ↗`;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Project Switcher - Multi-project management UI
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initialize project switcher
|
|
5
|
+
*/
|
|
6
|
+
export async function initProjectSwitcher() {
|
|
7
|
+
// Load initial project list
|
|
8
|
+
await loadProjects();
|
|
9
|
+
|
|
10
|
+
// Poll for project updates every 5 seconds
|
|
11
|
+
setInterval(loadProjects, 5000);
|
|
12
|
+
|
|
13
|
+
// Listen for socket events
|
|
14
|
+
setupSocketListeners();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load and display all projects
|
|
19
|
+
*/
|
|
20
|
+
async function loadProjects() {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch('/api/projects');
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
|
|
25
|
+
updateProjectSelector(data.projects, data.activeProjectId);
|
|
26
|
+
updateProjectCount(data.totalProjects);
|
|
27
|
+
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Failed to load projects:', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Update project selector dropdown
|
|
35
|
+
*/
|
|
36
|
+
function updateProjectSelector(projects, activeProjectId) {
|
|
37
|
+
const selector = document.getElementById('project-selector');
|
|
38
|
+
if (!selector) return;
|
|
39
|
+
|
|
40
|
+
// Clear existing options
|
|
41
|
+
selector.innerHTML = '';
|
|
42
|
+
|
|
43
|
+
// Add projects
|
|
44
|
+
projects.forEach(project => {
|
|
45
|
+
const option = document.createElement('option');
|
|
46
|
+
option.value = project.id;
|
|
47
|
+
option.textContent = project.name;
|
|
48
|
+
option.selected = project.id === activeProjectId;
|
|
49
|
+
selector.appendChild(option);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Update project count badge
|
|
55
|
+
*/
|
|
56
|
+
function updateProjectCount(count) {
|
|
57
|
+
const badge = document.getElementById('project-count');
|
|
58
|
+
if (!badge) return;
|
|
59
|
+
|
|
60
|
+
badge.textContent = `${count} project${count !== 1 ? 's' : ''}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Switch to a different project
|
|
65
|
+
*/
|
|
66
|
+
export async function switchProject(projectId) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch('/api/projects/switch', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({ projectId })
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
|
|
76
|
+
if (data.success) {
|
|
77
|
+
// Emit custom event for other components to react
|
|
78
|
+
const event = new CustomEvent('project-switched', {
|
|
79
|
+
detail: {
|
|
80
|
+
projectId,
|
|
81
|
+
projectName: data.project.name,
|
|
82
|
+
project: data.project
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
window.dispatchEvent(event);
|
|
86
|
+
|
|
87
|
+
// Show toast
|
|
88
|
+
showToast(`Switched to: ${data.project.name}`, 'success');
|
|
89
|
+
} else {
|
|
90
|
+
showToast('Failed to switch project', 'error');
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Failed to switch project:', error);
|
|
94
|
+
showToast('Error switching project', 'error');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Setup socket listeners for real-time updates
|
|
100
|
+
*/
|
|
101
|
+
function setupSocketListeners() {
|
|
102
|
+
if (typeof io === 'undefined') return;
|
|
103
|
+
|
|
104
|
+
const socket = io();
|
|
105
|
+
|
|
106
|
+
// New project registered
|
|
107
|
+
socket.on('project:registered', (data) => {
|
|
108
|
+
console.log('New project registered:', data);
|
|
109
|
+
loadProjects();
|
|
110
|
+
showToast(`New project joined: ${data.name}`, 'info');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Project switched
|
|
114
|
+
socket.on('project:switched', (data) => {
|
|
115
|
+
console.log('Project switched:', data);
|
|
116
|
+
loadProjects();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Project removed
|
|
120
|
+
socket.on('project:removed', (data) => {
|
|
121
|
+
console.log('Project removed:', data);
|
|
122
|
+
loadProjects();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Show toast notification
|
|
128
|
+
*/
|
|
129
|
+
function showToast(message, type = 'info') {
|
|
130
|
+
const toast = document.getElementById('toast');
|
|
131
|
+
if (!toast) return;
|
|
132
|
+
|
|
133
|
+
toast.textContent = message;
|
|
134
|
+
toast.className = 'toast show';
|
|
135
|
+
|
|
136
|
+
if (type === 'success') {
|
|
137
|
+
toast.style.background = '#28a745';
|
|
138
|
+
} else if (type === 'error') {
|
|
139
|
+
toast.style.background = '#dc3545';
|
|
140
|
+
} else if (type === 'warning') {
|
|
141
|
+
toast.style.background = '#ffc107';
|
|
142
|
+
toast.style.color = '#000';
|
|
143
|
+
} else {
|
|
144
|
+
toast.style.background = '#007bff';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
toast.classList.remove('show');
|
|
149
|
+
}, 3000);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Export for global access
|
|
153
|
+
window.switchProject = switchProject;
|
|
@@ -184,12 +184,33 @@ export function createNewTerminal() {
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
// 7.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
// 7. Get current project ID from API
|
|
188
|
+
let currentProjectId = null;
|
|
189
|
+
fetch('/api/projects')
|
|
190
|
+
.then(res => res.json())
|
|
191
|
+
.then(data => {
|
|
192
|
+
currentProjectId = data.activeProjectId;
|
|
193
|
+
|
|
194
|
+
// Store in session
|
|
195
|
+
activeTerminals.get(termId).projectId = currentProjectId;
|
|
196
|
+
|
|
197
|
+
// Init Backend Process with projectId
|
|
198
|
+
socket.emit('terminal:init', {
|
|
199
|
+
termId,
|
|
200
|
+
cols: term.cols,
|
|
201
|
+
rows: term.rows,
|
|
202
|
+
projectId: currentProjectId
|
|
203
|
+
});
|
|
204
|
+
})
|
|
205
|
+
.catch(err => {
|
|
206
|
+
console.error('Failed to get project info:', err);
|
|
207
|
+
// Fallback without projectId
|
|
208
|
+
socket.emit('terminal:init', {
|
|
209
|
+
termId,
|
|
210
|
+
cols: term.cols,
|
|
211
|
+
rows: term.rows
|
|
212
|
+
});
|
|
213
|
+
});
|
|
193
214
|
|
|
194
215
|
// 8. Initial token count update
|
|
195
216
|
setTimeout(() => updateTokenCounts(termId), 100);
|
|
@@ -477,6 +498,22 @@ function clearTerminal(termId) {
|
|
|
477
498
|
showToast('🗑️ Terminal cleared', 'info');
|
|
478
499
|
}
|
|
479
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Update terminal visibility based on active project
|
|
503
|
+
* @param {string} activeProjectId - Active project ID
|
|
504
|
+
*/
|
|
505
|
+
function updateTerminalVisibility(activeProjectId) {
|
|
506
|
+
activeTerminals.forEach((session, termId) => {
|
|
507
|
+
const shouldShow = !session.projectId || session.projectId === activeProjectId;
|
|
508
|
+
session.element.style.display = shouldShow ? 'block' : 'none';
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const visibleCount = Array.from(activeTerminals.values())
|
|
512
|
+
.filter(s => s.element.style.display !== 'none').length;
|
|
513
|
+
|
|
514
|
+
console.log(`Updated terminal visibility: ${visibleCount} visible for project ${activeProjectId}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
480
517
|
// Global Exports for HTML onclick
|
|
481
518
|
window.createNewTerminal = createNewTerminal;
|
|
482
519
|
window.closeTerminal = closeTerminalUI;
|
|
@@ -484,3 +521,4 @@ window.toggleMinimize = toggleMinimize;
|
|
|
484
521
|
window.toggleMaximize = toggleMaximize;
|
|
485
522
|
window.copyTerminalLog = copyTerminalLog;
|
|
486
523
|
window.clearTerminal = clearTerminal;
|
|
524
|
+
window.updateTerminalVisibility = updateTerminalVisibility;
|
|
@@ -10,6 +10,7 @@ import { initEditorTabs } from './features/editor-tabs.js';
|
|
|
10
10
|
import { initMonaco, updateMonacoTheme } from './features/monaco-manager.js';
|
|
11
11
|
import { initResizeHandler } from './features/resize.js';
|
|
12
12
|
import { initSavedCommands } from './features/commands.js';
|
|
13
|
+
import { initProjectSwitcher } from './features/project-switcher.js';
|
|
13
14
|
|
|
14
15
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
15
16
|
// Load system prompt text
|
|
@@ -47,12 +48,49 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
47
48
|
// Initialize Saved Commands
|
|
48
49
|
initSavedCommands();
|
|
49
50
|
|
|
51
|
+
// Initialize Project Switcher
|
|
52
|
+
await initProjectSwitcher();
|
|
53
|
+
|
|
50
54
|
// Set default tab to AI Assistant
|
|
51
55
|
if (window.switchTab) {
|
|
52
56
|
window.switchTab('ai-assistant');
|
|
53
57
|
}
|
|
54
58
|
});
|
|
55
59
|
|
|
60
|
+
// Global event handler for project switches
|
|
61
|
+
window.addEventListener('project-switched', async (event) => {
|
|
62
|
+
const { projectId, projectName, project } = event.detail;
|
|
63
|
+
console.log(`Project switched to: ${projectName}`);
|
|
64
|
+
|
|
65
|
+
// Reload project info
|
|
66
|
+
await loadProjectInfo();
|
|
67
|
+
|
|
68
|
+
// Filter terminal visibility (show only terminals for active project)
|
|
69
|
+
if (window.updateTerminalVisibility) {
|
|
70
|
+
window.updateTerminalVisibility(projectId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reload saved commands for new project
|
|
74
|
+
if (window.loadSavedCommands) {
|
|
75
|
+
await window.loadSavedCommands();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reset tree view (hide it)
|
|
79
|
+
const treeContainer = document.getElementById('structure-tree');
|
|
80
|
+
if (treeContainer) {
|
|
81
|
+
treeContainer.style.display = 'none';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear tree content
|
|
85
|
+
const treeContent = document.getElementById('tree-content');
|
|
86
|
+
if (treeContent) {
|
|
87
|
+
treeContent.innerHTML = '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// TODO: Refresh other context-dependent components
|
|
91
|
+
// - Git view refresh
|
|
92
|
+
});
|
|
93
|
+
|
|
56
94
|
async function checkServerStatus() {
|
|
57
95
|
const statusEl = document.getElementById('status');
|
|
58
96
|
const isHealthy = await checkHealth();
|
|
@@ -173,3 +211,26 @@ window.copyChromeUrl = function(event) {
|
|
|
173
211
|
}, 2000);
|
|
174
212
|
});
|
|
175
213
|
}
|
|
214
|
+
|
|
215
|
+
window.stopServer = async function() {
|
|
216
|
+
if (!confirm('Are you sure you want to stop the server?')) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await fetch('/api/shutdown', { method: 'POST' });
|
|
222
|
+
showToast('Server stopped successfully', 'success');
|
|
223
|
+
|
|
224
|
+
// Show a message that server is stopped
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
document.body.innerHTML = `
|
|
227
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; flex-direction: column; gap: 20px;">
|
|
228
|
+
<h2>🛑 Server Stopped</h2>
|
|
229
|
+
<p>You can close this tab now.</p>
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
}, 1000);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Failed to stop server:', error);
|
|
235
|
+
}
|
|
236
|
+
}
|