Newer
Older
abysiuscodium / extensions / abysius-ai / assets / chat.js
(function () {
    const vscode = acquireVsCodeApi();
    const messagesEl = document.getElementById('messages');
    const inputEl = document.getElementById('message-input');
    const sendBtn = document.getElementById('send-btn');
    const cancelBtn = document.getElementById('cancel-btn');

    let isStreaming = false;
    let currentAssistantId = null;

    // Auto-resize textarea
    inputEl.addEventListener('input', () => {
        inputEl.style.height = 'auto';
        inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
    });

    // Send on Enter (Shift+Enter for newline)
    inputEl.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            sendMessage();
        }
    });

    sendBtn.addEventListener('click', sendMessage);
    cancelBtn.addEventListener('click', cancelStream);

    function sendMessage() {
        const text = inputEl.value.trim();
        if (!text || isStreaming) return;

        vscode.postMessage({ type: 'sendMessage', text });
        inputEl.value = '';
        inputEl.style.height = 'auto';
        inputEl.style.height = '36px';
    }

    function cancelStream() {
        vscode.postMessage({ type: 'cancel' });
        setStreaming(false);
    }

    function setStreaming(streaming) {
        isStreaming = streaming;
        sendBtn.style.display = streaming ? 'none' : 'block';
        cancelBtn.style.display = streaming ? 'block' : 'none';
        inputEl.disabled = streaming;
    }

    function addMessage(msg) {
        const msgEl = document.createElement('div');
        msgEl.className = `message ${msg.role}`;
        msgEl.dataset.id = msg.id;
        msgEl.innerHTML = formatContent(msg.content);
        
        const timeEl = document.createElement('div');
        timeEl.className = 'message-time';
        timeEl.textContent = formatTime(msg.timestamp);
        msgEl.appendChild(timeEl);

        messagesEl.appendChild(msgEl);
        scrollToBottom();
        
        attachCodeActions(msgEl);
    }

    function appendChunk(id, chunk) {
        const msgEl = document.querySelector(`.message[data-id="${id}"]`);
        if (!msgEl) return;

        const contentEl = msgEl.querySelector('.content') || msgEl;
        const existing = contentEl.dataset.raw || '';
        const updated = existing + chunk;
        contentEl.dataset.raw = updated;
        
        // Re-render the full content
        const timeEl = msgEl.querySelector('.message-time');
        msgEl.innerHTML = formatContent(updated);
        if (timeEl) msgEl.appendChild(timeEl);
        
        scrollToBottom();
        attachCodeActions(msgEl);
    }

    function finalizeMessage(id) {
        const msgEl = document.querySelector(`.message[data-id="${id}"]`);
        if (msgEl) {
            msgEl.classList.remove('streaming');
        }
        setStreaming(false);
        currentAssistantId = null;
    }

    function showError(id, error) {
        const msgEl = document.querySelector(`.message[data-id="${id}"]`) || document.createElement('div');
        msgEl.className = 'message error';
        msgEl.textContent = error;
        if (!msgEl.parentNode) {
            messagesEl.appendChild(msgEl);
        }
        setStreaming(false);
        currentAssistantId = null;
        scrollToBottom();
    }

    function formatContent(text) {
        if (!text) return '';
        
        // Escape HTML
        let html = text
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');

        // Code blocks
        html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
            const escapedCode = code.replace(/^\n|\n$/g, '');
            return `<div class="code-block-header">
                <span>${lang || 'text'}</span>
                <div class="code-block-actions">
                    <button class="code-btn copy-btn" data-code="${encodeURIComponent(escapedCode)}">Copy</button>
                    <button class="code-btn insert-btn" data-code="${encodeURIComponent(escapedCode)}">Insert</button>
                </div>
            </div><pre><code class="language-${lang || 'text'}">${escapedCode}</code></pre>`;
        });

        // Inline code
        html = html.replace(/`([^`]+)`/g, '<code>$1</code>');

        // Bold
        html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
        html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');

        // Italic
        html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
        html = html.replace(/_([^_]+)_/g, '<em>$1</em>');

        // Strikethrough
        html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');

        // Headers
        html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
        html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
        html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');

        // Lists
        html = html.replace(/^\s*[-*+] (.*$)/gim, '<li>$1</li>');
        html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
        html = html.replace(/<\/ul>\n?<ul>/g, '');

        // Ordered lists
        html = html.replace(/^\s*\d+\.\s+(.*$)/gim, '<li>$1</li>');

        // Blockquotes
        html = html.replace(/^&gt; (.*$)/gim, '<blockquote>$1</blockquote>');
        html = html.replace(/(<blockquote>.*<\/blockquote>\n?)+/g, (matches) => {
            return '<blockquote>' + matches.replace(/<\/?blockquote>/g, '') + '</blockquote>';
        });

        // Links
        html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');

        // Horizontal rules
        html = html.replace(/^---+$/gim, '<hr/>');

        // Paragraphs (wrap remaining text)
        const lines = html.split('\n');
        let result = '';
        let inParagraph = false;
        
        for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed) {
                if (inParagraph) {
                    result += '</p>';
                    inParagraph = false;
                }
                result += '\n';
                continue;
            }
            
            const isBlock = /^<(h[1-6]|ul|ol|pre|blockquote|hr|div)/i.test(trimmed);
            
            if (isBlock) {
                if (inParagraph) {
                    result += '</p>';
                    inParagraph = false;
                }
                result += line + '\n';
            } else {
                if (!inParagraph) {
                    result += '<p>';
                    inParagraph = true;
                }
                result += line + ' ';
            }
        }
        
        if (inParagraph) {
            result += '</p>';
        }

        return result;
    }

    function attachCodeActions(container) {
        container.querySelectorAll('.copy-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const code = decodeURIComponent(btn.dataset.code);
                vscode.postMessage({ type: 'copyCode', code });
                btn.textContent = 'Copied!';
                setTimeout(() => btn.textContent = 'Copy', 1500);
            });
        });

        container.querySelectorAll('.insert-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const code = decodeURIComponent(btn.dataset.code);
                vscode.postMessage({ type: 'insertCode', code });
            });
        });
    }

    function scrollToBottom() {
        messagesEl.scrollTop = messagesEl.scrollHeight;
    }

    function formatTime(timestamp) {
        const date = new Date(timestamp);
        return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }

    function clearMessages() {
        messagesEl.innerHTML = '';
    }

    function showWelcome() {
        messagesEl.innerHTML = `
            <div class="welcome">
                <div class="icon">&#x2728;</div>
                <h3>Abysius AI</h3>
                <p>Ask me anything about your code.<br>I can help with writing, debugging, explaining, and refactoring.</p>
            </div>
        `;
    }

    // Initial state
    showWelcome();
    vscode.postMessage({ type: 'ready' });

    // Handle messages from extension
    window.addEventListener('message', (event) => {
        const msg = event.data;

        switch (msg.type) {
            case 'history':
                clearMessages();
                if (msg.messages.length === 0) {
                    showWelcome();
                } else {
                    msg.messages.forEach(m => addMessage(m));
                }
                break;

            case 'userMessage':
                addMessage(msg.message);
                break;

            case 'assistantStart':
                currentAssistantId = msg.id;
                setStreaming(true);
                const assistantEl = document.createElement('div');
                assistantEl.className = 'message assistant streaming';
                assistantEl.dataset.id = msg.id;
                assistantEl.innerHTML = '<span class="streaming-indicator"><span></span><span></span><span></span></span>';
                messagesEl.appendChild(assistantEl);
                scrollToBottom();
                break;

            case 'assistantChunk':
                // Remove the loading indicator on first chunk
                const el = document.querySelector(`.message[data-id="${msg.id}"]`);
                if (el && el.querySelector('.streaming-indicator')) {
                    el.innerHTML = '';
                }
                appendChunk(msg.id, msg.chunk);
                break;

            case 'assistantDone':
                finalizeMessage(msg.id);
                break;

            case 'assistantError':
                showError(msg.id, msg.error);
                break;

            case 'clear':
                clearMessages();
                showWelcome();
                break;
        }
    });
})();