181 lines
4.9 KiB
JavaScript
181 lines
4.9 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|