Files
Momento/memento-note/components/bridge-notes-dashboard.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 14:27:29 +00:00

221 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { motion } from 'motion/react'
import { Zap, Lightbulb, Trophy } from 'lucide-react'
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
}
interface BridgeNotesDashboardProps {
onNoteClick?: (noteId: string) => void
clusters: { id: number; name: string; color?: string }[]
}
export function BridgeNotesDashboard({ onNoteClick, clusters }: BridgeNotesDashboardProps) {
const [bridgeNotes, setBridgeNotes] = useState<BridgeNote[]>([])
const [suggestions, setSuggestions] = useState<BridgeSuggestion[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
try {
const bridgesRes = await fetch('/api/bridge-notes?details=true')
if (bridgesRes.ok) {
const bridgesData = await bridgesRes.json()
setBridgeNotes(bridgesData.bridgeNotes || [])
}
const suggestionsRes = await fetch('/api/bridge-notes/suggestions')
if (suggestionsRes.ok) {
const suggestionsData = await suggestionsRes.json()
setSuggestions(suggestionsData.suggestions || [])
}
} catch (error) {
console.error('Error loading bridge data:', error)
} finally {
setLoading(false)
}
}
const generateNewSuggestions = async () => {
try {
const res = await fetch('/api/bridge-notes/suggestions', { method: 'POST' })
if (res.ok) {
const data = await res.json()
setSuggestions(data.suggestions || [])
}
} catch (error) {
console.error('Error generating suggestions:', error)
}
}
const dismissSuggestion = async (clusterAId: number, clusterBId: number) => {
try {
await fetch('/api/bridge-notes', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clusterAId, clusterBId })
})
setSuggestions(prev => prev.filter(
s => !(s.clusterAId === clusterAId && s.clusterBId === clusterBId)
))
} catch (error) {
console.error('Error dismissing suggestion:', error)
}
}
if (loading) {
return (
<div className="bg-white dark:bg-white/5 rounded-2xl p-6 shadow-sm border border-border/40">
<div className="animate-pulse space-y-4">
<div className="h-6 bg-concrete/20 rounded w-1/3 mb-4" />
<div className="space-y-3">
<div className="h-24 bg-concrete/10 rounded-xl" />
<div className="h-24 bg-concrete/10 rounded-xl" />
</div>
</div>
</div>
)
}
return (
<div className="space-y-12">
{/* Stats Summary */}
<div className="grid grid-cols-2 gap-4">
<div className="p-5 rounded-2xl bg-white dark:bg-white/5 border border-border shadow-sm">
<div className="flex items-center gap-2 text-indigo-500 mb-2">
<Trophy size={14} />
<span className="text-[10px] font-bold uppercase tracking-widest">Bridges</span>
</div>
<div className="text-3xl font-memento-serif font-medium text-ink dark:text-dark-ink">{bridgeNotes.length}</div>
</div>
<div className="p-5 rounded-2xl bg-white dark:bg-white/5 border border-border shadow-sm">
<div className="flex items-center gap-2 text-ochre mb-2">
<Lightbulb size={14} />
<span className="text-[10px] font-bold uppercase tracking-widest">Suggestions</span>
</div>
<div className="text-3xl font-memento-serif font-medium text-ink dark:text-dark-ink">{suggestions.length}</div>
</div>
</div>
{/* Bridge Notes Section */}
<section>
<div className="flex items-center gap-2 mb-6 px-1">
<Zap size={16} className="text-ochre" />
<h3 className="text-sm font-bold uppercase tracking-widest text-ink dark:text-dark-ink">Powerful Bridge Notes</h3>
</div>
<div className="space-y-3">
{bridgeNotes.map((bridge) => (
<motion.div
key={bridge.noteId}
whileHover={{ x: 4 }}
onClick={() => onNoteClick?.(bridge.noteId)}
className="p-4 rounded-xl bg-white dark:bg-white/5 border border-border hover:border-ochre/40 transition-all cursor-pointer group"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-ink dark:text-dark-ink truncate flex-1">
{bridge.note?.title || 'Untitled'}
</h4>
<span className="text-[10px] font-bold text-ochre bg-ochre/10 px-2 py-0.5 rounded-full">
Score: {(bridge.bridgeScore * 100).toFixed(0)}%
</span>
</div>
<div className="flex items-center gap-2">
{bridge.clusterNames?.map((name, i) => {
const cluster = clusters.find(c => c.name === name)
return (
<div key={i} className="flex items-center gap-1">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: cluster?.color || '#cbd5e1' }} />
<span className="text-[9px] text-concrete font-medium whitespace-nowrap">{name}</span>
</div>
)
})}
</div>
</motion.div>
))}
{bridgeNotes.length === 0 && (
<div className="text-xs text-concrete italic p-4">No significant bridge notes found yet. Deepen your research to find new connections.</div>
)}
</div>
</section>
{/* Connection Suggestions */}
<section>
<div className="flex items-center gap-2 mb-6 px-1">
<Lightbulb size={16} className="text-indigo-500" />
<h3 className="text-sm font-bold uppercase tracking-widest text-ink dark:text-dark-ink">Missing Links (AI Generated)</h3>
</div>
<div className="space-y-4">
{suggestions.map((s, idx) => (
<div key={`${s.clusterAId}-${s.clusterBId}`} className="p-6 rounded-2xl bg-gradient-to-br from-indigo-500/5 to-transparent border border-indigo-500/10 hover:border-indigo-500/30 transition-all">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-4">
<div className="flex -space-x-2">
<div className="w-6 h-6 rounded-full border-2 border-paper bg-indigo-500 flex items-center justify-center text-[10px] text-white">A</div>
<div className="w-6 h-6 rounded-full border-2 border-paper bg-ochre flex items-center justify-center text-[10px] text-white">B</div>
</div>
<span className="text-[9px] font-bold uppercase tracking-widest text-indigo-500/60">
Bridging {s.clusterAName} & {s.clusterBName}
</span>
</div>
<h4 className="text-base font-memento-serif font-medium text-ink dark:text-dark-ink mb-2">{s.suggestedTitle}</h4>
<p className="text-xs text-muted-ink leading-relaxed mb-4">{s.suggestedContent}</p>
<div className="p-3 bg-white/40 dark:bg-white/5 rounded-xl border border-border/40 text-[10px] italic text-concrete flex gap-2">
<Zap size={12} className="shrink-0" />
<span>{s.justification}</span>
</div>
</div>
<button
onClick={() => dismissSuggestion(s.clusterAId, s.clusterBId)}
className="ml-4 p-2 text-concrete hover:text-ink dark:hover:text-dark-ink hover:bg-concrete/10 rounded-lg transition-colors"
title="Dismiss suggestion"
>
×
</button>
</div>
</div>
))}
{suggestions.length === 0 && !loading && (
<div className="text-center py-8 text-concrete">
<Lightbulb size={24} className="mx-auto mb-3 opacity-50" />
<p className="text-sm">No connection suggestions yet</p>
<p className="text-xs mt-1">All your clusters may already be connected!</p>
<button
onClick={generateNewSuggestions}
className="mt-4 px-4 py-2 bg-indigo-500 text-white text-xs rounded-lg hover:bg-indigo-600 transition-colors"
>
Generate Suggestions
</button>
</div>
)}
</div>
</section>
</div>
)
}