feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

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>
This commit is contained in:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -1,6 +1,8 @@
'use client'
import { useState, useEffect } from 'react'
import { motion } from 'motion/react'
import { Zap, Lightbulb, Trophy } from 'lucide-react'
interface BridgeNote {
noteId: string
@@ -26,13 +28,13 @@ interface BridgeSuggestion {
interface BridgeNotesDashboardProps {
onNoteClick?: (noteId: string) => void
clusters: { id: number; name: string; color?: string }[]
}
export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps) {
export function BridgeNotesDashboard({ onNoteClick, clusters }: BridgeNotesDashboardProps) {
const [bridgeNotes, setBridgeNotes] = useState<BridgeNote[]>([])
const [suggestions, setSuggestions] = useState<BridgeSuggestion[]>([])
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'bridges' | 'suggestions'>('bridges')
useEffect(() => {
loadData()
@@ -41,14 +43,12 @@ export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps)
const loadData = async () => {
setLoading(true)
try {
// Load bridge notes
const bridgesRes = await fetch('/api/bridge-notes?details=true')
if (bridgesRes.ok) {
const bridgesData = await bridgesRes.json()
setBridgeNotes(bridgesData.bridgeNotes || [])
}
// Load suggestions
const suggestionsRes = await fetch('/api/bridge-notes/suggestions')
if (suggestionsRes.ok) {
const suggestionsData = await suggestionsRes.json()
@@ -63,9 +63,7 @@ export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps)
const generateNewSuggestions = async () => {
try {
const res = await fetch('/api/bridge-notes/suggestions', {
method: 'POST'
})
const res = await fetch('/api/bridge-notes/suggestions', { method: 'POST' })
if (res.ok) {
const data = await res.json()
setSuggestions(data.suggestions || [])
@@ -82,8 +80,6 @@ export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps)
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clusterAId, clusterBId })
})
// Remove from local state
setSuggestions(prev => prev.filter(
s => !(s.clusterAId === clusterAId && s.clusterBId === clusterBId)
))
@@ -94,12 +90,12 @@ export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps)
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
<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-20 bg-gray-200 rounded"></div>
<div className="h-20 bg-gray-200 rounded"></div>
<div className="h-24 bg-concrete/10 rounded-xl" />
<div className="h-24 bg-concrete/10 rounded-xl" />
</div>
</div>
</div>
@@ -107,164 +103,118 @@ export function BridgeNotesDashboard({ onNoteClick }: BridgeNotesDashboardProps)
}
return (
<div className="bg-white rounded-lg shadow">
{/* Tabs */}
<div className="border-b">
<nav className="flex -mb-px">
<button
onClick={() => setActiveTab('bridges')}
className={`px-6 py-3 font-medium text-sm ${
activeTab === 'bridges'
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Bridge Notes ({bridgeNotes.length})
</button>
<button
onClick={() => setActiveTab('suggestions')}
className={`px-6 py-3 font-medium text-sm ${
activeTab === 'suggestions'
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Connection Opportunities ({suggestions.length})
</button>
</nav>
<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>
<div className="p-6">
{activeTab === 'bridges' && (
<div>
{bridgeNotes.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<svg className="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p>No bridge notes found yet</p>
<p className="text-sm mt-1">Bridge notes connect different clusters of ideas</p>
{/* 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="space-y-3">
{bridgeNotes.map((bridge) => (
<div
key={bridge.noteId}
onClick={() => onNoteClick?.(bridge.noteId)}
className="border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
Bridge Score: {bridge.bridgeScore.toFixed(2)}
</span>
<span className="text-sm text-gray-500">
Connects {bridge.clustersConnected.length} {bridge.clustersConnected.length === 1 ? 'cluster' : 'clusters'}
</span>
</div>
<h4 className="font-medium text-gray-900">
{bridge.note?.title || 'Untitled'}
</h4>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
{bridge.note?.content?.replace(/<[^>]+>/g, '').slice(0, 150) || 'No content'}
</p>
{bridge.clusterNames && bridge.clusterNames.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{bridge.clusterNames.map((name, i) => (
<span
key={i}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-700"
>
{name}
</span>
))}
</div>
)}
</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>
))}
)
})}
</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>
{activeTab === 'suggestions' && (
<div>
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-600">
AI-suggested ideas to connect your isolated clusters
</p>
{/* 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="px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
className="mt-4 px-4 py-2 bg-indigo-500 text-white text-xs rounded-lg hover:bg-indigo-600 transition-colors"
>
Generate New
Generate Suggestions
</button>
</div>
{suggestions.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<svg className="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<p>No connection suggestions yet</p>
<p className="text-sm mt-1">All your clusters may already be connected!</p>
</div>
) : (
<div className="space-y-4">
{suggestions.map((suggestion, index) => (
<div
key={`${suggestion.clusterAId}-${suggestion.clusterBId}`}
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">💡</span>
<span className="font-semibold text-gray-900">
{suggestion.suggestedTitle}
</span>
<span className="text-xs text-gray-500">#{index + 1}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600 mb-2">
<span className="px-2 py-0.5 rounded bg-blue-100 text-blue-700">
{suggestion.clusterAName}
</span>
<span></span>
<span className="px-2 py-0.5 rounded bg-purple-100 text-purple-700">
{suggestion.clusterBName}
</span>
</div>
<p className="text-sm text-gray-700 mb-2">
{suggestion.suggestedContent}
</p>
<p className="text-xs text-gray-500 italic">
"{suggestion.justification}"
</p>
</div>
<button
onClick={() => dismissSuggestion(suggestion.clusterAId, suggestion.clusterBId)}
className="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Dismiss suggestion"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
</section>
</div>
)
}