// static/js/auto_visual_script.js const API_URL = '/api/generate-visual'; const CONTAINER_ID = 'graph-container'; // Styling and Sizing Constants const NODE_WIDTH = 180; const NODE_HEIGHT = 60; const NODE_PADDING = 15; const FONT_SIZE = 14; let stage, layer; // --- Utility Functions --- function showAlert(message, type = 'success') { const alert = document.getElementById('message-alert'); if (!alert) { console.error('Alert element not found'); return; } alert.textContent = message; alert.className = `message-alert show message-${type}`; setTimeout(() => { alert.classList.remove('show'); }, 3000); } function calculateArrowPoints(n1_pos, n2_pos) { const x1 = n1_pos.x + NODE_WIDTH / 2; const y1 = n1_pos.y + NODE_HEIGHT / 2; const x2 = n2_pos.x + NODE_WIDTH / 2; const y2 = n2_pos.y + NODE_HEIGHT / 2; const dx = x2 - x1; const dy = y2 - y1; const angle = Math.atan2(dy, dx); const x1_adjusted = x1 + Math.cos(angle) * (NODE_WIDTH / 2); const y1_adjusted = y1 + Math.sin(angle) * (NODE_HEIGHT / 2); const x2_adjusted = x2 - Math.cos(angle) * (NODE_WIDTH / 2); const y2_adjusted = y2 - Math.sin(angle) * (NODE_HEIGHT / 2); return [x1_adjusted, y1_adjusted, x2_adjusted, y2_adjusted]; } // --- Canvas Initialization & Rendering --- function initializeCanvas() { const container = document.getElementById(CONTAINER_ID); if (!container) { console.error('Graph container not found!'); return; } const width = container.offsetWidth || 800; const height = container.offsetHeight || 600; console.log('Initializing canvas:', width, 'x', height); stage = new Konva.Stage({ container: CONTAINER_ID, width: width, height: height, draggable: true }); layer = new Konva.Layer(); stage.add(layer); const infoText = new Konva.Text({ x: width / 2, y: height / 2, text: 'Enter text on the left to generate your first diagram.', fontSize: 20, fill: '#a0aec0', align: 'center', verticalAlign: 'middle', }); infoText.offsetX(infoText.width() / 2); infoText.offsetY(infoText.height() / 2); layer.add(infoText); layer.draw(); } function applyDagreLayout(spec) { console.log('Applying Dagre layout to spec:', spec); if (!window.dagre) { console.error('Dagre library not loaded!'); return spec; } const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: spec.layout_dir || 'TB', nodesep: 50, ranksep: 80, marginx: 50, marginy: 50 }); g.setDefaultNodeLabel(() => ({})); g.setDefaultEdgeLabel(() => ({})); spec.nodes.forEach(n => { g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); }); spec.edges.forEach(e => { g.setEdge(e.from, e.to); }); dagre.layout(g); const graphWidth = g.graph().width; const graphHeight = g.graph().height; const centerX = graphWidth / 2; const centerY = graphHeight / 2; const offsetX = stage.width() / 2 - centerX; const offsetY = stage.height() / 2 - centerY; spec.nodes = spec.nodes.map(n => { const node_pos = g.node(n.id); return { ...n, x: node_pos.x - NODE_WIDTH / 2 + offsetX, y: node_pos.y - NODE_HEIGHT / 2 + offsetY, }; }); console.log('Layout applied, positioned nodes:', spec.nodes); return spec; } function renderVisualSpec(spec) { console.log('Rendering visual spec:', spec); if (!stage) { console.log('Stage not initialized, initializing now...'); initializeCanvas(); } if (!layer) { console.error('Layer not available!'); return; } layer.destroyChildren(); const positionedSpec = applyDagreLayout(spec); const visualType = positionedSpec.meta?.type || 'diagram'; const layoutDir = positionedSpec.layout_dir || 'TB'; const visualTypeDisplay = document.getElementById('visual-type-display'); if (visualTypeDisplay) { visualTypeDisplay.textContent = `Type: ${visualType.toUpperCase()} (${layoutDir})`; } const exportBtn = document.getElementById('export-png'); const feedbackBtn = document.getElementById('feedback-button'); if (exportBtn) exportBtn.style.display = 'block'; if (feedbackBtn) feedbackBtn.style.display = 'block'; const konvaNodes = {}; // Draw Nodes positionedSpec.nodes.forEach(n => { const group = new Konva.Group({ x: n.x, y: n.y, draggable: true, id: n.id, name: 'node' }); const rect = new Konva.Rect({ width: NODE_WIDTH, height: NODE_HEIGHT, cornerRadius: 10, fill: '#F3E8FF', stroke: '#7C3AED', strokeWidth: 2, }); const text = new Konva.Text({ text: n.label, width: NODE_WIDTH - NODE_PADDING * 2, padding: NODE_PADDING, fontSize: FONT_SIZE, fill: '#1a202c', align: 'center', verticalAlign: 'middle', y: (NODE_HEIGHT - FONT_SIZE - NODE_PADDING * 2) / 2 }); group.add(rect); group.add(text); group.on('mouseenter', () => { document.body.style.cursor = 'move'; rect.strokeWidth(3); layer.draw(); }); group.on('mouseleave', () => { document.body.style.cursor = 'default'; rect.strokeWidth(2); layer.draw(); }); group.on('dragmove', () => { layer.draw(); }); layer.add(group); konvaNodes[n.id] = group; }); // Draw Edges positionedSpec.edges.forEach(e => { const sourceNode = konvaNodes[e.from]; const targetNode = konvaNodes[e.to]; if (!sourceNode || !targetNode) { console.warn(`Edge references missing node: ${e.from} -> ${e.to}`); return; } const getPoints = () => calculateArrowPoints(sourceNode.position(), targetNode.position()); const arrow = new Konva.Arrow({ points: getPoints(), pointerLength: 10, pointerWidth: 10, fill: '#6B46C1', stroke: '#6B46C1', strokeWidth: 2, name: 'edge' }); sourceNode.on('dragmove', () => { arrow.points(getPoints()); }); targetNode.on('dragmove', () => { arrow.points(getPoints()); }); layer.add(arrow); arrow.moveToBottom(); }); layer.draw(); stage.position({ x: 0, y: 0 }); console.log('Visual rendered successfully'); } // --- API Call Handler --- async function generateVisual() { console.log('=== Generate Visual Called ==='); const button = document.getElementById('generate-visual'); if (!button) { console.error('Generate button not found!'); return; } const originalText = button.textContent; button.textContent = 'Generating...'; button.disabled = true; const textInput = document.getElementById('text-input'); if (!textInput) { console.error('Text input element with id "text-input" not found!'); console.log('Available inputs:', document.querySelectorAll('input, textarea')); showAlert('Configuration error: text input not found', 'error'); button.textContent = originalText; button.disabled = false; return; } const text = textInput.value; const llmToggle = document.getElementById('llm-toggle'); const useLLM = llmToggle ? llmToggle.checked : false; console.log('Input text:', text); console.log('Use LLM:', useLLM); if (!text.trim()) { showAlert('Please enter some text.', 'error'); button.textContent = originalText; button.disabled = false; return; } try { console.log('Making API call to:', API_URL); const requestBody = { text: text, use_llm: useLLM }; console.log('Request body:', JSON.stringify(requestBody)); const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); console.log('Response received:'); console.log('- Status:', response.status); console.log('- Status Text:', response.statusText); console.log('- OK:', response.ok); console.log('- Headers:', response.headers); let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); console.log('Response data:', data); } else { const textResponse = await response.text(); console.log('Non-JSON response:', textResponse); throw new Error('Server returned non-JSON response: ' + textResponse.substring(0, 100)); } if (response.ok) { if (!data.nodes || data.nodes.length === 0) { showAlert('No nodes generated. Try different text.', 'error'); } else { renderVisualSpec(data); showAlert('Diagram generated successfully!'); } } else { const errorMsg = data.error || 'Failed to generate visual.'; console.error('API returned error:', errorMsg); showAlert(`Error: ${errorMsg}`, 'error'); } } catch (error) { console.error('=== ERROR IN GENERATE VISUAL ==='); console.error('Error type:', error.name); console.error('Error message:', error.message); console.error('Error stack:', error.stack); if (error.name === 'TypeError' && error.message.includes('fetch')) { showAlert('Network error: Cannot connect to server. Is Flask running?', 'error'); } else { showAlert('Error: ' + error.message, 'error'); } } finally { button.textContent = originalText; button.disabled = false; } } function exportAsPNG() { console.log('Export PNG called'); if (stage && layer.getChildren().length > 0) { try { const dataURL = stage.toDataURL({ mimeType: 'image/png', quality: 1, pixelRatio: 2 }); const link = document.createElement('a'); link.download = 'auto-visual-diagram.png'; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); showAlert('Exported as PNG!'); } catch (error) { console.error('Export error:', error); showAlert('Export failed: ' + error.message, 'error'); } } else { showAlert('Generate a diagram first!', 'error'); } } // --- Initialization --- document.addEventListener('DOMContentLoaded', () => { console.log('=== DOM Content Loaded ==='); console.log('Konva available:', typeof Konva !== 'undefined'); console.log('Dagre available:', typeof dagre !== 'undefined'); // Check if required libraries are loaded if (typeof Konva === 'undefined') { console.error('Konva library not loaded!'); showAlert('Error: Konva library not loaded', 'error'); return; } if (typeof dagre === 'undefined') { console.error('Dagre library not loaded!'); showAlert('Error: Dagre library not loaded', 'error'); return; } // Initialize canvas initializeCanvas(); // Attach Event Listeners const generateBtn = document.getElementById('generate-visual'); const exportBtn = document.getElementById('export-png'); const feedbackBtn = document.getElementById('feedback-button'); if (generateBtn) { console.log('Generate button found, attaching listener'); generateBtn.addEventListener('click', generateVisual); } else { console.error('Generate button not found!'); } if (exportBtn) { exportBtn.addEventListener('click', exportAsPNG); } if (feedbackBtn) { feedbackBtn.addEventListener('click', () => { showAlert('Feedback feature coming soon!', 'success'); }); } // Handle window resize window.addEventListener('resize', () => { if (stage) { const container = document.getElementById(CONTAINER_ID); if (container) { stage.width(container.offsetWidth); stage.height(container.offsetHeight); layer.draw(); } } }); console.log('=== Initialization Complete ==='); });