Skip to content

Instantly share code, notes, and snippets.

@AguyfromaTown
Created January 31, 2025 23:44
Show Gist options
  • Save AguyfromaTown/78de9d69dea340426a82d0e3681caad0 to your computer and use it in GitHub Desktop.
Save AguyfromaTown/78de9d69dea340426a82d0e3681caad0 to your computer and use it in GitHub Desktop.
<html><head><base href="https://websim.creationengine.com/whiteboard-ai/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI-Powered Interactive Whiteboard - Blueprint Style (Optimized)</title>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: Arial, sans-serif;
background-color: #1e1e1e; /* Darker background */
color: #e0e0e0; /* Lighter text color */
}
#whiteboard-container {
position: absolute;
top: 40px;
left: 0;
width: 8000px; /* Changed from 4000px */
height: 8000px; /* Changed from 4000px */
transform: translate(0px, 0px) scale(1);
transform-origin: 0 0;
cursor: grab;
transition: transform 0.1s ease-out;
}
#whiteboard-container:active {
cursor: grabbing;
}
#canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.node {
position: absolute;
background-color: #2d2d2d; /* Darker node background */
border: 1px solid #3a3a3a; /* Subtle border */
border-radius: 5px;
padding: 0;
min-width: 150px;
color: #e0e0e0; /* Lighter text color */
cursor: move;
user-select: none;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: box-shadow 0.3s ease;
display: flex;
flex-direction: column;
}
.node:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.node textarea {
width: calc(100% - 20px);
height: calc(100% - 60px);
background-color: #2d2d2d; /* Match node background */
border: none;
color: #e0e0e0; /* Lighter text color */
padding: 10px;
margin: 0;
resize: none;
box-sizing: border-box;
font-family: inherit;
font-size: 14px;
line-height: 1.4;
flex-grow: 1;
}
.node-header {
background-color: #3a3a3a; /* Slightly lighter than node background */
padding: 5px 10px;
cursor: move;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.connection-point {
width: 12px;
height: 12px;
background-color: #4a4a4a; /* Lighter connection points */
border-radius: 50%;
position: absolute;
cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease;
}
.connection-point:hover {
transform: scale(1.2);
background-color: #5a5a5a;
}
.top { top: -6px; left: 50%; transform: translateX(-50%); }
.bottom { bottom: -6px; left: 50%; transform: translateX(-50%); }
#toolbar {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
display: flex;
gap: 10px;
}
#toolbar button, #api-key-modal button {
background-color: #3a3a3a; /* Dark button background */
color: #e0e0e0; /* Light text color */
border: none;
padding: 10px 15px;
cursor: pointer;
border-radius: 5px;
font-size: 14px;
transition: background-color 0.3s ease;
}
#toolbar button:hover, #api-key-modal button:hover {
background-color: #4a4a4a; /* Lighter on hover */
}
#api-key-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
}
.modal-content {
background-color: #34495e;
margin: 15% auto;
padding: 30px;
border: 1px solid #3498db;
width: 350px;
color: #ecf0f1;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
#api-key-input {
width: 100%;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #3498db;
background-color: #2c3e50;
color: #ecf0f1;
border-radius: 5px;
}
#api-key-status {
margin-top: 15px;
font-weight: bold;
text-align: center;
}
.loading {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2000;
}
.loading:after {
content: " ";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid #3498db;
border-color: #3498db transparent #3498db transparent;
animation: loading 1.2s linear infinite;
}
@keyframes loading {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.node .controls {
display: flex;
gap: 5px;
}
.node .controls button {
background: none;
border: none;
color: #ecf0f1;
cursor: pointer;
font-size: 16px;
padding: 2px 5px;
transition: color 0.2s ease;
}
.node .controls button:hover {
color: #3498db;
}
.minimized {
height: 40px !important;
overflow: hidden;
}
.resize-handle {
width: 10px;
height: 10px;
background-color: #3498db;
position: absolute;
right: 0;
bottom: 0;
cursor: se-resize;
z-index: 10;
}
.ai-expand {
background-color: #3a3a3a; /* Match other buttons */
color: #e0e0e0; /* Light text color */
border: none;
padding: 5px 10px;
margin: 5px 5px 10px 5px;
cursor: pointer;
border-radius: 3px;
font-size: 12px;
transition: background-color 0.3s ease;
align-self: flex-start;
}
.ai-expand:hover {
background-color: #4a4a4a; /* Lighter on hover */
}
#ascii-logo {
position: fixed;
top: 60px;
right: 20px;
font-family: monospace;
font-size: 4px;
line-height: 4px;
color: #3498db;
text-shadow: 0 0 2px #3498db;
white-space: pre;
z-index: 1000;
pointer-events: none;
}
/* Add styles for the search input */
#search-input {
background-color: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 10px;
border-radius: 5px;
font-size: 14px;
}
#search-input::placeholder {
color: #888;
}
#search-button {
background-color: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
#search-button:hover {
background-color: #4a4a4a;
}
.node-title {
flex-grow: 1;
padding: 5px;
font-size: 14px;
color: #e0e0e0;
background-color: transparent;
border: none;
outline: none;
}
.node-title:focus {
background-color: #4a4a4a;
}
#undo, #redo {
background-color: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 10px 15px;
cursor: pointer;
border-radius: 5px;
font-size: 14px;
transition: background-color 0.3s ease;
}
#undo:hover, #redo:hover {
background-color: #4a4a4a;
}
#undo:disabled, #redo:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.help-banner {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #34495e;
color: #ecf0f1;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 2000;
max-width: 80%;
width: 400px;
}
.help-banner h2 {
margin-top: 0;
color: #3498db;
}
.help-banner ul {
padding-left: 20px;
}
.help-banner li {
margin-bottom: 10px;
}
#close-banner {
background-color: #3498db;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
float: right;
margin-top: 10px;
}
#close-banner:hover {
background-color: #2980b9;
}
#minimap-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 200px;
height: 200px;
background-color: rgba(52, 73, 94, 0.7);
border: 1px solid #3498db;
border-radius: 5px;
overflow: hidden;
z-index: 1000;
}
#minimap {
position: relative;
width: 100%;
height: 100%;
}
#minimap-viewport {
position: absolute;
border: 2px solid #e74c3c;
background-color: rgba(231, 76, 60, 0.2);
z-index: 10;
}
.minimap-node {
position: absolute;
background-color: #3498db;
z-index: 5;
}
/* Add these new styles for the menu bar */
#menu-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #2c3e50;
z-index: 1000;
font-family: Arial, sans-serif;
}
#menu-bar > ul {
display: flex;
list-style-type: none;
margin: 0;
padding: 0;
}
#menu-bar > ul > li {
position: relative;
padding: 10px 20px;
color: #ecf0f1;
cursor: pointer;
}
#menu-bar > ul > li:hover {
background-color: #34495e;
}
#menu-bar ul ul {
display: none;
position: absolute;
top: 100%;
left: 0;
background-color: #2c3e50;
min-width: 150px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
z-index: 1;
}
#menu-bar ul ul li {
padding: 10px;
}
#menu-bar ul li:hover > ul {
display: block;
}
#menu-bar button {
width: 100%;
text-align: left;
background: none;
border: none;
color: #ecf0f1;
cursor: pointer;
font-size: 14px;
padding: 5px 0;
}
#menu-bar button:hover {
background-color: #34495e;
}
#search-input {
width: calc(100% - 30px);
margin-right: 5px;
}
#search-button {
width: 25px;
text-align: center;
}
/* Adjust the whiteboard container to account for the menu bar */
#whiteboard-container {
top: 40px; /* Adjust this value based on the height of your menu bar */
}
/* Adjust the ASCII logo position */
#ascii-logo {
top: 60px; /* Adjust this value to position below the menu bar */
}
</style>
</head>
<body>
<!-- Add this new div for the help banner -->
<div id="help-banner" class="help-banner">
<h2>How to use the Whiteboard</h2>
<ul>
<li>Click "Add Node" to create a new node</li>
<li>Drag nodes to move them around</li>
<li>Connect nodes by dragging from one connection point to another</li>
<li>Use the "AI Expand" button to generate related content</li>
<li>Use Undo/Redo buttons to revert or replay actions</li>
<li>Search for nodes using the search bar</li>
</ul>
<button id="close-banner">Close</button>
</div>
<!-- Add this new menu bar structure -->
<nav id="menu-bar">
<ul>
<li>
Board
<ul>
<li><button id="add-node">Add Node</button></li>
<li><button id="clear-all">Clear All</button></li>
</ul>
</li>
<li>
Edit
<ul>
<li><button id="undo">Undo</button></li>
<li><button id="redo">Redo</button></li>
</ul>
</li>
<li>
View
<ul>
<li><button id="zoom-in">Zoom In</button></li>
<li><button id="zoom-out">Zoom Out</button></li>
<li><button id="reset-view">Reset View</button></li>
</ul>
</li>
<li>
AI
<ul>
<li><button id="set-api-key">Set API Key</button></li>
</ul>
</li>
<li>
Search
<ul>
<li>
<input type="text" id="search-input" placeholder="Search nodes...">
<button id="search-button">🔍</button>
</li>
</ul>
</li>
<li>
Settings
<ul>
<!-- Add any additional configuration options here -->
</ul>
</li>
</ul>
</nav>
<!-- Remove the existing toolbar -->
<!-- <div id="toolbar"> ... </div> -->
<div id="ascii-logo">
_____ ______ ___ ___ ________ ___ ___ __ ___ ___ ___ _________ _______ ________ ________ ________ ________ ________
|\ _ \ _ \ |\ \ / /|\ __ \|\ \|\ \ |\ \|\ \|\ \|\ \|\___ ___\\ ___ \ |\ __ \|\ __ \|\ __ \|\ __ \|\ ___ \
\ \ \\\__\ \ \ \ \ \/ / | \ \|\ \ \ \ \ \ \ \ \ \ \\\ \ \ \|___ \ \_\ \ __/|\ \ \|\ /\ \ \|\ \ \ \|\ \ \ \|\ \ \ \_|\ \
\ \ \\|__| \ \ \ \ / / \ \ __ \ \ \ \ \ __\ \ \ \ __ \ \ \ \ \ \ \ \ \_|/_\ \ __ \ \ \\\ \ \ __ \ \ _ _\ \ \ \\ \
\ \ \ \ \ \ \/ / / \ \ \ \ \ \ \ \ \|\__\_\ \ \ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \|\ \ \ \\\ \ \ \ \ \ \ \\ \\ \ \_\\ \
\ \__\ \ \__\__/ / / \ \__\ \__\ \__\ \____________\ \__\ \__\ \__\ \ \__\ \ \_______\ \_______\ \_______\ \__\ \__\ \__\\ _\\ \_______\
\|__| \|__|\___/ / \|__|\|__|\|__|\|____________|\|__|\|__|\|__| \|__| \|_______|\|_______|\|_______|\|__|\|__|\|__|\|__|\|_______|
\|___|/
</div>
<div id="whiteboard-container">
<svg id="canvas"></svg>
</div>
<div id="api-key-modal">
<div class="modal-content">
<h2>Enter groq.com API Key</h2>
<input type="text" id="api-key-input" placeholder="Enter your API key">
<button id="save-api-key">Save</button>
<button id="cancel-api-key">Cancel</button>
<div id="api-key-status"></div>
</div>
</div>
<div class="loading" id="loading-spinner"></div>
<!-- Add this new div for the minimap just before the closing </body> tag -->
<div id="minimap-container">
<div id="minimap">
<div id="minimap-viewport"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.1.1/svg.min.js"></script>
<script>
// 1. Use const for variables that don't change
const draw = SVG().addTo('#canvas').size('8000', '8000');
const minimapScale = 0.05;
const minimapContainer = document.getElementById('minimap-container');
const minimap = document.getElementById('minimap');
const minimapViewport = document.getElementById('minimap-viewport');
// 2. Use a Map for faster node lookup
const nodesMap = new Map();
// 3. Debounce function for performance optimization
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
let nodes = [];
let connections = [];
let isDragging = false;
let isConnecting = false;
let startConnectPoint = null;
let apiKey = localStorage.getItem('groqApiKey') || '';
let isPanning = false;
let startPanX = 0, startPanY = 0;
let panX = 0, panY = 0;
let zoom = 1;
let history = [];
let historyIndex = -1;
function saveState() {
const state = {
nodes: nodes.map(node => ({
x: parseInt(node.style.left),
y: parseInt(node.style.top),
width: parseInt(node.style.width),
height: parseInt(node.style.height),
content: node.querySelector('textarea').value,
title: node.querySelector('.node-title').textContent
})),
connections: connections.map(conn => ({
startNode: nodes.indexOf(conn.start.closest('.node')),
endNode: nodes.indexOf(conn.end.closest('.node')),
startPoint: conn.start.classList.contains('top') ? 'top' : 'bottom',
endPoint: conn.end.classList.contains('top') ? 'top' : 'bottom'
}))
};
// If we're not at the end of the history, truncate it
if (historyIndex < history.length - 1) {
history = history.slice(0, historyIndex + 1);
}
history.push(JSON.stringify(state));
historyIndex = history.length - 1;
updateUndoRedoButtons();
}
function undo() {
if (historyIndex > 0) {
historyIndex--;
loadState(JSON.parse(history[historyIndex]));
updateUndoRedoButtons();
}
}
function redo() {
if (historyIndex < history.length - 1) {
historyIndex++;
loadState(JSON.parse(history[historyIndex]));
updateUndoRedoButtons();
}
}
function loadState(state) {
// Clear current state
nodes.forEach(node => node.remove());
connections.forEach(conn => conn.line.remove());
nodes = [];
connections = [];
// Recreate nodes
state.nodes.forEach(nodeState => {
const node = createNode(nodeState.x, nodeState.y, nodeState.content, false);
node.style.width = nodeState.width + 'px';
node.style.height = nodeState.height + 'px';
node.querySelector('.node-title').textContent = nodeState.title;
});
// Recreate connections
state.connections.forEach(connState => {
const startNode = nodes[connState.startNode];
const endNode = nodes[connState.endNode];
const startPoint = startNode.querySelector(`.${connState.startPoint}`);
const endPoint = endNode.querySelector(`.${connState.endPoint}`);
createConnection(startPoint, endPoint, false);
});
updateConnections();
}
function updateUndoRedoButtons() {
document.getElementById('undo').disabled = historyIndex <= 0;
document.getElementById('redo').disabled = historyIndex >= history.length - 1;
}
// 4. Optimize createNode function
function createNode(x, y, content = '', shouldSaveState = true) {
const node = document.createElement('div');
node.className = 'node';
node.style.cssText = `left:${x}px; top:${y}px; width: 200px; height: 150px;`;
node.innerHTML = `
<div class="node-header">
<span class="node-title" contenteditable="true">Node${nodes.length + 1}</span>
<div class="controls">
<button class="minimize">-</button>
<button class="delete">×</button>
</div>
</div>
<textarea placeholder="Enter note or prompt...">${content}</textarea>
<div class="connection-point top"></div>
<div class="connection-point bottom"></div>
<button class="ai-expand">AI Expand</button>
<div class="resize-handle"></div>
`;
document.getElementById('whiteboard-container').appendChild(node);
nodes.push(node);
nodesMap.set(node.id, node);
setupNodeEventListeners(node);
autoResizeTextarea.call(node.querySelector('textarea'));
if (shouldSaveState) {
saveState();
}
return node;
}
function setupNodeEventListeners(node) {
const nodeHeader = node.querySelector('.node-header');
nodeHeader.addEventListener('mousedown', startDragging);
node.querySelector('textarea').addEventListener('input', autoResizeTextarea);
node.querySelector('.ai-expand').addEventListener('click', (e) => expandWithAI(e, node));
node.querySelector('.minimize').addEventListener('click', () => toggleMinimize(node));
node.querySelector('.delete').addEventListener('click', () => deleteNode(node));
const topPoint = node.querySelector('.top');
const bottomPoint = node.querySelector('.bottom');
topPoint.addEventListener('mousedown', startConnecting);
bottomPoint.addEventListener('mousedown', startConnecting);
const resizeHandle = node.querySelector('.resize-handle');
resizeHandle.addEventListener('mousedown', startResizing);
}
function autoResizeTextarea() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
}
function startDragging(e) {
if (e.target.classList.contains('connection-point') || e.target.tagName === 'BUTTON') return;
isDragging = true;
const node = e.target.closest('.node');
const rect = node.getBoundingClientRect();
const startX = (e.clientX - rect.left) / zoom;
const startY = (e.clientY - rect.top) / zoom;
function dragMove(e) {
const newX = (e.clientX - panX) / zoom - startX;
const newY = (e.clientY - panY) / zoom - startY;
node.style.left = newX + 'px';
node.style.top = newY + 'px';
updateConnections();
}
function dragEnd() {
isDragging = false;
document.removeEventListener('mousemove', dragMove);
document.removeEventListener('mouseup', dragEnd);
saveState();
}
document.addEventListener('mousemove', dragMove);
document.addEventListener('mouseup', dragEnd);
}
function startConnecting(e) {
e.stopPropagation();
isConnecting = true;
startConnectPoint = e.target;
const tempLine = draw.line(e.clientX, e.clientY, e.clientX, e.clientY).stroke({ color: '#e74c3c', width: 2, linecap: 'round' });
function connectMove(e) {
tempLine.plot(
startConnectPoint.getBoundingClientRect().left + 5,
startConnectPoint.getBoundingClientRect().top + 5,
e.clientX,
e.clientY
);
}
function connectEnd(e) {
isConnecting = false;
document.removeEventListener('mousemove', connectMove);
document.removeEventListener('mouseup', connectEnd);
const endPoint = document.elementFromPoint(e.clientX, e.clientY);
if (endPoint && endPoint.classList.contains('connection-point') && endPoint !== startConnectPoint) {
createConnection(startConnectPoint, endPoint);
}
tempLine.remove();
}
document.addEventListener('mousemove', connectMove);
document.addEventListener('mouseup', connectEnd);
}
// Modify createConnection function to optionally save state
function createConnection(start, end, shouldSaveState = true) {
const line = draw.line(0, 0, 0, 0).stroke({ color: '#e74c3c', width: 2, linecap: 'round' });
const connection = { start, end, line };
connections.push(connection);
updateConnection(connection);
if (shouldSaveState) {
saveState();
}
}
function updateConnection(conn) {
const startNode = conn.start.closest('.node');
const endNode = conn.end.closest('.node');
// Get the position of the start node
const startX = parseFloat(startNode.style.left) + startNode.offsetWidth / 2;
const startY = parseFloat(startNode.style.top) + startNode.offsetHeight / 2;
// Get the position of the end node
const endX = parseFloat(endNode.style.left) + endNode.offsetWidth / 2;
const endY = parseFloat(endNode.style.top) + endNode.offsetHeight / 2;
// Plot the line in the SVG canvas
conn.line.plot(startX, startY, endX, endY);
}
// 5. Optimize updateConnections function
const updateConnections = debounce(() => {
connections.forEach(updateConnection);
}, 16); // 60fps
async function expandWithAI(e, node) {
if (!apiKey) {
alert('Please set your groq.com API key first.');
return;
}
const prompt = node.querySelector('textarea').value.trim();
if (!prompt) {
alert('Please enter a prompt in the node before expanding with AI.');
return;
}
const loadingSpinner = document.getElementById('loading-spinner');
loadingSpinner.style.display = 'block';
try {
const context = getConnectedNodesContent(node);
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer${apiKey}`,
},
body: JSON.stringify({
model: "mixtral-8x7b-32768",
messages: [
{role: "system", content: "You are a helpful assistant that expands on ideas and concepts. Use the provided context to maintain continuity in the conversation."},
{role: "user", content: `Context:${context}\n\nCurrent prompt:${prompt}`}
],
max_tokens: 150,
temperature: 0.7,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`API request failed:${JSON.stringify(errorData)}`);
}
const data = await response.json();
const aiResponse = data.choices[0].message.content.trim();
if (!aiResponse) {
throw new Error('No output received from API.');
}
const nodeRect = node.getBoundingClientRect();
const newNodeX = (nodeRect.right - panX) / zoom + 50;
const newNodeY = (nodeRect.top - panY) / zoom;
const newNode = createNode(newNodeX, newNodeY, aiResponse);
createConnection(node.querySelector('.bottom'), newNode.querySelector('.top'));
updateConnections();
} catch (error) {
console.error('Error calling groq.com API:', error);
alert(`Failed to get AI response:${error.message}`);
} finally {
loadingSpinner.style.display = 'none';
}
}
function getConnectedNodesContent(node) {
let context = '';
connections.forEach(conn => {
if (conn.start.closest('.node') === node) {
const connectedNode = conn.end.closest('.node');
context += connectedNode.querySelector('textarea').value.trim() + '\n';
} else if (conn.end.closest('.node') === node) {
const connectedNode = conn.start.closest('.node');
context += connectedNode.querySelector('textarea').value.trim() + '\n';
}
});
return context;
}
function toggleMinimize(node) {
node.classList.toggle('minimized');
const minimizeBtn = node.querySelector('.minimize');
minimizeBtn.textContent = node.classList.contains('minimized') ? '+' : '-';
}
function deleteNode(node) {
const index = nodes.indexOf(node);
if (index > -1) {
nodes.splice(index, 1);
}
connections = connections.filter(conn => {
if (conn.start.closest('.node') === node || conn.end.closest('.node') === node) {
conn.line.remove();
return false;
}
return true;
});
node.remove();
saveState();
}
document.addEventListener('mousedown', (e) => {
if (!e.target.closest('.node') && !isDragging && !isConnecting) {
isPanning = true;
startPanX = e.clientX - panX;
startPanY = e.clientY - panY;
document.getElementById('whiteboard-container').style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if (isPanning) {
panX = e.clientX - startPanX;
panY = e.clientY - startPanY;
updateTransform();
}
});
document.addEventListener('mouseup', () => {
if (isPanning) {
isPanning = false;
document.getElementById('whiteboard-container').style.cursor = 'grab';
}
});
document.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = zoom * delta;
if (newZoom < 0.1 || newZoom > 5) return;
zoom = newZoom;
const mouseX = e.clientX;
const mouseY = e.clientY;
panX -= (mouseX * (delta - 1));
panY -= (mouseY * (delta - 1));
updateWhiteboard();
}, { passive: false });
function updateTransform() {
document.getElementById('whiteboard-container').style.transform = `translate(${panX}px,${panY}px) scale(${zoom})`;
updateConnections();
}
document.getElementById('add-node').addEventListener('click', () => {
const centerX = (window.innerWidth / 2 - panX) / zoom;
const centerY = (window.innerHeight / 2 - panY) / zoom;
createNode(centerX, centerY, 'Start your brainstorming here!');
});
document.getElementById('clear-all').addEventListener('click', () => {
if (confirm('Are you sure you want to clear all nodes?')) {
nodes.forEach(node => node.remove());
connections.forEach(conn => conn.line.remove());
nodes = [];
connections = [];
}
});
document.getElementById('set-api-key').addEventListener('click', () => {
document.getElementById('api-key-modal').style.display = 'block';
document.getElementById('api-key-input').value = apiKey;
});
document.getElementById('save-api-key').addEventListener('click', () => {
apiKey = document.getElementById('api-key-input').value;
localStorage.setItem('groqApiKey', apiKey);
document.getElementById('api-key-modal').style.display = 'none';
document.getElementById('api-key-status').textContent = 'API key saved successfully!';
setTimeout(() => {
document.getElementById('api-key-status').textContent = '';
}, 3000);
});
document.getElementById('cancel-api-key').addEventListener('click', () => {
document.getElementById('api-key-modal').style.display = 'none';
});
function startResizing(e) {
e.stopPropagation();
const node = e.target.closest('.node');
const startX = e.clientX;
const startY = e.clientY;
const startWidth = node.offsetWidth;
const startHeight = node.offsetHeight;
function doDrag(e) {
const deltaX = (e.clientX - startX) / zoom;
const deltaY = (e.clientY - startY) / zoom;
const newWidth = Math.max(150, startWidth + deltaX);
const newHeight = Math.max(100, startHeight + deltaY);
node.style.width = newWidth + 'px';
node.style.height = newHeight + 'px';
const textarea = node.querySelector('textarea');
textarea.style.width = (newWidth - 20) + 'px';
textarea.style.height = (newHeight - 60) + 'px';
updateConnections();
}
function stopDrag() {
document.removeEventListener('mousemove', doDrag);
document.removeEventListener('mouseup', stopDrag);
}
document.addEventListener('mousemove', doDrag);
document.addEventListener('mouseup', stopDrag);
}
document.getElementById('zoom-in').addEventListener('click', () => {
zoom *= 1.2;
updateTransform();
});
document.getElementById('zoom-out').addEventListener('click', () => {
zoom /= 1.2;
updateTransform();
});
document.getElementById('reset-view').addEventListener('click', () => {
zoom = 1;
panX = 0;
panY = 0;
updateTransform();
});
// 6. Optimize searchNodes function
function searchNodes() {
const searchTerm = document.getElementById('search-input').value.trim().toLowerCase();
const foundNode = Array.from(nodesMap.values()).find(node =>
node.querySelector('.node-title').textContent.toLowerCase().includes(searchTerm)
);
if (foundNode) {
focusOnNode(foundNode);
} else {
alert('Node not found');
}
}
function focusOnNode(node) {
const rect = node.getBoundingClientRect();
const containerRect = document.getElementById('whiteboard-container').getBoundingClientRect();
// Calculate the center position of the node
const centerX = rect.left + rect.width / 2 - containerRect.left;
const centerY = rect.top + rect.height / 2 - containerRect.top;
// Update pan to center the node
panX = -centerX + window.innerWidth / 2;
panY = -centerY + window.innerHeight / 2;
// Set zoom to 1 (or any desired level)
zoom = 1;
updateTransform();
// Add focusing effect
node.style.boxShadow = '0 0 0 4px #3498db';
setTimeout(() => {
node.style.boxShadow = '';
}, 3000);
}
document.getElementById('search-button').addEventListener('click', searchNodes);
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchNodes();
}
});
// Add event listeners for undo and redo buttons
document.getElementById('undo').addEventListener('click', undo);
document.getElementById('redo').addEventListener('click', redo);
// Initialize history with the initial state
saveState();
// Create initial node
createNode((window.innerWidth / 2 - panX) / zoom, (window.innerHeight / 2 - panY) / zoom, 'Start your brainstorming here!');
// Show the help banner on startup
document.getElementById('help-banner').style.display = 'block';
// Close the banner when the close button is clicked
document.getElementById('close-banner').addEventListener('click', () => {
document.getElementById('help-banner').style.display = 'none';
});
// 7. Optimize updateMinimap function
const updateMinimap = debounce(() => {
const whiteboardContainer = document.getElementById('whiteboard-container');
const containerRect = whiteboardContainer.getBoundingClientRect();
minimap.style.width = `${containerRect.width * minimapScale}px`;
minimap.style.height = `${containerRect.height * minimapScale}px`;
minimap.innerHTML = '';
const fragment = document.createDocumentFragment();
nodes.forEach(node => {
const nodeRect = node.getBoundingClientRect();
const minimapNode = document.createElement('div');
minimapNode.className = 'minimap-node';
minimapNode.style.cssText = `
left:${(nodeRect.left - containerRect.left) * minimapScale}px;
top:${(nodeRect.top - containerRect.top) * minimapScale}px;
width:${nodeRect.width * minimapScale}px;
height:${nodeRect.height * minimapScale}px;
`;
fragment.appendChild(minimapNode);
});
minimap.appendChild(fragment);
const visibleWidth = window.innerWidth / zoom;
const visibleHeight = window.innerHeight / zoom;
const viewportLeft = -panX / zoom;
const viewportTop = -panY / zoom;
minimapViewport.style.cssText = `
width:${visibleWidth * minimapScale}px;
height:${visibleHeight * minimapScale}px;
left:${viewportLeft * minimapScale}px;
top:${viewportTop * minimapScale}px;
`;
}, 100);
// 8. Use requestAnimationFrame for smooth animations
function updateWhiteboard() {
requestAnimationFrame(() => {
document.getElementById('whiteboard-container').style.transform = `translate(${panX}px,${panY}px) scale(${zoom})`;
updateConnections();
updateMinimap();
});
}
// Add minimap dragging functionality
let isDraggingMinimap = false;
let minimapStartX, minimapStartY;
minimapViewport.addEventListener('mousedown', (e) => {
isDraggingMinimap = true;
minimapStartX = e.clientX;
minimapStartY = e.clientY;
});
document.addEventListener('mousemove', (e) => {
if (isDraggingMinimap) {
const deltaX = e.clientX - minimapStartX;
const deltaY = e.clientY - minimapStartY;
panX -= deltaX * zoom / minimapScale;
panY -= deltaY * zoom / minimapScale;
minimapStartX = e.clientX;
minimapStartY = e.clientY;
updateWhiteboard();
}
});
document.addEventListener('mouseup', () => {
isDraggingMinimap = false;
});
// Initial minimap update
updateMinimap();
</script>
</body>
</html>$
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment