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>
140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
'use client'
|
|
|
|
import { motion } from 'motion/react'
|
|
import { Layers, Zap, ArrowUpRight } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
|
|
|
interface Cluster {
|
|
clusterId: number
|
|
name?: string
|
|
noteIds: string[]
|
|
}
|
|
|
|
interface BridgeNote {
|
|
noteId: string
|
|
bridgeScore: number
|
|
clusterNames?: string[]
|
|
note?: { id: string; title: string | null }
|
|
}
|
|
|
|
const CLUSTER_COLORS = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF']
|
|
|
|
export interface DashboardMindOrbitProps {
|
|
clusters: Cluster[]
|
|
bridgeNotes: BridgeNote[]
|
|
loading?: boolean
|
|
onOpenInsights: () => void
|
|
onNoteSelect: (id: string) => void
|
|
prefersReducedMotion?: boolean
|
|
}
|
|
|
|
export function DashboardMindOrbit({
|
|
clusters,
|
|
bridgeNotes,
|
|
loading,
|
|
onOpenInsights,
|
|
onNoteSelect,
|
|
prefersReducedMotion,
|
|
}: DashboardMindOrbitProps) {
|
|
const { t } = useLanguage()
|
|
const topClusters = [...clusters]
|
|
.sort((a, b) => b.noteIds.length - a.noteIds.length)
|
|
.slice(0, 5)
|
|
const maxCount = topClusters[0]?.noteIds.length || 1
|
|
const topBridge = bridgeNotes[0]
|
|
|
|
if (loading) {
|
|
return <div className="h-[180px] rounded-2xl bg-stone-50 dark:bg-zinc-950/30 animate-pulse" />
|
|
}
|
|
|
|
if (topClusters.length === 0) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onOpenInsights}
|
|
className="w-full h-[180px] rounded-2xl border border-dashed border-border/35 bg-white/50 dark:bg-zinc-900/50 flex flex-col items-center justify-center gap-2 p-6 text-center hover:border-brand-accent/30 transition-all"
|
|
>
|
|
<Layers size={22} className="text-concrete/35" />
|
|
<p className="text-xs text-concrete italic max-w-[220px]">{t('homeDashboard.mindMapEmpty')}</p>
|
|
<span className="text-[9px] font-mono uppercase font-bold text-brand-accent">{t('homeDashboard.mindMapOpen')}</span>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 p-4">
|
|
<DashboardWidgetTitleRow
|
|
widgetId="mind-map"
|
|
icon={<Layers size={12} className="text-brand-accent" />}
|
|
title={t('homeDashboard.mindMap')}
|
|
actions={(
|
|
<button
|
|
type="button"
|
|
onClick={onOpenInsights}
|
|
className="inline-flex items-center gap-0.5 text-[8px] font-mono uppercase font-bold text-brand-accent hover:underline"
|
|
>
|
|
{t('homeDashboard.fullMap')}
|
|
<ArrowUpRight size={10} />
|
|
</button>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-3 min-h-[100px] py-2">
|
|
{topClusters.map((cluster, idx) => {
|
|
const color = CLUSTER_COLORS[cluster.clusterId % CLUSTER_COLORS.length]
|
|
const scale = 0.65 + (cluster.noteIds.length / maxCount) * 0.55
|
|
const size = Math.round(56 * scale)
|
|
const label = cluster.name || `${t('homeDashboard.theme')} ${cluster.clusterId + 1}`
|
|
return (
|
|
<motion.button
|
|
key={cluster.clusterId}
|
|
type="button"
|
|
whileHover={prefersReducedMotion ? undefined : { scale: 1.06 }}
|
|
whileTap={prefersReducedMotion ? undefined : { scale: 0.97 }}
|
|
onClick={onOpenInsights}
|
|
className="relative flex flex-col items-center gap-1.5 group"
|
|
style={{ width: size + 16 }}
|
|
>
|
|
<div
|
|
className="rounded-full border-2 flex items-center justify-center font-mono font-bold text-white shadow-sm group-hover:shadow-md transition-shadow"
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
backgroundColor: `${color}cc`,
|
|
borderColor: `${color}40`,
|
|
fontSize: Math.max(9, size * 0.22),
|
|
}}
|
|
>
|
|
{cluster.noteIds.length}
|
|
</div>
|
|
<span className="text-[8px] font-medium text-ink/80 dark:text-dark-ink/80 text-center line-clamp-2 leading-tight max-w-[72px] group-hover:text-brand-accent transition-colors">
|
|
{label}
|
|
</span>
|
|
</motion.button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{topBridge?.note && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onNoteSelect(topBridge.noteId)}
|
|
className="w-full mt-2 p-2.5 rounded-xl border border-brand-accent/20 bg-brand-accent/[0.04] hover:bg-brand-accent/[0.08] transition-all text-start flex items-center gap-2 group"
|
|
>
|
|
<Zap size={11} className="text-brand-accent shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate group-hover:text-brand-accent transition-colors">
|
|
{topBridge.note.title || t('homeDashboard.untitled')}
|
|
</p>
|
|
<p className="text-[8px] font-mono uppercase text-concrete">{t('homeDashboard.bridgeNote')}</p>
|
|
</div>
|
|
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded-full shrink-0">
|
|
{Math.round(topBridge.bridgeScore * 100)}%
|
|
</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|