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,9 +1,8 @@
'use client'
import { useState } from 'react'
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Sparkles, ThumbsUp, ThumbsDown, GitMerge, X } from 'lucide-react'
import { Columns2, GitMerge, X, ExternalLink } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Note } from '@/lib/types'
import { useLanguage } from '@/lib/i18n/LanguageProvider'
@@ -13,54 +12,40 @@ interface ComparisonModalProps {
onClose: () => void
notes: Array<Partial<Note>>
similarity?: number
onOpenNote?: (noteId: string) => void
onMergeNotes?: (noteIds: string[]) => void
}
function stripHtml(html: string): string {
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
}
function openNoteInNewTab(noteId: string) {
window.open(`/home?openNote=${encodeURIComponent(noteId)}`, '_blank', 'noopener,noreferrer')
}
export function ComparisonModal({
isOpen,
onClose,
notes,
similarity,
onOpenNote,
onMergeNotes
onMergeNotes,
}: ComparisonModalProps) {
const { t } = useLanguage()
const [feedback, setFeedback] = useState<'thumbs_up' | 'thumbs_down' | null>(null)
const handleFeedback = async (type: 'thumbs_up' | 'thumbs_down') => {
setFeedback(type)
try {
const noteIds = notes.map(n => n.id).filter(Boolean) as string[]
if (noteIds.length >= 2) {
await fetch('/api/ai/echo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'feedback', noteIds, feedback: type })
})
}
} catch {
// silent — feedback is best-effort
}
setTimeout(() => {
onClose()
}, 500)
}
const getNoteColor = (index: number) => {
const colors = [
'border-primary/20 dark:border-primary/30 hover:border-primary/30 dark:hover:border-primary/40',
'border-purple-200 dark:border-purple-800 hover:border-purple-300 dark:hover:border-purple-700',
'border-green-200 dark:border-green-800 hover:border-green-300 dark:hover:border-green-700'
'border-indigo-200/80 dark:border-indigo-800/80',
'border-purple-200/80 dark:border-purple-800/80',
'border-emerald-200/80 dark:border-emerald-800/80',
]
return colors[index % colors.length]
}
const getTitleColor = (index: number) => {
const colors = [
'text-primary dark:text-primary-foreground',
'text-purple-600 dark:text-purple-400',
'text-green-600 dark:text-green-400'
'text-indigo-700 dark:text-indigo-300',
'text-purple-700 dark:text-purple-300',
'text-emerald-700 dark:text-emerald-300',
]
return colors[index % colors.length]
}
@@ -72,133 +57,107 @@ export function ComparisonModal({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
showCloseButton={false}
className={cn(
"max-h-[90vh] overflow-hidden flex flex-col p-0",
maxModalWidth
)}
className={cn('max-h-[90vh] overflow-hidden flex flex-col p-0', maxModalWidth)}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b dark:border-zinc-700">
<div className="flex items-center justify-between p-6 border-b border-border/60">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-full">
<Sparkles className="h-5 w-5 text-amber-600 dark:text-amber-400" />
<div className="p-2 rounded-lg bg-indigo-500/10">
<Columns2 className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<DialogTitle className="text-xl font-semibold">
{t('memoryEcho.comparison.title')}
</DialogTitle>
<DialogDescription className="text-sm text-gray-600 dark:text-gray-400">
{t('memoryEcho.comparison.similarityInfo', { similarity: similarityPercentage })}
<DialogDescription className="text-sm text-muted-foreground">
{similarityPercentage > 0
? t('memoryEcho.comparison.similarityInfo', { similarity: similarityPercentage })
: t('memoryEcho.comparison.subtitle')}
</DialogDescription>
</div>
</div>
<button
type="button"
onClick={onClose}
className="p-1 rounded-md text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
aria-label={t('memoryEcho.editorSection.close')}
>
<X className="h-5 w-5" />
</button>
</div>
{/* AI Insight Section */}
<div className="px-6 py-3 border-b border-border/60 bg-muted/20">
<p className="text-sm text-muted-foreground">
{t('memoryEcho.comparison.stayOnCurrentNote')}
</p>
</div>
{similarityPercentage >= 80 && (
<div className="px-6 py-4 bg-amber-50 dark:bg-amber-950/20 border-b dark:border-zinc-700">
<div className="flex items-start gap-2">
<Sparkles className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-gray-700 dark:text-gray-300">
{t('memoryEcho.comparison.highSimilarityInsight')}
</p>
</div>
<div className="px-6 py-3 border-b border-border/60">
<p className="text-sm text-muted-foreground">
{t('memoryEcho.comparison.highSimilarityInsight')}
</p>
</div>
)}
{/* Notes Grid */}
<div className={cn(
"flex-1 overflow-y-auto p-6",
notes.length === 2 ? "grid grid-cols-2 gap-6" : "grid grid-cols-3 gap-4"
)}>
<div
className={cn(
'flex-1 overflow-y-auto p-6',
notes.length === 2 ? 'grid grid-cols-1 md:grid-cols-2 gap-6' : 'grid grid-cols-1 md:grid-cols-3 gap-4'
)}
>
{notes.map((note, index) => {
const title = note.title || t('memoryEcho.comparison.untitled')
const noteColor = getNoteColor(index)
const titleColor = getTitleColor(index)
const plainContent = stripHtml(note.content || '')
return (
<div
key={note.id || index}
onClick={() => {
if (onOpenNote && note.id) {
onOpenNote(note.id)
onClose()
}
}}
className={cn(
"cursor-pointer border dark:border-zinc-700 rounded-lg p-4 transition-all hover:shadow-md",
noteColor
'border rounded-xl p-4 bg-card flex flex-col',
getNoteColor(index)
)}
>
<h3 className={cn("font-semibold text-lg mb-3", titleColor)}>
<h3 className={cn('font-semibold text-lg mb-3', getTitleColor(index))}>
{title}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400 line-clamp-8 whitespace-pre-wrap">
{note.content}
</div>
<div className="mt-4 pt-3 border-t dark:border-zinc-700">
<p className="text-xs text-gray-500 flex items-center gap-1">
{t('memoryEcho.comparison.clickToView')}
<span className="transform rotate-[-45deg]"></span>
</p>
<div className="text-sm text-muted-foreground line-clamp-[14] whitespace-pre-wrap flex-1">
{plainContent}
</div>
{note.id && (
<div className="mt-4 pt-3 border-t border-border/60">
<button
type="button"
onClick={() => openNoteInNewTab(note.id!)}
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors"
>
<ExternalLink className="h-3 w-3" />
{t('memoryEcho.editorSection.openInEditor')}
</button>
</div>
)}
</div>
)
})}
</div>
{/* Footer - Feedback + Actions */}
<div className="px-6 py-4 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('memoryEcho.comparison.helpfulQuestion')}
</p>
<Button
size="sm"
variant={feedback === 'thumbs_up' ? 'default' : 'outline'}
onClick={() => handleFeedback('thumbs_up')}
className={cn(
feedback === 'thumbs_up' && "bg-green-600 hover:bg-green-700 text-white"
)}
>
<ThumbsUp className="h-4 w-4 mr-2" />
{t('memoryEcho.comparison.helpful')}
</Button>
<Button
size="sm"
variant={feedback === 'thumbs_down' ? 'default' : 'outline'}
onClick={() => handleFeedback('thumbs_down')}
className={cn(
feedback === 'thumbs_down' && "bg-red-600 hover:bg-red-700 text-white"
)}
>
<ThumbsDown className="h-4 w-4 mr-2" />
{t('memoryEcho.comparison.notHelpful')}
</Button>
</div>
{onMergeNotes && notes.length >= 2 && (
<Button
size="sm"
className="bg-purple-600 hover:bg-purple-700 text-white"
onClick={() => {
const noteIds = notes.map(n => n.id).filter(Boolean) as string[]
onMergeNotes(noteIds)
onClose()
}}
>
<GitMerge className="h-4 w-4 mr-2" />
{t('memoryEcho.editorSection.mergeAll')}
</Button>
)}
</div>
<div className="px-6 py-4 border-t border-border/60 bg-muted/30 flex items-center justify-end gap-2">
<Button size="sm" variant="outline" onClick={onClose}>
{t('memoryEcho.editorSection.backToNote')}
</Button>
{onMergeNotes && notes.length >= 2 && (
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700 text-white"
onClick={() => {
const noteIds = notes.map(n => n.id).filter(Boolean) as string[]
onMergeNotes(noteIds)
onClose()
}}
>
<GitMerge className="h-4 w-4 mr-2" />
{t('memoryEcho.editorSection.mergeAll')}
</Button>
)}
</div>
</DialogContent>
</Dialog>