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>
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Columns2, GitMerge, X, ExternalLink } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import { Note } from '@/lib/types'
|
|
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
|
|
|
interface ComparisonModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
notes: Array<Partial<Note>>
|
|
similarity?: number
|
|
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,
|
|
onMergeNotes,
|
|
}: ComparisonModalProps) {
|
|
const { t } = useLanguage()
|
|
|
|
const getNoteColor = (index: number) => {
|
|
const colors = [
|
|
'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-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]
|
|
}
|
|
|
|
const maxModalWidth = notes.length === 2 ? 'max-w-6xl' : 'max-w-7xl'
|
|
const similarityPercentage = similarity ? Math.round(similarity * 100) : 0
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
className={cn('max-h-[90vh] overflow-hidden flex flex-col p-0', maxModalWidth)}
|
|
>
|
|
<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 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-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-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
aria-label={t('memoryEcho.editorSection.close')}
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<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-3 border-b border-border/60">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('memoryEcho.comparison.highSimilarityInsight')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<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 plainContent = stripHtml(note.content || '')
|
|
|
|
return (
|
|
<div
|
|
key={note.id || index}
|
|
className={cn(
|
|
'border rounded-xl p-4 bg-card flex flex-col',
|
|
getNoteColor(index)
|
|
)}
|
|
>
|
|
<h3 className={cn('font-semibold text-lg mb-3', getTitleColor(index))}>
|
|
{title}
|
|
</h3>
|
|
<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>
|
|
|
|
<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>
|
|
)
|
|
}
|