import React, { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Network, Lightbulb, Layers, Sparkles, ArrowRight, RefreshCw, Trophy, Zap, Tag, Link as LinkIcon, Menu, FileText, AlertCircle, Clock, ChevronRight, TrendingUp, Sliders, CheckCircle2, Lock } from 'lucide-react'; import { Note, NoteCluster, BridgeNote, ConnectionSuggestion } from '../types'; import { runClustering, detectBridges, calculateCentroid, getMostCentralNoteTitles } from '../services/clusteringService'; import { nameCluster, suggestBridgeIdeas } from '../services/geminiService'; import { NetworkGraph } from './NetworkGraph'; interface InsightsViewProps { notes: Note[]; onUpdateNotes: (updatedNotes: Note[]) => void; onNoteSelect: (noteId: string) => void; onOpenSidebar?: () => void; } export const InsightsView: React.FC = ({ notes, onUpdateNotes, onNoteSelect, onOpenSidebar }) => { const [isCalculating, setIsCalculating] = useState(false); const [clusters, setClusters] = useState([]); const [bridgeNotes, setBridgeNotes] = useState([]); const [suggestions, setSuggestions] = useState([]); const [selectedClusterId, setSelectedClusterId] = useState(null); // Mobile responsive view selector const [viewMode, setViewMode] = useState<'graph' | 'dashboard'>('dashboard'); // Interactive automatic recalculation parameters simulator / status const [lastSyncTime, setLastSyncTime] = useState(() => { return new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); }); // Track changes to notes since last calculation to show a conditions indicator const [notesModifiedCount, setNotesModifiedCount] = useState(0); // Monitor edits to emulate the state "Recalcul quotidien planifié" or condition (>10 notes modified) useEffect(() => { // Whenever notes length or contents change, we simulate a tally setNotesModifiedCount(prev => Math.min(prev + 1, 12)); }, [notes.length]); const performAnalysis = async () => { setIsCalculating(true); try { // 1. Run clustering (DBSCAN acting on density with outlier filtering, label -1 is outlier) const { clusters: newClusters } = runClustering(notes); // 2. Name clusters (find the 5 notes closest to each cluster's centroid vector) const namedClusters = await Promise.all(newClusters.map(async (c) => { const centroid = calculateCentroid(c.noteIds, notes); // Find the 5 most central notes (closest to the cluster centroid by cosine similarity) const clusterNoteSummaries = getMostCentralNoteTitles(c.noteIds, centroid, notes, 5); const name = await nameCluster(clusterNoteSummaries); return { ...c, name, centroid }; })); // 3. Update notes with cluster IDs const updatedNotes = notes.map(n => { const cluster = namedClusters.find(c => c.noteIds.includes(n.id)); return { ...n, clusterId: cluster?.id }; }); onUpdateNotes(updatedNotes); // 4. Detect bridges (notes exhibiting similarity > 0.5 to >= 2 clusters) const bridges = detectBridges(updatedNotes, namedClusters); // 5. Build suggestions for unconnected cluster pairs // A pair is unconnected if there are no existing bridge notes linking them const newSuggestions: ConnectionSuggestion[] = []; if (namedClusters.length >= 2) { const unconnectedPairs: { cA: NoteCluster; cB: NoteCluster }[] = []; for (let i = 0; i < namedClusters.length; i++) { for (let j = i + 1; j < namedClusters.length; j++) { const cA = namedClusters[i]; const cB = namedClusters[j]; // Check if any bridge note connects these two clusters const hasBridge = bridges.some(b => b.connectedClusterIds.includes(cA.id) && b.connectedClusterIds.includes(cB.id) ); if (!hasBridge) { unconnectedPairs.push({ cA, cB }); } } } // Generate bridge suggestions for the top 3 unconnected pairs for (let k = 0; k < Math.min(unconnectedPairs.length, 3); k++) { const { cA, cB } = unconnectedPairs[k]; const cA_notes = updatedNotes.filter(n => cA.noteIds.includes(n.id)).map(n => n.title).slice(0, 3).join(', '); const cB_notes = updatedNotes.filter(n => cB.noteIds.includes(n.id)).map(n => n.title).slice(0, 3).join(', '); const bridgeIdeas = await suggestBridgeIdeas(cA.name, cB.name, cA_notes, cB_notes); bridgeIdeas.forEach((idea, idx) => { newSuggestions.push({ id: `suggestion-${cA.id}-${cB.id}-${idx}`, ...idea, clusterAId: cA.id, clusterBId: cB.id }); }); } } setClusters(namedClusters); setBridgeNotes(bridges); setSuggestions(newSuggestions); setLastSyncTime(new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })); setNotesModifiedCount(0); // Reset modified counter upon successful clustering recalculation } catch (error) { console.error("Analysis failed:", error); } finally { setIsCalculating(false); } }; useEffect(() => { if (notes.some(n => n.embedding) && clusters.length === 0) { performAnalysis(); } }, [notes]); const bridgeList = useMemo(() => { return bridgeNotes.map(b => { const note = notes.find(n => n.id === b.noteId); return { ...b, title: note?.title || 'Note de passage' }; }); }, [bridgeNotes, notes]); // Compute isolated clusters (ones with no bridge notes spanning to them) const isolatedClusters = useMemo(() => { const networkedClusterIds = new Set(bridgeNotes.flatMap(b => b.connectedClusterIds)); return clusters.filter(c => !networkedClusterIds.has(c.id)); }, [clusters, bridgeNotes]); // Find currently selected cluster info for the zoom drilldown list const selectedCluster = useMemo(() => { return clusters.find(c => c.id === selectedClusterId); }, [clusters, selectedClusterId]); const selectedClusterNotes = useMemo(() => { if (!selectedCluster) return []; return notes.filter(n => selectedCluster.noteIds.includes(n.id)); }, [notes, selectedCluster]); return (
{/* Header with Mobile Drawer Trigger & Responsiveness Tab controls */}
{onOpenSidebar && ( )}

Analyses & Cartographie

Modèles sémantiques & clusters de connaissances

{/* Mobile Tab Switcher */}
{/* Left: Interactive Canvas Network Graph View */}
{/* Right: Insight Dashboard Column */}
{/* Active Cluster Inspection Drawer / Side Card */} {selectedCluster && (
Focus Cluster Activé

{selectedCluster.name}

Cet ensemble thématique réunit {selectedClusterNotes.length} notes complémentaires. Cliquez sur une note pour y accéder directement :

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

Détectés sans à priori

Notes-Ponts
{bridgeNotes.length}

Passerelles d'idées

{/* NEW SECTION: Auto Recalculator Control Dashboard Section */}

Système de Recalcul

Synchronisé
CRON PLANIFIÉ

Quotidien (04:00)

DERNIÈRE SYNCHRONISATION

Aujourd'hui, {lastSyncTime}

{/* Recalcul Trigger Metrics */}
Notes éditées depuis recul : {notesModifiedCount} / 10 modifs
Le recalcul incrémental se déclenche automatiquement si modification de {'>'} 10 notes ou variation d'embeddings {'>'} 5%.
{/* Isolated Clusters List */}

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 flex items-center justify-between cursor-pointer" >
{c.name}
Non connecté ))} {isolatedClusters.length === 0 && (
Tous les clusters thématiques sont liés par au moins un point de passage sémantique !
)}
{/* Bridge Notes Section */}

Notes-Ponts Influentes

{bridgeList.map(bridge => ( onNoteSelect(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.connectedClusterIds.map(cid => { const c = clusters.find(cl => cl.id === cid); return (
{ e.stopPropagation(); setSelectedClusterId(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" >
{c?.name}
); })}
))} {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.
)}
{/* Connection Suggestions */}

Opportunités de Connexion (Ponts Suggérés)

{suggestions.map((s) => (
A
B
Relier {clusters.find(c => c.id === s.clusterAId)?.name} & {clusters.find(c => c.id === s.clusterBId)?.name}

{s.title}

{s.description}

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