function NoteCanvas({ note, token, settings, onBack }) { const [contentBlocks, setContentBlocks] = useState(() => { // Initial layout for blocks without position const blocks = note.content_blocks || []; const allZero = blocks.every(b => !b.x_pos && !b.y_pos); if (allZero && blocks.length > 0) { return blocks.map((b, i) => ({ ...b, x_pos: 50, y_pos: 50 + (i * 250), // Standard height + gap width: b.width || 400 })); } return blocks; }); const [draggedBlock, setDraggedBlock] = useState(null); const [expandedBlockId, setExpandedBlockId] = useState(null); const [expandedBlockPosition, setExpandedBlockPosition] = useState(null); const [extractionStatus, setExtractionStatus] = useState(note.note.extraction_completed ? 'completed' : 'pending'); const [loading, setLoading] = useState(false); const [showExtractModal, setShowExtractModal] = useState(false); const [showTextInput, setShowTextInput] = useState(false); const [newTextContent, setNewTextContent] = useState(''); const [showAudioModal, setShowAudioModal] = useState(false); const [audioMode, setAudioMode] = useState(null); // 'upload' or 'record' const [viewMode, setViewMode] = useState('canvas'); // 'canvas' or 'document' const [highlightedBlockId, setHighlightedBlockId] = useState(null); // Zoom and Pan state const [zoom, setZoom] = useState(1); const [panOffset, setPanOffset] = useState({ x: 0, y: 0 }); const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState(null); // Canvas Drag Logic const handleMouseDown = (e, block) => { if (viewMode !== 'canvas') return; // prevent drag if clicking buttons inputs or links if (['BUTTON', 'INPUT', 'TEXTAREA', 'A'].includes(e.target.tagName)) return; e.preventDefault(); setDraggedBlock({ id: block.id, startX: e.clientX, startY: e.clientY, initialX: block.x_pos || 0, initialY: block.y_pos || 0 }); }; const handleMouseMove = (e) => { if (!draggedBlock) return; const dx = e.clientX - draggedBlock.startX; const dy = e.clientY - draggedBlock.startY; setContentBlocks(blocks => blocks.map(b => { if (b.id === draggedBlock.id) { return { ...b, x_pos: draggedBlock.initialX + dx, y_pos: draggedBlock.initialY + dy }; } return b; })); }; const handleMouseUp = async () => { if (!draggedBlock) return; const block = contentBlocks.find(b => b.id === draggedBlock.id); if (block) { try { await fetch(`${API_URL}/notes/${note.note.id}/content/${block.id}/position`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ x: block.x_pos, y: block.y_pos }) }); } catch (err) { console.error('Failed to save position', err); } } setDraggedBlock(null); }; // Zoom and Pan handlers const handleZoomIn = () => setZoom(z => Math.min(2, z + 0.1)); const handleZoomOut = () => setZoom(z => Math.max(0.5, z - 0.1)); const handleZoomReset = () => { setZoom(1); setPanOffset({ x: 0, y: 0 }); }; const handleWheel = (e) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY * -0.001; setZoom(z => Math.min(2, Math.max(0.5, z + delta))); } }; const handlePanStart = (e) => { if (e.button === 1 || e.shiftKey || isPanning) { // Middle mouse or Shift+drag setIsPanning(true); setPanStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y }); e.preventDefault(); } }; const handlePanMove = (e) => { if (isPanning && panStart) { setPanOffset({ x: e.clientX - panStart.x, y: e.clientY - panStart.y }); } }; const handlePanEnd = () => { setIsPanning(false); setPanStart(null); }; // Keyboard shortcuts React.useEffect(() => { const handleKeyDown = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.key === '+' || e.key === '=') handleZoomIn(); if (e.key === '-' || e.key === '_') handleZoomOut(); if (e.key === '0') handleZoomReset(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); const addTextBlock = () => { setShowTextInput(true); }; const saveTextBlock = async () => { if (!newTextContent.trim()) { setShowTextInput(false); setNewTextContent(''); return; } try { // Smart positioning: find empty spot const BLOCK_WIDTH = 350, BLOCK_HEIGHT = 200, SPACING = 30, START_X = 50, START_Y = 50, GRID_COLS = 3; let newX = START_X, newY = START_Y; // Try grid positions outerLoop: for (let row = 0; row < 10; row++) { for (let col = 0; col < GRID_COLS; col++) { const testX = START_X + (col * (BLOCK_WIDTH + SPACING)); const testY = START_Y + (row * (BLOCK_HEIGHT + SPACING)); const hasOverlap = contentBlocks.some(b => Math.abs(testX - (b.x_pos || 0)) < (BLOCK_WIDTH - 50) && Math.abs(testY - (b.y_pos || 0)) < (BLOCK_HEIGHT - 50)); if (!hasOverlap) { newX = testX; newY = testY; break outerLoop; } } } const response = await fetch(`${API_URL}/notes/${note.note.id}/content/text`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ content_type: 'text', content_data: { text: newTextContent }, position: contentBlocks.length, x: newX, y: newY }) }); if (response.ok) { const data = await response.json(); setContentBlocks([...contentBlocks, { id: data.block_id, content_type: 'text', content_data: { text: newTextContent }, x_pos: newX, y_pos: newY }]); setNewTextContent(''); setShowTextInput(false); } } catch (err) { alert('Failed to add text block'); } }; const addFileBlock = async (file) => { const formData = new FormData(); formData.append('file', file); formData.append('position', contentBlocks.length); // Smart positioning const BLOCK_WIDTH = 350, BLOCK_HEIGHT = 200, SPACING = 30, START_X = 50, START_Y = 50, GRID_COLS = 3; let newX = START_X, newY = START_Y; outerLoop: for (let row = 0; row < 10; row++) { for (let col = 0; col < GRID_COLS; col++) { const testX = START_X + (col * (BLOCK_WIDTH + SPACING)); const testY = START_Y + (row * (BLOCK_HEIGHT + SPACING)); const hasOverlap = contentBlocks.some(b => Math.abs(testX - (b.x_pos || 0)) < (BLOCK_WIDTH - 50) && Math.abs(testY - (b.y_pos || 0)) < (BLOCK_HEIGHT - 50)); if (!hasOverlap) { newX = testX; newY = testY; break outerLoop; } } } try { setLoading(true); const response = await fetch(`${API_URL}/notes/${note.note.id}/content/file`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (response.ok) { const data = await response.json(); setContentBlocks([...contentBlocks, { id: data.block_id, content_type: file.type.startsWith('image/') ? 'image' : file.type === 'application/pdf' ? 'pdf' : 'audio', filename: data.filename, file_id: data.file_id, file_path: data.file_path, x_pos: newX, y_pos: newY }]); // Immediately save position await fetch(`${API_URL}/notes/${note.note.id}/content/${data.block_id}/position`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ x: newX, y: newY }) }); } } catch (err) { alert('Failed to upload file'); } finally { setLoading(false); } }; const addLinkBlock = async () => { const url = prompt('Enter URL:'); if (!url) return; try { // Smart positioning const BLOCK_WIDTH = 350, BLOCK_HEIGHT = 200, SPACING = 30, START_X = 50, START_Y = 50, GRID_COLS = 3; let newX = START_X, newY = START_Y; outerLoop: for (let row = 0; row < 10; row++) { for (let col = 0; col < GRID_COLS; col++) { const testX = START_X + (col * (BLOCK_WIDTH + SPACING)); const testY = START_Y + (row * (BLOCK_HEIGHT + SPACING)); const hasOverlap = contentBlocks.some(b => Math.abs(testX - (b.x_pos || 0)) < (BLOCK_WIDTH - 50) && Math.abs(testY - (b.y_pos || 0)) < (BLOCK_HEIGHT - 50)); if (!hasOverlap) { newX = testX; newY = testY; break outerLoop; } } } const formData = new FormData(); formData.append('url', url); formData.append('position', contentBlocks.length); const response = await fetch(`${API_URL}/notes/${note.note.id}/content/link`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (response.ok) { const data = await response.json(); setContentBlocks([...contentBlocks, { id: data.block_id, content_type: 'link', content_data: { url }, x_pos: newX, y_pos: newY }]); await fetch(`${API_URL}/notes/${note.note.id}/content/${data.block_id}/position`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ x: newX, y: newY }) }); } } catch (err) { alert('Failed to add link'); } }; const deleteBlock = async (blockId) => { if (!confirm('Delete this block?')) return; try { const response = await fetch(`${API_URL}/notes/${note.note.id}/content/${blockId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { setContentBlocks(contentBlocks.filter(b => b.id !== blockId)); } } catch (err) { alert('Failed to delete block'); } }; const extractContent = async (extractNow) => { try { setLoading(true); setShowExtractModal(false); const response = await fetch(`${API_URL}/notes/${note.note.id}/extract`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ extract_now: extractNow, force: false }) }); const data = await response.json(); if (extractNow) { setExtractionStatus('processing'); const interval = setInterval(async () => { const statusRes = await fetch(`${API_URL}/notes/${note.note.id}/extraction-status`, { headers: { 'Authorization': `Bearer ${token}` } }); const statusData = await statusRes.json(); if (statusData.status === 'completed') { setExtractionStatus('completed'); clearInterval(interval); alert('Extraction completed!'); } else if (statusData.status === 'failed') { setExtractionStatus('failed'); clearInterval(interval); alert('Extraction failed'); } }, 2000); } else { alert('Content will be extracted later'); } } catch (err) { alert('Failed to start extraction'); } finally { setLoading(false); } }; const handleViewInCanvas = (blockId) => { setViewMode('canvas'); setHighlightedBlockId(blockId); setTimeout(() => { const element = document.getElementById(`block-${blockId}`); if (element) { // Smooth scroll manually because position absolute element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } setTimeout(() => setHighlightedBlockId(null), 2000); }, 100); }; return (

{note.note.title}

{/* View Mode Toggle */}
{viewMode === 'canvas' ? ( <>
{note.note.canvas_enabled && } {/* Zoom Controls - Fixed outside canvas */}
{Math.round(zoom * 100)}%
{ handleMouseMove(e); handlePanMove(e); }} onMouseUp={() => { handleMouseUp(); handlePanEnd(); }} onMouseLeave={() => { handleMouseUp(); handlePanEnd(); }} onMouseDown={handlePanStart} onWheel={handleWheel} > {showTextInput && (