'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'] const EASE = [0.16, 1, 0.3, 1] as const function clusterPoint(index: number, count: number): { x: number; y: number } { if (count === 1) return { x: 50, y: 42 } const angle = -Math.PI / 2 + (index * 2 * Math.PI) / count const rx = count <= 3 ? 28 : 36 const ry = count <= 3 ? 24 : 30 return { x: 50 + Math.cos(angle) * rx, y: 44 + Math.sin(angle) * ry } } export interface DashboardMindOrbitProps { clusters: Cluster[] bridgeNotes: BridgeNote[] loading?: boolean onOpenInsights: () => void onOpenCluster?: (clusterId: number) => void onNoteSelect: (id: string) => void prefersReducedMotion?: boolean } export function DashboardMindOrbit({ clusters, bridgeNotes, loading, onOpenInsights, onOpenCluster, 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] const reduced = !!prefersReducedMotion const points = topClusters.map((_, idx) => clusterPoint(idx, topClusters.length)) if (loading) { return
} if (topClusters.length === 0) { return ( ) } return (
} title={t('homeDashboard.mindMap')} actions={( )} /> {points.map((point, idx) => { const color = CLUSTER_COLORS[topClusters[idx].clusterId % CLUSTER_COLORS.length] return ( ) })}
{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(52 * scale) const label = cluster.name?.trim() || t('homeDashboard.theme') const point = points[idx] return ( (onOpenCluster ? onOpenCluster(cluster.clusterId) : onOpenInsights())} className="absolute flex flex-col items-center gap-1 group z-[1] hover:z-10" aria-label={label} style={{ left: `${point.x}%`, top: `${point.y}%`, width: Math.max(size + 24, 128), }} initial={reduced ? { x: '-50%', y: '-50%' } : { x: '-50%', y: '-50%', scale: 0.82, opacity: 0.35 }} animate={{ x: '-50%', y: '-50%', scale: 1, opacity: 1 }} transition={{ duration: reduced ? 0 : 0.38, delay: reduced ? 0 : 0.18 + idx * 0.05, ease: EASE }} >
{cluster.noteIds.length}
{label} {label}
) })} {topBridge?.note && ( 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" initial={reduced ? false : { opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: reduced ? 0 : 0.32, delay: reduced ? 0 : 0.48, ease: EASE }} >

{topBridge.note.title || t('homeDashboard.untitled')}

{t('homeDashboard.bridgeNote')}

{Math.round(topBridge.bridgeScore * 100)}%
)}
) }