{% extends "appbuilder/base.html" %} {% block head_css %} {{ super() }} {% endblock %} {% block content %}
{{ content|safe }}
{% endblock %} {% block tail %} {{ super() }} let currentSessionId = null; let csrfToken = null; // Expandable Code Blocks Functions function toggleCodeBlock(header) { const content = header.nextElementSibling; const isExpanded = content.classList.contains('expanded'); console.log('Toggling code block, currently expanded:', isExpanded); if (isExpanded) { // Collapse content.classList.remove('expanded'); header.classList.remove('expanded'); content.style.maxHeight = '0'; console.log('Collapsed'); } else { // Expand content.classList.add('expanded'); header.classList.add('expanded'); // Set a reasonable max height for the content content.style.maxHeight = '1000px'; console.log('Expanded, content height:', content.scrollHeight); } } // Make functions globally available window.toggleCodeBlock = toggleCodeBlock; document.addEventListener('DOMContentLoaded', function() { initializeCSRF().then(() => { initializeSession(); }); document.getElementById('chatInputForm').addEventListener('submit', sendMessage); document.getElementById('newChatBtn').addEventListener('click', newChat); document.getElementById('clearChatBtn').addEventListener('click', clearChat); }); async function initializeCSRF() { try { const response = await fetch('/api/v1/security/csrf_token/'); if (response.ok) { const data = await response.json(); csrfToken = data.result; } } catch (error) { console.error('Error fetching CSRF token:', error); } } async function initializeSession() { try { const headers = { 'Content-Type': 'application/json' }; if (csrfToken) { headers['X-CSRFToken'] = csrfToken; } const response = await fetch('/aisupersetassistantview/api/new_session', { method: 'POST', headers: headers }); if (response.ok) { const data = await response.json(); currentSessionId = data.session_id; document.getElementById('sessionId').textContent = currentSessionId.substring(0, 8) + '...'; document.getElementById('connectionStatus').textContent = 'Connected'; } else { throw new Error('Failed to initialize session'); } } catch (error) { console.error('Error initializing session:', error); document.getElementById('connectionStatus').textContent = 'Error'; } } async function sendMessage(event) { event.preventDefault(); const messageInput = document.getElementById('messageInput'); const sendBtn = document.getElementById('sendBtn'); const message = messageInput.value.trim(); if (!message || !currentSessionId) return; messageInput.disabled = true; sendBtn.disabled = true; sendBtn.textContent = 'Sending...'; addMessage(message, 'user'); messageInput.value = ''; showTypingIndicator(); try { let assistantMessageDiv = null; let assistantContentDiv = null; function createAssistantMessage() { const messagesContainer = document.getElementById('chatMessages'); assistantMessageDiv = document.createElement('div'); assistantMessageDiv.className = 'message assistant'; assistantContentDiv = document.createElement('div'); assistantContentDiv.innerHTML = ''; const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = new Date().toLocaleTimeString(); assistantMessageDiv.appendChild(assistantContentDiv); assistantMessageDiv.appendChild(timeDiv); messagesContainer.appendChild(assistantMessageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } const headers = { 'Content-Type': 'application/json' }; if (csrfToken) { headers['X-CSRFToken'] = csrfToken; } const response = await fetch('/aisupersetassistantview/api/chat_stream', { method: 'POST', headers: headers, body: JSON.stringify({ message: message, session_id: currentSessionId }) }); if (!response.ok) { throw new Error('Failed to send message'); } hideTypingIndicator(); createAssistantMessage(); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); if (data.type === 'session' && data.session_id) { currentSessionId = data.session_id; document.getElementById('sessionId').textContent = currentSessionId.substring(0, 8) + '...'; } else if (data.type === 'chunk' && data.content) { assistantContentDiv.innerHTML += data.content; const messagesContainer = document.getElementById('chatMessages'); messagesContainer.scrollTop = messagesContainer.scrollHeight; } else if (data.type === 'error') { assistantContentDiv.innerHTML = data.content; } else if (data.type === 'done') { // First, convert tool blocks BEFORE general markdown processing let finalContent = assistantContentDiv.innerHTML; console.log('=== PROCESSING FINAL CONTENT ==='); console.log('Content length:', finalContent.length); console.log('Full content:', finalContent); console.log('================================'); // Check if "Start Running Tool:" exists in content if (finalContent.includes('Start Running Tool:')) { console.log('✓ "Start Running Tool:" found in content'); } else { console.log('✗ "Start Running Tool:" NOT found in content'); } // Convert tool execution blocks to expandable format // The content has REAL newlines, not \n strings // Strategy: Tools can start in parallel, so we need to match backwards from each "Tool Output" // First, extract all "Start Running Tool" blocks and all "Tool Output" blocks separately const toolStarts = []; const toolOutputs = []; // Find all "Start Running Tool" blocks (include the label in fullMatch) const startPattern = /(Start Running Tool:\s*```\s*[\s\S]*?```)/g; let match; while ((match = startPattern.exec(finalContent)) !== null) { toolStarts.push({ fullMatch: match[1], data: match[1].replace(/Start Running Tool:\s*```\s*/, '').replace(/```\s*$/, '').trim(), index: match.index }); } // Find all "Tool Output" blocks (include the label in fullMatch) const outputPattern = /(Tool Output:\s*```\s*[\s\S]*?```)/g; while ((match = outputPattern.exec(finalContent)) !== null) { toolOutputs.push({ fullMatch: match[1], data: match[1].replace(/Tool Output:\s*```\s*/, '').replace(/```\s*$/, '').trim(), index: match.index }); } console.log('Found', toolStarts.length, 'tool starts and', toolOutputs.length, 'tool outputs'); // Log each tool start position and preview toolStarts.forEach((start, idx) => { console.log('Tool Start #' + (idx + 1) + ' at index ' + start.index + ':', start.data.substring(0, 80)); }); // Log each tool output position and preview toolOutputs.forEach((output, idx) => { console.log('Tool Output #' + (idx + 1) + ' at index ' + output.index + ':', output.data.substring(0, 80)); }); // Now pair them up - match in order (first start with first output, etc.) const pairs = []; const minPairs = Math.min(toolStarts.length, toolOutputs.length); for (let i = 0; i < minPairs; i++) { pairs.push({ start: toolStarts[i], output: toolOutputs[i] }); console.log('Pairing tool #' + (i + 1) + ':', toolStarts[i].data.substring(0, 50), '...'); } console.log('Created', pairs.length, 'pairs'); // IMPORTANT: When tools run in parallel, the content structure is: // "Start Running Tool #1" then "Start Running Tool #2" then "Tool Output #1" then "Tool Output #2" // We can't treat them as contiguous blocks because Start#1...Output#1 would include Start#2 // Solution: Collect all the segments to replace (both starts and outputs) separately, // sort them by position, then replace them with placeholders in reverse order to avoid index shifting const segmentsToReplace = []; // Add all start blocks toolStarts.forEach((start, idx) => { segmentsToReplace.push({ type: 'start', index: start.index, endIndex: start.index + start.fullMatch.length, data: start.data, pairIndex: idx, fullMatch: start.fullMatch }); }); // Add all output blocks toolOutputs.forEach((output, idx) => { segmentsToReplace.push({ type: 'output', index: output.index, endIndex: output.index + output.fullMatch.length, data: output.data, pairIndex: idx, fullMatch: output.fullMatch }); }); // Sort by position (ascending) segmentsToReplace.sort((a, b) => a.index - b.index); console.log('Segments to replace (in document order):'); segmentsToReplace.forEach((seg, idx) => { console.log(' Segment #' + (idx + 1) + ' (' + seg.type + ' for pair ' + seg.pairIndex + '): ' + seg.index + ' - ' + seg.endIndex); }); // Now replace in REVERSE order to avoid index shifting // But first, we need to build the complete tool block HTML for each pair const pairPlaceholders = pairs.map((pair, idx) => { return `§§§TOOL_BLOCK_START§§§${pair.start.data}§§§TOOL_BLOCK_MID§§§${pair.output.data}§§§TOOL_BLOCK_END§§§`; }); // Process segments in reverse order for (let i = segmentsToReplace.length - 1; i >= 0; i--) { const segment = segmentsToReplace[i]; // If this is a "start" segment, replace with the full tool block placeholder // If this is an "output" segment, just remove it (empty string) const replacement = segment.type === 'start' ? pairPlaceholders[segment.pairIndex] : ''; // Replace this segment in the content finalContent = finalContent.substring(0, segment.index) + replacement + finalContent.substring(segment.endIndex); console.log('Replaced ' + segment.type + ' #' + segment.pairIndex + ' at ' + segment.index + '-' + segment.endIndex); } console.log('Rebuilt content with', pairs.length, 'placeholders'); console.log('Total tool blocks paired:', pairs.length); if (pairs.length === 0) { console.log('⚠️ WARNING: No tool blocks matched! Checking for partial patterns...'); console.log('Has "Start Running Tool:"?', finalContent.includes('Start Running Tool:')); console.log('Has "Tool Output:"?', finalContent.includes('Tool Output:')); console.log('Has triple backticks?', finalContent.includes('```')); } // Now do standard markdown conversion finalContent = finalContent .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/```([\s\S]*?)```/g, '
$1
') .replace(/`([^`]+)`/g, '$1') .replace(/\n/g, '
'); // Finally, convert tool block placeholders to expandable HTML let placeholderCount = 0; // Check how many placeholders exist in the content const placeholderMatches = finalContent.match(/§§§TOOL_BLOCK_START§§§/g); console.log('Placeholders in content:', placeholderMatches ? placeholderMatches.length : 0); // Check if the section sign survived console.log('Content contains §§§ ?', finalContent.includes('§§§')); console.log('First 500 chars after markdown:', finalContent.substring(0, 500)); finalContent = finalContent.replace(/§§§TOOL_BLOCK_START§§§([\s\S]*?)§§§TOOL_BLOCK_MID§§§([\s\S]*?)§§§TOOL_BLOCK_END§§§/g, function(match, toolData, outputData) { placeholderCount++; console.log('Converting placeholder #' + placeholderCount); const cleanToolData = toolData.replace(/
/g, '\n').replace(/</g, '<').replace(/>/g, '>'); const cleanOutputData = outputData.replace(/
/g, '\n').replace(/</g, '<').replace(/>/g, '>'); return `
${cleanToolData}
Tool Output:
${cleanOutputData}

`; }); console.log('Converted', placeholderCount, 'placeholders to HTML'); assistantContentDiv.innerHTML = finalContent; // Add event listeners to all expandable headers (CSP-compliant) const expandableHeaders = assistantMessageDiv.querySelectorAll('[data-toggle="code-block"]'); expandableHeaders.forEach(header => { header.addEventListener('click', function() { toggleCodeBlock(this); }); header.style.cursor = 'pointer'; }); console.log('Final content set with', expandableHeaders.length, 'tool blocks'); break; } } catch (e) { console.error('Error parsing streaming data:', e); } } } } } catch (error) { console.error('Error sending message:', error); hideTypingIndicator(); addMessage('Sorry, I encountered an error. Please try again.', 'assistant'); } finally { messageInput.disabled = false; sendBtn.disabled = false; sendBtn.textContent = 'Send'; messageInput.focus(); } } function addMessage(content, sender) { const messagesContainer = document.getElementById('chatMessages'); const messageDiv = document.createElement('div'); messageDiv.className = `message ${sender}`; let formattedContent = content; // For assistant messages, process tool blocks first if (sender === 'assistant') { // Extract all "Start Running Tool" and "Tool Output" blocks const toolStarts = []; const toolOutputs = []; const startPattern = /(Start Running Tool:\s*```\s*[\s\S]*?```)/g; let match; while ((match = startPattern.exec(formattedContent)) !== null) { toolStarts.push({ fullMatch: match[1], data: match[1].replace(/Start Running Tool:\s*```\s*/, '').replace(/```\s*$/, '').trim(), index: match.index }); } const outputPattern = /(Tool Output:\s*```\s*[\s\S]*?```)/g; while ((match = outputPattern.exec(formattedContent)) !== null) { toolOutputs.push({ fullMatch: match[1], data: match[1].replace(/Tool Output:\s*```\s*/, '').replace(/```\s*$/, '').trim(), index: match.index }); } // Pair them up - match in order (first start with first output, etc.) const pairs = []; const minPairs = Math.min(toolStarts.length, toolOutputs.length); for (let i = 0; i < minPairs; i++) { pairs.push({ start: toolStarts[i], output: toolOutputs[i] }); } // Use the same replacement strategy as in sendMessage to handle parallel tool execution const segmentsToReplace = []; // Add all start blocks toolStarts.forEach((start, idx) => { segmentsToReplace.push({ type: 'start', index: start.index, endIndex: start.index + start.fullMatch.length, data: start.data, pairIndex: idx, fullMatch: start.fullMatch }); }); // Add all output blocks toolOutputs.forEach((output, idx) => { segmentsToReplace.push({ type: 'output', index: output.index, endIndex: output.index + output.fullMatch.length, data: output.data, pairIndex: idx, fullMatch: output.fullMatch }); }); // Sort by position segmentsToReplace.sort((a, b) => a.index - b.index); // Build placeholders const pairPlaceholders = pairs.map((pair, idx) => { return `§§§TOOL_BLOCK_START§§§${pair.start.data}§§§TOOL_BLOCK_MID§§§${pair.output.data}§§§TOOL_BLOCK_END§§§`; }); // Replace in reverse order for (let i = segmentsToReplace.length - 1; i >= 0; i--) { const segment = segmentsToReplace[i]; const replacement = segment.type === 'start' ? pairPlaceholders[segment.pairIndex] : ''; formattedContent = formattedContent.substring(0, segment.index) + replacement + formattedContent.substring(segment.endIndex); } } // Now do standard markdown conversion formattedContent = formattedContent .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/```([\s\S]*?)```/g, '
$1
') .replace(/`([^`]+)`/g, '$1') .replace(/\n/g, '
'); // Convert tool block placeholders to expandable HTML if (sender === 'assistant') { formattedContent = formattedContent.replace(/§§§TOOL_BLOCK_START§§§([\s\S]*?)§§§TOOL_BLOCK_MID§§§([\s\S]*?)§§§TOOL_BLOCK_END§§§/g, function(match, toolData, outputData) { const cleanToolData = toolData.replace(/
/g, '\n').replace(/</g, '<').replace(/>/g, '>'); const cleanOutputData = outputData.replace(/
/g, '\n').replace(/</g, '<').replace(/>/g, '>'); return `
${cleanToolData}
Tool Output:
${cleanOutputData}

`; }); } messageDiv.innerHTML = `
${formattedContent}
${new Date().toLocaleTimeString()}
`; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; // Add event listeners to expandable headers (CSP-compliant) if (sender === 'assistant') { const expandableHeaders = messageDiv.querySelectorAll('[data-toggle="code-block"]'); expandableHeaders.forEach(header => { header.addEventListener('click', function() { toggleCodeBlock(this); }); header.style.cursor = 'pointer'; }); } } function showTypingIndicator() { document.getElementById('typingIndicator').classList.add('show'); const messagesContainer = document.getElementById('chatMessages'); messagesContainer.scrollTop = messagesContainer.scrollHeight; } function hideTypingIndicator() { document.getElementById('typingIndicator').classList.remove('show'); } async function newChat() { if (confirm('Start a new chat session? This will clear the current conversation.')) { await initializeSession(); clearChatDisplay(); addMessage('Hello! I\'m your AI Superset Assistant 🚀\n\nI can help you with:\n• Creating and structuring DAGs\n• Debugging workflow issues\n• Performance optimization\n• Best practices and patterns\n\nWhat would you like to know about Apache Superset?', 'assistant'); } } function clearChat() { if (confirm('Clear the current chat history?')) { clearChatDisplay(); addMessage('Chat cleared. How can I help you with Apache Superset?', 'assistant'); } } function clearChatDisplay() { const messagesContainer = document.getElementById('chatMessages'); messagesContainer.innerHTML = ''; } function showExamples() { const examples = [ "How do I create a simple DAG?", "Help me debug a failing task", "What are DAG best practices?", "How can I optimize my workflow performance?", "Show me sensor examples" ]; const messageInput = document.getElementById('messageInput'); const randomExample = examples[Math.floor(Math.random() * examples.length)]; messageInput.value = randomExample; messageInput.focus(); } document.getElementById('messageInput').addEventListener('keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); document.querySelector('.chat-input-form').dispatchEvent(new Event('submit')); } }); {% endblock %}