'use client'
import { motion, useReducedMotion } from 'motion/react'
import { Inbox, GraduationCap, Mail, Pin, Bot, BarChart3 } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { RevisionHeatmap } from '@/components/flashcards/revision-heatmap'
import { UsageMeter } from '@/components/usage-meter'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
import { pathNoteTitle } from '@/lib/dashboard/path-title'
import type { DashboardWidgetId } from '@/lib/dashboard/layout'
interface DashboardWidgetShellProps {
widgetId: DashboardWidgetId
icon: React.ReactNode
title: string
children: React.ReactNode
compact?: boolean
headerActions?: React.ReactNode
}
export function DashboardWidgetShell({
widgetId,
icon,
title,
children,
compact,
headerActions,
}: DashboardWidgetShellProps) {
return (
{children}
)
}
function inboxDisplayTitle(
note: { title: string | null; excerpt?: string },
fallback: string,
): string {
const titled = note.title?.trim()
if (titled) return titled
const excerpt = note.excerpt?.trim()
if (!excerpt) return fallback
return excerpt.length > 80 ? `${excerpt.slice(0, 80).trim()}…` : excerpt
}
export function DashboardInboxWidget({
notes,
loading,
onOpen,
onSelect,
formatRelativeTime,
}: {
count: number
notes: Array<{
id: string
title: string | null
excerpt?: string
notebookId: string | null
updatedAt?: string
}>
loading: boolean
onOpen: () => void
onSelect: (id: string, notebookId: string | null) => void
formatRelativeTime?: (date: string) => string
}) {
const { t } = useLanguage()
const reduced = !!useReducedMotion()
const untitled = t('homeDashboard.untitled')
return (
}
title={t('homeDashboard.inbox')}
compact
>
{loading ? (
) : notes.length === 0 ? (
{t('homeDashboard.inboxEmpty')}
) : (
{notes.map((note, idx) => (
onSelect(note.id, note.notebookId)}
initial={reduced ? false : { opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: reduced ? 0 : 0.22, delay: reduced ? 0 : Math.min(idx * 0.05, 0.15), ease: [0.16, 1, 0.3, 1] }}
className="w-full text-start p-2.5 rounded-xl border border-border/20 hover:border-brand-accent/30 hover:bg-brand-accent/[0.03] transition-all"
>
{inboxDisplayTitle(note, untitled)}
{t('homeDashboard.inbox')}
{note.updatedAt && formatRelativeTime
? ` · ${formatRelativeTime(note.updatedAt)}`
: ''}
))}
)}
)
}
export function DashboardRevisionWidget({
dueCount,
loading,
onOpen,
}: {
dueCount: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
}
title={t('homeDashboard.review')}
compact
>
{loading ? (
) : (
)}
)
}
export function DashboardStatsWidget({
clusterCount,
bridgeCount,
noteCount,
loading,
onOpen,
}: {
clusterCount: number
bridgeCount: number
noteCount: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
}
title={t('homeDashboard.widgets.stats')}
compact
>
{loading ? (
) : (
)}
)
}
export function DashboardAgentActivityWidget({
actions,
loading,
onOpen,
}: {
actions: { id: string; agentName: string; result: string | null; createdAt: string }[]
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
}
title={t('homeDashboard.widgets.agent-activity')}
compact
>
{loading ? (
) : actions.length === 0 ? (
{t('homeDashboard.widgetAgentActivityEmpty')}
) : (
{actions.slice(0, 3).map(a => (
))}
)}
)
}
export function DashboardGmailWidget({
connected,
recentCaptures,
loading,
onOpen,
}: {
connected: boolean
recentCaptures: number
loading: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
return (
}
title={t('homeDashboard.gmailCaptures')}
compact
>
{loading ? (
) : connected ? (
) : (
)}
)
}
export function DashboardPinnedWidget({
notes,
loading,
onSelect,
formatRelativeTime,
}: {
notes: {
id: string
title: string | null
excerpt?: string
notebookId: string | null
updatedAt?: string
notebook?: { name: string; color: string | null } | null
}[]
loading: boolean
onSelect: (id: string, notebookId: string | null) => void
formatRelativeTime?: (date: string) => string
}) {
const { t } = useLanguage()
const untitled = t('homeDashboard.untitled')
return (
}
title={t('homeDashboard.widgets.pinned')}
compact
>
{loading ? (
) : notes.length === 0 ? (
{t('homeDashboard.widgetPinnedEmpty')}
) : (
{notes.map(n => {
const displayTitle = pathNoteTitle(n.title, n.excerpt) || untitled
const notebookName = n.notebook?.name || t('homeDashboard.pinnedNoNotebook')
const notebookColor = n.notebook?.color || '#C4A574'
return (
)
})}
)}
)
}
export function DashboardActivityWidget({
data,
loading,
}: {
data: { date: string; count: number }[]
loading: boolean
}) {
const { t } = useLanguage()
const hasActivity = data.some(d => d.count > 0)
return (
}
title={t('homeDashboard.widgets.activity')}
>
{loading ? (
) : !hasActivity ? (
{t('homeDashboard.activityEmptyHint')}
) : (
<>
>
)}
)
}
export function DashboardUsageWidget() {
const { t } = useLanguage()
return (
}
title={t('homeDashboard.widgets.usage')}
compact
>
{t('homeDashboard.widgetUsageHint')}
)
}
export function DashboardFlashcardsProgressWidget({
retentionRate,
streak,
totalCards,
dueCount = 0,
loading,
onOpen,
}: {
retentionRate: number
streak: number
totalCards: number
dueCount?: number
loading?: boolean
onOpen: () => void
}) {
const { t } = useLanguage()
const retention = Math.max(0, Math.min(100, retentionRate))
return (
}
title={t('homeDashboard.widgets.flashcards-progress')}
compact
>
{loading ? (
) : totalCards === 0 && dueCount === 0 ? (
{t('homeDashboard.flashEmptyHint')}
) : (
)}
)
}