vg-coder-cli 2.0.41 → 2.0.43
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 +421 -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 +710 -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/vg-coder/controller.js +3 -3
|
@@ -0,0 +1,710 @@
|
|
|
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 markdown-it and mermaid from npm packages (bundled by webpack)
|
|
8
|
+
import markdownit from 'markdown-it';
|
|
9
|
+
import mermaid from 'mermaid';
|
|
10
|
+
|
|
11
|
+
// State
|
|
12
|
+
let messages = [];
|
|
13
|
+
let selectedFiles = [];
|
|
14
|
+
let isProcessing = false;
|
|
15
|
+
let currentChatId = null;
|
|
16
|
+
let autoSaveTimeout = null;
|
|
17
|
+
let md = null; // markdown-it instance
|
|
18
|
+
|
|
19
|
+
// Initialize markdown-it with safe settings
|
|
20
|
+
function initMarkdown() {
|
|
21
|
+
if (md) return md;
|
|
22
|
+
|
|
23
|
+
md = markdownit({
|
|
24
|
+
html: false, // Disable HTML tags for security
|
|
25
|
+
breaks: true, // Convert line breaks to <br>
|
|
26
|
+
linkify: true, // Auto-convert URLs to links
|
|
27
|
+
typographer: true // Smart quotes
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log('[AgentPanel] markdown-it initialized');
|
|
31
|
+
return md;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Initialize mermaid with dark theme
|
|
35
|
+
function initMermaid() {
|
|
36
|
+
try {
|
|
37
|
+
mermaid.initialize({
|
|
38
|
+
startOnLoad: false, // Manual trigger
|
|
39
|
+
theme: 'dark', // Dark theme
|
|
40
|
+
securityLevel: 'loose', // Allow shadow DOM interaction
|
|
41
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
|
|
42
|
+
});
|
|
43
|
+
console.log('[AgentPanel] mermaid initialized');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[AgentPanel] Failed to initialize mermaid:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize Agent Panel
|
|
51
|
+
*/
|
|
52
|
+
export function initAgentPanel() {
|
|
53
|
+
// Initialize libraries immediately
|
|
54
|
+
initMarkdown();
|
|
55
|
+
initMermaid();
|
|
56
|
+
|
|
57
|
+
// Listen for panel open event
|
|
58
|
+
document.addEventListener('tool-panel-opened', (e) => {
|
|
59
|
+
if (e.detail.panelId === 'agent' && e.detail.side === 'right') {
|
|
60
|
+
renderAgentPanel();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log('[AgentPanel] Initialized');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render Agent Panel UI
|
|
69
|
+
*/
|
|
70
|
+
async function renderAgentPanel() {
|
|
71
|
+
const container = getById('agent-panel-content');
|
|
72
|
+
if (!container) {
|
|
73
|
+
console.error('[AgentPanel] Container not found');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if already rendered
|
|
78
|
+
if (container.querySelector('.agent-chat-messages')) {
|
|
79
|
+
console.log('[AgentPanel] Already rendered');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create UI
|
|
84
|
+
container.innerHTML = `
|
|
85
|
+
<div class="agent-chat-messages" id="agent-messages"></div>
|
|
86
|
+
|
|
87
|
+
<div class="agent-chat-input-area">
|
|
88
|
+
<div class="agent-input-wrapper" id="agent-input-wrapper">
|
|
89
|
+
<div class="agent-file-list" id="agent-file-list"></div>
|
|
90
|
+
<textarea
|
|
91
|
+
class="agent-chat-textarea"
|
|
92
|
+
id="agent-chat-input"
|
|
93
|
+
placeholder="Nhập tin nhắn..."
|
|
94
|
+
></textarea>
|
|
95
|
+
<div class="agent-input-controls">
|
|
96
|
+
<div class="agent-input-actions">
|
|
97
|
+
<input id="agent-file-input" type="file" multiple style="display: none" />
|
|
98
|
+
<button class="agent-btn" id="agent-file-btn" title="Attach files">
|
|
99
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
100
|
+
<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>
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
103
|
+
<button class="agent-btn" id="agent-clear-btn" title="Clear chat">
|
|
104
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
105
|
+
<polyline points="3 6 5 6 21 6"></polyline>
|
|
106
|
+
<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>
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<button class="agent-send-btn" id="agent-send-btn" title="Send message">
|
|
111
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
112
|
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
113
|
+
<polyline points="5 12 12 5 19 12"></polyline>
|
|
114
|
+
</svg>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
// Attach event listeners
|
|
122
|
+
attachEventListeners();
|
|
123
|
+
|
|
124
|
+
// Don't auto-load - let user click reload
|
|
125
|
+
console.log('[AgentPanel] Panel ready, waiting for user action');
|
|
126
|
+
|
|
127
|
+
// Render empty state
|
|
128
|
+
renderMessages();
|
|
129
|
+
|
|
130
|
+
console.log('[AgentPanel] Rendered successfully');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Attach event listeners
|
|
135
|
+
*/
|
|
136
|
+
function attachEventListeners() {
|
|
137
|
+
// Send button
|
|
138
|
+
const sendBtn = getById('agent-send-btn');
|
|
139
|
+
if (sendBtn) {
|
|
140
|
+
sendBtn.addEventListener('click', () => {
|
|
141
|
+
if (!isProcessing) sendMessage();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Input enter key
|
|
146
|
+
const input = getById('agent-chat-input');
|
|
147
|
+
if (input) {
|
|
148
|
+
input.addEventListener('keydown', (e) => {
|
|
149
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
if (!isProcessing) sendMessage();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Auto-expand textarea
|
|
156
|
+
input.addEventListener('input', function() {
|
|
157
|
+
this.style.height = '40px';
|
|
158
|
+
if (this.scrollHeight > 40) {
|
|
159
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// File button
|
|
165
|
+
const fileBtn = getById('agent-file-btn');
|
|
166
|
+
const fileInput = getById('agent-file-input');
|
|
167
|
+
if (fileBtn && fileInput) {
|
|
168
|
+
fileBtn.addEventListener('click', () => fileInput.click());
|
|
169
|
+
fileInput.addEventListener('change', (e) => {
|
|
170
|
+
handleAddFiles(e.target.files);
|
|
171
|
+
fileInput.value = '';
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Clear button
|
|
176
|
+
const clearBtn = getById('agent-clear-btn');
|
|
177
|
+
if (clearBtn) {
|
|
178
|
+
clearBtn.addEventListener('click', handleClearChat);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Drag & drop
|
|
182
|
+
const dropZone = getById('agent-input-wrapper');
|
|
183
|
+
if (dropZone) {
|
|
184
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
185
|
+
dropZone.addEventListener(eventName, (e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
}, false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
dropZone.addEventListener('dragover', () => {
|
|
192
|
+
if (!isProcessing) dropZone.classList.add('drag-active');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
dropZone.addEventListener('dragleave', () => {
|
|
196
|
+
dropZone.classList.remove('drag-active');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
dropZone.addEventListener('drop', (e) => {
|
|
200
|
+
dropZone.classList.remove('drag-active');
|
|
201
|
+
if (isProcessing) return;
|
|
202
|
+
handleAddFiles(e.dataTransfer.files);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Initialize chat session
|
|
209
|
+
*/
|
|
210
|
+
async function initializeChatSession() {
|
|
211
|
+
// Check if AIChat is available
|
|
212
|
+
if (!window.AIChat) {
|
|
213
|
+
console.warn('[AgentPanel] AIChat not available yet');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Load from adapter cache (single source of truth)
|
|
219
|
+
console.log('[AgentPanel] Loading conversation history from adapter...');
|
|
220
|
+
const historyMessages = await window.AIChat.getCurrentMessages();
|
|
221
|
+
|
|
222
|
+
if (historyMessages && historyMessages.length > 0) {
|
|
223
|
+
console.log(`[AgentPanel] Loaded ${historyMessages.length} messages from adapter cache`);
|
|
224
|
+
|
|
225
|
+
// Convert history format to agent panel format
|
|
226
|
+
messages = historyMessages.map(msg => ({
|
|
227
|
+
role: msg.role,
|
|
228
|
+
content: msg.content,
|
|
229
|
+
timestamp: new Date(msg.timestamp).toLocaleTimeString(),
|
|
230
|
+
status: 'done'
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
// Get chat ID from URL
|
|
234
|
+
const chatId = window.AIChat.getChatIdFromUrl?.();
|
|
235
|
+
if (chatId) {
|
|
236
|
+
currentChatId = chatId;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
renderMessages();
|
|
240
|
+
} else {
|
|
241
|
+
console.log('[AgentPanel] No conversation history found');
|
|
242
|
+
messages = [];
|
|
243
|
+
renderMessages();
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('[AgentPanel] Failed to load history:', error);
|
|
247
|
+
messages = [];
|
|
248
|
+
renderMessages();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Render messages
|
|
254
|
+
*/
|
|
255
|
+
function renderMessages() {
|
|
256
|
+
const container = getById('agent-messages');
|
|
257
|
+
if (!container) return;
|
|
258
|
+
|
|
259
|
+
if (messages.length === 0) {
|
|
260
|
+
container.innerHTML = `
|
|
261
|
+
<div class="agent-chat-empty">
|
|
262
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
263
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
|
264
|
+
</svg>
|
|
265
|
+
<span>No conversation loaded</span>
|
|
266
|
+
<button class="agent-reload-btn" id="agent-reload-btn">
|
|
267
|
+
🔄 Load Conversation History
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
// Attach reload button listener
|
|
273
|
+
const reloadBtn = getById('agent-reload-btn');
|
|
274
|
+
if (reloadBtn) {
|
|
275
|
+
reloadBtn.addEventListener('click', async () => {
|
|
276
|
+
reloadBtn.disabled = true;
|
|
277
|
+
reloadBtn.textContent = '⏳ Loading...';
|
|
278
|
+
await initializeChatSession();
|
|
279
|
+
reloadBtn.disabled = false;
|
|
280
|
+
reloadBtn.textContent = '🔄 Load Conversation History';
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
container.innerHTML = messages.map(msg => {
|
|
288
|
+
const isUser = msg.role === 'user';
|
|
289
|
+
const contentHtml = isUser ? escapeHtml(msg.content) : md.render(msg.content);
|
|
290
|
+
|
|
291
|
+
return `
|
|
292
|
+
<div class="agent-message ${msg.role}">
|
|
293
|
+
<div class="agent-message-content">
|
|
294
|
+
<div class="markdown-body">${contentHtml}</div>
|
|
295
|
+
<div class="agent-message-meta">
|
|
296
|
+
<span>${msg.timestamp}</span>
|
|
297
|
+
<span>${getStatusIcon(msg.status, msg.role)}</span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
}).join('');
|
|
303
|
+
|
|
304
|
+
// Process mermaid diagrams
|
|
305
|
+
processMermaidDiagrams(container);
|
|
306
|
+
|
|
307
|
+
// Add run bash buttons
|
|
308
|
+
addRunBashButtons(container);
|
|
309
|
+
|
|
310
|
+
// Scroll to bottom
|
|
311
|
+
container.scrollTop = container.scrollHeight;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Add "Run Bash" buttons to bash code blocks
|
|
316
|
+
*/
|
|
317
|
+
function addRunBashButtons(container) {
|
|
318
|
+
const bashCodeBlocks = container.querySelectorAll('code.language-bash');
|
|
319
|
+
if (bashCodeBlocks.length === 0) return;
|
|
320
|
+
|
|
321
|
+
console.log(`[AgentPanel] Found ${bashCodeBlocks.length} bash code block(s)`);
|
|
322
|
+
|
|
323
|
+
bashCodeBlocks.forEach((codeBlock, index) => {
|
|
324
|
+
const preElement = codeBlock.parentElement;
|
|
325
|
+
if (!preElement || preElement.tagName !== 'PRE') return;
|
|
326
|
+
|
|
327
|
+
// Check if button already exists
|
|
328
|
+
if (preElement.querySelector('.agent-run-bash-btn')) return;
|
|
329
|
+
|
|
330
|
+
// Create button
|
|
331
|
+
const button = document.createElement('button');
|
|
332
|
+
button.className = 'agent-run-bash-btn';
|
|
333
|
+
button.innerHTML = '▶ Run Bash';
|
|
334
|
+
button.title = 'Run bash command in terminal';
|
|
335
|
+
|
|
336
|
+
// Click handler
|
|
337
|
+
button.addEventListener('click', async (e) => {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
e.stopPropagation();
|
|
340
|
+
|
|
341
|
+
const code = codeBlock.textContent || '';
|
|
342
|
+
if (!code.trim()) {
|
|
343
|
+
console.warn('[AgentPanel] No bash code found');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log('[AgentPanel] Run bash triggered');
|
|
348
|
+
|
|
349
|
+
// Copy to clipboard
|
|
350
|
+
try {
|
|
351
|
+
await navigator.clipboard.writeText(code);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
console.error('[AgentPanel] Clipboard copy failed:', err);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Dispatch event
|
|
358
|
+
dispatchPasteRun(code);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Wrap pre in container if not already
|
|
362
|
+
if (!preElement.parentElement.classList.contains('agent-code-block-wrapper')) {
|
|
363
|
+
const wrapper = document.createElement('div');
|
|
364
|
+
wrapper.className = 'agent-code-block-wrapper';
|
|
365
|
+
preElement.parentNode.insertBefore(wrapper, preElement);
|
|
366
|
+
wrapper.appendChild(preElement);
|
|
367
|
+
wrapper.appendChild(button);
|
|
368
|
+
} else {
|
|
369
|
+
preElement.parentElement.appendChild(button);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(`[AgentPanel] Run bash button added (${index + 1})`);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Dispatch paste-run event for bash execution
|
|
378
|
+
*/
|
|
379
|
+
function dispatchPasteRun(code) {
|
|
380
|
+
const EVENT_TYPE = 'vg:paste-run';
|
|
381
|
+
|
|
382
|
+
// Use global dispatcher if available
|
|
383
|
+
const dispatcher = window.__VG_EVENT_DISPATCHER__ || window.globalDispatcher || null;
|
|
384
|
+
|
|
385
|
+
const eventPayload = {
|
|
386
|
+
type: EVENT_TYPE,
|
|
387
|
+
source: 'agent-panel',
|
|
388
|
+
target: 'bubble-runner',
|
|
389
|
+
payload: {
|
|
390
|
+
code,
|
|
391
|
+
from: 'run-bash-button',
|
|
392
|
+
},
|
|
393
|
+
context: 'window',
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (dispatcher?.dispatchCrossContext) {
|
|
397
|
+
dispatcher.dispatchCrossContext(eventPayload);
|
|
398
|
+
console.log('[AgentPanel] Dispatched via globalDispatcher:', EVENT_TYPE);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Fallback: CustomEvent
|
|
403
|
+
window.dispatchEvent(
|
|
404
|
+
new CustomEvent(EVENT_TYPE, {
|
|
405
|
+
detail: eventPayload,
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
console.log('[AgentPanel] Dispatched via CustomEvent:', EVENT_TYPE);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Process mermaid diagrams
|
|
414
|
+
* Uses mermaid.render() which is more compatible with shadow DOM
|
|
415
|
+
*/
|
|
416
|
+
async function processMermaidDiagrams(container) {
|
|
417
|
+
const mermaidCodeBlocks = container.querySelectorAll('code.language-mermaid');
|
|
418
|
+
if (mermaidCodeBlocks.length === 0) return;
|
|
419
|
+
|
|
420
|
+
console.log(`[AgentPanel] Found ${mermaidCodeBlocks.length} mermaid diagram(s)`);
|
|
421
|
+
|
|
422
|
+
for (let i = 0; i < mermaidCodeBlocks.length; i++) {
|
|
423
|
+
const codeBlock = mermaidCodeBlocks[i];
|
|
424
|
+
const code = codeBlock.textContent;
|
|
425
|
+
const preElement = codeBlock.parentElement;
|
|
426
|
+
|
|
427
|
+
if (!preElement || preElement.tagName !== 'PRE') {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Use mermaid.render() which is more compatible with shadow DOM
|
|
433
|
+
const id = `agent-mermaid-${Date.now()}-${i}`;
|
|
434
|
+
const { svg, bindFunctions } = await mermaid.render(id, code);
|
|
435
|
+
|
|
436
|
+
// Create wrapper div for mermaid
|
|
437
|
+
const wrapper = document.createElement('div');
|
|
438
|
+
wrapper.className = 'agent-mermaid';
|
|
439
|
+
wrapper.innerHTML = svg;
|
|
440
|
+
|
|
441
|
+
// Replace pre/code with wrapper
|
|
442
|
+
preElement.replaceWith(wrapper);
|
|
443
|
+
|
|
444
|
+
// Bind any interactive functions if present
|
|
445
|
+
if (bindFunctions) {
|
|
446
|
+
bindFunctions(wrapper);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log(`[AgentPanel] Rendered mermaid diagram ${i + 1}`);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.error(`[AgentPanel] Mermaid render error (diagram ${i + 1}):`, err);
|
|
452
|
+
|
|
453
|
+
// On error, show error message instead of broken diagram
|
|
454
|
+
const errorDiv = document.createElement('div');
|
|
455
|
+
errorDiv.className = 'agent-mermaid-error';
|
|
456
|
+
errorDiv.style.cssText = 'color: #ef4444; padding: 12px; background: #2a1515; border-radius: 8px; border: 1px solid #7f1d1d; margin: 8px 0;';
|
|
457
|
+
errorDiv.textContent = `Mermaid rendering error: ${err.message}`;
|
|
458
|
+
preElement.replaceWith(errorDiv);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Send message
|
|
465
|
+
*/
|
|
466
|
+
async function sendMessage() {
|
|
467
|
+
const input = getById('agent-chat-input');
|
|
468
|
+
const prompt = input.value.trim();
|
|
469
|
+
|
|
470
|
+
if (!prompt && selectedFiles.length === 0) return;
|
|
471
|
+
|
|
472
|
+
if (!window.AIChat) {
|
|
473
|
+
alert('❌ AIChat engine chưa được inject!');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Generate chat ID for first message
|
|
478
|
+
const isFirstMessage = messages.length === 0;
|
|
479
|
+
|
|
480
|
+
let userMsg = prompt;
|
|
481
|
+
if (selectedFiles.length > 0) {
|
|
482
|
+
userMsg += `\n\n📎 Files: ${selectedFiles.map(f => f.name).join(', ')}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
addMessage('user', userMsg, 'sending');
|
|
486
|
+
|
|
487
|
+
const payloadFiles = [...selectedFiles];
|
|
488
|
+
input.value = '';
|
|
489
|
+
selectedFiles = [];
|
|
490
|
+
renderFileList();
|
|
491
|
+
setProcessing(true);
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
updateLastMessage({ status: 'done' });
|
|
495
|
+
addMessage('assistant', '...', 'processing');
|
|
496
|
+
|
|
497
|
+
await window.AIChat.send({ prompt, files: payloadFiles });
|
|
498
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
499
|
+
|
|
500
|
+
const aiResponse = await window.AIChat.copyLastTurnAsMarkdown();
|
|
501
|
+
updateLastMessage({ content: aiResponse || '(AI không trả về nội dung)', status: 'done' });
|
|
502
|
+
|
|
503
|
+
// Get chat ID from URL (adapter manages chat ID)
|
|
504
|
+
if (isFirstMessage && !currentChatId) {
|
|
505
|
+
const chatId = window.AIChat?.getChatIdFromUrl?.();
|
|
506
|
+
if (chatId) {
|
|
507
|
+
currentChatId = chatId;
|
|
508
|
+
console.log(`[AgentPanel] Using chat ID from URL: ${currentChatId}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error('[AgentPanel] Error sending message:', error);
|
|
513
|
+
|
|
514
|
+
const errorHtml = `
|
|
515
|
+
<div class="agent-error-box">
|
|
516
|
+
<div class="agent-error-title">❌ Lỗi: ${escapeHtml(error.message)}</div>
|
|
517
|
+
<button onclick="window.retryAgentMessage()" class="agent-retry-btn">
|
|
518
|
+
🔄 Thử lại (Retry)
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
`;
|
|
522
|
+
updateLastMessage({ content: errorHtml, status: 'error' });
|
|
523
|
+
} finally {
|
|
524
|
+
setProcessing(false);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Retry message
|
|
530
|
+
*/
|
|
531
|
+
window.retryAgentMessage = async function() {
|
|
532
|
+
if (!window.AIChat) {
|
|
533
|
+
alert('❌ AIChat engine chưa sẵn sàng!');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log('[AgentPanel] Retrying...');
|
|
538
|
+
setProcessing(true);
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
updateLastMessage({ content: '🔄 Đang thử lại...', status: 'processing' });
|
|
542
|
+
|
|
543
|
+
const aiResponse = await window.AIChat.copyLastTurnAsMarkdown();
|
|
544
|
+
updateLastMessage({ content: aiResponse || '(AI không trả về nội dung)', status: 'done' });
|
|
545
|
+
|
|
546
|
+
console.log('[AgentPanel] Retry successful');
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error('[AgentPanel] Retry failed:', error);
|
|
549
|
+
|
|
550
|
+
const errorHtml = `
|
|
551
|
+
<div class="agent-error-box">
|
|
552
|
+
<div class="agent-error-title">❌ Vẫn lỗi: ${escapeHtml(error.message)}</div>
|
|
553
|
+
<button onclick="window.retryAgentMessage()" class="agent-retry-btn">
|
|
554
|
+
🔄 Thử lại (Retry)
|
|
555
|
+
</button>
|
|
556
|
+
</div>
|
|
557
|
+
`;
|
|
558
|
+
updateLastMessage({ content: errorHtml, status: 'error' });
|
|
559
|
+
} finally {
|
|
560
|
+
setProcessing(false);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Add message
|
|
566
|
+
*/
|
|
567
|
+
function addMessage(role, content, status = 'done') {
|
|
568
|
+
messages.push({
|
|
569
|
+
id: Date.now(),
|
|
570
|
+
role,
|
|
571
|
+
content,
|
|
572
|
+
status,
|
|
573
|
+
timestamp: new Date().toLocaleTimeString('vi-VN')
|
|
574
|
+
});
|
|
575
|
+
renderMessages();
|
|
576
|
+
// Adapter auto-saves, no manual save needed
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Update last message
|
|
581
|
+
*/
|
|
582
|
+
function updateLastMessage(updates) {
|
|
583
|
+
if (messages.length === 0) return;
|
|
584
|
+
Object.assign(messages[messages.length - 1], updates);
|
|
585
|
+
renderMessages();
|
|
586
|
+
// Adapter handles storage
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Auto-save is handled by adapter
|
|
591
|
+
* No manual save needed
|
|
592
|
+
*/
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Handle clear chat
|
|
596
|
+
*/
|
|
597
|
+
function handleClearChat() {
|
|
598
|
+
if (messages.length === 0) return;
|
|
599
|
+
|
|
600
|
+
if (confirm('Clear chat history?')) {
|
|
601
|
+
console.log(`[AgentPanel] Deleted chat ${currentChatId}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
messages = [];
|
|
605
|
+
selectedFiles = [];
|
|
606
|
+
renderFileList();
|
|
607
|
+
renderMessages();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Handle add files
|
|
612
|
+
*/
|
|
613
|
+
function handleAddFiles(newFiles) {
|
|
614
|
+
if (isProcessing) return;
|
|
615
|
+
for (const file of newFiles) {
|
|
616
|
+
selectedFiles.push(file);
|
|
617
|
+
}
|
|
618
|
+
renderFileList();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Render file list
|
|
623
|
+
*/
|
|
624
|
+
function renderFileList() {
|
|
625
|
+
const listContainer = getById('agent-file-list');
|
|
626
|
+
if (!listContainer) return;
|
|
627
|
+
|
|
628
|
+
listContainer.innerHTML = selectedFiles.map((file, index) => `
|
|
629
|
+
<div class="agent-file-badge">
|
|
630
|
+
<span class="agent-file-name">📎 ${file.name}</span>
|
|
631
|
+
<span class="agent-file-size">(${(file.size / 1024).toFixed(0)}KB)</span>
|
|
632
|
+
<button class="agent-file-remove" onclick="window.removeAgentFile(${index})">×</button>
|
|
633
|
+
</div>
|
|
634
|
+
`).join('');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Remove file
|
|
639
|
+
*/
|
|
640
|
+
window.removeAgentFile = function(index) {
|
|
641
|
+
if (isProcessing) return;
|
|
642
|
+
selectedFiles.splice(index, 1);
|
|
643
|
+
renderFileList();
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Set processing state
|
|
648
|
+
*/
|
|
649
|
+
function setProcessing(processing) {
|
|
650
|
+
isProcessing = processing;
|
|
651
|
+
|
|
652
|
+
const input = getById('agent-chat-input');
|
|
653
|
+
const sendBtn = getById('agent-send-btn');
|
|
654
|
+
const fileBtn = getById('agent-file-btn');
|
|
655
|
+
const dropZone = getById('agent-input-wrapper');
|
|
656
|
+
|
|
657
|
+
if (input) {
|
|
658
|
+
input.disabled = processing;
|
|
659
|
+
input.placeholder = processing ? 'AI đang suy nghĩ...' : 'Nhập tin nhắn...';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (sendBtn) {
|
|
663
|
+
sendBtn.disabled = processing;
|
|
664
|
+
sendBtn.style.opacity = processing ? '0.3' : '1';
|
|
665
|
+
|
|
666
|
+
if (processing) {
|
|
667
|
+
sendBtn.innerHTML = `
|
|
668
|
+
<svg class="agent-spinner" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
669
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
|
670
|
+
</svg>
|
|
671
|
+
`;
|
|
672
|
+
} else {
|
|
673
|
+
sendBtn.innerHTML = `
|
|
674
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
675
|
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
676
|
+
<polyline points="5 12 12 5 19 12"></polyline>
|
|
677
|
+
</svg>
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (fileBtn) {
|
|
683
|
+
fileBtn.disabled = processing;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (dropZone) {
|
|
687
|
+
dropZone.style.opacity = processing ? '0.5' : '1';
|
|
688
|
+
dropZone.style.pointerEvents = processing ? 'none' : 'auto';
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Get status icon
|
|
694
|
+
*/
|
|
695
|
+
function getStatusIcon(status, role) {
|
|
696
|
+
if (status === 'sending') return '<span class="agent-status-sending">sending...</span>';
|
|
697
|
+
if (status === 'processing') return '<span class="agent-status-processing">● thinking</span>';
|
|
698
|
+
if (status === 'error') return '<span class="agent-status-error">failed</span>';
|
|
699
|
+
return '';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Escape HTML
|
|
704
|
+
*/
|
|
705
|
+
function escapeHtml(text) {
|
|
706
|
+
if (!text) return '';
|
|
707
|
+
const div = document.createElement('div');
|
|
708
|
+
div.textContent = text;
|
|
709
|
+
return div.innerHTML;
|
|
710
|
+
}
|