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>
259 lines
8.8 KiB
TypeScript
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>
|
|
)
|
|
}
|