'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'motion/react' import { Sparkles, Zap, Lightbulb, Bot, ChevronLeft, ChevronRight, ExternalLink, GitCompare, X, RefreshCw, Loader2, Link2, Brain, ArrowRight, } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row' import { MEMORY_ECHO_LEGACY_EN_FALLBACKS } from '@/lib/ai/memory-echo-i18n' // ─── Types ───────────────────────────────────────────── export interface IntelBriefingInsight { id: string insight: string score: number date: string viewed: boolean note1: { id: string; title: string | null } note2: { id: string; title: string | null } note1Excerpt?: string note2Excerpt?: string } export interface IntelBridgeNote { noteId: string bridgeScore: number clusterNames?: string[] note?: { id: string; title: string | null; content?: string } } export interface IntelBridgeSuggestion { clusterAId: number clusterBId: number clusterAName: string clusterBName: string suggestedTitle: string suggestedContent: string justification: string } export interface IntelAgentAction { id: string agentName: string result: string | null createdAt: string } type IntelFilter = 'all' | 'connections' | 'bridges' | 'ideas' | 'agents' type IntelItem = | { kind: 'insight'; id: string; priority: number; insight: IntelBriefingInsight } | { kind: 'bridge'; id: string; priority: number; bridge: IntelBridgeNote } | { kind: 'suggestion'; id: string; priority: number; suggestion: IntelBridgeSuggestion } | { kind: 'agent'; id: string; priority: number; agent: IntelAgentAction } const CLUSTER_COLORS = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF'] const FILTER_KINDS: Record = { all: null, connections: ['insight'], bridges: ['bridge'], ideas: ['suggestion'], agents: ['agent'], } function stripHtml(html: string): string { return html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim() } function formatRelativeTime( dateStr: string, t: (key: string, params?: Record) => string, ): string { const diff = Date.now() - new Date(dateStr).getTime() const mins = Math.floor(diff / 60000) const hours = Math.floor(diff / 3600000) const days = Math.floor(diff / 86400000) if (mins < 1) return t('time.justNow') if (mins < 60) return t('time.minutesAgo', { count: mins }) if (hours < 24) return t('time.hoursAgo', { count: hours }) return t('time.daysAgo', { count: days }) } // ─── Visual: lien entre deux notes ───────────────────── function ConnectionDiagram({ note1Title, note2Title, score, color = '#6366F1', }: { note1Title: string note2Title: string score: number color?: string }) { return (

{note1Title}

{Math.round(score * 100)}%

{note2Title}

) } // ─── Props ───────────────────────────────────────────── export interface IntelligenceHubProps { loading: boolean aiActive: boolean hasAiConsent: boolean providerReady: boolean memoryEchoEnabled: boolean insights: IntelBriefingInsight[] bridgeNotes: IntelBridgeNote[] bridgeSuggestions: IntelBridgeSuggestion[] agentActions: IntelAgentAction[] onNoteSelect: (noteId: string) => void onRefreshEcho: () => void onEnableAi: () => void echoRefreshing: boolean onDismissInsight: (insight: IntelBriefingInsight) => void onDismissBridgeSuggestion: (s: IntelBridgeSuggestion) => void onCreateBridgeSuggestion: (s: IntelBridgeSuggestion) => void onOpenInsightNote: (insight: IntelBriefingInsight, noteId: string) => void dismissingInsightId: string | null actingBridgeSuggestionKey: string | null prefersReducedMotion: boolean } // ─── Component ───────────────────────────────────────── export function IntelligenceHub({ loading, aiActive, hasAiConsent, providerReady, memoryEchoEnabled, insights, bridgeNotes, bridgeSuggestions, agentActions, onNoteSelect, onRefreshEcho, onEnableAi, echoRefreshing, onDismissInsight, onDismissBridgeSuggestion, onCreateBridgeSuggestion, onOpenInsightNote, dismissingInsightId, actingBridgeSuggestionKey, prefersReducedMotion, }: IntelligenceHubProps) { const router = useRouter() const { t } = useLanguage() const [filter, setFilter] = useState('all') const [activeIndex, setActiveIndex] = useState(0) const localizeInsightText = useCallback((insight: IntelBriefingInsight) => { const text = insight.insight.trim() if (MEMORY_ECHO_LEGACY_EN_FALLBACKS.has(text)) { if (insight.note1Excerpt && insight.note2Excerpt) { return t('homeDashboard.connectionBetween', { note1: insight.note1.title || insight.note1Excerpt.slice(0, 50), note2: insight.note2.title || insight.note2Excerpt.slice(0, 50), }) } return t('memoryEcho.defaultInsight') } return text }, [t]) const allItems = useMemo(() => { const insightNoteIds = new Set() for (const i of insights) { insightNoteIds.add(i.note1.id) insightNoteIds.add(i.note2.id) } const items: IntelItem[] = [] for (const insight of insights) { items.push({ kind: 'insight', id: `insight-${insight.id}`, priority: (insight.viewed ? 40 : 100) + insight.score * 10, insight, }) } for (const bridge of bridgeNotes) { if (insightNoteIds.has(bridge.noteId)) continue items.push({ kind: 'bridge', id: `bridge-${bridge.noteId}`, priority: 60 + bridge.bridgeScore * 10, bridge, }) } for (const suggestion of bridgeSuggestions) { items.push({ kind: 'suggestion', id: `suggestion-${suggestion.clusterAId}-${suggestion.clusterBId}`, priority: 80, suggestion, }) } for (const agent of agentActions) { items.push({ kind: 'agent', id: `agent-${agent.id}`, priority: 30, agent, }) } return items.sort((a, b) => b.priority - a.priority).slice(0, 10) }, [insights, bridgeNotes, bridgeSuggestions, agentActions]) const counts = useMemo(() => ({ all: allItems.length, connections: allItems.filter(i => i.kind === 'insight').length, bridges: allItems.filter(i => i.kind === 'bridge').length, ideas: allItems.filter(i => i.kind === 'suggestion').length, agents: allItems.filter(i => i.kind === 'agent').length, }), [allItems]) const filteredItems = useMemo(() => { const kinds = FILTER_KINDS[filter] if (!kinds) return allItems return allItems.filter(i => kinds.includes(i.kind)) }, [allItems, filter]) useEffect(() => { setActiveIndex(0) }, [filter]) useEffect(() => { if (activeIndex >= filteredItems.length && filteredItems.length > 0) { setActiveIndex(filteredItems.length - 1) } }, [activeIndex, filteredItems.length]) const activeItem = filteredItems[activeIndex] ?? null const newCount = insights.filter(i => !i.viewed).length const goPrev = () => setActiveIndex(i => Math.max(0, i - 1)) const goNext = () => setActiveIndex(i => Math.min(filteredItems.length - 1, i + 1)) const filters: { key: IntelFilter; label: string }[] = [ { key: 'all', label: t('homeDashboard.intelFilterAll') }, { key: 'connections', label: t('homeDashboard.intelFilterConnections') }, { key: 'bridges', label: t('homeDashboard.intelFilterBridges') }, { key: 'ideas', label: t('homeDashboard.intelFilterIdeas') }, { key: 'agents', label: t('homeDashboard.intelFilterAgents') }, ] const slideVariants = prefersReducedMotion ? { initial: {}, animate: {}, exit: {} } : { initial: { opacity: 0, x: 24, scale: 0.98 }, animate: { opacity: 1, x: 0, scale: 1 }, exit: { opacity: 0, x: -24, scale: 0.98 }, } const renderSpotlight = (item: IntelItem) => { switch (item.kind) { case 'insight': { const { insight } = item const text = localizeInsightText(insight) const excerpt = insight.note1Excerpt || insight.note2Excerpt return (
{t('homeDashboard.semanticConnection')} {!insight.viewed && ( )}

