Files
Momento/memento-note/components/brainstorm/playback-bar.tsx
Antigravity bd495be965
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
2026-05-16 12:59:30 +00:00

203 lines
7.6 KiB
TypeScript

'use client'
import React, { useState, useCallback, useRef, useEffect } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { Play, Pause, SkipBack, SkipForward, ChevronDown, ChevronUp, RotateCcw } from 'lucide-react'
import { useBrainstormSnapshots } from '@/hooks/use-brainstorm'
import { useLanguage } from '@/lib/i18n'
interface SnapshotIdea {
id: string
title: string
waveNumber: number
positionX: number | null
positionY: number | null
parentIdeaId: string | null
noveltyScore: number | null
createdByType: string | null
status: string
}
interface Snapshot {
id: string
step: number
label: string | null
ideaGraph: string
createdAt: string
}
interface PlaybackBarProps {
sessionId: string
onSnapshotSelect: (ideas: SnapshotIdea[]) => void
onExitPlayback: () => void
}
export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: PlaybackBarProps) {
const { t } = useLanguage()
const { data: snapshots } = useBrainstormSnapshots(sessionId)
const [isOpen, setIsOpen] = useState(false)
const [currentStep, setCurrentStep] = useState(-1)
const [isPlaying, setIsPlaying] = useState(false)
const playIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const parsedSnapshots: (Snapshot & { ideas: SnapshotIdea[] })[] = React.useMemo(() => {
return (snapshots || []).map((s: Snapshot) => ({
...s,
ideas: JSON.parse(s.ideaGraph) as SnapshotIdea[],
}))
}, [snapshots])
const handleStepChange = useCallback((step: number) => {
if (step === -1) {
setCurrentStep(-1)
onExitPlayback()
return
}
const snapshot = parsedSnapshots[step]
if (snapshot) {
setCurrentStep(step)
onSnapshotSelect(snapshot.ideas)
}
}, [parsedSnapshots, onSnapshotSelect, onExitPlayback])
const togglePlay = useCallback(() => {
if (isPlaying) {
setIsPlaying(false)
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
return
}
setIsPlaying(true)
const startStep = currentStep === -1 ? 0 : currentStep + 1
let step = startStep
playIntervalRef.current = setInterval(() => {
if (step >= parsedSnapshots.length) {
setIsPlaying(false)
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
return
}
handleStepChange(step)
step++
}, 1500)
}, [isPlaying, currentStep, parsedSnapshots.length, handleStepChange])
useEffect(() => {
return () => {
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
}
}, [])
if (!parsedSnapshots || parsedSnapshots.length === 0) return null
const isLive = currentStep === -1
return (
<AnimatePresence>
<motion.div
initial={{ y: 60 }}
animate={{ y: isOpen ? 0 : 40 }}
className="absolute bottom-0 left-0 right-16 z-30"
>
<div className="mx-6 mb-4 bg-white/90 dark:bg-[#1A1A1A]/90 backdrop-blur-xl border border-border rounded-2xl shadow-2xl overflow-hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full px-5 py-2.5 flex items-center justify-between hover:bg-foreground/5 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${isLive ? 'bg-emerald-500 animate-pulse' : isPlaying ? 'bg-orange-500 animate-pulse' : 'bg-muted-foreground'}`} />
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{isLive
? t('brainstorm.liveStatus')
: t('brainstorm.playbackStep', { current: currentStep + 1, total: parsedSnapshots.length })}
</span>
{parsedSnapshots[currentStep]?.label && (
<span className="text-[10px] text-muted-foreground/60 truncate max-w-[200px]">
{parsedSnapshots[currentStep].label}
</span>
)}
</div>
<div className="flex items-center gap-2">
{isLive ? (
<span className="text-[9px] font-bold text-emerald-500 uppercase tracking-wider">{t('brainstorm.liveStatus')}</span>
) : (
<button
onClick={(e) => { e.stopPropagation(); handleStepChange(-1) }}
className="p-1.5 hover:bg-foreground/10 rounded-lg transition-colors"
title={t('brainstorm.playbackReturnToLive')}
>
<RotateCcw size={12} className="text-muted-foreground" />
</button>
)}
{isOpen ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</div>
</button>
{isOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
className="border-t border-border"
>
<div className="px-5 py-3 flex items-center gap-3">
<button
onClick={() => handleStepChange(Math.max(0, currentStep - 1))}
disabled={currentStep <= 0}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors disabled:opacity-30"
>
<SkipBack size={14} />
</button>
<button
onClick={togglePlay}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors"
>
{isPlaying ? <Pause size={14} /> : <Play size={14} />}
</button>
<button
onClick={() => handleStepChange(Math.min(parsedSnapshots.length - 1, currentStep + 1))}
disabled={currentStep >= parsedSnapshots.length - 1}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors disabled:opacity-30"
>
<SkipForward size={14} />
</button>
<div className="flex-1 mx-3">
<input
type="range"
min={-1}
max={parsedSnapshots.length - 1}
value={currentStep}
onChange={(e) => handleStepChange(parseInt(e.target.value))}
className="w-full h-1.5 bg-border rounded-full appearance-none cursor-pointer accent-orange-500"
/>
<div className="flex justify-between mt-1">
<span className="text-[8px] text-muted-foreground/40">{t('brainstorm.liveStatus')}</span>
<span className="text-[8px] text-muted-foreground/40">
{t('brainstorm.playbackStepsCount', { count: parsedSnapshots.length })}
</span>
</div>
</div>
</div>
<div className="px-5 pb-3 flex gap-1 overflow-x-auto">
{parsedSnapshots.map((s, idx) => (
<button
key={s.id}
onClick={() => handleStepChange(idx)}
className={`shrink-0 px-2.5 py-1 rounded-lg text-[9px] font-bold uppercase tracking-wider transition-all ${
idx === currentStep
? 'bg-orange-500 text-white'
: 'bg-foreground/5 text-muted-foreground hover:bg-foreground/10'
}`}
>
{s.label ? (s.label.length > 20 ? s.label.substring(0, 20) + '…' : s.label) : `#${idx + 1}`}
</button>
))}
</div>
</motion.div>
)}
</div>
</motion.div>
</AnimatePresence>
)
}