let sessions = {}; let activeSessionId = null; let activeAbortController = null; // Initialize on DOM load document.addEventListener('DOMContentLoaded', () => { loadSessions(); const textarea = document.getElementById('userInput'); if (textarea) { textarea.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; if (this.scrollHeight > 200) { this.style.overflowY = 'auto'; } else { this.style.overflowY = 'hidden'; } }); textarea.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); } }); function loadSessions() { try { const stored = localStorage.getItem('guardrail_chat_sessions'); if (stored) { const parsed = JSON.parse(stored); sessions = parsed.sessions || {}; activeSessionId = parsed.activeSessionId || null; } } catch (e) { console.error('Error loading sessions:', e); } // If no sessions, create a default one const keys = Object.keys(sessions); if (keys.length === 0) { createNewSession(); } else { if (!activeSessionId || !sessions[activeSessionId]) { activeSessionId = keys[0]; } renderSidebar(); renderActiveSession(); } } function saveSessions() { try { localStorage.setItem('guardrail_chat_sessions', JSON.stringify({ sessions, activeSessionId })); } catch (e) { console.error('Error saving sessions:', e); } } function createNewSession() { const sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); sessions[sessionId] = { id: sessionId, title: 'New Chat', messages: [], timestamp: Date.now() }; activeSessionId = sessionId; saveSessions(); renderSidebar(); renderActiveSession(); } function clearChat() { // Linked to the "New Chat" button createNewSession(); } function renderSidebar() { const list = document.getElementById('historyList'); if (!list) return; list.innerHTML = ''; // Sort sessions by timestamp descending (newest first) const sortedSessions = Object.values(sessions).sort((a, b) => b.timestamp - a.timestamp); sortedSessions.forEach(session => { const item = document.createElement('div'); item.className = 'history-item' + (session.id === activeSessionId ? ' active' : ''); const titleSpan = document.createElement('span'); titleSpan.className = 'history-title'; titleSpan.innerText = '💬 ' + session.title; titleSpan.onclick = () => switchSession(session.id); const deleteBtn = document.createElement('button'); deleteBtn.className = 'btn-delete-chat'; deleteBtn.innerText = '❌'; deleteBtn.onclick = (e) => { e.stopPropagation(); deleteSession(session.id); }; item.appendChild(titleSpan); item.appendChild(deleteBtn); list.appendChild(item); }); } function switchSession(sessionId) { if (sessionId === activeSessionId) return; activeSessionId = sessionId; saveSessions(); renderSidebar(); renderActiveSession(); } function deleteSession(sessionId) { if (sessions[sessionId]) { delete sessions[sessionId]; const keys = Object.keys(sessions); if (keys.length === 0) { createNewSession(); return; } if (activeSessionId === sessionId) { // Switch to the newest remaining session const sorted = Object.values(sessions).sort((a, b) => b.timestamp - a.timestamp); activeSessionId = sorted[0].id; } saveSessions(); renderSidebar(); renderActiveSession(); } } function renderActiveSession() { const thread = document.getElementById('chatThread'); const welcome = document.getElementById('welcomeContainer'); thread.innerHTML = ''; const activeSession = sessions[activeSessionId]; if (!activeSession || activeSession.messages.length === 0) { thread.style.display = 'none'; welcome.style.display = 'block'; } else { welcome.style.display = 'none'; thread.style.display = 'flex'; activeSession.messages.forEach(msg => { appendMessageToUI(msg.content, msg.role, msg.id, msg.details); }); } } async function sendMessage() { const input = document.getElementById('userInput'); const text = input.value.trim(); if (!text) return; input.value = ''; input.style.height = 'auto'; input.style.overflowY = 'hidden'; const currentSession = sessions[activeSessionId]; if (!currentSession) return; // Toggle Send button to Stop button const sendBtn = document.getElementById('sendBtn'); if (sendBtn) { sendBtn.innerHTML = ''; sendBtn.setAttribute('onclick', 'stopGeneration()'); sendBtn.title = 'Stop Generation'; } activeAbortController = new AbortController(); const { signal } = activeAbortController; // Hide welcome block and show thread document.getElementById('welcomeContainer').style.display = 'none'; const thread = document.getElementById('chatThread'); thread.style.display = 'flex'; // If it's a new session, update title based on first query if (currentSession.title === 'New Chat') { currentSession.title = text.length > 22 ? text.substring(0, 19) + '...' : text; } // Update timestamp to float this session to top currentSession.timestamp = Date.now(); // Generate random message IDs const userMsgId = 'msg_user_' + Date.now(); const assistantMsgId = 'msg_ast_' + Date.now(); // Save user message to state currentSession.messages.push({ id: userMsgId, role: 'user', content: text }); saveSessions(); renderSidebar(); // Append User message to UI appendMessageToUI(text, 'user', userMsgId); // Append loading placeholder const loaderId = appendMessageToUI('Thinking...', 'assistant loading', 'loader_' + Date.now()); // Build message history for the AI memory const messageHistory = currentSession.messages .filter(msg => msg.role === 'user' || msg.role === 'assistant') .map(msg => ({ role: msg.role, content: msg.content })); try { const response = await fetch('/v1/shield/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: text, messages: messageHistory, stream: true }), signal: signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let hasAppendedAssistantMessage = false; let accumulatedText = ''; // Remove loading placeholder const loaderEl = document.getElementById(loaderId); if (loaderEl) loaderEl.remove(); while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // Keep partial line in buffer for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data: ')) { const jsonStr = trimmed.slice(6); try { const data = JSON.parse(jsonStr); if (data.safe) { if (data.response) { accumulatedText += data.response; if (!hasAppendedAssistantMessage) { // Append empty assistant message to state and UI currentSession.messages.push({ id: assistantMsgId, role: 'assistant', content: accumulatedText }); appendMessageToUI(accumulatedText, 'assistant', assistantMsgId); hasAppendedAssistantMessage = true; } else { // Update state currentSession.messages[currentSession.messages.length - 1].content = accumulatedText; // Update UI const msgElement = document.getElementById(assistantMsgId); if (msgElement) { const bubble = msgElement.querySelector('.bubble'); bubble.innerHTML = parseMarkdown(accumulatedText); highlightCode(bubble); } } // Auto-scroll const viewport = document.querySelector('.chat-viewport'); viewport.scrollTop = viewport.scrollHeight; } } else { // Blocked mid-stream or initially if (hasAppendedAssistantMessage) { // Remove partial message from state currentSession.messages.pop(); const msgElement = document.getElementById(assistantMsgId); if (msgElement) msgElement.remove(); } const status = (data.details && data.details.gateway_status) ? data.details.gateway_status : 'validation_failed'; const blockText = `⚠️ Request Blocked: Response violated guardrail safety parameters (${status})`; // Save blocked message to state currentSession.messages.push({ id: assistantMsgId, role: 'blocked', content: blockText, details: data.details }); appendMessageToUI(blockText, 'blocked', assistantMsgId, data.details); saveSessions(); return; } } catch (err) { console.error('Error parsing SSE JSON:', err); } } } } // Final save on successful completion saveSessions(); } catch (e) { const loaderEl = document.getElementById(loaderId); if (loaderEl) loaderEl.remove(); if (e.name === 'AbortError') { if (accumulatedText) { // Remove loading styling from bubble const msgElement = document.getElementById(assistantMsgId); if (msgElement) { msgElement.classList.remove('loading'); } } else { const stopText = 'Generation stopped.'; currentSession.messages.push({ id: 'stop_' + Date.now(), role: 'assistant', content: stopText }); appendMessageToUI(stopText, 'assistant', 'stop_' + Date.now()); } saveSessions(); } else { const errorText = '❌ Error: Failed to communicate with the guardrail gateway.'; currentSession.messages.push({ id: 'err_' + Date.now(), role: 'blocked', content: errorText }); appendMessageToUI(errorText, 'blocked', 'err_' + Date.now()); saveSessions(); } } finally { // Restore Send button UI const sendBtn = document.getElementById('sendBtn'); if (sendBtn) { sendBtn.innerHTML = ''; sendBtn.setAttribute('onclick', 'sendMessage()'); sendBtn.title = 'Send Message'; } activeAbortController = null; } } function stopGeneration() { if (activeAbortController) { activeAbortController.abort(); activeAbortController = null; } } function appendMessageToUI(text, type, msgId, details = null) { const thread = document.getElementById('chatThread'); if (!thread) return msgId; const msgDiv = document.createElement('div'); msgDiv.className = `message ${type}`; msgDiv.id = msgId; let contentHtml = ''; if (type === 'user') { contentHtml = escapeHTML(text); } else if (type === 'assistant') { contentHtml = parseMarkdown(text); } else if (type === 'blocked') { contentHtml = escapeHTML(text); if (details && details.input_guard && details.input_guard.matched_rule !== 'none') { contentHtml += `