'use client' import React, { useCallback, useMemo, useRef, useState } from 'react' import dynamic from 'next/dynamic' import { BrainstormSession, BrainstormIdea } from '@/types/brainstorm' import { useExpandIdea, useDismissIdea, useConvertIdea, useExportBrainstorm, brainstormQuotaMessageKey, } from '@/hooks/use-brainstorm' import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Button } from '@/components/ui/button' import { Sparkles, X, FileText, Download } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false, }) interface GraphNode { id: string name: string val: number color: string borderColor: string wave: number status: string idea: BrainstormIdea x?: number y?: number __bckgDimensions?: [number, number] } interface GraphLink { source: string | GraphNode target: string | GraphNode color: string } const WAVE_COLORS: Record = { 0: '#ffffff', 1: '#f97316', 2: '#3b82f6', 3: '#a855f7', } const WAVE_BORDER: Record = { 0: '#e5e5e5', 1: '#ea580c', 2: '#2563eb', 3: '#9333ea', } const STATUS_ALPHA: Record = { active: 1, dismissed: 0.25, converted: 0.9, } interface BrainstormCanvasProps { session: BrainstormSession } export function BrainstormCanvas({ session }: BrainstormCanvasProps) { const { t } = useLanguage() const fgRef = useRef(null) const [selectedIdea, setSelectedIdea] = useState(null) const [isSheetOpen, setIsSheetOpen] = useState(false) const expandIdea = useExpandIdea(session.id) const dismissIdea = useDismissIdea(session.id) const convertIdea = useConvertIdea(session.id) const exportBrainstorm = useExportBrainstorm(session.id) const { graphData } = useMemo(() => { const nodes: GraphNode[] = [] const links: GraphLink[] = [] nodes.push({ id: 'seed', name: session.seedIdea.length > 30 ? session.seedIdea.slice(0, 30) + '...' : session.seedIdea, val: 25, color: WAVE_COLORS[0], borderColor: WAVE_BORDER[0], wave: 0, status: 'active', idea: { id: 'seed', sessionId: session.id, waveNumber: 0, title: session.seedIdea, description: t('brainstorm.originalSeedDescription'), connectionToSeed: null, noveltyScore: null, parentIdeaId: null, convertedToNoteId: null, relatedNoteIds: null, status: 'active', positionX: null, positionY: null, createdAt: session.createdAt, } as BrainstormIdea, }) for (const idea of session.ideas) { const parentNode = idea.parentIdeaId ? idea.parentIdeaId : 'seed' nodes.push({ id: idea.id, name: idea.title, val: idea.status === 'dismissed' ? 8 : 15, color: WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3], borderColor: idea.status === 'converted' ? '#22c55e' : (WAVE_BORDER[idea.waveNumber] || WAVE_BORDER[3]), wave: idea.waveNumber, status: idea.status, idea, }) const linkColor = WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3] links.push({ source: parentNode, target: idea.id, color: idea.status === 'dismissed' ? '#444444' : linkColor, }) } return { graphData: { nodes, links } } }, [session, t]) const handleNodeClick = useCallback((node: any) => { setSelectedIdea(node.idea) setIsSheetOpen(true) }, []) const handleExpand = useCallback(async () => { if (!selectedIdea || selectedIdea.id === 'seed') return try { await expandIdea.mutateAsync({ ideaId: selectedIdea.id }) toast.success(t('brainstorm.toastExpandSuccess')) } catch (err: unknown) { const quotaKey = brainstormQuotaMessageKey(err) toast.error(quotaKey ? t(quotaKey) : (err instanceof Error ? err.message : t('brainstorm.toastExpandFailed'))) } }, [selectedIdea, expandIdea, t]) const handleDismiss = useCallback(async () => { if (!selectedIdea || selectedIdea.id === 'seed') return try { await dismissIdea.mutateAsync(selectedIdea.id) setIsSheetOpen(false) setSelectedIdea(null) toast.success(t('brainstorm.toastDismissSuccess')) } catch (err: any) { toast.error(err.message || t('brainstorm.toastDismissFailed')) } }, [selectedIdea, dismissIdea, t]) const handleConvert = useCallback(async () => { if (!selectedIdea || selectedIdea.id === 'seed') return try { await convertIdea.mutateAsync(selectedIdea.id) toast.success(t('brainstorm.toastConvertSuccess')) } catch (err: any) { toast.error(err.message || t('brainstorm.toastConvertFailed')) } }, [selectedIdea, convertIdea, t]) const handleExport = useCallback(async () => { try { const note = await exportBrainstorm.mutateAsync() toast.success(t('brainstorm.toastExportNoteSuccess')) } catch (err: any) { toast.error(err.message || t('brainstorm.toastExportFailed')) } }, [exportBrainstorm, t]) const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { const label = node.name const fontSize = Math.max(12 / globalScale, 4) ctx.font = `${fontSize}px Sans-Serif` const textWidth = ctx.measureText(label).width const nodeSize = node.val const bgDimensions: [number, number] = [textWidth + nodeSize * 0.8, nodeSize * 1.2] node.__bckgDimensions = bgDimensions const alpha = STATUS_ALPHA[node.status] || 1 ctx.globalAlpha = alpha ctx.fillStyle = node.color ctx.strokeStyle = node.borderColor ctx.lineWidth = node.status === 'converted' ? 3 / globalScale : 1.5 / globalScale ctx.beginPath() ctx.roundRect( node.x! - bgDimensions[0] / 2, node.y! - bgDimensions[1] / 2, bgDimensions[0], bgDimensions[1], nodeSize * 0.3 ) ctx.fill() ctx.stroke() ctx.fillStyle = node.wave === 0 ? '#000000' : '#ffffff' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(label, node.x!, node.y!) if (node.status === 'converted') { ctx.fillStyle = '#22c55e' ctx.font = `${fontSize * 1.2}px Sans-Serif` ctx.fillText('✓', node.x! + bgDimensions[0] / 2 - fontSize * 0.5, node.y! - bgDimensions[1] / 2 + fontSize * 0.5) } ctx.globalAlpha = 1 }, []) const waveLegend = useMemo( () => [ { label: t('brainstorm.legendSeed'), color: WAVE_COLORS[0] }, { label: t('brainstorm.legendVariations'), color: WAVE_COLORS[1] }, { label: t('brainstorm.legendAnalogies'), color: WAVE_COLORS[2] }, { label: t('brainstorm.legendDisruptions'), color: WAVE_COLORS[3] }, ], [t] ) return (
{ const dims = node.__bckgDimensions if (!dims) return ctx.fillStyle = color ctx.beginPath() ctx.roundRect(node.x! - dims[0] / 2, node.y! - dims[1] / 2, dims[0], dims[1], (node.val || 10) * 0.3) ctx.fill() }} onNodeClick={handleNodeClick} linkColor={(link: any) => link.color} linkWidth={1} linkDirectionalArrowLength={3} linkDirectionalArrowRelPos={1} backgroundColor="#09090b" nodeVal={(node: any) => node.val} cooldownTicks={100} enableNodeDrag={true} enableZoomInteraction={true} enablePanInteraction={true} warmupTicks={50} />
{waveLegend.map(w => (
{w.label}
))}
{selectedIdea && ( <> {selectedIdea.id === 'seed' ? session.seedIdea : selectedIdea.title}
{selectedIdea.description && selectedIdea.id !== 'seed' && (

{t('brainstorm.ideaDetailDescription')}

{selectedIdea.description}

)} {selectedIdea.connectionToSeed && (

{t('brainstorm.ideaDetailConnection')}

{selectedIdea.connectionToSeed}

)} {(selectedIdea.noveltyScore != null && selectedIdea.noveltyScore > 0) && (

{t('brainstorm.ideaDetailNovelty')}

{selectedIdea.noveltyScore}/10
)} {selectedIdea.waveNumber > 0 && (

{t('brainstorm.ideaDetailWave')}

{selectedIdea.waveNumber === 1 ? t('brainstorm.waveFlavorVariation') : selectedIdea.waveNumber === 2 ? t('brainstorm.waveFlavorAnalogy') : t('brainstorm.waveFlavorDisruption')}
)} {selectedIdea.status === 'converted' && (

{t('brainstorm.convertedToNoteStatus')}

)} {selectedIdea.id !== 'seed' && selectedIdea.status === 'active' && (
)}
)}
) }