Files
Momento/memento-note/components/brainstorm/brainstorm-page.tsx
Antigravity bb75b2e763
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
docs: add comprehensive Stripe billing guide
Covers architecture, configuration steps, user flows, API routes,
webhooks, pricing, testing with Stripe CLI, production checklist,
and troubleshooting.
2026-05-16 21:10:26 +00:00

820 lines
39 KiB
TypeScript

'use client'
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useSession } from 'next-auth/react'
import { motion, AnimatePresence } from 'motion/react'
import {
Zap,
History,
Plus,
Wind,
FileText,
ChevronRight,
UserPlus,
Activity,
Download,
Share2,
Check,
Users,
Globe,
Lock,
} from 'lucide-react'
import dynamic from 'next/dynamic'
import { useLanguage } from '@/lib/i18n'
import {
useBrainstormSession,
useBrainstormSessions,
useCreateBrainstorm,
useExpandIdea,
useDismissIdea,
useConvertIdea,
useExportBrainstorm,
useFinalizeBrainstorm,
useDeleteBrainstorm,
useJoinBrainstorm,
useAddManualIdea,
useBrainstormActivity,
useUpdateBrainstormSettings,
} from '@/hooks/use-brainstorm'
import { useBrainstormSocket } from '@/hooks/use-brainstorm-socket'
import { LiveCursors, PresenceAvatars, useCursorTracking } from '@/components/brainstorm/live-cursors'
import { ActivityFeed } from '@/components/brainstorm/activity-feed'
import { BrainstormShareDialog } from '@/components/brainstorm/brainstorm-share-dialog'
import { GhostCursor } from '@/components/brainstorm/ghost-cursor'
import { PlaybackBar } from '@/components/brainstorm/playback-bar'
import type { BrainstormIdea, BrainstormNoteRef, BrainstormActivityItem } from '@/types/brainstorm'
const WaveCanvas = dynamic(
() =>
import('@/components/brainstorm/wave-canvas').then((m) => ({
default: m.WaveCanvas,
})),
{ ssr: false }
)
function CursorTrackerEffect({ containerRef, moveCursor }: { containerRef: React.RefObject<HTMLDivElement | null>; moveCursor: (c: { x: number; y: number } | null) => void }) {
useCursorTracking(containerRef, moveCursor)
return null
}
const WAVE_COLORS: Record<number, { border: string; bg: string; text: string }> = {
1: { border: 'border-orange-200', bg: 'bg-orange-50', text: 'text-orange-600' },
2: { border: 'border-brand-accent', bg: 'bg-brand-accent', text: 'text-brand-accent' },
3: { border: 'border-violet-200', bg: 'bg-violet-50', text: 'text-violet-600' },
}
export function BrainstormPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { t, language } = useLanguage()
const { data: authSession } = useSession()
const urlSessionId = searchParams.get('session')
const urlSeed = searchParams.get('seed')
const urlSourceNoteId = searchParams.get('sourceNoteId')
const urlInviteToken = searchParams.get('invite')
const [seedInput, setSeedInput] = useState('')
const [selectedIdeaId, setSelectedIdeaId] = useState<string | null>(null)
const [activeSessionId, setActiveSessionId] = useState<string | null>(urlSessionId)
const [autoStarted, setAutoStarted] = useState(false)
useEffect(() => {
if (urlSessionId && urlSessionId !== activeSessionId) {
setActiveSessionId(urlSessionId)
}
}, [urlSessionId])
const [showActivityFeed, setShowActivityFeed] = useState(false)
const [showShareDialog, setShowShareDialog] = useState(false)
const [manualEditCount, setManualEditCount] = useState(0)
const [shareStatus, setShareStatus] = useState<'idle' | 'copying' | 'copied'>('idle')
const { data: sessions, isLoading: sessionsLoading } = useBrainstormSessions()
const { data: sessionResult, isLoading: sessionLoading } = useBrainstormSession(activeSessionId)
const session = sessionResult?.session || null
const sessionMeta = sessionResult?.meta
const isGuest = sessionMeta?.role === 'guest'
const canEdit = sessionMeta?.canEdit ?? true
const updateSettings = useUpdateBrainstormSettings(activeSessionId || '')
const createBrainstorm = useCreateBrainstorm()
const expandIdea = useExpandIdea(activeSessionId || '')
const dismissIdea = useDismissIdea(activeSessionId || '')
const convertIdea = useConvertIdea(activeSessionId || '')
const exportBrainstorm = useExportBrainstorm(activeSessionId || '')
const finalizeBrainstorm = useFinalizeBrainstorm(activeSessionId || '')
const deleteBrainstorm = useDeleteBrainstorm()
const joinMutation = useJoinBrainstorm()
const addManualIdea = useAddManualIdea(activeSessionId || '')
const { data: activities } = useBrainstormActivity(activeSessionId)
const [impactToast, setImpactToast] = useState<{ notesEnriched: number; notesMarkedDry: number } | null>(null)
const [exportError, setExportError] = useState<string | null>(null)
const [exportToast, setExportToast] = useState<{ noteTitle: string; notebookName: string } | null>(null)
const [convertToast, setConvertToast] = useState<{ noteTitle: string; noteId: string } | null>(null)
const [remoteMove, setRemoteMove] = useState<{ ideaId: string; x: number; y: number; _seq: number } | null>(null)
const [playbackIdeas, setPlaybackIdeas] = useState<any[] | null>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const moveSeq = useRef(0)
const { others: socketOthers, moveCursor, activities: socketActivities, socketRef, aiProcessingNodeId } = useBrainstormSocket(
activeSessionId,
authSession?.user?.id || null,
authSession?.user?.name || null,
useCallback((data: { ideaId: string; positionX: number; positionY: number }) => {
moveSeq.current++
setRemoteMove({ ideaId: data.ideaId, x: data.positionX, y: data.positionY, _seq: moveSeq.current })
}, [])
)
const mergedActivities: BrainstormActivityItem[] = useMemo(() => {
const restActivities: BrainstormActivityItem[] = (activities || []) as BrainstormActivityItem[]
const socketActs = (socketActivities || []).map((a: any) => ({
id: `${a.userId}-${a.action}-${Date.now()}-${Math.random()}`,
action: a.action,
details: a.details,
createdAt: new Date().toISOString(),
user: { name: a.userName || null, image: null },
}))
const seen = new Set<string>()
const merged = [...socketActs, ...restActivities]
return merged.filter((a) => {
const key = `${a.action}-${a.user?.name}-${a.details?.ideaTitle || ''}`
if (seen.has(key)) return false
seen.add(key)
return true
}).slice(0, 50)
}, [activities, socketActivities])
const selectedIdea = useMemo(() => {
if (!selectedIdeaId || !session) return null
return session.ideas.find((i) => i.id === selectedIdeaId) || null
}, [selectedIdeaId, session])
useEffect(() => {
if (urlSeed && !autoStarted && !activeSessionId && !createBrainstorm.isPending) {
setAutoStarted(true)
createBrainstorm.mutateAsync({
seedIdea: urlSeed,
sourceNoteId: urlSourceNoteId || undefined,
locale: language,
}).then((result) => {
setActiveSessionId(result.session.id)
router.replace('/brainstorm?session=' + result.session.id)
}).catch(() => {})
}
}, [urlSeed, autoStarted, activeSessionId])
useEffect(() => {
if (urlInviteToken && !autoStarted) {
setAutoStarted(true)
joinMutation.mutateAsync(urlInviteToken).then((result) => {
setActiveSessionId(result.sessionId)
router.replace('/brainstorm?session=' + result.sessionId)
}).catch(() => {})
}
}, [urlInviteToken])
const handleStartBrainstorm = async (seed?: string) => {
const input = seed || seedInput
if (!input.trim()) return
try {
const result = await createBrainstorm.mutateAsync({ seedIdea: input.trim(), locale: language })
setActiveSessionId(result.session.id)
setSeedInput('')
} catch {}
}
const handleDelete = async (sessionId: string) => {
try {
await deleteBrainstorm.mutateAsync(sessionId)
if (activeSessionId === sessionId) {
setActiveSessionId(null)
setSelectedIdeaId(null)
}
} catch {}
}
const handleDeepen = async (idea: BrainstormIdea) => {
try {
await expandIdea.mutateAsync({ ideaId: idea.id, locale: language })
} catch {}
}
const handleDismiss = async (ideaId: string) => {
try {
await dismissIdea.mutateAsync(ideaId)
setSelectedIdeaId(null)
} catch {}
}
const handleConvert = async (idea: BrainstormIdea) => {
try {
const result = await convertIdea.mutateAsync(idea.id)
if (result?.id) {
setConvertToast({ noteTitle: result.title || idea.title, noteId: result.id })
setTimeout(() => {
setConvertToast(null)
router.push(`/home?openNote=${result.id}`)
}, 2000)
}
} catch {}
}
const handleExport = async () => {
setExportError(null)
try {
const result = await exportBrainstorm.mutateAsync()
if (result?.id) {
const notebookName = result._notebookName || t('brainstorm.exportDefaultNotebookName')
setExportToast({ noteTitle: result.title || t('brainstorm.exportDefaultNoteTitle'), notebookName })
setTimeout(() => {
setExportToast(null)
router.push(`/home?openNote=${result.id}`)
}, 2000)
return
}
const impact = await finalizeBrainstorm.mutateAsync()
if (impact) {
setImpactToast(impact)
setTimeout(() => setImpactToast(null), 5000)
}
} catch (err: any) {
setExportError(err?.message || t('brainstorm.exportFailedMessage'))
setTimeout(() => setExportError(null), 4000)
}
}
const handlePositionUpdate = async (id: string, pos: { x: number; y: number }) => {
if (!activeSessionId) return
socketRef.current?.emit('idea:moved', { ideaId: id, positionX: pos.x, positionY: pos.y, userId: authSession?.user?.id || '' })
try {
await fetch(`/api/brainstorm/${activeSessionId}/update-position`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId: id, positionX: pos.x, positionY: pos.y }),
})
} catch {}
}
const handleCreateIdea = useCallback(({ title, parentIdeaId }: { title: string; parentIdeaId?: string; x: number; y: number }) => {
addManualIdea.mutate({ title, parentIdeaId, locale: language })
}, [addManualIdea, language])
const isGenerating = createBrainstorm.isPending || expandIdea.isPending
return (
<div className="h-full flex flex-col bg-[#F8F7F2] dark:bg-[#0A0A0A] overflow-hidden">
<div className="p-12 border-b border-border/20 backdrop-blur-md bg-white/20 dark:bg-black/20 z-10 relative overflow-hidden">
<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-brand-accent shadow-[0_0_20px_rgba(164,113,72,0.2)] flex items-center justify-center text-white"
>
<Wind size={28} />
</motion.div>
<div className="flex-1">
<h1 className="text-4xl font-serif font-medium text-foreground tracking-tight">
{t('brainstorm.title')}
</h1>
<div className="flex items-center gap-2 mt-1">
<span className="w-8 h-px bg-brand-accent/40" />
<p className="text-[10px] text-muted-foreground tracking-[0.3em] uppercase font-bold">
{t('brainstorm.subtitle') || 'Unfold dimensions of potentiality'}
</p>
</div>
</div>
{session && !isGuest && (
<div className="flex items-center gap-3">
<button
onClick={handleExport}
disabled={exportBrainstorm.isPending}
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-muted-foreground hover:text-brand-accent transition-all shadow-sm disabled:opacity-50"
title={t('brainstorm.export') || 'Export'}
>
<Download size={14} />
<span className="hidden sm:inline">{t('brainstorm.export') || 'Export'}</span>
</button>
<button
onClick={() => setShowActivityFeed(!showActivityFeed)}
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 ${showActivityFeed ? 'bg-foreground text-background' : 'bg-white dark:bg-white/5 text-muted-foreground hover:text-foreground'}`}
>
<Activity size={14} />
<span className="hidden sm:inline">{t('brainstorm.activityTitle') || 'Activity'}</span>
</button>
<button
onClick={() => setShowShareDialog(true)}
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-muted-foreground hover:text-foreground transition-all shadow-sm"
>
<Share2 size={14} />
<span className="hidden sm:inline">{t('brainstorm.invite') || 'Invite'}</span>
</button>
<div className="flex items-center px-3 py-1.5 bg-white dark:bg-white/5 border border-border rounded-xl shadow-sm transition-all hover:border-emerald-500/30">
<div className="flex items-center gap-2 mr-3" title={t('brainstorm.liveCollaborationTitle')}>
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.6)]"></span>
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-emerald-600 dark:text-emerald-400 hidden sm:inline-block">{t('brainstorm.liveStatus')}</span>
</div>
<div className="flex items-center group/avatars">
{authSession?.user?.name && (
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm bg-brand-accent z-10 transition-transform hover:scale-110 hover:z-20"
title={`${authSession.user.name} (You)`}
>
{authSession.user.name.charAt(0).toUpperCase()}
</div>
)}
{socketOthers.slice(0, 3).map((user, idx) => (
<div
key={user.userId}
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm transition-transform hover:scale-110 hover:z-20"
style={{ backgroundColor: user.color, marginLeft: '-8px', zIndex: 9 - idx }}
title={user.name}
>
{user.name.charAt(0).toUpperCase()}
</div>
))}
{socketOthers.length > 3 && (
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-muted-foreground bg-black/5 dark:bg-white/10 ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm"
style={{ marginLeft: '-8px', zIndex: 5 }}
title={`${socketOthers.length - 3} other participants`}
>
+{socketOthers.length - 3}
</div>
)}
</div>
</div>
</div>
)}
</div>
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-brand-accent/20 to-brand-accent/10 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()}
placeholder={t('brainstorm.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-foreground shadow-sm group-hover:shadow-md border-border/40 focus:border-brand-accent/40 focus:ring-4 focus:ring-brand-accent/5"
/>
<button
onClick={() => handleStartBrainstorm()}
disabled={isGenerating || !seedInput.trim()}
className="absolute right-4 top-4 bottom-4 px-6 bg-foreground dark:bg-brand-accent text-background 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-white/30 border-t-white rounded-full animate-spin" />
) : (
<Plus size={24} />
)}
</button>
</div>
<AnimatePresence>
{createBrainstorm.isPending && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-6 flex items-center gap-4 text-brand-accent/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-brand-accent"
/>
))}
</div>
<span className="text-base tracking-tight">
{t('brainstorm.generating') || 'AI is harvesting seeds of thought...'}
</span>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex-1 flex overflow-hidden relative">
<div className="flex-1 relative" ref={canvasContainerRef}>
{session && <LiveCursors others={socketOthers} />}
<GhostCursor
isActive={isGenerating || !!aiProcessingNodeId}
targetId={aiProcessingNodeId || (expandIdea.isPending ? selectedIdeaId : null)}
containerRef={canvasContainerRef}
/>
<CursorTrackerEffect containerRef={canvasContainerRef} moveCursor={moveCursor} />
{sessionLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-foreground/20 border-t-foreground rounded-full animate-spin" />
</div>
) : session ? (
<WaveCanvas
session={session}
onNodeSelect={setSelectedIdeaId}
onPositionUpdate={handlePositionUpdate}
selectedNodeId={selectedIdeaId}
onCreateIdea={handleCreateIdea}
remoteMove={remoteMove}
manualEditTrigger={manualEditCount}
playbackIdeas={playbackIdeas}
/>
) : (
<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-muted-foreground animate-pulse" />
<p className="text-xl font-serif italic text-muted-foreground">
{t('brainstorm.canvasWaitingHint')}
</p>
</div>
)}
<AnimatePresence>
{session && (
<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-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-6">
{[1, 2, 3].map((w) => (
<div key={w} className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full"
style={{
backgroundColor: WAVE_COLORS[w]?.border?.replace('border-', '') === 'orange-200' ? '#fb923c' : w === 2 ? '#60a5fa' : '#a78bfa',
boxShadow: `0 0 8px ${w === 1 ? 'rgba(251,146,60,0.6)' : w === 2 ? 'rgba(96,165,250,0.6)' : 'rgba(167,139,250,0.6)'}`,
}}
/>
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t('brainstorm.waveBadge', { wave: w })}
</span>
</div>
))}
</div>
{canEdit && (
<button
onClick={() => setManualEditCount((c) => c + 1)}
className="px-6 py-3 bg-[#F4F1EA] 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-muted-foreground hover:bg-foreground hover:text-background transition-all"
>
<Plus size={14} />
{t('brainstorm.addIdea')}
</button>
)}
</motion.div>
)}
</AnimatePresence>
{session && (
<ActivityFeed
activities={mergedActivities}
isOpen={showActivityFeed}
onToggle={() => setShowActivityFeed(!showActivityFeed)}
t={t}
/>
)}
{session && activeSessionId && (
<PlaybackBar
sessionId={activeSessionId}
onSnapshotSelect={setPlaybackIdeas}
onExitPlayback={() => setPlaybackIdeas(null)}
/>
)}
</div>
<AnimatePresence>
{selectedIdea && (
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
className="w-[400px] border-l border-border bg-white dark:bg-[#1A1A1A] flex flex-col z-20 shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
>
<div className="p-8 flex-1 overflow-y-auto">
<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-300 dark:border-orange-700 bg-orange-50 dark:bg-orange-500/15 text-orange-600 dark:text-orange-400'
: selectedIdea.waveNumber === 2
? 'border-brand-accent dark:border-blue-700 bg-brand-accent dark:bg-brand-accent/15 text-brand-accent dark:text-brand-accent'
: 'border-violet-300 dark:border-violet-700 bg-violet-50 dark:bg-violet-500/15 text-violet-600 dark:text-violet-400'
}`}
>
{t('brainstorm.wave') || 'Wave'} {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">
{t('brainstorm.noteCreated') || 'Note Created'}
</span>
)}
<button
onClick={() => setSelectedIdeaId(null)}
className="p-2 hover:bg-foreground/5 rounded-full transition-colors text-muted-foreground"
>
<ChevronRight size={20} />
</button>
</div>
</div>
<h2 className="text-3xl font-serif font-medium text-foreground 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-brand-accent" />
<span className="text-xs font-bold text-muted-foreground">
{t('brainstorm.novelty')}: {selectedIdea.noveltyScore ?? t('common.notAvailable')}/10
</span>
</div>
<div className="flex items-center gap-1.5">
{selectedIdea.createdByType === 'human' ? (
<>
<div className="w-4 h-4 rounded-full bg-brand-accent flex items-center justify-center text-[8px] font-bold text-white">
{(selectedIdea as any).creator?.name?.charAt(0)?.toUpperCase() || 'U'}
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-brand-accent">
{t('brainstorm.humanIdea') || 'Human'}
</span>
</>
) : (
<>
<span className="text-violet-500 text-xs"></span>
<span className="text-[10px] font-bold uppercase tracking-widest text-violet-500">
{t('brainstorm.aiIdea') || 'AI'}
</span>
</>
)}
</div>
</div>
<p className="text-foreground/80 leading-relaxed font-light mb-10 text-lg">
{selectedIdea.description}
</p>
{selectedIdea.connectionToSeed && (
<div className="p-6 bg-slate-50 dark:bg-white/[0.03] rounded-2xl border border-border/40 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-3">
{t('brainstorm.originConnection') || 'Origin connection'}
</h4>
<p className="text-sm italic text-muted-foreground leading-relaxed">
&ldquo;{selectedIdea.connectionToSeed}&rdquo;
</p>
</div>
)}
{selectedIdea.noteRefs && selectedIdea.noteRefs.length > 0 && (
<div className="space-y-3 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-1">
{t('brainstorm.ideaOrigin') || 'Origin of the idea'}
</h4>
{selectedIdea.noteRefs.map((ref: BrainstormNoteRef) => {
const isPositive = ref.relation === 'derived_from' || ref.relation === 'extends'
const isNegative = ref.relation === 'opposes'
const badgeColor = isPositive
? 'bg-emerald-500/10 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-700'
: isNegative
? 'bg-rose-500/10 dark:bg-rose-500/20 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-700'
: 'bg-amber-500/10 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-700'
const relLabel = t(`brainstorm.${ref.relation}`) || ref.relation
const vis = (ref as any).visibility || 'participants'
const isRestricted = vis === 'owner_only'
return (
<div
key={ref.id}
className="p-4 rounded-xl border border-border bg-white dark:bg-white/5"
>
<div className="flex items-start gap-3">
<span className={`shrink-0 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border ${badgeColor}`}>
{relLabel}
</span>
{isRestricted && !isGuest && (
<span className="shrink-0 px-1.5 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-wider border border-amber-200 dark:border-amber-700 bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center gap-0.5">
<Lock size={8} /> {t('brainstorm.ownerBadge')}
</span>
)}
<div className="flex-1 min-w-0">
{ref.note ? (
<p className="text-sm font-medium text-foreground truncate">
{ref.note.title || t('notes.untitled')}
</p>
) : (
<p className="text-sm italic text-muted-foreground">
{t('brainstorm.noNoteLink') || 'Purely generative idea'}
</p>
)}
<p className="text-xs italic text-muted-foreground mt-1 leading-relaxed">
{ref.explanation}
</p>
</div>
{ref.noteId && (
<button
onClick={() => router.push(`/home?openNote=${ref.noteId}`)}
className="shrink-0 px-2 py-1 text-[9px] font-bold uppercase tracking-wider rounded-lg bg-foreground/5 hover:bg-foreground/10 text-muted-foreground hover:text-foreground transition-all"
>
{t('brainstorm.viewNote') || 'View'}
</button>
)}
</div>
</div>
)
})}
</div>
)}
{canEdit && (
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeepen(selectedIdea)}
disabled={expandIdea.isPending}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-brand-accent/40 hover:bg-brand-accent/5 transition-all group disabled:opacity-50"
>
<Wind size={24} className="text-muted-foreground group-hover:text-brand-accent mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground group-hover:text-foreground">
{expandIdea.isPending ? (t('brainstorm.deepening') || 'Generating...') : (t('brainstorm.deepen') || 'Deepen')}
</span>
</button>
<button
onClick={() => handleConvert(selectedIdea)}
disabled={selectedIdea.status === 'converted' || convertIdea.isPending}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-emerald-400/40 hover:bg-emerald-500/5 transition-all group disabled:opacity-50"
>
<FileText size={24} className="text-muted-foreground group-hover:text-emerald-500 mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground group-hover:text-foreground">
{convertIdea.isPending ? (t('brainstorm.converting') || 'Converting...') : (t('brainstorm.extract') || 'Create Note')}
</span>
</button>
</div>
<button
onClick={() => handleDismiss(selectedIdea.id)}
className="w-full py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all border border-transparent hover:border-rose-500/10"
>
{t('brainstorm.dismiss') || 'Not pertinent'}
</button>
</div>
)}
{isGuest && (
<div className="mt-6 px-4 py-3 bg-brand-accent/5 border border-brand-accent/10 rounded-xl flex items-center gap-2">
<Globe size={14} className="text-brand-accent shrink-0" />
<span className="text-[11px] text-brand-accent">
{t('brainstorm.guestReadOnlyNotice')}
</span>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="w-16 border-l border-border flex flex-col items-center py-6 gap-6 bg-white dark:bg-[#1A1A1A] z-10">
<History size={18} className="text-muted-foreground" />
<div className="w-px flex-1 bg-border/40" />
<div className="flex flex-col gap-3 overflow-y-auto px-2">
{sessions?.map((s) => (
<button
key={s.id}
onClick={() => setActiveSessionId(s.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 === s.id
? 'bg-foreground text-background scale-110 shadow-lg'
: (s as any)._owned === false
? 'bg-brand-accent dark:bg-brand-accent/10 text-brand-accent hover:bg-blue-100 hover:text-brand-accent'
: 'bg-white dark:bg-white/10 text-muted-foreground hover:bg-foreground/5 hover:text-foreground'
}`}
title={s.seedIdea}
>
{s.seedIdea.charAt(0).toUpperCase()}
</button>
))}
</div>
<div className="w-px h-12 bg-border/40" />
</div>
</div>
{activeSessionId && session && (
<BrainstormShareDialog
open={showShareDialog}
onOpenChange={setShowShareDialog}
sessionId={activeSessionId}
seedIdea={session.seedIdea}
isPublic={(session as any).isPublic || false}
guestCanEdit={(session as any).guestCanEdit || false}
/>
)}
<AnimatePresence>
{impactToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-foreground text-background rounded-2xl shadow-2xl flex items-center gap-4 text-sm font-medium"
>
<span>
{impactToast.notesEnriched === 0 && impactToast.notesMarkedDry === 0
? t('brainstorm.linkCopied')
: <>
{impactToast.notesEnriched > 0 && t('brainstorm.impactNotesEnriched', { count: impactToast.notesEnriched })}
{impactToast.notesEnriched > 0 && impactToast.notesMarkedDry > 0 && t('brainstorm.impactSeparator')}
{impactToast.notesMarkedDry > 0 && t('brainstorm.impactNotesMarkedDry', { count: impactToast.notesMarkedDry })}
</>
}
</span>
<button onClick={() => setImpactToast(null)} className="text-background/60 hover:text-background">
</button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{exportError && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-rose-500 text-white rounded-2xl shadow-2xl flex items-center gap-4 text-sm font-medium"
>
<span>{exportError}</span>
<button onClick={() => setExportError(null)} className="text-white/60 hover:text-white">
</button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{exportToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-emerald-600 text-white rounded-2xl shadow-2xl flex items-center gap-3 text-sm font-medium"
>
<Wind size={18} />
<div className="flex flex-col">
<span className="font-bold">{exportToast.noteTitle}</span>
<span className="text-[11px] text-emerald-100">
{t('brainstorm.exportNotebookPrefix')} {exportToast.notebookName}
</span>
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
{t('brainstorm.exportOpening')}
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{convertToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-emerald-600 text-white rounded-2xl shadow-2xl flex items-center gap-3 text-sm font-medium"
>
<FileText size={18} />
<div className="flex flex-col">
<span className="font-bold">{convertToast.noteTitle}</span>
<span className="text-[11px] text-emerald-100">{t('brainstorm.noteCreated') || 'Note Created'}</span>
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
{t('brainstorm.exportOpening')}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}