feat: add slides generation tool with multiple slide types
- 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>
This commit is contained in:
@@ -19,6 +19,13 @@ import {
|
||||
Users,
|
||||
Globe,
|
||||
Lock,
|
||||
Menu,
|
||||
Star,
|
||||
Sparkles,
|
||||
List,
|
||||
LayoutGrid,
|
||||
X,
|
||||
Edit2,
|
||||
} from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -36,6 +43,9 @@ import {
|
||||
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'
|
||||
@@ -106,12 +116,20 @@ export function BrainstormPage() {
|
||||
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)
|
||||
@@ -260,11 +278,35 @@ export function BrainstormPage() {
|
||||
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-12 border-b border-border/20 backdrop-blur-md bg-white/20 dark:bg-black/20 z-10 relative 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={{
|
||||
@@ -275,7 +317,15 @@ export function BrainstormPage() {
|
||||
/>
|
||||
|
||||
<div className="max-w-4xl mx-auto relative">
|
||||
<div className="flex items-center gap-5 mb-8">
|
||||
{/* 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={{
|
||||
@@ -283,12 +333,12 @@ export function BrainstormPage() {
|
||||
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"
|
||||
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-4xl font-serif font-medium text-foreground tracking-tight">
|
||||
<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">
|
||||
@@ -302,8 +352,8 @@ export function BrainstormPage() {
|
||||
{session && !isGuest && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={exportBrainstorm.isPending}
|
||||
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'}
|
||||
>
|
||||
@@ -375,7 +425,7 @@ export function BrainstormPage() {
|
||||
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"
|
||||
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()}
|
||||
@@ -413,6 +463,52 @@ export function BrainstormPage() {
|
||||
</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>
|
||||
|
||||
@@ -429,6 +525,48 @@ export function BrainstormPage() {
|
||||
<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}
|
||||
@@ -507,11 +645,16 @@ export function BrainstormPage() {
|
||||
<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)]"
|
||||
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
|
||||
@@ -531,10 +674,20 @@ export function BrainstormPage() {
|
||||
{t('brainstorm.noteCreated') || 'Note Created'}
|
||||
</span>
|
||||
)}
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={() => setSelectedIdeaId(null)}
|
||||
className="p-2 hover:bg-foreground/5 rounded-full transition-colors text-muted-foreground"
|
||||
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>
|
||||
@@ -699,20 +852,30 @@ export function BrainstormPage() {
|
||||
<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 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" />
|
||||
@@ -730,6 +893,105 @@ export function BrainstormPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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
|
||||
|
||||
Reference in New Issue
Block a user