Initial commit
This commit is contained in:
180
dashboard/js/components/chat.js
Normal file
180
dashboard/js/components/chat.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Chat — message input + conversation view + streaming
|
||||
*/
|
||||
|
||||
import { sendMessage } from '../app.js';
|
||||
|
||||
let messagesEl;
|
||||
let inputEl;
|
||||
let sendBtn;
|
||||
let streamBtn;
|
||||
let indicatorEl;
|
||||
let streamingBubble = null;
|
||||
|
||||
// Minimal markdown renderer
|
||||
function renderMarkdown(text) {
|
||||
let html = text
|
||||
// Code blocks
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
// Bold
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
// Lists
|
||||
.replace(/^\- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
||||
// Paragraphs (double newlines)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
// Single newlines
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
// Wrap loose <li> in <ul>
|
||||
html = html.replace(/(<li>.*?<\/li>)/gs, '<ul>$1</ul>');
|
||||
html = html.replace(/<\/ul>\s*<ul>/g, '');
|
||||
|
||||
return `<p>${html}</p>`;
|
||||
}
|
||||
|
||||
export function initChat() {
|
||||
const main = document.getElementById('main');
|
||||
|
||||
main.innerHTML = `
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<div class="chat-empty">
|
||||
<div class="icon">⚙</div>
|
||||
<p>Select or create a session to start</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-log-panel" id="event-log-panel"></div>
|
||||
<div class="chat-input-area">
|
||||
<div class="chat-input-wrapper">
|
||||
<textarea id="chat-input" placeholder="Send a message..." rows="1" disabled></textarea>
|
||||
<button class="btn btn-primary" id="btn-send" disabled>Send</button>
|
||||
<button class="btn" id="btn-stream" disabled title="Send with SSE streaming">Stream</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesEl = document.getElementById('chat-messages');
|
||||
inputEl = document.getElementById('chat-input');
|
||||
sendBtn = document.getElementById('btn-send');
|
||||
streamBtn = document.getElementById('btn-stream');
|
||||
|
||||
// Auto-resize textarea
|
||||
inputEl.addEventListener('input', () => {
|
||||
inputEl.style.height = 'auto';
|
||||
inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
|
||||
});
|
||||
|
||||
// Send
|
||||
sendBtn.addEventListener('click', () => doSend(false));
|
||||
streamBtn.addEventListener('click', () => doSend(true));
|
||||
|
||||
// Keyboard shortcuts
|
||||
inputEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
doSend(false);
|
||||
}
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
doSend(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doSend(stream) {
|
||||
const text = inputEl.value.trim();
|
||||
if (!text) return;
|
||||
inputEl.value = '';
|
||||
inputEl.style.height = 'auto';
|
||||
sendMessage(text, stream);
|
||||
}
|
||||
|
||||
export function addMessage(role, content) {
|
||||
// Remove empty state
|
||||
const empty = messagesEl.querySelector('.chat-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
// Remove streaming bubble if finalizing
|
||||
if (role === 'assistant' && streamingBubble) {
|
||||
streamingBubble.remove();
|
||||
streamingBubble = null;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${role}`;
|
||||
|
||||
if (role === 'assistant') {
|
||||
div.innerHTML = renderMarkdown(content);
|
||||
} else if (role === 'system') {
|
||||
div.textContent = content;
|
||||
} else {
|
||||
div.textContent = content;
|
||||
}
|
||||
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
export function setStreamingContent(content, agent) {
|
||||
// Remove empty state
|
||||
const empty = messagesEl.querySelector('.chat-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
if (!streamingBubble) {
|
||||
streamingBubble = document.createElement('div');
|
||||
streamingBubble.className = 'message assistant';
|
||||
messagesEl.appendChild(streamingBubble);
|
||||
}
|
||||
|
||||
let badgeHtml = '';
|
||||
if (agent) {
|
||||
badgeHtml = `<span class="agent-badge role-badge ${agent}">${agent}</span><br>`;
|
||||
}
|
||||
streamingBubble.innerHTML = badgeHtml + renderMarkdown(content);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
export function clearChat() {
|
||||
if (!messagesEl) return;
|
||||
streamingBubble = null;
|
||||
messagesEl.innerHTML = `
|
||||
<div class="chat-empty">
|
||||
<div class="icon">⚙</div>
|
||||
<p>Select or create a session to start</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function setInputEnabled(enabled) {
|
||||
if (!inputEl) return;
|
||||
inputEl.disabled = !enabled;
|
||||
sendBtn.disabled = !enabled;
|
||||
streamBtn.disabled = !enabled;
|
||||
if (enabled) inputEl.focus();
|
||||
}
|
||||
|
||||
export function showExecutionIndicator(text) {
|
||||
hideExecutionIndicator();
|
||||
|
||||
const empty = messagesEl.querySelector('.chat-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
indicatorEl = document.createElement('div');
|
||||
indicatorEl.className = 'execution-indicator';
|
||||
indicatorEl.innerHTML = `<div class="spinner"></div><span>${text || 'Executing...'}</span>`;
|
||||
messagesEl.appendChild(indicatorEl);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
export function hideExecutionIndicator() {
|
||||
if (indicatorEl) {
|
||||
indicatorEl.remove();
|
||||
indicatorEl = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user