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.
@@ -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
+ }