import React, { useEffect, useRef } from 'react'; import * as d3 from 'd3'; import { BrainstormSession, BrainstormIdea, Note } from '../../types'; interface WaveCanvasProps { session: BrainstormSession; ideas: BrainstormIdea[]; onNodeSelect: (id: string) => void; onPositionUpdate: (id: string, pos: { x: number; y: number }) => void; selectedNodeId: string | null; relatedNotes: Note[]; } export const WaveCanvas: React.FC = ({ session, ideas, onNodeSelect, onPositionUpdate, selectedNodeId, relatedNotes }) => { const svgRef = useRef(null); const containerRef = useRef(null); useEffect(() => { if (!svgRef.current || !containerRef.current) return; const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; const centerX = width / 2; const centerY = height / 2; const svg = d3.select(svgRef.current); svg.selectAll("*").remove(); const g = svg.append("g"); // Zoom behavior const zoom = d3.zoom() .scaleExtent([0.1, 5]) .on("zoom", (event) => { g.attr("transform", event.transform); }); svg.call(zoom); // Initial transform to center svg.call(zoom.transform, d3.zoomIdentity.translate(centerX, centerY).scale(0.8)); // Data structures for d3 interface D3Node extends d3.SimulationNodeDatum { id: string; type: 'root' | 'idea' | 'note'; wave?: number; title: string; color: string; radius: number; status?: string; } interface D3Link extends d3.SimulationLinkDatum { source: string | D3Node; target: string | D3Node; type: 'wave' | 'context' | 'parent'; } const nodes: D3Node[] = []; const links: D3Link[] = []; // Root node const rootNode: D3Node = { id: 'root', type: 'root', title: session.seedIdea, color: '#141414', radius: 40, fx: 0, fy: 0 }; nodes.push(rootNode); // Idea nodes const colors = { 1: '#fb923c', // orange 2: '#60a5fa', // blue 3: '#a78bfa' // violet }; ideas.forEach(idea => { nodes.push({ id: idea.id, type: 'idea', wave: idea.waveNumber, title: idea.title, color: colors[idea.waveNumber as 1|2|3] || '#94a3b8', radius: 28, status: idea.status, x: idea.position?.x, y: idea.position?.y }); if (idea.parentIdeaId) { links.push({ source: idea.parentIdeaId, target: idea.id, type: 'parent' }); } else { links.push({ source: 'root', target: idea.id, type: 'wave' }); } }); // Radial layout forces const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(d => { if (d.type === 'wave') { const targetNode = nodes.find(n => n.id === (typeof d.target === 'string' ? d.target : (d.target as any).id)); return (targetNode?.wave || 1) * 200; } if (d.type === 'parent') return 180; return 100; })) .force("charge", d3.forceManyBody().strength(-800)) .force("radial", d3.forceRadial(d => { if (d.type === 'root') return 0; if (d.id.includes('-')) return (d.wave || 1) * 200 + 100; // Deepened ideas push out return (d.wave || 1) * 200; }, 0, 0).strength(0.8)) .force("collision", d3.forceCollide().radius(d => d.radius + 30)); // Drawing rings const ringRadii = [200, 400, 600]; g.selectAll(".ring") .data(ringRadii) .enter() .append("circle") .attr("class", "ring") .attr("r", d => d) .attr("fill", "none") .attr("stroke", "#e2e8f0") .attr("stroke-width", 1) .attr("stroke-dasharray", "4,4") .style("opacity", 0.5); // Links const link = g.append("g") .selectAll("line") .data(links) .enter() .append("line") .attr("stroke", d => d.type === 'wave' ? "#cbd5e1" : d.type === 'parent' ? "#fde047" : "#94a3b8") .attr("stroke-width", d => d.type === 'wave' ? 1.5 : 2) .attr("stroke-dasharray", d => d.type === 'parent' ? "none" : "4,4"); // Nodes const node = g.append("g") .selectAll(".node") .data(nodes) .enter() .append("g") .attr("class", "node") .style("opacity", d => d.status === 'dismissed' ? 0.4 : 1) .on("click", (event, d) => { if (d.type === 'idea') onNodeSelect(d.id); }) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) as any); node.append("circle") .attr("r", d => d.radius) .attr("fill", d => d.status === 'converted' ? '#ecfdf5' : (d.type === 'root' ? '#141414' : '#fff')) .attr("stroke", d => d.status === 'converted' ? '#10b981' : d.color) .attr("stroke-width", d => d.id === selectedNodeId ? 4 : 2) .attr("class", "cursor-pointer transition-all hover:scale-110") .style("filter", d => d.id === selectedNodeId ? `drop-shadow(0 0 12px ${d.color}cc)` : "none"); // State indicators (converted) node.filter(d => d.status === 'converted') .append("path") .attr("d", d3.symbol().type(d3.symbolCircle).size(150)) .attr("fill", "#10b981"); // Icons/Text in nodes node.append("text") .attr("dy", d => d.type === 'root' ? ".35em" : d.radius + 20) .attr("text-anchor", "middle") .attr("fill", d => d.type === 'root' ? "#fff" : (d.status === 'dismissed' ? "#94a3b8" : "#141414")) .attr("class", d => d.type === 'root' ? "text-[10px] font-bold pointer-events-none tracking-widest" : "text-[11px] font-bold uppercase tracking-tight pointer-events-none") .text(d => d.type === 'root' ? "SEED" : d.title.length > 18 ? d.title.substring(0, 18) + "..." : d.title); if (rootNode) { g.append("text") .attr("text-anchor", "middle") .attr("dy", 80) .attr("class", "text-2xl font-serif italic fill-ink dark:fill-dark-ink pointer-events-none shadow-sm") .text(session.seedIdea); } simulation.on("tick", () => { link .attr("x1", d => (d.source as any).x) .attr("y1", d => (d.source as any).y) .attr("x2", d => (d.target as any).x) .attr("y2", d => (d.target as any).y); node .attr("transform", d => `translate(${d.x},${d.y})`); }); function dragstarted(event: any, d: D3Node) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event: any, d: D3Node) { d.fx = event.x; d.fy = event.y; } function dragended(event: any, d: D3Node) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; if (d.type === 'idea') { onPositionUpdate(d.id, { x: event.x, y: event.y }); } } return () => { simulation.stop(); }; }, [session, ideas, selectedNodeId, onNodeSelect]); return (

Spatial Exploration Mode

); };