« {text} »

{excerpt && (

{excerpt}

)}
) } case 'bridge': { const { bridge } = item const title = bridge.note?.title || t('homeDashboard.untitled') const excerpt = bridge.note?.content ? stripHtml(bridge.note.content).slice(0, 160) : '' const names = bridge.clusterNames ?? [] return (
{t('homeDashboard.bridgeNote')}
{Math.round(bridge.bridgeScore * 100)}%
) } case 'suggestion': { const { suggestion } = item const key = `${suggestion.clusterAId}-${suggestion.clusterBId}` const busy = actingBridgeSuggestionKey === key return (
{t('homeDashboard.intelMissingLink')}
{suggestion.clusterAName}
{suggestion.clusterBName}

{suggestion.suggestedTitle}

{suggestion.suggestedContent}

) } case 'agent': { const { agent } = item return (
{agent.agentName} {formatRelativeTime(agent.createdAt, t)}
{agent.result ? (

{agent.result}

) : (

{t('homeDashboard.intelAgentNoResult')}

)}
) } } } const spotlightAccent = (item: IntelItem | null) => { if (!item) return 'from-stone-100/50 to-transparent border-border/30' switch (item.kind) { case 'insight': return 'from-indigo-500/[0.06] via-transparent to-transparent border-indigo-400/25' case 'bridge': return 'from-ochre/[0.06] via-transparent to-transparent border-ochre/25' case 'suggestion': return 'from-violet-500/[0.06] via-transparent to-transparent border-violet-400/25' case 'agent': return 'from-ochre/[0.04] via-transparent to-transparent border-border/30' } } return (
{/* Header */}
)} title={t('homeDashboard.aiFound')} actions={( <> {newCount > 0 && ( {newCount} {t('homeDashboard.new')} )} {aiActive && ( )} )} /> {loading ? (
) : !aiActive ? (

{!hasAiConsent ? t('homeDashboard.aiConsentRequired') : !providerReady ? t('homeDashboard.aiProviderUnavailable') : t('homeDashboard.memoryEchoDisabled')}

{!hasAiConsent && ( )}
) : allItems.length === 0 ? (

{t('homeDashboard.noConnections')}

) : ( <> {/* Filtres */}
{filters.map(f => { const count = counts[f.key] if (f.key !== 'all' && count === 0) return null const active = filter === f.key return ( ) })}
{filteredItems.length === 0 ? (

{t('homeDashboard.intelFilterEmpty')}

) : ( <> {/* Spotlight carousel */}
{filteredItems.length > 1 && ( <> )}
{activeItem && ( {renderSpotlight(activeItem)} )}
{/* Navigation dots + position */} {filteredItems.length > 1 && (
{filteredItems.map((item, idx) => (
{t('homeDashboard.intelPosition', { current: activeIndex + 1, total: filteredItems.length })}
)} {/* File d'attente compacte */} {filteredItems.length > 1 && (
{filteredItems.map((item, idx) => { const isActive = idx === activeIndex let label = '' let Icon = Sparkles let accent = 'text-indigo-500' if (item.kind === 'insight') { label = item.insight.note1.title?.slice(0, 28) || t('homeDashboard.untitled') Icon = Sparkles accent = 'text-indigo-500' } else if (item.kind === 'bridge') { label = item.bridge.note?.title?.slice(0, 28) || t('homeDashboard.untitled') Icon = Zap accent = 'text-ochre' } else if (item.kind === 'suggestion') { label = item.suggestion.suggestedTitle.slice(0, 28) Icon = Lightbulb accent = 'text-violet-500' } else { label = item.agent.agentName Icon = Bot accent = 'text-ochre' } return ( ) })}
)} )} )} ) }