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>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user