PubmedResearchRover / static /js /auto_visual_script.js
flyfir248's picture
feat(visualizer): Stabilize Graph Generation and Secure LLM API Setup
e2e41d5
Raw
History Blame
13 kB
// 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 ===');
});