Files
Momento/memento-note/components/note-card.tsx
sepehr 153c921960
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
Replaced ~100+ hardcoded French and English text strings across 30+ components
with proper i18n t() calls. Added 57 new translation keys to all 15 locale files
(ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh).

Key changes:
- contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders)
- ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history)
- note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist)
- lab-skeleton.tsx: French loading text → t()
- admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed
- New AI chat component, agent cards, sidebar, settings panel i18n cleanup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 21:14:45 +02:00

776 lines
27 KiB
TypeScript

'use client'
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, LogOut, Trash2 } from 'lucide-react'
import { useState, useEffect, useTransition, useOptimistic, memo } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation'
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge, createNote, restoreNote, permanentDeleteNote } from '@/app/actions/notes'
import { cn } from '@/lib/utils'
import { formatDistanceToNow, Locale } from 'date-fns'
import { enUS } from 'date-fns/locale/en-US'
import { fr } from 'date-fns/locale/fr'
import { es } from 'date-fns/locale/es'
import { de } from 'date-fns/locale/de'
import { faIR } from 'date-fns/locale/fa-IR'
import { it } from 'date-fns/locale/it'
import { pt } from 'date-fns/locale/pt'
import { ru } from 'date-fns/locale/ru'
import { zhCN } from 'date-fns/locale/zh-CN'
import { ja } from 'date-fns/locale/ja'
import { ko } from 'date-fns/locale/ko'
import { ar } from 'date-fns/locale/ar'
import { hi } from 'date-fns/locale/hi'
import { nl } from 'date-fns/locale/nl'
import { pl } from 'date-fns/locale/pl'
import { MarkdownContent } from './markdown-content'
import { LabelBadge } from './label-badge'
import { NoteImages } from './note-images'
import { NoteChecklist } from './note-checklist'
import { NoteActions } from './note-actions'
import { CollaboratorDialog } from './collaborator-dialog'
import { CollaboratorAvatars } from './collaborator-avatars'
import { ConnectionsBadge } from './connections-badge'
import { ConnectionsOverlay } from './connections-overlay'
import { ComparisonModal } from './comparison-modal'
import { FusionModal } from './fusion-modal'
import { useConnectionsCompare } from '@/hooks/use-connections-compare'
import { useLabels } from '@/context/LabelContext'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useLanguage } from '@/lib/i18n'
import { useNotebooks } from '@/context/notebooks-context'
import { toast } from 'sonner'
// Mapping of supported languages to date-fns locales
const localeMap: Record<string, Locale> = {
en: enUS,
fr: fr,
es: es,
de: de,
fa: faIR,
it: it,
pt: pt,
ru: ru,
zh: zhCN,
ja: ja,
ko: ko,
ar: ar,
hi: hi,
nl: nl,
pl: pl,
}
function getDateLocale(language: string): Locale {
return localeMap[language] || enUS
}
// Map icon names to lucide-react components
const ICON_MAP: Record<string, LucideIcon> = {
'folder': Folder,
'briefcase': Briefcase,
'document': FileText,
'lightning': Zap,
'chart': BarChart3,
'globe': Globe,
'sparkle': Sparkles,
'book': Book,
'heart': Heart,
'crown': Crown,
'music': Music,
'building': Building2,
}
// Function to get icon component by name
function getNotebookIcon(iconName: string): LucideIcon {
const IconComponent = ICON_MAP[iconName] || Folder
return IconComponent
}
interface NoteCardProps {
note: Note
onEdit?: (note: Note, readOnly?: boolean) => void
isDragging?: boolean
isDragOver?: boolean
onDragStart?: (noteId: string) => void
onDragEnd?: () => void
onResize?: () => void
onSizeChange?: (newSize: 'small' | 'medium' | 'large') => void
isTrashView?: boolean
}
// Helper function to get initials from name
function getInitials(name: string): string {
if (!name) return '??'
const trimmedName = name.trim()
const parts = trimmedName.split(' ')
if (parts.length === 1) {
return trimmedName.substring(0, 2).toUpperCase()
}
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
// Helper function to get avatar color based on name hash
function getAvatarColor(name: string): string {
const colors = [
'bg-primary',
'bg-purple-600',
'bg-emerald-600',
'bg-amber-600',
'bg-pink-600',
'bg-teal-600',
'bg-blue-600',
'bg-indigo-600',
]
const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return colors[hash % colors.length]
}
export const NoteCard = memo(function NoteCard({
note,
onEdit,
onDragStart,
onDragEnd,
isDragging,
onResize,
onSizeChange,
isTrashView
}: NoteCardProps) {
const router = useRouter()
const searchParams = useSearchParams()
const { refreshLabels } = useLabels()
const { triggerRefresh } = useNoteRefresh()
const { data: session } = useSession()
const { t, language } = useLanguage()
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
const [, startTransition] = useTransition()
const [isDeleting, setIsDeleting] = useState(false)
const [isHidden, setIsHidden] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
const [collaborators, setCollaborators] = useState<any[]>([])
const [owner, setOwner] = useState<any>(null)
const [showConnectionsOverlay, setShowConnectionsOverlay] = useState(false)
const [comparisonNotes, setComparisonNotes] = useState<string[] | null>(null)
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
// Move note to a notebook
const handleMoveToNotebook = async (notebookId: string | null) => {
await moveNoteToNotebookOptimistic(note.id, notebookId)
setShowNotebookMenu(false)
// No need for router.refresh() - triggerRefresh() is already called in moveNoteToNotebookOptimistic
}
// Optimistic UI state for instant feedback
const [optimisticNote, addOptimisticNote] = useOptimistic(
note,
(state, newProps: Partial<Note>) => ({ ...state, ...newProps })
)
// Local color state so color persists after transition ends
const [localColor, setLocalColor] = useState(note.color)
const colorClasses = NOTE_COLORS[(localColor || optimisticNote.color) as NoteColor] || NOTE_COLORS.default
// Check if this note is currently open in the editor
const isNoteOpenInEditor = searchParams.get('note') === note.id
// Only fetch comparison notes when we have IDs to compare
const { notes: comparisonNotesData, isLoading: isLoadingComparison } = useConnectionsCompare(
comparisonNotes && comparisonNotes.length > 0 ? comparisonNotes : null
)
const currentUserId = session?.user?.id
const canManageCollaborators = currentUserId && note.userId && currentUserId === note.userId
const isSharedNote = currentUserId && note.userId && currentUserId !== note.userId
const isOwner = currentUserId && note.userId && currentUserId === note.userId
// Load collaborators only for shared notes (not owned by current user)
useEffect(() => {
// Skip API call for notes owned by current user — no need to fetch collaborators
if (!isSharedNote) {
// For own notes, set owner to current user
if (currentUserId && session?.user) {
setOwner({
id: currentUserId,
name: session.user.name,
email: session.user.email,
image: session.user.image,
})
}
return
}
let isMounted = true
const loadCollaborators = async () => {
if (note.userId && isMounted) {
try {
const users = await getNoteAllUsers(note.id)
if (isMounted) {
setCollaborators(users)
if (users.length > 0) {
setOwner(users[0])
}
}
} catch (error) {
console.error('Failed to load collaborators:', error)
if (isMounted) {
setCollaborators([])
}
}
}
}
loadCollaborators()
return () => {
isMounted = false
}
}, [note.id, note.userId, isSharedNote, currentUserId, session?.user])
const handleDelete = async () => {
setIsDeleting(true)
setIsHidden(true) // masquage immédiat
try {
await deleteNote(note.id)
await refreshLabels()
triggerRefresh() // met à jour la liste et le compteur du carnet
} catch (error) {
console.error('Failed to delete note:', error)
setIsHidden(false)
setIsDeleting(false)
}
}
const handleRestore = async () => {
setIsDeleting(true)
setIsHidden(true)
try {
await restoreNote(note.id)
triggerRefresh()
toast.success(t('trash.noteRestored'))
} catch (error) {
console.error('Failed to restore note:', error)
setIsHidden(false)
setIsDeleting(false)
}
}
const handlePermanentDelete = async () => {
setIsDeleting(true)
setIsHidden(true)
try {
await permanentDeleteNote(note.id)
triggerRefresh()
toast.success(t('trash.notePermanentlyDeleted'))
} catch (error) {
console.error('Failed to permanently delete note:', error)
setIsHidden(false)
setIsDeleting(false)
}
}
const handleTogglePin = async () => {
startTransition(async () => {
addOptimisticNote({ isPinned: !note.isPinned })
await togglePin(note.id, !note.isPinned)
if (!note.isPinned) {
toast.success(t('notes.pinned') || 'Note pinned')
} else {
toast.info(t('notes.unpinned') || 'Note unpinned')
}
})
}
const handleToggleArchive = async () => {
startTransition(async () => {
addOptimisticNote({ isArchived: !note.isArchived })
await toggleArchive(note.id, !note.isArchived)
})
}
const handleColorChange = async (color: string) => {
setLocalColor(color) // instant visual update, survives transition
startTransition(async () => {
addOptimisticNote({ color })
await updateNote(note.id, { color }, { skipRevalidation: false })
})
}
const handleSizeChange = async (size: 'small' | 'medium' | 'large') => {
startTransition(async () => {
// Instant visual feedback for the card itself
addOptimisticNote({ size })
// Notify parent so it can update its local state
onSizeChange?.(size)
// Trigger layout refresh
onResize?.()
setTimeout(() => onResize?.(), 300)
// Update server in background
try {
await updateSize(note.id, size);
} catch (error) {
console.error('Failed to update note size:', error);
}
})
}
const handleCheckItem = async (checkItemId: string) => {
if (note.type === 'checklist' && Array.isArray(note.checkItems)) {
const updatedItems = note.checkItems.map(item =>
item.id === checkItemId ? { ...item, checked: !item.checked } : item
)
startTransition(async () => {
addOptimisticNote({ checkItems: updatedItems })
await updateNote(note.id, { checkItems: updatedItems })
// No router.refresh() — optimistic update is sufficient and avoids grid rebuild
})
}
}
const handleLeaveShare = async () => {
if (confirm(t('notes.confirmLeaveShare'))) {
try {
await leaveSharedNote(note.id)
setIsDeleting(true) // Hide the note from view
} catch (error) {
console.error('Failed to leave share:', error)
}
}
}
const handleRemoveFusedBadge = async (e: React.MouseEvent) => {
e.stopPropagation() // Prevent opening the note editor
startTransition(async () => {
addOptimisticNote({ autoGenerated: null })
await removeFusedBadge(note.id)
// No router.refresh() — optimistic update is sufficient and avoids grid rebuild
})
}
if (isDeleting) return null
const getMinHeight = (size?: string) => {
switch (size) {
case 'medium': return '350px'
case 'large': return '500px'
default: return '150px' // small
}
}
if (isHidden) return null
return (
<Card
data-testid="note-card"
data-draggable="true"
data-note-id={note.id}
data-size={optimisticNote.size}
style={{ minHeight: getMinHeight(optimisticNote.size) }}
draggable={true}
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', note.id)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/html', '') // Prevent ghost image in some browsers
onDragStart?.(note.id)
}}
onDragEnd={() => onDragEnd?.()}
className={cn(
'note-card group relative rounded-lg overflow-hidden p-6 border-transparent shadow-sm',
'transition-all duration-200 ease-out',
'hover:shadow-md hover:border-border/50 hover:-translate-y-0.5',
colorClasses.bg,
colorClasses.card,
colorClasses.hover,
isDragging && 'shadow-lg'
)}
onClick={(e) => {
// Only trigger edit if not clicking on buttons
const target = e.target as HTMLElement
if (!target.closest('button') && !target.closest('[role="checkbox"]') && !target.closest('.muuri-drag-handle') && !target.closest('.drag-handle')) {
// For shared notes, pass readOnly flag
onEdit?.(note, !!isSharedNote) // Pass second parameter as readOnly flag (convert to boolean)
}
}}
>
{/* Drag Handle - Only visible on mobile/touch devices */}
<div
className="muuri-drag-handle absolute top-2 left-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden"
aria-label={t('notes.dragToReorder') || 'Drag to reorder'}
title={t('notes.dragToReorder') || 'Drag to reorder'}
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
</div>
{/* Move to Notebook Dropdown Menu */}
<div onClick={(e) => e.stopPropagation()} className="absolute top-2 right-2 z-20">
<DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 bg-primary/10 dark:bg-primary/20 hover:bg-primary/20 dark:hover:bg-primary/30 text-primary dark:text-primary-foreground"
title={t('notebookSuggestion.moveToNotebook')}
>
<FolderOpen className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
{t('notebookSuggestion.moveToNotebook')}
</div>
<DropdownMenuItem onClick={() => handleMoveToNotebook(null)}>
<StickyNote className="h-4 w-4 mr-2" />
{t('notebookSuggestion.generalNotes')}
</DropdownMenuItem>
{notebooks.map((notebook: any) => {
const NotebookIcon = getNotebookIcon(notebook.icon || 'folder')
return (
<DropdownMenuItem
key={notebook.id}
onClick={() => handleMoveToNotebook(notebook.id)}
>
<NotebookIcon className="h-4 w-4 mr-2" />
{notebook.name}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Pin Button - Visible on hover or if pinned */}
<Button
variant="ghost"
size="sm"
data-testid="pin-button"
className={cn(
"absolute top-2 right-12 z-20 h-8 w-8 p-0 rounded-md transition-opacity",
optimisticNote.isPinned ? "opacity-100" : "opacity-0 group-hover:opacity-100"
)}
onClick={(e) => {
e.stopPropagation()
handleTogglePin()
}}
>
<Pin
className={cn("h-4 w-4", optimisticNote.isPinned ? "fill-current text-primary" : "text-muted-foreground")}
/>
</Button>
{/* Reminder Icon - Move slightly if pin button is there */}
{note.reminder && new Date(note.reminder) > new Date() && (
<Bell
className="absolute top-3 right-10 h-4 w-4 text-primary"
/>
)}
{/* Fusion Badge */}
{optimisticNote.aiProvider === 'fusion' && (
<div className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 border border-purple-200 dark:border-purple-800 flex items-center gap-1 group/badge relative mb-2 w-fit">
<Link2 className="h-2.5 w-2.5" />
{t('memoryEcho.fused')}
<button
onClick={handleRemoveFusedBadge}
className="ml-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
title={t('notes.remove') || 'Remove'}
>
<Trash2 className="h-2.5 w-2.5" />
</button>
</div>
)}
{/* Title */}
{optimisticNote.title && (
<h3 className="text-lg font-heading font-semibold mb-2 pr-20 text-foreground leading-tight tracking-tight">
{optimisticNote.title}
</h3>
)}
{/* Search Match Type Badge */}
{optimisticNote.matchType && (
<Badge
variant={optimisticNote.matchType === 'exact' ? 'default' : 'secondary'}
className={cn(
'mb-2 text-xs',
optimisticNote.matchType === 'exact'
? 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800'
: 'bg-primary/10 text-primary border-primary/20 dark:bg-primary/20 dark:text-primary-foreground'
)}
>
{t(`semanticSearch.${optimisticNote.matchType === 'exact' ? 'exactMatch' : 'related'}`)}
</Badge>
)}
{/* Shared badge */}
{isSharedNote && owner && (
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-primary dark:text-primary-foreground font-medium">
{t('notes.sharedBy')} {owner.name || owner.email}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-gray-500 hover:text-red-600 dark:hover:text-red-400"
onClick={(e) => {
e.stopPropagation()
handleLeaveShare()
}}
>
<LogOut className="h-3 w-3 mr-1" />
{t('notes.leaveShare')}
</Button>
</div>
)}
{/* Images Component */}
<NoteImages images={optimisticNote.images || []} title={optimisticNote.title} />
{/* Link Previews */}
{Array.isArray(optimisticNote.links) && optimisticNote.links.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{optimisticNote.links.map((link, idx) => (
<a
key={idx}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="block border rounded-md overflow-hidden bg-white/50 dark:bg-black/20 hover:bg-white/80 dark:hover:bg-black/40 transition-colors"
onClick={(e) => e.stopPropagation()}
>
{link.imageUrl && (
<div className="h-24 bg-cover bg-center" style={{ backgroundImage: `url(${link.imageUrl})` }} />
)}
<div className="p-2">
<h4 className="font-medium text-xs truncate text-gray-900 dark:text-gray-100">{link.title || link.url}</h4>
{link.description && <p className="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">{link.description}</p>}
<span className="text-[10px] text-primary mt-1 block">
{new URL(link.url).hostname}
</span>
</div>
</a>
))}
</div>
)}
{/* Content */}
{optimisticNote.type === 'text' ? (
<div className="text-sm text-foreground line-clamp-10">
<MarkdownContent
content={optimisticNote.content}
className="prose-h1:text-xl prose-h1:font-semibold prose-h1:leading-snug prose-h1:mt-1 prose-h1:mb-2 prose-h2:text-lg prose-h2:font-medium prose-h3:text-base prose-p:text-sm prose-p:leading-relaxed"
/>
</div>
) : (
<NoteChecklist
items={optimisticNote.checkItems || []}
onToggleItem={handleCheckItem}
/>
)}
{/* Labels - using shared LabelBadge component */}
{optimisticNote.notebookId && Array.isArray(optimisticNote.labels) && optimisticNote.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-3">
{optimisticNote.labels.map((label) => (
<LabelBadge key={label} label={label} />
))}
</div>
)}
{/* Footer with Date only */}
<div className="mt-3 flex items-center justify-end">
{/* Creation Date */}
<div className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: getDateLocale(language) })}
</div>
</div>
{/* Owner Avatar - Aligned with action buttons at bottom */}
{owner && (
<div
className={cn(
"absolute bottom-2 left-2 z-20",
"w-6 h-6 rounded-full text-white text-[10px] font-semibold flex items-center justify-center",
getAvatarColor(owner.name || owner.email || 'Unknown')
)}
title={owner.name || owner.email || 'Unknown'}
>
{getInitials(owner.name || owner.email || '??')}
</div>
)}
{/* Action Bar Component - Always show for now to fix regression */}
{true && (
<NoteActions
isPinned={optimisticNote.isPinned}
isArchived={optimisticNote.isArchived}
currentColor={optimisticNote.color}
currentSize={optimisticNote.size as 'small' | 'medium' | 'large'}
onTogglePin={handleTogglePin}
onToggleArchive={handleToggleArchive}
onColorChange={handleColorChange}
onSizeChange={handleSizeChange}
onDelete={() => setShowDeleteDialog(true)}
onShareCollaborators={() => setShowCollaboratorDialog(true)}
isTrashView={isTrashView}
onRestore={handleRestore}
onPermanentDelete={handlePermanentDelete}
className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
/>
)}
{/* Collaborator Dialog */}
{currentUserId && note.userId && (
<div onClick={(e) => e.stopPropagation()}>
<CollaboratorDialog
open={showCollaboratorDialog}
onOpenChange={setShowCollaboratorDialog}
noteId={note.id}
noteOwnerId={note.userId}
currentUserId={currentUserId}
/>
</div>
)}
{/* Connections Badge - Bottom right (spec: amber, absolute) */}
<div className="absolute bottom-2 right-2 z-10">
<ConnectionsBadge
noteId={note.id}
onClick={() => {
if (!isNoteOpenInEditor) {
setShowConnectionsOverlay(true)
}
}}
/>
</div>
{/* Connections Overlay */}
<div onClick={(e) => e.stopPropagation()}>
<ConnectionsOverlay
isOpen={showConnectionsOverlay}
onClose={() => setShowConnectionsOverlay(false)}
noteId={note.id}
onOpenNote={(connNoteId) => {
setShowConnectionsOverlay(false)
const params = new URLSearchParams(searchParams.toString())
params.set('note', connNoteId)
router.push(`?${params.toString()}`)
}}
onCompareNotes={(noteIds) => {
setComparisonNotes(noteIds)
}}
onMergeNotes={async (noteIds) => {
const fetchedNotes = await Promise.all(noteIds.map(async (id) => {
try {
const res = await fetch(`/api/notes/${id}`)
if (!res.ok) return null
const data = await res.json()
return data.success && data.data ? data.data : null
} catch { return null }
}))
setFusionNotes(fetchedNotes.filter((n: any) => n !== null) as Array<Partial<Note>>)
}}
/>
</div>
{/* Comparison Modal */}
{comparisonNotes && comparisonNotesData.length > 0 && (
<div onClick={(e) => e.stopPropagation()}>
<ComparisonModal
isOpen={!!comparisonNotes}
onClose={() => setComparisonNotes(null)}
notes={comparisonNotesData}
onOpenNote={(noteId) => {
const foundNote = comparisonNotesData.find(n => n.id === noteId)
if (foundNote) {
onEdit?.(foundNote, false)
}
}}
/>
</div>
)}
{/* Fusion Modal */}
{fusionNotes.length > 0 && (
<div onClick={(e) => e.stopPropagation()}>
<FusionModal
isOpen={fusionNotes.length > 0}
onClose={() => setFusionNotes([])}
notes={fusionNotes}
onConfirmFusion={async ({ title, content }, options) => {
await createNote({
title,
content,
labels: options.keepAllTags
? [...new Set(fusionNotes.flatMap(n => n.labels || []))]
: fusionNotes[0].labels || [],
color: fusionNotes[0].color,
type: 'text',
isMarkdown: true,
autoGenerated: true,
aiProvider: 'fusion',
notebookId: fusionNotes[0].notebookId ?? undefined
})
if (options.archiveOriginals) {
for (const n of fusionNotes) {
if (n.id) await updateNote(n.id, { isArchived: true })
}
}
toast.success(t('toast.notesFusionSuccess'))
setFusionNotes([])
triggerRefresh()
}}
/>
</div>
)}
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('notes.confirmDeleteTitle') || t('notes.delete')}</AlertDialogTitle>
<AlertDialogDescription>
{t('notes.confirmDelete') || 'Are you sure you want to delete this note?'}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('common.cancel') || 'Cancel'}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleDelete}>
{t('notes.delete') || 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
)
})