vg-coder-cli 2.0.41 → 2.0.42
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/dist/vg-coder-bundle.js +3 -3
- package/package.json +1 -1
- package/src/server/views/css/agent-panel.css +358 -0
- package/src/server/views/css/browser-panel.css +97 -0
- package/src/server/views/css/tool-window.css +70 -0
- package/src/server/views/dashboard.css +3 -0
- package/src/server/views/dashboard.html +53 -0
- package/src/server/views/js/features/agent-panel.js +586 -0
- package/src/server/views/js/features/browser-panel.js +130 -0
- package/src/server/views/js/features/keyboard-shortcuts.js +1 -1
- package/src/server/views/js/features/resize.js +102 -0
- package/src/server/views/js/features/tool-window.js +176 -14
- package/src/server/views/js/main.js +4 -0
- package/src/server/views/js/utils/chat-history-manager.js +237 -0
- package/src/server/views/vg-coder/controller.js +3 -3
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Panel - AI Chat Interface
|
|
3
|
+
* Provides chat UI with markdown rendering, file upload, and history management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getById } from '../utils.js';
|
|
7
|
+
import * as ChatHistory from '../utils/chat-history-manager.js';
|
|
8
|
+
// Import markdown-it and mermaid from npm packages (bundled by webpack)
|
|
9
|
+
import markdownit from 'markdown-it';
|
|
10
|
+
import mermaid from 'mermaid';
|
|
11
|
+
|
|
12
|
+
// State
|
|
13
|
+
let messages = [];
|
|
14
|
+
let selectedFiles = [];
|
|
15
|
+
let isProcessing = false;
|
|
16
|
+
let currentChatId = null;
|
|
17
|
+
let autoSaveTimeout = null;
|
|
18
|
+
let md = null; // markdown-it instance
|
|
19
|
+
|
|
20
|
+
// Initialize markdown-it with safe settings
|
|
21
|
+
function initMarkdown() {
|
|
22
|
+
if (md) return md;
|
|
23
|
+
|
|
24
|
+
md = markdownit({
|
|
25
|
+
html: false, // Disable HTML tags for security
|
|
26
|
+
breaks: true, // Convert line breaks to <br>
|
|
27
|
+
linkify: true, // Auto-convert URLs to links
|
|
28
|
+
typographer: true // Smart quotes
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log('[AgentPanel] markdown-it initialized');
|
|
32
|
+
return md;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Initialize mermaid with dark theme
|
|
36
|
+
function initMermaid() {
|
|
37
|
+
try {
|
|
38
|
+
mermaid.initialize({
|
|
39
|
+
startOnLoad: false, // Manual trigger
|
|
40
|
+
theme: 'dark', // Dark theme
|
|
41
|
+
securityLevel: 'loose', // Allow shadow DOM interaction
|
|
42
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
|
|
43
|
+
});
|
|
44
|
+
console.log('[AgentPanel] mermaid initialized');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[AgentPanel] Failed to initialize mermaid:', error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize Agent Panel
|
|
52
|
+
*/
|
|
53
|
+
export function initAgentPanel() {
|
|
54
|
+
// Initialize libraries immediately
|
|
55
|
+
initMarkdown();
|
|
56
|
+
initMermaid();
|
|
57
|
+
|
|
58
|
+
// Listen for panel open event
|
|
59
|
+
document.addEventListener('tool-panel-opened', (e) => {
|
|
60
|
+
if (e.detail.panelId === 'agent' && e.detail.side === 'right') {
|
|
61
|
+
renderAgentPanel();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log('[AgentPanel] Initialized');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render Agent Panel UI
|
|
70
|
+
*/
|
|
71
|
+
async function renderAgentPanel() {
|
|
72
|
+
const container = getById('agent-panel-content');
|
|
73
|
+
if (!container) {
|
|
74
|
+
console.error('[AgentPanel] Container not found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if already rendered
|
|
79
|
+
if (container.querySelector('.agent-chat-messages')) {
|
|
80
|
+
console.log('[AgentPanel] Already rendered');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create UI
|
|
85
|
+
container.innerHTML = `
|
|
86
|
+
<div class="agent-chat-messages" id="agent-messages"></div>
|
|
87
|
+
|
|
88
|
+
<div class="agent-chat-input-area">
|
|
89
|
+
<div class="agent-input-wrapper" id="agent-input-wrapper">
|
|
90
|
+
<div class="agent-file-list" id="agent-file-list"></div>
|
|
91
|
+
<textarea
|
|
92
|
+
class="agent-chat-textarea"
|
|
93
|
+
id="agent-chat-input"
|
|
94
|
+
placeholder="Nhập tin nhắn..."
|
|
95
|
+
></textarea>
|
|
96
|
+
<div class="agent-input-controls">
|
|
97
|
+
<div class="agent-input-actions">
|
|
98
|
+
<input id="agent-file-input" type="file" multiple style="display: none" />
|
|
99
|
+
<button class="agent-btn" id="agent-file-btn" title="Attach files">
|
|
100
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
101
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
102
|
+
</svg>
|
|
103
|
+
</button>
|
|
104
|
+
<button class="agent-btn" id="agent-clear-btn" title="Clear chat">
|
|
105
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
106
|
+
<polyline points="3 6 5 6 21 6"></polyline>
|
|
107
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
108
|
+
</svg>
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
<button class="agent-send-btn" id="agent-send-btn" title="Send message">
|
|
112
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
113
|
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
114
|
+
<polyline points="5 12 12 5 19 12"></polyline>
|
|
115
|
+
</svg>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// Attach event listeners
|
|
123
|
+
attachEventListeners();
|
|
124
|
+
|
|
125
|
+
// Load last chat or start fresh
|
|
126
|
+
await initializeChatSession();
|
|
127
|
+
|
|
128
|
+
// Render messages
|
|
129
|
+
renderMessages();
|
|
130
|
+
|
|
131
|
+
console.log('[AgentPanel] Rendered successfully');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attach event listeners
|
|
136
|
+
*/
|
|
137
|
+
function attachEventListeners() {
|
|
138
|
+
// Send button
|
|
139
|
+
const sendBtn = getById('agent-send-btn');
|
|
140
|
+
if (sendBtn) {
|
|
141
|
+
sendBtn.addEventListener('click', () => {
|
|
142
|
+
if (!isProcessing) sendMessage();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Input enter key
|
|
147
|
+
const input = getById('agent-chat-input');
|
|
148
|
+
if (input) {
|
|
149
|
+
input.addEventListener('keydown', (e) => {
|
|
150
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
if (!isProcessing) sendMessage();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Auto-expand textarea
|
|
157
|
+
input.addEventListener('input', function() {
|
|
158
|
+
this.style.height = '40px';
|
|
159
|
+
if (this.scrollHeight > 40) {
|
|
160
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// File button
|
|
166
|
+
const fileBtn = getById('agent-file-btn');
|
|
167
|
+
const fileInput = getById('agent-file-input');
|
|
168
|
+
if (fileBtn && fileInput) {
|
|
169
|
+
fileBtn.addEventListener('click', () => fileInput.click());
|
|
170
|
+
fileInput.addEventListener('change', (e) => {
|
|
171
|
+
handleAddFiles(e.target.files);
|
|
172
|
+
fileInput.value = '';
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Clear button
|
|
177
|
+
const clearBtn = getById('agent-clear-btn');
|
|
178
|
+
if (clearBtn) {
|
|
179
|
+
clearBtn.addEventListener('click', handleClearChat);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Drag & drop
|
|
183
|
+
const dropZone = getById('agent-input-wrapper');
|
|
184
|
+
if (dropZone) {
|
|
185
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
186
|
+
dropZone.addEventListener(eventName, (e) => {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
}, false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
dropZone.addEventListener('dragover', () => {
|
|
193
|
+
if (!isProcessing) dropZone.classList.add('drag-active');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
dropZone.addEventListener('dragleave', () => {
|
|
197
|
+
dropZone.classList.remove('drag-active');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
dropZone.addEventListener('drop', (e) => {
|
|
201
|
+
dropZone.classList.remove('drag-active');
|
|
202
|
+
if (isProcessing) return;
|
|
203
|
+
handleAddFiles(e.dataTransfer.files);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Initialize chat session
|
|
210
|
+
*/
|
|
211
|
+
async function initializeChatSession() {
|
|
212
|
+
// Try to load last chat
|
|
213
|
+
const lastChatId = ChatHistory.getLastChatId();
|
|
214
|
+
|
|
215
|
+
if (lastChatId) {
|
|
216
|
+
console.log(`[AgentPanel] Loading last chat: ${lastChatId}`);
|
|
217
|
+
currentChatId = lastChatId;
|
|
218
|
+
|
|
219
|
+
const chatData = ChatHistory.loadChat(currentChatId);
|
|
220
|
+
if (chatData && chatData.messages) {
|
|
221
|
+
messages = chatData.messages;
|
|
222
|
+
console.log(`[AgentPanel] Loaded ${messages.length} messages`);
|
|
223
|
+
} else {
|
|
224
|
+
messages = [];
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
console.log('[AgentPanel] Starting fresh chat');
|
|
228
|
+
messages = [];
|
|
229
|
+
currentChatId = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Render messages
|
|
235
|
+
*/
|
|
236
|
+
function renderMessages() {
|
|
237
|
+
const container = getById('agent-messages');
|
|
238
|
+
if (!container) return;
|
|
239
|
+
|
|
240
|
+
if (messages.length === 0) {
|
|
241
|
+
container.innerHTML = `
|
|
242
|
+
<div class="agent-chat-empty">
|
|
243
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
244
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
|
245
|
+
</svg>
|
|
246
|
+
<span>Kéo thả file hoặc nhập tin nhắn</span>
|
|
247
|
+
</div>
|
|
248
|
+
`;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
container.innerHTML = messages.map(msg => {
|
|
253
|
+
const isUser = msg.role === 'user';
|
|
254
|
+
const contentHtml = isUser ? escapeHtml(msg.content) : md.render(msg.content);
|
|
255
|
+
|
|
256
|
+
return `
|
|
257
|
+
<div class="agent-message ${msg.role}">
|
|
258
|
+
<div class="agent-message-content">
|
|
259
|
+
<div class="markdown-body">${contentHtml}</div>
|
|
260
|
+
<div class="agent-message-meta">
|
|
261
|
+
<span>${msg.timestamp}</span>
|
|
262
|
+
<span>${getStatusIcon(msg.status, msg.role)}</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
`;
|
|
267
|
+
}).join('');
|
|
268
|
+
|
|
269
|
+
// Process mermaid diagrams
|
|
270
|
+
processMermaidDiagrams(container);
|
|
271
|
+
|
|
272
|
+
// Scroll to bottom
|
|
273
|
+
container.scrollTop = container.scrollHeight;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Process mermaid diagrams
|
|
278
|
+
* Uses mermaid.render() which is more compatible with shadow DOM
|
|
279
|
+
*/
|
|
280
|
+
async function processMermaidDiagrams(container) {
|
|
281
|
+
const mermaidCodeBlocks = container.querySelectorAll('code.language-mermaid');
|
|
282
|
+
if (mermaidCodeBlocks.length === 0) return;
|
|
283
|
+
|
|
284
|
+
console.log(`[AgentPanel] Found ${mermaidCodeBlocks.length} mermaid diagram(s)`);
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < mermaidCodeBlocks.length; i++) {
|
|
287
|
+
const codeBlock = mermaidCodeBlocks[i];
|
|
288
|
+
const code = codeBlock.textContent;
|
|
289
|
+
const preElement = codeBlock.parentElement;
|
|
290
|
+
|
|
291
|
+
if (!preElement || preElement.tagName !== 'PRE') {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Use mermaid.render() which is more compatible with shadow DOM
|
|
297
|
+
const id = `agent-mermaid-${Date.now()}-${i}`;
|
|
298
|
+
const { svg, bindFunctions } = await mermaid.render(id, code);
|
|
299
|
+
|
|
300
|
+
// Create wrapper div for mermaid
|
|
301
|
+
const wrapper = document.createElement('div');
|
|
302
|
+
wrapper.className = 'agent-mermaid';
|
|
303
|
+
wrapper.innerHTML = svg;
|
|
304
|
+
|
|
305
|
+
// Replace pre/code with wrapper
|
|
306
|
+
preElement.replaceWith(wrapper);
|
|
307
|
+
|
|
308
|
+
// Bind any interactive functions if present
|
|
309
|
+
if (bindFunctions) {
|
|
310
|
+
bindFunctions(wrapper);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`[AgentPanel] Rendered mermaid diagram ${i + 1}`);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`[AgentPanel] Mermaid render error (diagram ${i + 1}):`, err);
|
|
316
|
+
|
|
317
|
+
// On error, show error message instead of broken diagram
|
|
318
|
+
const errorDiv = document.createElement('div');
|
|
319
|
+
errorDiv.className = 'agent-mermaid-error';
|
|
320
|
+
errorDiv.style.cssText = 'color: #ef4444; padding: 12px; background: #2a1515; border-radius: 8px; border: 1px solid #7f1d1d; margin: 8px 0;';
|
|
321
|
+
errorDiv.textContent = `Mermaid rendering error: ${err.message}`;
|
|
322
|
+
preElement.replaceWith(errorDiv);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Send message
|
|
329
|
+
*/
|
|
330
|
+
async function sendMessage() {
|
|
331
|
+
const input = getById('agent-chat-input');
|
|
332
|
+
const prompt = input.value.trim();
|
|
333
|
+
|
|
334
|
+
if (!prompt && selectedFiles.length === 0) return;
|
|
335
|
+
|
|
336
|
+
if (!window.AIChat) {
|
|
337
|
+
alert('❌ AIChat engine chưa được inject!');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Generate chat ID for first message
|
|
342
|
+
const isFirstMessage = messages.length === 0;
|
|
343
|
+
|
|
344
|
+
let userMsg = prompt;
|
|
345
|
+
if (selectedFiles.length > 0) {
|
|
346
|
+
userMsg += `\n\n📎 Files: ${selectedFiles.map(f => f.name).join(', ')}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
addMessage('user', userMsg, 'sending');
|
|
350
|
+
|
|
351
|
+
const payloadFiles = [...selectedFiles];
|
|
352
|
+
input.value = '';
|
|
353
|
+
selectedFiles = [];
|
|
354
|
+
renderFileList();
|
|
355
|
+
setProcessing(true);
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
updateLastMessage({ status: 'done' });
|
|
359
|
+
addMessage('assistant', '...', 'processing');
|
|
360
|
+
|
|
361
|
+
await window.AIChat.send({ prompt, files: payloadFiles });
|
|
362
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
363
|
+
|
|
364
|
+
const aiResponse = await window.AIChat.copyLastTurnAsMarkdown();
|
|
365
|
+
updateLastMessage({ content: aiResponse || '(AI không trả về nội dung)', status: 'done' });
|
|
366
|
+
|
|
367
|
+
// Create chat ID after first message
|
|
368
|
+
if (isFirstMessage && !currentChatId) {
|
|
369
|
+
currentChatId = ChatHistory.generateChatId();
|
|
370
|
+
console.log(`[AgentPanel] Created new chat: ${currentChatId}`);
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error('[AgentPanel] Error sending message:', error);
|
|
374
|
+
|
|
375
|
+
const errorHtml = `
|
|
376
|
+
<div class="agent-error-box">
|
|
377
|
+
<div class="agent-error-title">❌ Lỗi: ${escapeHtml(error.message)}</div>
|
|
378
|
+
<button onclick="window.retryAgentMessage()" class="agent-retry-btn">
|
|
379
|
+
🔄 Thử lại (Retry)
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
`;
|
|
383
|
+
updateLastMessage({ content: errorHtml, status: 'error' });
|
|
384
|
+
} finally {
|
|
385
|
+
setProcessing(false);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Retry message
|
|
391
|
+
*/
|
|
392
|
+
window.retryAgentMessage = async function() {
|
|
393
|
+
if (!window.AIChat) {
|
|
394
|
+
alert('❌ AIChat engine chưa sẵn sàng!');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log('[AgentPanel] Retrying...');
|
|
399
|
+
setProcessing(true);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
updateLastMessage({ content: '🔄 Đang thử lại...', status: 'processing' });
|
|
403
|
+
|
|
404
|
+
const aiResponse = await window.AIChat.copyLastTurnAsMarkdown();
|
|
405
|
+
updateLastMessage({ content: aiResponse || '(AI không trả về nội dung)', status: 'done' });
|
|
406
|
+
|
|
407
|
+
console.log('[AgentPanel] Retry successful');
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('[AgentPanel] Retry failed:', error);
|
|
410
|
+
|
|
411
|
+
const errorHtml = `
|
|
412
|
+
<div class="agent-error-box">
|
|
413
|
+
<div class="agent-error-title">❌ Vẫn lỗi: ${escapeHtml(error.message)}</div>
|
|
414
|
+
<button onclick="window.retryAgentMessage()" class="agent-retry-btn">
|
|
415
|
+
🔄 Thử lại (Retry)
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
`;
|
|
419
|
+
updateLastMessage({ content: errorHtml, status: 'error' });
|
|
420
|
+
} finally {
|
|
421
|
+
setProcessing(false);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Add message
|
|
427
|
+
*/
|
|
428
|
+
function addMessage(role, content, status = 'done') {
|
|
429
|
+
messages.push({
|
|
430
|
+
id: Date.now(),
|
|
431
|
+
role,
|
|
432
|
+
content,
|
|
433
|
+
status,
|
|
434
|
+
timestamp: new Date().toLocaleTimeString('vi-VN')
|
|
435
|
+
});
|
|
436
|
+
renderMessages();
|
|
437
|
+
autoSaveChat();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Update last message
|
|
442
|
+
*/
|
|
443
|
+
function updateLastMessage(updates) {
|
|
444
|
+
if (messages.length === 0) return;
|
|
445
|
+
Object.assign(messages[messages.length - 1], updates);
|
|
446
|
+
renderMessages();
|
|
447
|
+
autoSaveChat();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Auto-save chat
|
|
452
|
+
*/
|
|
453
|
+
function autoSaveChat() {
|
|
454
|
+
if (!currentChatId) return;
|
|
455
|
+
|
|
456
|
+
if (autoSaveTimeout) {
|
|
457
|
+
clearTimeout(autoSaveTimeout);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
autoSaveTimeout = setTimeout(() => {
|
|
461
|
+
const success = ChatHistory.saveChat(currentChatId, messages);
|
|
462
|
+
if (success) {
|
|
463
|
+
console.log(`[AgentPanel] Auto-saved chat ${currentChatId}`);
|
|
464
|
+
}
|
|
465
|
+
}, 500);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Clear chat
|
|
470
|
+
*/
|
|
471
|
+
function handleClearChat() {
|
|
472
|
+
if (!confirm('Xóa toàn bộ lịch sử chat?')) return;
|
|
473
|
+
|
|
474
|
+
if (currentChatId) {
|
|
475
|
+
ChatHistory.deleteChat(currentChatId);
|
|
476
|
+
console.log(`[AgentPanel] Deleted chat ${currentChatId}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
messages = [];
|
|
480
|
+
selectedFiles = [];
|
|
481
|
+
currentChatId = null;
|
|
482
|
+
renderFileList();
|
|
483
|
+
renderMessages();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Handle add files
|
|
488
|
+
*/
|
|
489
|
+
function handleAddFiles(newFiles) {
|
|
490
|
+
if (isProcessing) return;
|
|
491
|
+
for (const file of newFiles) {
|
|
492
|
+
selectedFiles.push(file);
|
|
493
|
+
}
|
|
494
|
+
renderFileList();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Render file list
|
|
499
|
+
*/
|
|
500
|
+
function renderFileList() {
|
|
501
|
+
const listContainer = getById('agent-file-list');
|
|
502
|
+
if (!listContainer) return;
|
|
503
|
+
|
|
504
|
+
listContainer.innerHTML = selectedFiles.map((file, index) => `
|
|
505
|
+
<div class="agent-file-badge">
|
|
506
|
+
<span class="agent-file-name">📎 ${file.name}</span>
|
|
507
|
+
<span class="agent-file-size">(${(file.size / 1024).toFixed(0)}KB)</span>
|
|
508
|
+
<button class="agent-file-remove" onclick="window.removeAgentFile(${index})">×</button>
|
|
509
|
+
</div>
|
|
510
|
+
`).join('');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Remove file
|
|
515
|
+
*/
|
|
516
|
+
window.removeAgentFile = function(index) {
|
|
517
|
+
if (isProcessing) return;
|
|
518
|
+
selectedFiles.splice(index, 1);
|
|
519
|
+
renderFileList();
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Set processing state
|
|
524
|
+
*/
|
|
525
|
+
function setProcessing(processing) {
|
|
526
|
+
isProcessing = processing;
|
|
527
|
+
|
|
528
|
+
const input = getById('agent-chat-input');
|
|
529
|
+
const sendBtn = getById('agent-send-btn');
|
|
530
|
+
const fileBtn = getById('agent-file-btn');
|
|
531
|
+
const dropZone = getById('agent-input-wrapper');
|
|
532
|
+
|
|
533
|
+
if (input) {
|
|
534
|
+
input.disabled = processing;
|
|
535
|
+
input.placeholder = processing ? 'AI đang suy nghĩ...' : 'Nhập tin nhắn...';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (sendBtn) {
|
|
539
|
+
sendBtn.disabled = processing;
|
|
540
|
+
sendBtn.style.opacity = processing ? '0.3' : '1';
|
|
541
|
+
|
|
542
|
+
if (processing) {
|
|
543
|
+
sendBtn.innerHTML = `
|
|
544
|
+
<svg class="agent-spinner" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
545
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
|
546
|
+
</svg>
|
|
547
|
+
`;
|
|
548
|
+
} else {
|
|
549
|
+
sendBtn.innerHTML = `
|
|
550
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
551
|
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
552
|
+
<polyline points="5 12 12 5 19 12"></polyline>
|
|
553
|
+
</svg>
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (fileBtn) {
|
|
559
|
+
fileBtn.disabled = processing;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (dropZone) {
|
|
563
|
+
dropZone.style.opacity = processing ? '0.5' : '1';
|
|
564
|
+
dropZone.style.pointerEvents = processing ? 'none' : 'auto';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Get status icon
|
|
570
|
+
*/
|
|
571
|
+
function getStatusIcon(status, role) {
|
|
572
|
+
if (status === 'sending') return '<span class="agent-status-sending">sending...</span>';
|
|
573
|
+
if (status === 'processing') return '<span class="agent-status-processing">● thinking</span>';
|
|
574
|
+
if (status === 'error') return '<span class="agent-status-error">failed</span>';
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Escape HTML
|
|
580
|
+
*/
|
|
581
|
+
function escapeHtml(text) {
|
|
582
|
+
if (!text) return '';
|
|
583
|
+
const div = document.createElement('div');
|
|
584
|
+
div.textContent = text;
|
|
585
|
+
return div.innerHTML;
|
|
586
|
+
}
|