Created
January 31, 2025 23:44
-
-
Save AguyfromaTown/78de9d69dea340426a82d0e3681caad0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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