Files
Momento/architectural-grid/src/components/BrainstormView/BrainstormView.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

798 lines
35 KiB
TypeScript

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<BrainstormViewProps> = ({ notes, onConvertNote }) => {
const [seedInput, setSeedInput] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessions, setSessions] = useState<BrainstormSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [ideas, setIdeas] = useState<BrainstormIdea[]>([]);
const [selectedIdeaId, setSelectedIdeaId] = useState<string | null>(null);
const [editingNodeId, setEditingNodeId] = useState<string | null>(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<WebSocket | null>(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<BrainstormIdea>) => {
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 (
<div className="h-full flex flex-col bg-[#F8F7F2] dark:bg-[#0A0A0A] overflow-hidden">
{/* Header / Start area */}
<div className="p-12 border-b border-border/20 backdrop-blur-md bg-white/20 dark:bg-dark-paper/20 z-10 relative overflow-hidden">
{/* Architectural Grid Background */}
<div className="absolute inset-0 pointer-events-none opacity-[0.03] dark:opacity-[0.05]"
style={{ backgroundImage: 'linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
<div className="max-w-4xl mx-auto relative">
<div className="flex items-center gap-5 mb-8">
<motion.div
animate={{ rotate: isGenerating ? 360 : 0 }}
transition={{ repeat: isGenerating ? Infinity : 0, duration: 20, ease: "linear" }}
className="w-14 h-14 rounded-2xl bg-ochre shadow-[0_0_20px_rgba(212,163,115,0.2)] flex items-center justify-center text-paper"
>
<Wind size={28} />
</motion.div>
<div className="flex-1">
<h1 className="text-4xl font-serif font-medium text-ink dark:text-dark-ink tracking-tight">Waves of Thought</h1>
<div className="flex items-center gap-2 mt-1">
<span className="w-8 h-px bg-ochre/40" />
<p className="text-[10px] text-concrete tracking-[0.3em] uppercase font-bold">Unfold dimensions of potentiality</p>
</div>
</div>
{activeSession && (
<div className="flex items-center gap-3">
<button
onClick={handleExport}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-concrete hover:text-ochre transition-all shadow-sm"
title="Export to Note"
>
<Download size={14} />
<span className="hidden sm:inline">Export</span>
</button>
<button
onClick={() => setShowActivity(!showActivity)}
className={`flex items-center gap-2 px-4 py-2 border border-border rounded-xl text-xs font-bold uppercase tracking-widest transition-all shadow-sm ${showActivity ? 'bg-ink text-paper' : 'bg-white dark:bg-white/5 text-concrete hover:text-ink'}`}
title="Show Activity"
>
<Activity size={14} />
<span className="hidden sm:inline">Activity</span>
</button>
<button
onClick={handleInvite}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-concrete hover:text-ink transition-all shadow-sm"
>
{shareStatus === 'copied' ? <Check size={14} className="text-emerald-500" /> : <Share2 size={14} />}
{shareStatus === 'copied' ? 'Link Copied' : 'Invite'}
</button>
<div className="flex items-center -space-x-2 mr-2">
{collaborators.map((user) => (
<div
key={user.id}
className="relative group/avatar"
>
<div
className="w-8 h-8 rounded-full border-2 border-paper dark:border-dark-paper flex items-center justify-center text-[10px] font-bold text-white shadow-sm cursor-help relative z-10"
style={{ backgroundColor: user.color || '#999' }}
>
{getInitials(user.name)}
</div>
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-ink text-paper text-[10px] rounded font-bold whitespace-nowrap opacity-0 group-hover/avatar:opacity-100 pointer-events-none transition-opacity z-20">
{user.name}
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-ink" />
</div>
</div>
))}
</div>
<div className="flex items-center gap-1 px-3 py-2 bg-emerald-500/10 rounded-full mr-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<Users size={14} className="text-emerald-500" />
</div>
</div>
)}
</div>
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-ochre/20 to-accent/20 rounded-[28px] blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity duration-700" />
<input
type="text"
value={seedInput}
onChange={(e) => 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'}`}
/>
<button
onClick={() => handleStartBrainstorm(seedInput)}
disabled={isGenerating || !seedInput.trim()}
className="absolute right-4 top-4 bottom-4 px-6 bg-ink dark:bg-ochre text-paper rounded-xl disabled:opacity-50 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 min-w-[70px] shadow-lg"
>
{isGenerating ? (
<div className="w-6 h-6 border-3 border-paper/30 border-t-paper rounded-full animate-spin" />
) : (
<Plus size={24} />
)}
</button>
</div>
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="mt-6 p-5 bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 rounded-2xl flex items-start gap-4 text-rose-600 dark:text-rose-400 text-sm overflow-hidden shadow-sm"
>
<div className="w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-500/20 flex items-center justify-center shrink-0 mt-0.5">
<div className="w-2 h-2 rounded-full bg-rose-500" />
</div>
<div className="flex-1">
<p className="font-bold uppercase tracking-wider text-[10px] mb-1">Obstruction detected</p>
<span>{error}</span>
</div>
</motion.div>
)}
{isGenerating && !error && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-6 flex items-center gap-4 text-ochre/80 italic font-serif"
>
<div className="flex gap-1.5">
{[0.2, 0.4, 0.6].map((d, i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: d }}
className="w-1.5 h-1.5 rounded-full bg-ochre"
/>
))}
</div>
<span className="text-base tracking-tight">Gemini is harvesting seeds of thought from the digital ether...</span>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex-1 flex overflow-hidden relative">
{/* Main Canvas Area */}
<div className="flex-1 relative bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#ffffff10_1px,transparent_1px)] [background-size:20px_20px]">
{activeSession ? (
<div onClick={() => setSelectedIdeaId(null)} className="w-full h-full">
<WaveCanvas
session={activeSession}
ideas={activeIdeas}
onNodeSelect={(id) => {
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}
/>
</div>
) : (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-20 flex-col gap-6">
<Wind size={120} strokeWidth={1} className="text-concrete animate-pulse" />
<p className="text-xl font-serif italic text-concrete">The canvas is waiting for your spark...</p>
</div>
)}
{/* Floating UI overlays */}
<AnimatePresence>
{activeSession && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="absolute bottom-6 left-6 flex gap-2"
>
<div className="px-4 py-2 bg-paper/80 dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-400 shadow-[0_0_8px_rgba(251,146,60,0.6)]" />
<span className="text-[10px] font-bold uppercase tracking-widest text-concrete">Wave 1</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-400 shadow-[0_0_8px_rgba(96,165,250,0.6)]" />
<span className="text-[10px] font-bold uppercase tracking-widest text-concrete">Wave 2</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-violet-400 shadow-[0_0_8px_rgba(167,139,250,0.6)]" />
<span className="text-[10px] font-bold uppercase tracking-widest text-concrete">Wave 3</span>
</div>
</div>
<button
onClick={() => setEditingNodeId('new')}
className="px-6 py-3 bg-paper dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-concrete hover:bg-ink hover:text-paper transition-all"
>
<Plus size={14} />
Add Manual Idea
</button>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Activity Sidebar */}
<AnimatePresence>
{showActivity && (
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="fixed right-0 top-0 h-full w-80 bg-paper dark:bg-dark-paper border-l border-border shadow-2xl z-[70] flex flex-col"
>
<div className="p-6 border-b border-border flex items-center justify-between bg-ink text-paper">
<div className="flex items-center gap-2">
<Activity size={18} />
<h3 className="font-bold uppercase tracking-widest text-xs">Flux d'activité</h3>
</div>
<button onClick={() => setShowActivity(false)} className="p-1 hover:bg-white/10 rounded-lg">
<X size={18} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{activities.length === 0 ? (
<p className="text-xs text-concrete text-center italic mt-10">Aucune activité pour le moment</p>
) : (
activities.map((act) => (
<motion.div
key={act.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="p-3 bg-white dark:bg-white/5 rounded-xl border border-border/50 relative overflow-hidden group"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-ochre/40" />
<p className="text-[11px] font-medium text-ink dark:text-dark-ink">{act.message}</p>
<span className="text-[9px] text-concrete font-bold mt-1 block">{act.timestamp}</span>
</motion.div>
))
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Right Sidebar Detail Panel */}
<AnimatePresence>
{selectedIdea && (
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
className="w-[400px] border-l border-border bg-paper dark:bg-dark-paper flex flex-col z-20 shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
>
<div className="p-8 flex-1 overflow-y-auto custom-scrollbar">
<div className="flex items-center justify-between mb-8">
<div className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border
${selectedIdea.waveNumber === 1 ? 'border-orange-200 bg-orange-50 text-orange-600' :
selectedIdea.waveNumber === 2 ? 'border-blue-200 bg-blue-50 text-blue-600' :
'border-violet-200 bg-violet-50 text-violet-600'}`}>
Vague {selectedIdea.waveNumber}
</div>
<div className="flex items-center gap-2">
{selectedIdea.status === 'converted' && (
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest bg-emerald-500/10 px-2 py-1 rounded-full">Note Created</span>
)}
<button onClick={() => setSelectedIdeaId(null)} className="p-2 hover:bg-ink/5 rounded-full transition-colors">
<ChevronRight size={20} />
</button>
</div>
</div>
<h2 className="text-3xl font-serif font-medium text-ink dark:text-dark-ink mb-2">{selectedIdea.title}</h2>
<div className="flex items-center gap-4 mb-8">
<div className="flex items-center gap-1">
<Zap size={14} className="text-ochre" />
<span className="text-xs font-bold text-concrete">Novelty: {selectedIdea.noveltyScore}/10</span>
</div>
</div>
<p className="text-ink/80 dark:text-dark-ink/80 leading-relaxed font-light mb-10 text-lg">
{selectedIdea.description}
</p>
<div className="p-6 bg-slate-50 dark:bg-white/5 rounded-2xl border border-border/40 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-concrete mb-3">Origin connection</h4>
<p className="text-sm italic text-muted-ink leading-relaxed">
"{selectedIdea.connectionToSeed}"
</p>
</div>
{selectedIdea.relatedNoteIds && selectedIdea.relatedNoteIds.length > 0 && (
<div className="space-y-4 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-concrete px-1">Semantic Context</h4>
{selectedIdea.relatedNoteIds.map(noteId => {
const note = notes.find(n => n.id === noteId);
return note ? (
<div key={noteId} className="p-4 rounded-xl border border-border bg-white dark:bg-white/5 hover:border-ink/20 transition-all cursor-pointer group">
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-ink dark:text-dark-ink truncate">{note.title}</h5>
<ArrowRight size={14} className="text-concrete group-hover:text-ink transition-colors" />
</div>
</div>
) : null;
})}
</div>
)}
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeepenIdea(selectedIdea)}
disabled={isGenerating}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-ochre/40 hover:bg-ochre/5 transition-all group disabled:opacity-50"
>
<Wind size={24} className="text-concrete group-hover:text-ochre mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-ink group-hover:text-ink text-center">AI Expand</span>
</button>
<button
onClick={() => setEditingNodeId(selectedIdea.id)}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-ink/40 hover:bg-ink/5 transition-all group disabled:opacity-50"
>
<PlusCircle size={24} className="text-concrete group-hover:text-ink mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-ink group-hover:text-ink text-center">Add Child</span>
</button>
<button
onClick={() => handleConvertToNote(selectedIdea)}
disabled={selectedIdea.status === 'converted'}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-accent/40 hover:bg-accent/5 transition-all group disabled:opacity-50 whitespace-nowrap"
>
<FileText size={24} className="text-concrete group-hover:text-accent mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-ink group-hover:text-ink text-center">Extract Note</span>
</button>
</div>
<button
onClick={() => handleDismissIdea(selectedIdea.id)}
className="w-full py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all border border-transparent hover:border-rose-500/10"
>
Not pertinent
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* History Rail */}
<div className="w-16 border-l border-border flex flex-col items-center py-6 gap-6 bg-paper dark:bg-dark-paper z-10">
<History size={18} className="text-concrete" />
<div className="w-px flex-1 bg-border/40" />
<div className="flex flex-col gap-3 overflow-y-auto px-2 custom-scrollbar">
{sessions.map(session => (
<button
key={session.id}
onClick={() => setActiveSessionId(session.id)}
className={`w-10 h-10 min-h-[40px] rounded-xl flex items-center justify-center text-xs font-bold transition-all shrink-0
${activeSessionId === session.id ? 'bg-ink text-paper scale-110 shadow-lg' : 'bg-paper dark:bg-white/10 text-concrete hover:bg-black/5 hover:text-ink'}`}
title={session.seedIdea}
>
{session.seedIdea.charAt(0).toUpperCase()}
</button>
))}
</div>
<div className="w-px h-12 bg-border/40" />
</div>
</div>
</div>
);
};