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>
221 lines
8.9 KiB
TypeScript
221 lines
8.9 KiB
TypeScript
'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>
|
||
)
|
||
}
|