'use client' import { useState, useEffect, useMemo } from 'react' import { NetworkGraph } from '@/components/network-graph' import { useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'motion/react' import { Sparkles, RefreshCw, Layers, Trophy, Zap, Lightbulb, Sliders, CheckCircle2, Clock, AlertCircle, ChevronRight, Database, } from 'lucide-react' interface Note { id: string title: string | null content: string clusterId?: number } interface Cluster { id: string clusterId: number name?: string noteIds: string[] color?: string } interface BridgeNote { noteId: string bridgeScore: number clustersConnected: number[] clusterNames?: string[] note?: { id: string title: string | null content: string } } interface BridgeSuggestion { clusterAId: number clusterBId: number clusterAName: string clusterBName: string suggestedTitle: string suggestedContent: string justification: string } const COLOR_PALETTE = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF'] export default function InsightsPage() { const router = useRouter() const [notes, setNotes] = useState([]) const [clusters, setClusters] = useState([]) const [bridgeNotes, setBridgeNotes] = useState([]) const [suggestions, setSuggestions] = useState([]) const [loading, setLoading] = useState(true) const [isCalculating, setIsCalculating] = useState(false) const [isReindexing, setIsReindexing] = useState(false) const [embeddingStats, setEmbeddingStats] = useState<{ indexed: number; total: number } | null>(null) const [isStale, setIsStale] = useState(false) const [selectedClusterId, setSelectedClusterId] = useState(null) const [viewMode, setViewMode] = useState<'graph' | 'dashboard'>('dashboard') const [lastSyncTime, setLastSyncTime] = useState('') useEffect(() => { loadInitialData() }, []) // ─── Données calculées ─────────────────────────────────────────────────────── const selectedCluster = useMemo( () => clusters.find(c => c.id === selectedClusterId) ?? null, [clusters, selectedClusterId] ) const selectedClusterNotes = useMemo( () => (selectedCluster ? notes.filter(n => selectedCluster.noteIds.includes(n.id)) : []), [notes, selectedCluster] ) const isolatedClusters = useMemo(() => { const networkedIds = new Set( bridgeNotes.flatMap(b => b.clustersConnected.map(cid => String(cid))) ) return clusters.filter(c => !networkedIds.has(c.id)) }, [clusters, bridgeNotes]) const bridgeList = useMemo( () => bridgeNotes.map(b => ({ ...b, title: b.note?.title || 'Note sans titre' })), [bridgeNotes] ) // ─── Chargement initial ────────────────────────────────────────────────────── const loadInitialData = async () => { setLoading(true) try { const res = await fetch('/api/clusters') if (res.ok) { const data = await res.json() if (data.clusters?.length > 0) { const clustersWithColors = data.clusters.map((c: Cluster, i: number) => ({ ...c, id: c.clusterId.toString(), color: COLOR_PALETTE[i % COLOR_PALETTE.length] })) setNotes(data.notes || []) setClusters(clustersWithColors) setIsStale(!!data.stale) // Bridge notes incluses dans la réponse GET /clusters (enrichies) if (data.bridgeNotes?.length > 0) { setBridgeNotes(data.bridgeNotes) } else { const bridgeRes = await fetch('/api/bridge-notes?details=true') if (bridgeRes.ok) { const bridgeData = await bridgeRes.json() setBridgeNotes(bridgeData.bridgeNotes || []) } } const suggestionsRes = await fetch('/api/bridge-notes/suggestions') if (suggestionsRes.ok) { const suggestionsData = await suggestionsRes.json() setSuggestions(suggestionsData.suggestions || []) } setLastSyncTime( new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) ) if (typeof data.embeddingCount === 'number' && typeof data.totalNotes === 'number') { setEmbeddingStats({ indexed: data.embeddingCount, total: data.totalNotes }) } } else { setIsStale(false) if (typeof data.embeddingCount === 'number' && typeof data.totalNotes === 'number') { setEmbeddingStats({ indexed: data.embeddingCount, total: data.totalNotes }) } } } } catch (error) { console.error('Error loading data:', error) } finally { setLoading(false) } } const handleReindexEmbeddings = async () => { setIsReindexing(true) try { const res = await fetch('/api/notes/reindex', { method: 'POST' }) if (!res.ok) throw new Error('reindex failed') const data = await res.json() setEmbeddingStats(prev => ({ indexed: data.count ?? prev?.indexed ?? 0, total: data.total ?? prev?.total ?? notes.length, })) setLastSyncTime( new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) ) setIsStale(true) } catch (error) { console.error('Error reindexing embeddings:', error) } finally { setIsReindexing(false) } } // ─── Analyse (POST) ────────────────────────────────────────────────────────── const performAnalysis = async () => { setIsCalculating(true) try { const res = await fetch('/api/clusters', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: true }) }) if (res.ok) { const data = await res.json() const clustersWithColors = (data.clusters || []).map((c: Cluster, i: number) => ({ ...c, id: c.clusterId.toString(), color: COLOR_PALETTE[i % COLOR_PALETTE.length] })) setNotes(data.notes || []) setClusters(clustersWithColors) setBridgeNotes(data.bridgeNotes || []) setIsStale(false) const suggestionsRes = await fetch('/api/bridge-notes/suggestions') if (suggestionsRes.ok) { const suggestionsData = await suggestionsRes.json() setSuggestions(suggestionsData.suggestions || []) } setLastSyncTime( new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) ) if (data.notes?.length) { setEmbeddingStats(prev => ({ indexed: prev?.indexed ?? data.notes.length, total: data.notes.length, })) } } } catch (error) { console.error('Error running analysis:', error) } finally { setIsCalculating(false) } } const handleNoteClick = (noteId: string) => { router.push(`/home?openNote=${noteId}`) } // ─── Rendu ─────────────────────────────────────────────────────────────────── return (
{/* ── Header ── */}

Analyses & Cartographie

Modèles sémantiques & clusters de connaissances

{/* Tab switcher mobile */}
{/* ── Chargement ── */} {loading && (

Chargement de vos clusters...

)} {/* ── État vide ── */} {!loading && clusters.length === 0 && !isCalculating && (

Vos notes forment des thèmes

Cliquez sur “Re-analyser” pour découvrir les groupes sémantiques de vos notes et les connexions cachées entre vos idées.

)} {/* ── Calcul en cours ── */} {isCalculating && !loading && (

Analyse en cours...

Calcul des similarités sémantiques et détection des clusters de connaissances

)} {/* ── Contenu principal ── */} {!loading && clusters.length > 0 && !isCalculating && (
{/* ── Graphe (gauche) ── */}
{/* ── Dashboard (droite) ── */}
{/* Avertissement d'obsolescence (stale banner) */} {isStale && !isCalculating && (
Vos notes ont été modifiées. Mettez à jour vos insights pour une cartographie sémantique précise.
)} {/* ① Panneau d'inspection cluster */} {selectedCluster && (
Focus Cluster Activé

{selectedCluster.name || `Cluster ${selectedCluster.clusterId}`}

Cet ensemble thématique réunit{' '} {selectedClusterNotes.length} notes . Cliquez pour accéder directement :

{selectedClusterNotes.map(note => ( ))}
)} {/* ② Stats */}
Clusters Actifs
{clusters.length}

Détectés sans à priori

Notes-Ponts
{bridgeNotes.length}

Passerelles d'idées

{/* ③ Système de Recalcul */}

Système de Recalcul

Synchronisé
CRON PLANIFIÉ

Quotidien (04:00)

DERNIÈRE SYNC

{lastSyncTime || '—'}

Notes indexées (texte complet) : {embeddingStats ? `${embeddingStats.indexed} / ${embeddingStats.total}` : '—'}

Chaque note est convertie en texte brut intégral puis découpée en chunks si nécessaire (ex. 17 679 caractères → plusieurs vecteurs fusionnés). Aucune limite artificielle à 200 ou 800 caractères pour la similarité.

« Re-analyser » réindexe aussi les embeddings puis regénère les clusters.
{/* ④ Clusters Isolés */}

Clusters Isolés ({isolatedClusters.length})

Sans points d'accroche
{isolatedClusters.map(c => ( setSelectedClusterId(c.id)} className="p-3.5 rounded-xl bg-white dark:bg-zinc-800 border border-border/30 hover:border-black/10 dark:hover:border-white/10 flex items-center justify-between cursor-pointer transition-all" >
{c.name || `Cluster ${c.clusterId}`}
Non connecté ))} {isolatedClusters.length === 0 && (
Tous les clusters thématiques sont liés par au moins un point de passage sémantique !
)}
{/* ⑤ Notes-Ponts Influentes */}

Notes-Ponts Influentes

{bridgeList.map(bridge => ( handleNoteClick(bridge.noteId)} className="p-4 rounded-xl bg-white dark:bg-zinc-800 border border-border/30 hover:border-ochre/40 hover:shadow-sm transition-all cursor-pointer group" >

{bridge.title}

Lien : {(bridge.bridgeScore * 100).toFixed(0)}%
{bridge.clustersConnected.map(cid => { const cluster = clusters.find(c => c.id === String(cid)) return (
{ e.stopPropagation() setSelectedClusterId(String(cid)) }} className="flex items-center gap-1.5 px-2 py-0.5 bg-black/[0.02] dark:bg-white/[0.02] border border-border/30 rounded-md hover:border-concrete/40 transition-colors cursor-pointer" >
{cluster?.name || `Cluster ${cid}`}
) })}
))} {bridgeList.length === 0 && !isCalculating && (
Aucune note-pont significative n'a été détectée. Créez des notes transversales pour forger de nouveaux liens créatifs.
)}
{/* ⑥ Opportunités de Connexion */}

Opportunités de Connexion

{suggestions.map(s => (
A
B
Relier {s.clusterAName} & {s.clusterBName}

{s.suggestedTitle}

{s.suggestedContent}

{s.justification}
))} {isCalculating && (
{[1, 2].map(i => (
))}
)} {!isCalculating && suggestions.length === 0 && (
Toutes vos thématiques clés sont déjà formidablement interconnectées !
)}
)}
) }