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