Spaces:
Paused
Paused
| // 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 ==='); | |
| }); |