'use client' import { useMemo, useState, Fragment } from 'react' import { useAiConsent } from '@/components/legal/ai-consent-provider' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import type { Note } from '@/lib/types' import type { ColumnFilter, ColumnSort, NotebookSchemaPayload, NotePropertyValues, } from '@/lib/structured-views/types' import { filterNotesWithProperties, sortNotesWithProperties, } from '@/lib/structured-views/property-utils' import { PropertyValueEditor } from './property-value-editor' import { getNoteDisplayTitle } from '@/lib/note-preview' import { useLanguage } from '@/lib/i18n' import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date' import { enUS, fr, faIR } from 'date-fns/locale' import { ChevronDown, ChevronUp, Filter, Trash2, Sparkles, Brain, Loader2, ArrowUpRight, Link2 } from 'lucide-react' import { cn } from '@/lib/utils' import { openNotePeek } from '@/lib/note-peek-sync' const localeMap: Record = { en: enUS, fr, fa: faIR } function getDateLocale(lang: string) { return localeMap[lang] || enUS } const getBestFallbackKeyword = (title: string): string => { const STOP_WORDS = new Set([ 'dans', 'avec', 'pour', 'plus', 'tout', 'tous', 'cette', 'ceux', 'mais', 'sans', 'faire', 'fait', 'comme', 'sont', 'ont', 'etaient', 'etait', 'sujet', 'fiche', 'page', 'note', 'notes', 'guide', 'guides', 'projet', 'projets', 'test', 'tests', 'demo', 'base', 'bases', 'donnee', 'donnees', 'tableau', 'tableaux', 'quel', 'quels', 'quelle', 'quelles', 'about', 'with', 'from', 'that', 'this', 'your', 'have', 'were', 'was', 'project', 'projects', 'test', 'tests', 'page', 'pages', 'file', 'files', 'some', 'more', 'their', 'them', 'they', 'what', 'which', 'where', 'when', 'how', 'who', 'why' ]) const words = title .toLowerCase() .split(/[^\p{L}\d]+/u) .filter(w => w.length > 3 && !STOP_WORDS.has(w)) if (words.length === 0) return '' words.sort((a, b) => b.length - a.length) return words[0] } type NotesStructuredTableProps = { notes: Note[] schema: NotebookSchemaPayload noteValues: Record onOpen: (note: Note) => void onPropertyChange: (noteId: string, propertyId: string, value: unknown) => void onDeleteProperty?: (propertyId: string) => Promise } export function NotesStructuredTable({ notes, schema, noteValues, onOpen, onPropertyChange, onDeleteProperty, }: NotesStructuredTableProps) { const { t, language } = useLanguage() const untitled = t('notes.untitled') const [sort, setSort] = useState({ propertyId: 'updatedAt', direction: 'desc' }) const [filterPropId, setFilterPropId] = useState(null) const [filterOp, setFilterOp] = useState('contains') const [filterValue, setFilterValue] = useState('') const [propertyToDelete, setPropertyToDelete] = useState<{ id: string; name: string } | null>(null) const [deletingProperty, setDeletingProperty] = useState(false) // Memory Echo states const { requestAiConsent } = useAiConsent() const [activeEchoNoteId, setActiveEchoNoteId] = useState(null) const [echoLoading, setEchoLoading] = useState(false) const [echoError, setEchoError] = useState(null) const [echoConnections, setEchoConnections] = useState>([]) const handleFetchRealEcho = async (noteId: string) => { if (activeEchoNoteId === noteId) { setActiveEchoNoteId(null) setEchoConnections([]) setEchoError(null) return } setActiveEchoNoteId(noteId) setEchoLoading(true) setEchoConnections([]) setEchoError(null) try { // 1. GDPR AI Consent check const consented = await requestAiConsent() if (!consented) { setEchoError("Le consentement pour le traitement par IA est requis pour utiliser la résonance sémantique.") setEchoLoading(false) return } // 2. Fetch connections const res = await fetch(`/api/ai/echo/connections?noteId=${noteId}&limit=5`) if (res.status === 403) { setEchoError("Le consentement pour le traitement par IA est requis pour utiliser la résonance sémantique.") setEchoLoading(false) return } const json = await res.json() if (json.connections && Array.isArray(json.connections) && json.connections.length > 0) { setEchoConnections(json.connections) } else { // 3. Fallback: Si aucune connexion pré-calculée en DB, on fait une recherche textuelle en direct const targetNote = notes.find(n => n.id === noteId) const q = (targetNote?.title || '').trim() if (!q) { setEchoError("Cette note n'a pas encore de titre ou de contenu suffisant pour trouver des résonances sémantiques.") setEchoLoading(false) return } let fallbackRes = await fetch(`/api/notes?search=${encodeURIComponent(q)}&limit=5`) let fallbackJson = await fallbackRes.json() let filteredNotes = (fallbackJson.success && Array.isArray(fallbackJson.data)) ? fallbackJson.data.filter((n: any) => n.id !== noteId) : [] if (filteredNotes.length === 0) { // Fallback to searching significant words if full title matches nothing else const fallbackWord = getBestFallbackKeyword(q) if (fallbackWord) { const resFallback = await fetch(`/api/notes?search=${encodeURIComponent(fallbackWord)}&limit=5`) const jsonFallback = await resFallback.json() if (jsonFallback.success && Array.isArray(jsonFallback.data)) { filteredNotes = jsonFallback.data.filter((n: any) => n.id !== noteId) } } } if (filteredNotes.length === 0) { setEchoError(`Aucune note similaire à "${q}" n'a été détectée dans votre espace de travail.`) } else { setEchoConnections(filteredNotes.map((n: any, idx: number) => ({ noteId: n.id, title: n.title || 'Sans titre', similarity: 0.82 - idx * 0.04, isTextMatch: true }))) } } } catch (e) { console.error(e) setEchoError("Une erreur est survenue lors de la recherche.") } finally { setEchoLoading(false) } } const filters: ColumnFilter[] = useMemo(() => { if (!filterPropId) return [] return [{ propertyId: filterPropId, operator: filterOp, value: filterValue }] }, [filterPropId, filterOp, filterValue]) const displayed = useMemo(() => { const filtered = filterNotesWithProperties(notes, noteValues, filters, schema.properties) return sortNotesWithProperties(filtered, noteValues, sort, schema.properties) }, [notes, noteValues, filters, sort, schema.properties]) const toggleSort = (propertyId: ColumnSort['propertyId']) => { setSort((prev) => prev.propertyId === propertyId ? { propertyId, direction: prev.direction === 'asc' ? 'desc' : 'asc' } : { propertyId, direction: 'asc' }, ) } const SortIcon = ({ field }: { field: ColumnSort['propertyId'] }) => sort.propertyId !== field ? null : sort.direction === 'asc' ? ( ) : ( ) return (
{filterPropId && ( <> {filterOp !== 'empty' && ( setFilterValue(e.target.value)} className="rounded-lg border border-border bg-background px-2 py-1 min-w-[120px]" placeholder={t('structuredViews.filterValue')} /> )} )}
{schema.properties.map((p) => ( ))} {displayed.map((note) => { const vals = noteValues[note.id] ?? {} return ( {schema.properties.map((p) => ( ))} {/* Memory Echo collapsible details */} {activeEchoNoteId === note.id && ( )} ) })}
toggleSort('title')} > {t('notes.tableTitle')} {onDeleteProperty && ( )} toggleSort('updatedAt')} > {t('notes.tableModified')}
e.stopPropagation()} >
onPropertyChange(note.id, p.id, v)} />
{formatAbsoluteDateLocalized(new Date(note.updatedAt), language, 'MMM d, yyyy', getDateLocale(language))}
{t('structuredViewBlock.echoPopoverTitle') || 'Résonance Sémantique 🔮'}
{echoLoading ? (
{t('structuredViewBlock.echoLoading') || 'Recherche de connexions sémantiques...'}
) : echoError ? (

{echoError}

) : echoConnections.length === 0 ? (

{t('structuredViewBlock.noEchoFound') || 'Aucune résonance sémantique détectée.'}

) : (
{echoConnections.map((conn) => (
{conn.isTextMatch ? 'Mot-clé' : `${Math.round((conn.similarity || 0) * 100)}%`}
))}
)}
{displayed.length === 0 && (

{t('structuredViews.noMatchingNotes')}

)}
{ if (!open) setPropertyToDelete(null) }} > {t('structuredViews.deletePropertyTitle')} {t('structuredViews.deletePropertyConfirm', { name: propertyToDelete?.name ?? '' })} {t('general.cancel')} { e.preventDefault() if (!propertyToDelete || !onDeleteProperty) return setDeletingProperty(true) try { await onDeleteProperty(propertyToDelete.id) if (filterPropId === propertyToDelete.id) setFilterPropId(null) setPropertyToDelete(null) } finally { setDeletingProperty(false) } }} > {t('structuredViews.deleteProperty')}
) }