Diagram-Tool-with-AI / index.html
awacke1's picture
Update index.html
8486ac9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Diagram Tool with AI</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior: none;
}
.shape {
cursor: move;
user-select: none;
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1); /* Smooth animation for position changes */
}
.shape-text {
pointer-events: none;
user-select: none;
font-size: 14px;
font-weight: 500;
}
#canvas-svg {
touch-action: none;
}
.connector-line {
stroke-width: 2.5;
stroke-linecap: round;
}
.highlight-connector {
stroke: #fbbf24; /* amber-400 */
stroke-width: 4;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
}
/* Modal styles */
#ai-modal {
transition: opacity 0.3s ease-in-out;
}
#ai-modal.hidden {
opacity: 0;
pointer-events: none;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: #4f46e5; /* indigo-600 */
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 flex flex-col h-screen antialiased">
<!-- Header -->
<header class="bg-white shadow-md p-4 z-20">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-xl font-bold text-gray-800">Diagram Tool with AI</h1>
</div>
</header>
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Toolbar -->
<aside class="w-64 bg-white p-4 space-y-4 shadow-lg overflow-y-auto z-10">
<h2 class="text-lg font-semibold text-gray-700 border-b pb-2">Tools</h2>
<div class="space-y-2">
<button id="add-rect" class="w-full bg-sky-500 hover:bg-sky-600 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-square" viewBox="0 0 16 16"><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/></svg>
<span>Rectangle</span>
</button>
<button id="add-circle" class="w-full bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/></svg>
<span>Circle</span>
</button>
<button id="add-diamond" class="w-full bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-diamond-fill" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M6.95.435c.58-.58 1.52-.58 2.1 0l6.515 6.516c.58.58.58 1.519 0 2.098L9.05 15.565c-.58.58-1.519.58-2.1 0L.435 9.05c-.58-.58-.58-1.519 0-2.098L6.95.435z"/></svg>
<span>Diamond</span>
</button>
</div>
<div class="border-t pt-4 space-y-2">
<button id="connect-btn" class="w-full bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200">Connect Shapes</button>
<button id="clear-btn" class="w-full bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200">Clear Canvas</button>
</div>
<div class="border-t pt-4 space-y-2">
<button id="ai-magic-btn" class="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white font-bold py-2 px-4 rounded-lg transition-all duration-200 flex items-center justify-center space-x-2">
<span></span>
<span>Auto-Arrange & Explain</span>
</button>
</div>
<div id="status-box" class="mt-4 p-3 bg-gray-100 rounded-lg text-sm text-gray-700 text-center">
Add a shape to get started.
</div>
</aside>
<!-- Canvas Area -->
<main class="flex-1 bg-gray-200 p-4">
<div id="canvas-container" class="w-full h-full bg-white rounded-lg shadow-inner overflow-hidden relative">
<svg id="canvas-svg" class="w-full h-full"></svg>
</div>
</main>
</div>
<!-- AI Modal -->
<div id="ai-modal" class="fixed inset-0 bg-gray-900 bg-opacity-60 flex items-center justify-center p-4 z-50 hidden">
<div id="ai-modal-content" class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-2xl transform transition-all">
<!-- Loading State -->
<div id="ai-loading" class="text-center">
<div class="spinner mx-auto mb-4"></div>
<h3 class="text-lg font-semibold text-gray-800">AI is thinking...</h3>
<p class="text-gray-600">Analyzing your diagram to find the best layout.</p>
</div>
<!-- Result State -->
<div id="ai-result" class="hidden">
<h3 class="text-2xl font-bold text-gray-800 mb-4">✨ Diagram Explanation</h3>
<div id="ai-explanation" class="prose prose-sm max-w-none text-gray-700 bg-gray-50 p-4 rounded-lg"></div>
<button id="close-modal-btn" class="mt-6 w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200">Close</button>
</div>
</div>
</div>
<script>
// --- DOM Element References ---
const canvasContainer = document.getElementById('canvas-container');
const svg = document.getElementById('canvas-svg');
const addRectBtn = document.getElementById('add-rect');
const addCircleBtn = document.getElementById('add-circle');
const addDiamondBtn = document.getElementById('add-diamond');
const connectBtn = document.getElementById('connect-btn');
const clearBtn = document.getElementById('clear-btn');
const statusBox = document.getElementById('status-box');
const aiMagicBtn = document.getElementById('ai-magic-btn');
const aiModal = document.getElementById('ai-modal');
const aiModalContent = document.getElementById('ai-modal-content');
const aiLoading = document.getElementById('ai-loading');
const aiResult = document.getElementById('ai-result');
const aiExplanation = document.getElementById('ai-explanation');
const closeModalBtn = document.getElementById('close-modal-btn');
// --- State Management ---
let shapes = [];
let connectors = [];
let isDragging = false;
let isConnecting = false;
let draggedShape = null;
let connectionStartShape = null;
let offset = { x: 0, y: 0 };
// --- Core Functions ---
function render() {
svg.innerHTML = '';
const connectorsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
connectors.forEach(conn => {
const fromShape = shapes.find(s => s.id === conn.from);
const toShape = shapes.find(s => s.id === conn.to);
if (fromShape && toShape) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', fromShape.x);
line.setAttribute('y1', fromShape.y);
line.setAttribute('x2', toShape.x);
line.setAttribute('y2', toShape.y);
line.setAttribute('class', 'connector-line stroke-gray-500');
connectorsGroup.appendChild(line);
}
});
svg.appendChild(connectorsGroup);
const shapesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
shapes.forEach(shape => {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.setAttribute('class', 'shape');
group.setAttribute('data-id', shape.id);
group.setAttribute('transform', `translate(${shape.x}, ${shape.y})`);
let element;
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
textElement.setAttribute('class', 'shape-text fill-white');
textElement.setAttribute('text-anchor', 'middle');
textElement.setAttribute('dominant-baseline', 'central');
switch (shape.type) {
case 'rect':
element = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
element.setAttribute('x', -shape.width / 2);
element.setAttribute('y', -shape.height / 2);
element.setAttribute('width', shape.width);
element.setAttribute('height', shape.height);
element.setAttribute('class', 'fill-sky-500');
element.setAttribute('rx', 8);
textElement.textContent = 'Process';
break;
case 'circle':
element = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
element.setAttribute('r', shape.radius);
element.setAttribute('class', 'fill-emerald-500');
textElement.textContent = 'Start/End';
break;
case 'diamond':
element = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const points = `0,${-shape.height / 2} ${shape.width / 2},0 0,${shape.height / 2} ${-shape.width / 2},0`;
element.setAttribute('points', points);
element.setAttribute('class', 'fill-indigo-500');
textElement.textContent = 'Decision';
break;
}
if (connectionStartShape && connectionStartShape.id === shape.id) {
element.classList.add('highlight-connector');
}
group.appendChild(element);
group.appendChild(textElement);
shapesGroup.appendChild(group);
});
svg.appendChild(shapesGroup);
}
function addShape(type) {
const canvasRect = canvasContainer.getBoundingClientRect();
const newShape = {
id: `shape-${crypto.randomUUID().substring(0, 8)}`,
type: type,
x: canvasRect.width / 2 + (Math.random() - 0.5) * 100,
y: canvasRect.height / 2 + (Math.random() - 0.5) * 100,
width: 120,
height: 60,
radius: 40,
};
shapes.push(newShape);
updateStatus('Shape added. Drag to position.');
render();
}
function getMousePosition(evt) {
const CTM = svg.getScreenCTM();
const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY;
return {
x: (clientX - CTM.e) / CTM.a,
y: (clientY - CTM.f) / CTM.d
};
}
function updateStatus(message) {
statusBox.textContent = message;
}
// --- Event Handlers ---
function handlePointerDown(evt) {
evt.preventDefault();
const target = evt.target.closest('.shape');
if (!target) return;
const shapeId = target.getAttribute('data-id');
const shape = shapes.find(s => s.id === shapeId);
if (isConnecting) {
if (!connectionStartShape) {
connectionStartShape = shape;
updateStatus('Selected start shape. Click another to connect.');
} else {
if (connectionStartShape.id !== shape.id) {
connectors.push({ from: connectionStartShape.id, to: shape.id });
updateStatus('Shapes connected!');
} else {
updateStatus('Cannot connect a shape to itself.');
}
connectionStartShape = null;
isConnecting = false;
connectBtn.classList.remove('bg-amber-500');
connectBtn.classList.add('bg-gray-600');
}
} else {
isDragging = true;
draggedShape = shape;
const mousePos = getMousePosition(evt);
offset.x = mousePos.x - draggedShape.x;
offset.y = mousePos.y - draggedShape.y;
updateStatus('Dragging shape...');
}
render();
}
function handlePointerMove(evt) {
if (!isDragging || !draggedShape) return;
evt.preventDefault();
const mousePos = getMousePosition(evt);
draggedShape.x = mousePos.x - offset.x;
draggedShape.y = mousePos.y - offset.y;
render();
}
function handlePointerUp(evt) {
if (isDragging) {
updateStatus('Drag shape or add another.');
}
isDragging = false;
draggedShape = null;
}
// --- Gemini API Integration ---
async function callGeminiForLayout() {
if (shapes.length < 2) {
updateStatus("Add at least two shapes to use the AI feature.");
return;
}
// Show loading modal
aiModal.classList.remove('hidden');
aiLoading.classList.remove('hidden');
aiResult.classList.add('hidden');
const canvasRect = canvasContainer.getBoundingClientRect();
// 1. Serialize the diagram data
const diagramData = {
nodes: shapes.map(s => ({ id: s.id, type: s.type, label: s.type === 'rect' ? 'Process' : (s.type === 'circle' ? 'Start/End' : 'Decision') })),
edges: connectors.map(c => ({ from: c.from, to: c.to })),
canvas: { width: canvasRect.width, height: canvasRect.height }
};
// 2. Create the prompt for the Gemini API
const prompt = `
You are an expert diagramming assistant. Your task is to analyze a given diagram structure and provide an optimal layout and a clear explanation.
The diagram is represented by a JSON object with nodes and edges.
- "nodes" contains a list of shapes with their ID and type.
- "edges" defines the connections between nodes using their IDs.
- "canvas" provides the dimensions of the drawing area.
Based on the following diagram data, please return a JSON object that contains:
1. A "layout" object where keys are the node IDs and values are objects with optimal 'x' and 'y' coordinates for a clean, top-to-bottom flowchart layout. The coordinates should be within the canvas dimensions.
2. A "explanation" string that describes the process flow of the diagram in a clear, step-by-step manner.
Diagram Data:
${JSON.stringify(diagramData, null, 2)}
`;
// 3. Define the expected JSON schema for the response
const schema = {
type: "OBJECT",
properties: {
"layout": {
type: "OBJECT",
"description": "An object where keys are node IDs and values are objects with x and y coordinates.",
},
"explanation": {
type: "STRING",
"description": "A step-by-step explanation of the diagram's flow."
}
},
required: ["layout", "explanation"]
};
try {
// 4. Call the Gemini API
let chatHistory = [{ role: "user", parts: [{ text: prompt }] }];
const payload = {
contents: chatHistory,
generationConfig: {
responseMimeType: "application/json",
responseSchema: schema,
}
};
const apiKey = ""; // Leave empty, will be handled by the environment
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`API call failed with status: ${response.status}`);
}
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const jsonText = result.candidates[0].content.parts[0].text;
const parsedJson = JSON.parse(jsonText);
// 5. Apply the AI suggestions
applyAILayout(parsedJson.layout);
displayAIExplanation(parsedJson.explanation);
} else {
throw new Error("Invalid response structure from API.");
}
} catch (error) {
console.error("Error calling Gemini API:", error);
updateStatus("Error: Could not get AI suggestions.");
aiModal.classList.add('hidden'); // Hide modal on error
}
}
function applyAILayout(layout) {
shapes.forEach(shape => {
if (layout[shape.id]) {
shape.x = layout[shape.id].x;
shape.y = layout[shape.id].y;
}
});
render();
}
function displayAIExplanation(explanation) {
aiExplanation.innerHTML = explanation.replace(/\n/g, '<br>'); // Simple formatting
aiLoading.classList.add('hidden');
aiResult.classList.remove('hidden');
}
// --- Event Listeners ---
addRectBtn.addEventListener('click', () => addShape('rect'));
addCircleBtn.addEventListener('click', () => addShape('circle'));
addDiamondBtn.addEventListener('click', () => addShape('diamond'));
connectBtn.addEventListener('click', () => {
isConnecting = !isConnecting;
if (isConnecting) {
connectionStartShape = null;
connectBtn.classList.remove('bg-gray-600');
connectBtn.classList.add('bg-amber-500');
updateStatus('CONNECT MODE: Click a shape to start.');
} else {
connectionStartShape = null;
connectBtn.classList.remove('bg-amber-500');
connectBtn.classList.add('bg-gray-600');
updateStatus('Connect mode disabled.');
}
render();
});
clearBtn.addEventListener('click', () => {
shapes = [];
connectors = [];
isConnecting = false;
connectionStartShape = null;
connectBtn.classList.remove('bg-amber-500');
connectBtn.classList.add('bg-gray-600');
updateStatus('Canvas cleared. Add a new shape.');
render();
});
aiMagicBtn.addEventListener('click', callGeminiForLayout);
closeModalBtn.addEventListener('click', () => {
aiModal.classList.add('hidden');
});
// Close modal if clicking outside the content
aiModal.addEventListener('click', (e) => {
if (e.target === aiModal) {
aiModal.classList.add('hidden');
}
});
svg.addEventListener('mousedown', handlePointerDown);
svg.addEventListener('mousemove', handlePointerMove);
svg.addEventListener('mouseup', handlePointerUp);
svg.addEventListener('mouseleave', handlePointerUp);
svg.addEventListener('touchstart', handlePointerDown, { passive: false });
svg.addEventListener('touchmove', handlePointerMove, { passive: false });
svg.addEventListener('touchend', handlePointerUp);
svg.addEventListener('touchcancel', handlePointerUp);
// Initial render
render();
</script>
</body>
</html>