Files
Momento/memento-note/components/dashboard-path-widgets.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

259 lines
8.8 KiB
TypeScript

'use client'
import { CheckCircle2, Circle } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { DashboardWidgetShell } from '@/components/dashboard-catalog-widgets'
export interface DailyReviewItem {
key: string
label: string
done: boolean
count?: number
onClick: () => void
}
export function DashboardDailyReview({
items,
loading,
}: {
items: DailyReviewItem[]
loading?: boolean
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="daily-review"
icon={<CheckCircle2 size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.daily-review')}
compact
>
{loading ? (
<div className="space-y-2">
{[0, 1, 2].map(i => <div key={i} className="h-8 rounded-lg bg-stone-50 animate-pulse" />)}
</div>
) : (
<ul className="space-y-1.5">
{items.map(item => (
<li key={item.key}>
<button
type="button"
onClick={item.onClick}
className="w-full flex items-center gap-2.5 p-2 rounded-xl hover:bg-brand-accent/[0.04] transition-colors text-start"
>
{item.done ? (
<CheckCircle2 size={14} className="text-brand-accent shrink-0" />
) : (
<Circle size={14} className="text-concrete/50 shrink-0" />
)}
<span className={`text-[10px] flex-1 ${item.done ? 'text-concrete line-through' : 'text-ink dark:text-dark-ink'}`}>
{item.label}
{item.count !== undefined && item.count > 0 ? ` (${item.count})` : ''}
</span>
</button>
</li>
))}
</ul>
)}
<p className="text-[9px] text-concrete mt-2 leading-relaxed">{t('homeDashboard.dailyReviewHint')}</p>
</DashboardWidgetShell>
)
}
export function DashboardOpenLoops({
loops,
loading,
onSelect,
}: {
loops: Array<{ id: string; title: string | null; notebookId: string | null; daysStale: number }>
loading?: boolean
onSelect: (id: string, notebookId: string | null) => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="open-loops"
icon={<Circle size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.open-loops')}
compact
>
{loading ? (
<div className="h-16 rounded-lg bg-stone-50 animate-pulse" />
) : loops.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.openLoopsEmpty')}</p>
) : (
<div className="space-y-1">
{loops.map(loop => (
<button
key={loop.id}
type="button"
onClick={() => onSelect(loop.id, loop.notebookId)}
className="w-full flex items-center justify-between gap-2 p-2 rounded-lg hover:bg-brand-accent/[0.04] text-start transition-colors"
>
<span className="text-[10px] text-ink dark:text-dark-ink truncate flex-1">
{loop.title || t('homeDashboard.untitled')}
</span>
<span className="text-[8px] font-mono text-concrete shrink-0">
{t('homeDashboard.openLoopsStale', { days: loop.daysStale })}
</span>
</button>
))}
</div>
)}
</DashboardWidgetShell>
)
}
export function DashboardDailyNoteWidget({
loading,
onOpen,
}: {
loading?: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="daily-note"
icon={<CheckCircle2 size={12} className="text-brand-accent" />}
title={t('homeDashboard.widgets.daily-note')}
compact
>
{loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" />
) : (
<button
type="button"
onClick={onOpen}
className="w-full p-3 rounded-xl border border-border/20 hover:border-brand-accent/30 text-start transition-all"
>
<p className="text-[11px] font-serif font-semibold text-ink dark:text-dark-ink">
{new Date().toLocaleDateString(undefined, { weekday: 'long', day: 'numeric', month: 'long' })}
</p>
<p className="text-[9px] font-mono uppercase text-brand-accent mt-1">{t('homeDashboard.dailyNoteOpen')} </p>
</button>
)}
</DashboardWidgetShell>
)
}
export function DashboardLinkSuggestions({
paths,
loading,
onAction,
}: {
paths: Array<{ id: string; title: string; description: string; score?: number }>
loading?: boolean
onAction: (id: string) => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="link-suggestions"
icon={<Circle size={12} className="text-emerald-600" />}
title={t('homeDashboard.widgets.link-suggestions')}
>
{loading ? (
<div className="space-y-2">
<div className="h-12 rounded-lg bg-stone-50 animate-pulse" />
</div>
) : paths.length === 0 ? (
<p className="text-[10px] text-concrete italic">{t('homeDashboard.linkSuggestionsEmpty')}</p>
) : (
<div className="space-y-2">
{paths.map(p => (
<button
key={p.id}
type="button"
onClick={() => onAction(p.id)}
className="w-full p-3 rounded-xl border border-border/20 hover:border-emerald-500/30 text-start transition-all"
>
<div className="flex items-center justify-between gap-2 mb-1">
<p className="text-[11px] font-mono font-bold text-ink dark:text-dark-ink truncate">{p.title}</p>
{p.score ? (
<span className="text-[8px] font-mono text-emerald-600 shrink-0">{p.score}%</span>
) : null}
</div>
<p className="text-[10px] text-concrete line-clamp-2">{p.description}</p>
<p className="text-[8px] font-mono uppercase text-emerald-600 mt-2">{t('homeDashboard.pathActions.addLink')} </p>
</button>
))}
</div>
)}
</DashboardWidgetShell>
)
}
export function DashboardBridgesWidget({
suggestions,
loading,
actingKey,
onCreate,
onDismiss,
}: {
suggestions: Array<{
clusterAId: number
clusterBId: number
clusterAName: string
clusterBName: string
suggestedTitle: string
justification: string
}>
loading?: boolean
actingKey?: string | null
onCreate: (s: { clusterAId: number; clusterBId: number; clusterAName: string; clusterBName: string; suggestedTitle: string; suggestedContent: string; justification: string }) => void
onDismiss: (s: { clusterAId: number; clusterBId: number }) => void
}) {
const { t } = useLanguage()
return (
<DashboardWidgetShell
widgetId="bridges"
icon={<Circle size={12} className="text-violet-600" />}
title={t('homeDashboard.widgets.bridges')}
>
{loading ? (
<div className="h-20 rounded-lg bg-stone-50 animate-pulse" />
) : suggestions.length === 0 ? (
<p className="text-[10px] text-concrete italic">{t('homeDashboard.bridgesEmpty')}</p>
) : (
<div className="space-y-2">
{suggestions.slice(0, 3).map(s => {
const key = `${s.clusterAId}-${s.clusterBId}`
return (
<div key={key} className="p-3 rounded-xl border border-border/20 bg-stone-50/40 dark:bg-zinc-950/30">
<p className="text-[10px] font-mono font-bold text-ink dark:text-dark-ink">{s.suggestedTitle}</p>
<p className="text-[9px] text-concrete mt-1">
{t('homeDashboard.suggestedBridge', { clusterA: s.clusterAName, clusterB: s.clusterBName })}
</p>
<p className="text-[10px] text-concrete mt-1 line-clamp-2">{s.justification}</p>
<div className="flex gap-2 mt-2">
<button
type="button"
disabled={actingKey === key}
onClick={() => onCreate({ ...s, suggestedContent: s.justification })}
className="text-[8px] font-mono uppercase font-bold text-brand-accent hover:underline disabled:opacity-50"
>
{t('homeDashboard.createBridgeNote')}
</button>
<button
type="button"
disabled={actingKey === key}
onClick={() => onDismiss(s)}
className="text-[8px] font-mono uppercase text-concrete hover:text-rose-500 disabled:opacity-50"
>
{t('homeDashboard.dismissConnection')}
</button>
</div>
</div>
)
})}
</div>
)}
</DashboardWidgetShell>
)
}