Files
Momento/memento-note/components/dashboard-agent-carousel.tsx
Antigravity 30da592ba2
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s
feat(dashboard): tableau de bord Second Brain configurable avec chargement progressif
Briefing granulaire, pistes rapides puis enrichissement async, layout persisté v5,
suggestions agents, intégration Gmail et navigation sidebar alignée sur /home.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:50:53 +00:00

159 lines
5.1 KiB
TypeScript

'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { Search, Bot, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
export interface AgentSuggestion {
id: string
topic: string
reason: string
relatedNoteCount: number
suggestedFrequency: string
}
export interface DashboardAgentCarouselProps {
suggestions: AgentSuggestion[]
loading?: boolean
actingId: string | null
formatFrequency: (f: string) => string
onAccept: (id: string) => void
onDismiss: (id: string) => void
prefersReducedMotion?: boolean
}
export function DashboardAgentCarousel({
suggestions,
loading,
actingId,
formatFrequency,
onAccept,
onDismiss,
prefersReducedMotion,
}: DashboardAgentCarouselProps) {
const { t } = useLanguage()
const [idx, setIdx] = useState(0)
const navActions = suggestions.length > 1 ? (
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setIdx(i => Math.max(0, i - 1))}
disabled={idx === 0}
className="p-1 rounded border border-border/30 disabled:opacity-25"
aria-label={t('homeDashboard.intelPrev')}
>
<ChevronLeft size={12} />
</button>
<span className="text-[8px] font-mono text-concrete px-1">
{idx + 1}/{suggestions.length}
</span>
<button
type="button"
onClick={() => setIdx(i => Math.min(suggestions.length - 1, i + 1))}
disabled={idx >= suggestions.length - 1}
className="p-1 rounded border border-border/30 disabled:opacity-25"
aria-label={t('homeDashboard.intelNext')}
>
<ChevronRight size={12} />
</button>
</div>
) : null
if (loading) {
return <div className="h-[140px] rounded-2xl bg-stone-50 dark:bg-zinc-950/30 animate-pulse" />
}
return (
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 p-4">
<DashboardWidgetTitleRow
widgetId="agents"
icon={<Bot size={12} className="text-brand-accent" />}
title={t('homeDashboard.suggestedResearch')}
actions={navActions}
/>
{suggestions.length === 0 ? (
<p className="text-[11px] text-concrete italic leading-relaxed py-2">
{t('homeDashboard.agentsEmpty')}
</p>
) : (
<AgentSlide
current={suggestions[idx]}
actingId={actingId}
formatFrequency={formatFrequency}
onAccept={onAccept}
onDismiss={onDismiss}
prefersReducedMotion={prefersReducedMotion}
t={t}
/>
)}
</div>
)
}
function AgentSlide({
current,
actingId,
formatFrequency,
onAccept,
onDismiss,
prefersReducedMotion,
t,
}: {
current: AgentSuggestion
actingId: string | null
formatFrequency: (f: string) => string
onAccept: (id: string) => void
onDismiss: (id: string) => void
prefersReducedMotion?: boolean
t: (key: string) => string
}) {
const slide = prefersReducedMotion
? { initial: {}, animate: {}, exit: {} }
: { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -8 } }
return (
<AnimatePresence mode="wait">
<motion.div
key={current.id}
initial={slide.initial}
animate={slide.animate}
exit={slide.exit}
transition={{ duration: prefersReducedMotion ? 0 : 0.18 }}
className="p-3.5 rounded-xl border border-border/25 bg-stone-50/60 dark:bg-zinc-950/40"
>
<div className="flex items-start gap-2 mb-2">
<Search size={12} className="text-brand-accent shrink-0 mt-0.5" />
<p className="text-sm font-semibold text-ink dark:text-dark-ink leading-snug">{current.topic}</p>
</div>
<p className="text-[11px] text-concrete leading-relaxed line-clamp-2 mb-3">{current.reason}</p>
<p className="text-[8px] font-mono uppercase text-concrete mb-3">
{current.relatedNoteCount} {t('homeDashboard.notes')} · {formatFrequency(current.suggestedFrequency)}
</p>
<div className="flex gap-2">
<button
type="button"
disabled={actingId === current.id}
onClick={() => onDismiss(current.id)}
className="text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg border border-border/40 text-concrete hover:text-ink transition-colors disabled:opacity-40"
>
{t('homeDashboard.dismiss')}
</button>
<button
type="button"
disabled={actingId === current.id}
onClick={() => onAccept(current.id)}
className="flex-1 inline-flex items-center justify-center gap-1 text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg bg-ink text-white dark:bg-white dark:text-black font-bold hover:opacity-90 disabled:opacity-40"
>
{actingId === current.id ? <Loader2 size={10} className="animate-spin" /> : null}
{t('homeDashboard.createAgent')}
</button>
</div>
</motion.div>
</AnimatePresence>
)
}