import React, { useState, useEffect, useMemo, useRef } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Zap, Search, ArrowRight, History, Plus, Wind, PlusCircle, FileText, ChevronRight, Maximize2, Share2, Users, Check, Download, Activity, X } from 'lucide-react'; import { v4 as uuidv4 } from 'uuid'; import { WaveCanvas } from './WaveCanvas'; import { BrainstormSession, BrainstormIdea, Note } from '../../types'; import { generateBrainstormWave, generateExpansion, getEmbedding, cosineSimilarity } from '../../services/geminiService'; interface BrainstormViewProps { notes: Note[]; onConvertNote: (idea: BrainstormIdea) => void; } export const BrainstormView: React.FC = ({ notes, onConvertNote }) => { const [seedInput, setSeedInput] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [ideas, setIdeas] = useState([]); const [selectedIdeaId, setSelectedIdeaId] = useState(null); const [editingNodeId, setEditingNodeId] = useState(null); const [manualTitle, setManualTitle] = useState(''); const [shareStatus, setShareStatus] = useState<'idle' | 'copying' | 'copied'>('idle'); const [showActivity, setShowActivity] = useState(false); const [activities, setActivities] = useState<{ id: string; type: string; message: string; timestamp: string }[]>([]); const [collaborators, setCollaborators] = useState<{ id: string; name: string; color: string }[]>([]); const socketRef = useRef(null); // Mock current user for presence const currentUser = useMemo(() => ({ id: 'me-' + Math.random().toString(36).substr(2, 9), name: 'Sepehr' // Derived from user email in metadata if possible, or guest }), []); const getInitials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); const stringToColor = (str: string) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } const colors = ['#f43f5e', '#ef4444', '#f59e0b', '#10b981', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', '#f472b6']; return colors[Math.abs(hash) % colors.length]; }; const addActivity = (message: string, type: string = 'info', broadcast: boolean = true) => { const newActivity = { id: uuidv4(), type, message, timestamp: new Date().toLocaleTimeString() }; setActivities(prev => [newActivity, ...prev].slice(0, 50)); if (broadcast && socketRef.current?.readyState === WebSocket.OPEN) { socketRef.current.send(JSON.stringify({ type: 'activity', activity: newActivity })); } }; // WebSocket Connection useEffect(() => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const socket = new WebSocket(`${protocol}//${window.location.host}`); socketRef.current = socket; socket.onopen = () => { console.log('WS Shared Brainstorm connected'); if (activeSessionId) { socket.send(JSON.stringify({ type: 'join', sessionId: activeSessionId, user: { ...currentUser, color: stringToColor(currentUser.name) } })); } }; socket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'presence') { setCollaborators(data.users); } if (data.type === 'idea_added') { const newIdea = data.idea; setIdeas(prev => { if (prev.find(i => i.id === newIdea.id)) return prev; return [...prev, newIdea]; }); } if (data.type === 'idea_updated') { const updatedIdea = data.idea; setIdeas(prev => prev.map(i => i.id === updatedIdea.id ? updatedIdea : i)); } if (data.type === 'activity') { setActivities(prev => [data.activity, ...prev].slice(0, 50)); } }; return () => { socket.close(); }; }, []); // Sync session joining useEffect(() => { if (socketRef.current?.readyState === WebSocket.OPEN && activeSessionId) { socketRef.current.send(JSON.stringify({ type: 'join', sessionId: activeSessionId, user: { ...currentUser, color: stringToColor(currentUser.name) } })); } }, [activeSessionId, currentUser]); useEffect(() => { fetch('/api/brainstorm/sessions') .then(res => res.json()) .then(data => { setSessions(data); // Check for initial session from URL parameter (passed via window by App.tsx) const initialId = (window as any).initialSessionId; if (initialId && data.find((s: any) => s.id === initialId)) { setActiveSessionId(initialId); delete (window as any).initialSessionId; } }) .catch(err => console.error("Failed to load sessions", err)); }, []); useEffect(() => { if (activeSessionId) { fetch(`/api/brainstorm/${activeSessionId}`) .then(res => res.json()) .then(data => { if (data.ideas) { setIdeas(prev => { const filtered = prev.filter(i => i.sessionId !== activeSessionId); return [...filtered, ...data.ideas]; }); } }) .catch(err => console.error("Failed to load ideas", err)); } }, [activeSessionId]); const activeSession = useMemo(() => sessions.find(s => s.id === activeSessionId), [activeSessionId, sessions]); const activeIdeas = useMemo(() => ideas.filter(i => i.sessionId === activeSessionId), [activeSessionId, ideas]); const selectedIdea = useMemo(() => ideas.find(i => i.id === selectedIdeaId), [selectedIdeaId, ideas]); useEffect(() => { const handleRemoteStart = (e: any) => { if (e.detail?.seed) { handleStartBrainstorm(e.detail.seed, e.detail.sourceNoteId); } }; window.addEventListener('start-brainstorm', handleRemoteStart); return () => window.removeEventListener('start-brainstorm', handleRemoteStart); }, [notes]); const handleStartBrainstorm = async (seed: string, sourceNoteId?: string) => { if (!seed.trim()) return; setIsGenerating(true); setError(null); try { // 1. Create session on backend const sessionRes = await fetch('/api/brainstorm/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seedIdea: seed, sourceNoteId }) }); const session = await sessionRes.json(); if (!sessionRes.ok) throw new Error(session.error || "Failed to create session"); setSessions(prev => [session, ...prev]); setActiveSessionId(session.id); setSeedInput(''); // 2. Generate waves in frontend concurrently const contextSummaries = notes.slice(0, 5).map(n => n.title).join(', '); const wavePromises = [1, 2, 3].map(async (num) => { try { const generated = await generateBrainstormWave(seed, num, contextSummaries); return generated.map(g => ({ ...g, waveNumber: num })); } catch (e) { console.error(`Wave ${num} failed`, e); return []; } }); const wavesResults = await Promise.all(wavePromises); const allNewIdeas = wavesResults.flat(); if (allNewIdeas.length === 0) { throw new Error("No ideas were generated. Gemini might be shy today."); } // 3. Save ideas to backend const ideasRes = await fetch(`/api/brainstorm/${session.id}/ideas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ideas: allNewIdeas }) }); const savedIdeas = await ideasRes.json(); setIdeas(prev => [...prev, ...savedIdeas]); addActivity(`Generated ${savedIdeas.length} ideas for Wave ${allNewIdeas[0]?.waveNumber || ''}`); // Notify others savedIdeas.forEach((idea: any) => { socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea })); }); } catch (err: any) { console.error("Brainstorm failed:", err); setError(err.message || "An unexpected error occurred while brainstorming."); } finally { setIsGenerating(false); } }; const updateIdea = async (ideaId: string, updates: Partial) => { try { const res = await fetch(`/api/brainstorm/ideas/${ideaId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); const updated = await res.json(); setIdeas(prev => prev.map(i => i.id === ideaId ? updated : i)); // Notify others socketRef.current?.send(JSON.stringify({ type: 'idea_updated', idea: updated })); } catch (err) { console.error("Update failed", err); } }; const handleDeepenIdea = async (idea: BrainstormIdea) => { setIsGenerating(true); try { const generated = await generateExpansion(idea.title, idea.description); const newIdeasData = generated.map(g => ({ ...g, waveNumber: Math.min(idea.waveNumber + 1, 3), parentIdeaId: idea.id })); const res = await fetch(`/api/brainstorm/${idea.sessionId}/ideas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ideas: newIdeasData }) }); const savedIdeas = await res.json(); setIdeas(prev => [...prev, ...savedIdeas]); addActivity(`Expanded idea: ${idea.title}`); // Notify others savedIdeas.forEach((i: any) => { socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea: i })); }); } catch (err) { console.error("Deepen failed", err); setError("Failed to expand this idea."); } finally { setIsGenerating(false); } }; const handleDismissIdea = (ideaId: string) => { updateIdea(ideaId, { status: 'dismissed' }); setSelectedIdeaId(null); }; const handleConvertToNote = (idea: BrainstormIdea) => { updateIdea(idea.id, { status: 'converted' }); onConvertNote(idea); }; const handleManualAdd = async (title: string, parentId?: string) => { if (!title.trim() || !activeSessionId) return; setIsGenerating(true); try { let finalParentId = parentId; let waveNumber = 1; if (parentId) { const p = ideas.find(i => i.id === parentId); if (p) waveNumber = Math.min(p.waveNumber + 1, 3); } else if (activeIdeas.length > 0) { // Semantic auto-placement if no parent is specified try { const newEmbedding = await getEmbedding(title); let bestSim = -1; let bestParent: BrainstormIdea | null = null; for (const idea of activeIdeas) { const ideaEmbedding = await getEmbedding(idea.title + " " + idea.description); const sim = cosineSimilarity(newEmbedding, ideaEmbedding); if (sim > bestSim) { bestSim = sim; bestParent = idea; } } if (bestParent && bestSim > 0.7) { finalParentId = bestParent.id; waveNumber = Math.min(bestParent.waveNumber + 1, 3); } } catch (e) { console.error("Semantic placement failed", e); } } const newIdeaData = [{ title: title, description: "", waveNumber: waveNumber, connectionToSeed: finalParentId ? `Manual addition (auto-linked)` : "Manual addition to root", noveltyScore: 5, parentIdeaId: finalParentId }]; const res = await fetch(`/api/brainstorm/${activeSessionId}/ideas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ideas: newIdeaData }) }); const saved = await res.json(); setIdeas(prev => [...prev, ...saved]); addActivity(`Manually added idea: ${title}`); // Notify saved.forEach((i: any) => socketRef.current?.send(JSON.stringify({ type: 'idea_added', idea: i }))); setEditingNodeId(null); setManualTitle(''); } catch (err) { console.error("Manual add failed", err); } finally { setIsGenerating(false); } }; const handleInvite = () => { if (!activeSessionId) return; const shareUrl = `${window.location.origin}${window.location.pathname}?session=${activeSessionId}`; navigator.clipboard.writeText(shareUrl); setShareStatus('copied'); addActivity(`Invitation link copied to clipboard`); setTimeout(() => setShareStatus('idle'), 2000); }; const handleExport = () => { if (!activeSession) return; let markdown = `# Brainstorm : ${activeSession.seedIdea}\n\n`; markdown += `Date : ${new Date(activeSession.createdAt).toLocaleDateString()}\n\n`; [1, 2, 3].forEach(waveNum => { const waveIdeas = activeIdeas.filter(i => i.waveNumber === waveNum); if (waveIdeas.length > 0) { markdown += `## Vague ${waveNum}\n\n`; waveIdeas.forEach(idea => { markdown += `### ${idea.title}\n`; markdown += `${idea.description}\n`; markdown += `*Score de nouveauté : ${idea.noveltyScore}/10*\n`; markdown += `*Connexion : ${idea.connectionToSeed}*\n\n`; }); } }); onConvertNote({ id: uuidv4(), title: `Brainstorm Export: ${activeSession.seedIdea}`, description: markdown, sessionId: activeSession.id, waveNumber: 0, connectionToSeed: "Export", noveltyScore: 10, status: 'converted' }); addActivity(`Session exported to notes`); }; return (
{/* Header / Start area */}
{/* Architectural Grid Background */}

Waves of Thought

Unfold dimensions of potentiality

{activeSession && (
{collaborators.map((user) => (
{getInitials(user.name)}
{user.name}
))}
)}
setSeedInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleStartBrainstorm(seedInput)} placeholder="Enter a concept to unfold..." className={`w-full relative bg-white dark:bg-[#1A1A1A] border-2 rounded-2xl px-8 py-7 pr-20 outline-none transition-all text-2xl font-serif italic text-ink dark:text-dark-ink shadow-sm group-hover:shadow-md ${error ? 'border-rose-400 focus:ring-rose-100 shadow-rose-100' : 'border-border/40 focus:border-ochre/40 focus:ring-4 focus:ring-ochre/5'}`} />
{error && (

Obstruction detected

{error}
)} {isGenerating && !error && (
{[0.2, 0.4, 0.6].map((d, i) => ( ))}
Gemini is harvesting seeds of thought from the digital ether...
)}
{/* Main Canvas Area */}
{activeSession ? (
setSelectedIdeaId(null)} className="w-full h-full"> { setSelectedIdeaId(id); }} onPositionUpdate={(id, pos) => updateIdea(id, { position: pos })} onAddChild={(id) => { setSelectedIdeaId(id); setEditingNodeId(id); }} onManualSubmit={handleManualAdd} onManualCancel={() => setEditingNodeId(null)} editingNodeId={editingNodeId} selectedNodeId={selectedIdeaId} relatedNotes={notes} />
) : (

The canvas is waiting for your spark...

)} {/* Floating UI overlays */} {activeSession && (
Wave 1
Wave 2
Wave 3
)}
{/* Activity Sidebar */} {showActivity && (

Flux d'activité

{activities.length === 0 ? (

Aucune activité pour le moment

) : ( activities.map((act) => (

{act.message}

{act.timestamp} )) )}
)} {/* Right Sidebar Detail Panel */} {selectedIdea && (
Vague {selectedIdea.waveNumber}
{selectedIdea.status === 'converted' && ( Note Created )}

{selectedIdea.title}

Novelty: {selectedIdea.noveltyScore}/10

{selectedIdea.description}

Origin connection

"{selectedIdea.connectionToSeed}"

{selectedIdea.relatedNoteIds && selectedIdea.relatedNoteIds.length > 0 && (

Semantic Context

{selectedIdea.relatedNoteIds.map(noteId => { const note = notes.find(n => n.id === noteId); return note ? (
{note.title}
) : null; })}
)}
)}
{/* History Rail */}
{sessions.map(session => ( ))}
); };