- Add slides.tool.ts with support for title, bullets, chart, stats, table, cards, timeline, quote, comparison, equation, image, summary slide types - Chart types: bar, horizontal-bar, line, donut, radar - Integrate with agent executor and canvas system - Add multilingual support (en/fr) - Various UI improvements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1082 lines
53 KiB
TypeScript
1082 lines
53 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,
|
|
Menu,
|
|
Star,
|
|
Sparkles,
|
|
List,
|
|
LayoutGrid,
|
|
X,
|
|
Edit2,
|
|
} 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,
|
|
useStarIdea,
|
|
useSummarizeBrainstorm,
|
|
useRenameBrainstormSession,
|
|
} 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 starIdea = useStarIdea(activeSessionId || '')
|
|
const summarize = useSummarizeBrainstorm(activeSessionId || '')
|
|
const renameSession = useRenameBrainstormSession(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 [viewMode, setViewMode] = useState<'canvas' | 'list'>('canvas')
|
|
const [summaryOpen, setSummaryOpen] = useState(false)
|
|
const [summaryText, setSummaryText] = useState<string | null>(null)
|
|
const [renamingSession, setRenamingSession] = useState<string | null>(null)
|
|
const [renameInput, setRenameInput] = useState('')
|
|
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 handleStarIdea = async (ideaId: string) => {
|
|
try { await starIdea.mutateAsync(ideaId) } catch {}
|
|
}
|
|
|
|
const handleSummarize = async () => {
|
|
setSummaryOpen(true)
|
|
if (summaryText) return
|
|
try {
|
|
const text = await summarize.mutateAsync(language)
|
|
setSummaryText(text)
|
|
} catch { setSummaryText(null) }
|
|
}
|
|
|
|
const handleRenameSession = async (sessionId: string) => {
|
|
if (!renameInput.trim() || renameInput.trim() === session?.seedIdea) {
|
|
setRenamingSession(null)
|
|
return
|
|
}
|
|
try {
|
|
await renameSession.mutateAsync(renameInput.trim())
|
|
} catch {}
|
|
setRenamingSession(null)
|
|
}
|
|
|
|
const isGenerating = createBrainstorm.isPending || expandIdea.isPending
|
|
|
|
return (
|
|
<div className="h-full flex flex-col bg-[#F8F7F2] dark:bg-[#0A0A0A] overflow-hidden">
|
|
<div className="p-4 sm:p-8 md: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">
|
|
{/* Hamburger mobile */}
|
|
<button
|
|
className="md:hidden absolute -top-1 -start-1 p-2 text-foreground/70 hover:bg-foreground/5 rounded-lg transition-colors"
|
|
onClick={() => window.dispatchEvent(new CustomEvent('open-mobile-sidebar'))}
|
|
aria-label="Ouvrir la navigation"
|
|
>
|
|
<Menu size={20} />
|
|
</button>
|
|
<div className="flex items-center gap-3 sm:gap-5 mb-4 sm:mb-8 ps-9 md:ps-0">
|
|
<motion.div
|
|
animate={{ rotate: isGenerating ? 360 : 0 }}
|
|
transition={{
|
|
repeat: isGenerating ? Infinity : 0,
|
|
duration: 20,
|
|
ease: 'linear',
|
|
}}
|
|
className="w-10 h-10 sm:w-14 sm:h-14 rounded-xl sm:rounded-2xl bg-brand-accent shadow-[0_0_20px_rgba(164,113,72,0.2)] flex items-center justify-center text-white shrink-0"
|
|
>
|
|
<Wind size={28} />
|
|
</motion.div>
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl sm: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={handleSummarize}
|
|
disabled={summarize.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-4 sm:px-8 py-4 sm:py-7 pr-16 sm:pr-20 outline-none transition-all text-lg sm: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>
|
|
|
|
{/* Seed prompts — shown only when no session and not generating */}
|
|
{!session && !createBrainstorm.isPending && !activeSessionId && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 6 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.2 }}
|
|
className="mt-5 flex flex-wrap gap-2"
|
|
>
|
|
{[
|
|
t('brainstorm.exampleSeed1') || 'Simplify my morning routine',
|
|
t('brainstorm.exampleSeed2') || 'Ideas for a creative project this weekend',
|
|
t('brainstorm.exampleSeed3') || 'Systems to manage my energy better',
|
|
t('brainstorm.exampleSeed4') || 'What I learned this week',
|
|
].map((example) => (
|
|
<button
|
|
key={example}
|
|
onClick={() => handleStartBrainstorm(example)}
|
|
className="px-4 py-2 rounded-full border border-border bg-white dark:bg-white/5 text-sm text-muted-foreground hover:text-foreground hover:border-brand-accent/40 hover:bg-brand-accent/5 transition-all"
|
|
>
|
|
{example}
|
|
</button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* View mode toggle — shown on mobile only when a session is active */}
|
|
{session && (
|
|
<div className="mt-4 md:hidden flex items-center gap-2">
|
|
<button
|
|
onClick={() => setViewMode('canvas')}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-widest transition-all ${viewMode === 'canvas' ? 'bg-foreground text-background' : 'text-muted-foreground hover:bg-foreground/5'}`}
|
|
>
|
|
<LayoutGrid size={12} /> {t('brainstorm.viewCanvas') || 'Canvas'}
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-widest transition-all ${viewMode === 'list' ? 'bg-foreground text-background' : 'text-muted-foreground hover:bg-foreground/5'}`}
|
|
>
|
|
<List size={12} /> {t('brainstorm.viewList') || 'List'}
|
|
</button>
|
|
<span className="text-[10px] text-muted-foreground ms-auto">
|
|
{session.ideas.filter(i => i.status !== 'dismissed').length} {t('brainstorm.ideasCount') || 'ideas'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</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 && viewMode === 'list' ? (
|
|
/* Mobile list view */
|
|
<div className="h-full overflow-y-auto p-4 space-y-6">
|
|
{[1, 2, 3].map(wave => {
|
|
const waveIdeas = session.ideas.filter(i => i.waveNumber === wave && i.status !== 'dismissed')
|
|
if (waveIdeas.length === 0) return null
|
|
return (
|
|
<div key={wave}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div
|
|
className="w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: wave === 1 ? '#fb923c' : wave === 2 ? '#60a5fa' : '#a78bfa' }}
|
|
/>
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
|
{t('brainstorm.waveBadge', { wave })} — {waveIdeas.length}
|
|
</span>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{waveIdeas.map(idea => (
|
|
<button
|
|
key={idea.id}
|
|
onClick={() => { setSelectedIdeaId(idea.id) }}
|
|
className={`w-full text-start p-4 rounded-xl border transition-all ${selectedIdeaId === idea.id ? 'border-brand-accent/40 bg-brand-accent/5' : 'border-border bg-white dark:bg-white/5 hover:border-foreground/20'}`}
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-foreground text-sm leading-snug">{idea.title}</p>
|
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{idea.description}</p>
|
|
</div>
|
|
<div className="shrink-0 flex items-center gap-1.5">
|
|
{idea.isStarred && <Star size={12} className="text-amber-400 fill-amber-400" />}
|
|
{idea.status === 'converted' && <Check size={12} className="text-emerald-500" />}
|
|
<ChevronRight size={14} className="text-muted-foreground/40" />
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</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={{ y: '100%', x: 0 }}
|
|
animate={{ y: 0, x: 0 }}
|
|
exit={{ y: '100%', x: 0 }}
|
|
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
|
className="fixed bottom-0 start-0 end-0 max-h-[80vh] z-30 md:relative md:bottom-auto md:start-auto md:end-auto md:max-h-none md:static md:w-[400px] border-t md:border-t-0 md:border-l border-border bg-white dark:bg-[#1A1A1A] flex flex-col rounded-t-2xl md:rounded-none shadow-[0_-20px_40px_rgba(0,0,0,0.08)] md:shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
|
|
>
|
|
{/* Drag handle for mobile */}
|
|
<div className="md:hidden flex justify-center pt-3 pb-1 shrink-0">
|
|
<div className="w-10 h-1 rounded-full bg-border" />
|
|
</div>
|
|
<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>
|
|
)}
|
|
{canEdit && (
|
|
<button
|
|
onClick={() => handleStarIdea(selectedIdea.id)}
|
|
disabled={starIdea.isPending}
|
|
className={`p-2 rounded-full transition-colors ${selectedIdea.isStarred ? 'text-amber-400 hover:text-amber-500' : 'text-muted-foreground hover:text-amber-400'}`}
|
|
title={selectedIdea.isStarred ? (t('brainstorm.unstar') || 'Unstar') : (t('brainstorm.star') || 'Star')}
|
|
>
|
|
<Star size={18} className={selectedIdea.isStarred ? 'fill-amber-400' : ''} />
|
|
</button>
|
|
)}
|
|
<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">
|
|
“{selectedIdea.connectionToSeed}”
|
|
</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) => (
|
|
<div key={s.id} className="relative group/session">
|
|
<button
|
|
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>
|
|
{activeSessionId === s.id && (
|
|
<button
|
|
onClick={() => { setRenamingSession(s.id); setRenameInput(s.seedIdea) }}
|
|
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-foreground/10 hover:bg-foreground/20 flex items-center justify-center opacity-0 group-hover/session:opacity-100 transition-opacity"
|
|
title={t('brainstorm.renameSession') || 'Rename'}
|
|
>
|
|
<Edit2 size={8} className="text-foreground" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</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}
|
|
/>
|
|
)}
|
|
|
|
{/* AI Summary modal */}
|
|
<AnimatePresence>
|
|
{summaryOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
|
|
onClick={() => setSummaryOpen(false)}
|
|
>
|
|
<motion.div
|
|
initial={{ y: 40, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: 40, opacity: 0 }}
|
|
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="w-full max-w-lg bg-white dark:bg-[#1A1A1A] rounded-2xl shadow-2xl p-8 relative"
|
|
>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<Sparkles size={20} className="text-brand-accent" />
|
|
<h3 className="font-serif text-xl font-medium">{t('brainstorm.exportModalTitle') || 'Session summary'}</h3>
|
|
</div>
|
|
<button onClick={() => setSummaryOpen(false)} className="p-1.5 hover:bg-foreground/5 rounded-full text-muted-foreground">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
{summarize.isPending ? (
|
|
<div className="flex items-center gap-3 text-muted-foreground italic py-8 justify-center">
|
|
<div className="flex gap-1.5">
|
|
{[0, 1, 2].map(i => (
|
|
<motion.div key={i} animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }} transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }} className="w-1.5 h-1.5 rounded-full bg-brand-accent" />
|
|
))}
|
|
</div>
|
|
<span>{t('brainstorm.generating') || 'Generating synthesis...'}</span>
|
|
</div>
|
|
) : summaryText ? (
|
|
<p className="text-foreground/80 leading-relaxed font-light text-base">{summaryText}</p>
|
|
) : (
|
|
<p className="text-muted-foreground italic text-sm text-center py-6">{t('brainstorm.summaryError') || 'Could not generate summary'}</p>
|
|
)}
|
|
<div className="mt-6 flex items-center gap-3 justify-between">
|
|
{summaryText && (
|
|
<button
|
|
onClick={() => { setSummaryText(null); handleSummarize() }}
|
|
className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
{t('brainstorm.regenerateSummary') || 'Regenerate'}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => { setSummaryOpen(false); handleExport() }}
|
|
disabled={exportBrainstorm.isPending || summarize.isPending}
|
|
className="ms-auto flex items-center gap-2 px-5 py-2.5 bg-foreground text-background rounded-xl text-xs font-bold uppercase tracking-widest disabled:opacity-50 hover:opacity-80 transition-opacity"
|
|
>
|
|
<Download size={13} />
|
|
{t('brainstorm.exportAsNote') || 'Export as note'}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Rename session modal */}
|
|
<AnimatePresence>
|
|
{renamingSession && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
|
|
onClick={() => setRenamingSession(null)}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.95, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0.95, opacity: 0 }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="w-full max-w-sm bg-white dark:bg-[#1A1A1A] rounded-2xl shadow-2xl p-6"
|
|
>
|
|
<h3 className="font-serif text-lg font-medium mb-4">{t('brainstorm.renameSession') || 'Rename session'}</h3>
|
|
<input
|
|
autoFocus
|
|
type="text"
|
|
value={renameInput}
|
|
onChange={e => setRenameInput(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') handleRenameSession(renamingSession!); if (e.key === 'Escape') setRenamingSession(null) }}
|
|
className="w-full border border-border rounded-xl px-4 py-3 text-sm bg-transparent outline-none focus:border-brand-accent/40 focus:ring-2 focus:ring-brand-accent/10"
|
|
/>
|
|
<div className="flex gap-2 mt-4 justify-end">
|
|
<button onClick={() => setRenamingSession(null)} className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors">{t('common.cancel') || 'Cancel'}</button>
|
|
<button onClick={() => handleRenameSession(renamingSession!)} disabled={renameSession.isPending} className="px-4 py-2 text-sm bg-foreground text-background rounded-xl disabled:opacity-50">{t('common.save') || 'Save'}</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<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>
|
|
)
|
|
}
|