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>
159 lines
5.1 KiB
TypeScript
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>
|
|
)
|
|
}
|