'use client' import { useEffect, useState, useMemo, useCallback, Fragment } from 'react' import { useRouter } from 'next/navigation' import { useNotebookSchema } from '@/hooks/use-notebook-schema' import { useNotebooks } from '@/context/notebooks-context' import { useLanguage } from '@/lib/i18n' import type { Editor } from '@tiptap/core' import type { Note } from '@/lib/types' import { StructuredViewsContainer } from '@/components/structured-views/structured-views-container' import { LayoutGrid, Table, ExternalLink, Loader2, AlertCircle, Plus, Trash2, FolderPlus, Settings, Sparkles, Check, CheckSquare, Type, List, HelpCircle, BarChart3, Brain, ArrowUpRight, Link2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { openNotePeek } from '@/lib/note-peek-sync' interface LocalColumn { id: string name: string type: 'text' | 'checkbox' | 'select' options?: string[] } interface LocalRow { id: string values: Record } interface StructuredViewBlockEmbedProps { notebookId: string | null displayMode: 'table' | 'gallery' filterJson: string isLocal: boolean localColumnsJson: string localRowsJson: string updateAttributes: (attrs: Record) => void editor: Editor } export function StructuredViewBlockEmbed({ notebookId, displayMode, filterJson, isLocal, localColumnsJson, localRowsJson, updateAttributes, editor, }: StructuredViewBlockEmbedProps) { const { t, language } = useLanguage() const router = useRouter() const { notebooks, refreshNotebooks } = useNotebooks() // New features states const [showAnalytics, setShowAnalytics] = useState(false) const [activeEchoRowId, setActiveEchoRowId] = useState(null) const [echoLoading, setEchoLoading] = useState(false) const [echoError, setEchoError] = useState(null) const [echoConnections, setEchoConnections] = useState>([]) // Parse local database data const localColumns: LocalColumn[] = useMemo(() => { try { return JSON.parse(localColumnsJson || '[]') } catch { return [] } }, [localColumnsJson]) const localRows: LocalRow[] = useMemo(() => { try { return JSON.parse(localRowsJson || '[]') } catch { return [] } }, [localRowsJson]) // State for notebook creation from local DB const [converting, setConverting] = useState(false) const [newNotebookName, setNewNotebookName] = useState('') const [showConvertModal, setShowConvertModal] = useState(false) const effectiveNotebookId = useMemo(() => { return notebookId || ((editor.storage as any).structuredViewBlock?.notebookId as string | null) || null }, [notebookId, (editor.storage as any).structuredViewBlock?.notebookId]) // Get current notebook info for visual style and naming const currentNotebook = useMemo(() => { return notebooks.find((n) => n.id === effectiveNotebookId) }, [notebooks, effectiveNotebookId]) // Schema state hook const schemaHook = useNotebookSchema(effectiveNotebookId) // Fetch notebook notes const [notes, setNotes] = useState([]) const [notesLoading, setNotesLoading] = useState(false) const [notesError, setNotesError] = useState(null) const fetchNotes = useCallback(async () => { if (isLocal || !effectiveNotebookId) { setNotes([]) return } setNotesLoading(true) setNotesError(null) try { const res = await fetch(`/api/notes?notebookId=${effectiveNotebookId}&limit=50`) const json = await res.json() if (json.success && Array.isArray(json.data)) { setNotes(json.data) } else { setNotesError(t('structuredViewBlock.loadError')) } } catch (e) { setNotesError(e instanceof Error ? e.message : t('structuredViewBlock.notesLoadError')) } finally { setNotesLoading(false) } }, [effectiveNotebookId, isLocal, t]) useEffect(() => { void fetchNotes() }, [fetchNotes]) // Handle mode switch between Table and Gallery const setMode = (mode: 'table' | 'gallery') => { updateAttributes({ displayMode: mode }) } // Handle open note from the embedded view (using split peek) const handleOpenNote = (note: Note) => { openNotePeek({ noteId: note.id }) } // Reload action for retry const handleRetry = () => { void schemaHook.reload() void fetchNotes() } // ---------------------------------------------------- // LOCAL DATABASE ACTIONS (Notion-like inline table) // ---------------------------------------------------- const updateLocalData = (cols: LocalColumn[], rows: LocalRow[]) => { updateAttributes({ localColumnsJson: JSON.stringify(cols), localRowsJson: JSON.stringify(rows), }) } const addLocalColumn = () => { const newId = `col-${Date.now()}` const newCol: LocalColumn = { id: newId, name: t('structuredViewBlock.propertyName', { index: localColumns.length + 1 }), type: 'text', } const updatedCols = [...localColumns, newCol] const updatedRows = localRows.map(row => ({ ...row, values: { ...row.values, [newId]: '' } })) updateLocalData(updatedCols, updatedRows) toast.success(t('structuredViewBlock.columnAdded')) } const deleteLocalColumn = (colId: string) => { const updatedCols = localColumns.filter(c => c.id !== colId) const updatedRows = localRows.map(row => { const nextValues = { ...row.values } delete nextValues[colId] return { ...row, values: nextValues } }) updateLocalData(updatedCols, updatedRows) toast.success(t('structuredViewBlock.columnRemoved')) } const updateLocalColumnName = (colId: string, name: string) => { const updatedCols = localColumns.map(c => c.id === colId ? { ...c, name } : c) updateLocalData(updatedCols, localRows) } const updateLocalColumnType = (colId: string, type: 'text' | 'checkbox' | 'select') => { const updatedCols = localColumns.map(c => { if (c.id === colId) { const next: LocalColumn = { ...c, type } if (type === 'select') { next.options = [t('structuredViewBlock.defaultOption1'), t('structuredViewBlock.defaultOption2')] } else { delete next.options } return next } return c }) // Reset values for rows to match type const updatedRows = localRows.map(row => { const nextValues = { ...row.values } if (type === 'checkbox') { nextValues[colId] = false } else { nextValues[colId] = '' } return { ...row, values: nextValues } }) updateLocalData(updatedCols, updatedRows) } const updateLocalColumnOptions = (colId: string, optionsText: string) => { const options = optionsText.split(',').map(o => o.trim()).filter(Boolean) const updatedCols = localColumns.map(c => c.id === colId ? { ...c, options } : c) updateLocalData(updatedCols, localRows) } const addLocalRow = () => { const newId = `row-${Date.now()}` const newValues: Record = {} localColumns.forEach(c => { newValues[c.id] = c.type === 'checkbox' ? false : '' }) const newRow: LocalRow = { id: newId, values: newValues } updateLocalData(localColumns, [...localRows, newRow]) } const deleteLocalRow = (rowId: string) => { updateLocalData(localColumns, localRows.filter(r => r.id !== rowId)) } const updateLocalCellValue = (rowId: string, colId: string, value: any) => { const updatedRows = localRows.map(row => { if (row.id === rowId) { return { ...row, values: { ...row.values, [colId]: value } } } return row }) updateLocalData(localColumns, updatedRows) } // ---------------------------------------------------- // CONVERT LOCAL DATABASE TO DYNAMIC NOTEBOOK VIEW // ---------------------------------------------------- const handleConvertToNotebook = async () => { const name = newNotebookName.trim() if (!name) { toast.error(t('structuredViewBlock.convertNotebookNameRequired')) return } setConverting(true) try { // 1. Create Notebook const createNbRes = await fetch('/api/notebooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, color: '#A47148', order: 0 }) }) const createdNbJson = await createNbRes.json() if (!createNbRes.ok || !createdNbJson.success) { throw new Error(createdNbJson.error || t('structuredViewBlock.convertNotebookError')) } const newNotebook = createdNbJson.data // 2. Enable Structured Mode (Creates schema) const enableSchemaRes = await fetch(`/api/notebooks/${newNotebook.id}/schema`, { method: 'POST' }) const schemaJson = await enableSchemaRes.json() if (!enableSchemaRes.ok || !schemaJson.success) { throw new Error(schemaJson.error || t('structuredViewBlock.convertSchemaError')) } const schema = schemaJson.data.schema // 3. Add properties matching columns const columnMapping: Record = {} // maps local col ID to prisma prop ID for (const col of localColumns) { // Skip title/name column since Note already has a title field if (col.id === 'col-title') continue const propRes = await fetch(`/api/notebooks/${newNotebook.id}/schema`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'addProperty', name: col.name, type: col.type, options: col.options || [] }) }) const propJson = await propRes.json() if (!propRes.ok || !propJson.success) { throw new Error(propJson.error || t('structuredViewBlock.convertPropertyError')) } // Find added property ID in the updated schema const addedProp = propJson.data.schema.properties.find((p: any) => p.name === col.name) if (addedProp) { columnMapping[col.id] = addedProp.id } } // 4. Create Notes for each row for (const row of localRows) { const titleVal = row.values['col-title'] || t('structuredViewBlock.untitled') const noteRes = await fetch('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: titleVal, notebookId: newNotebook.id, content: '' }) }) const noteJson = await noteRes.json() if (!noteRes.ok || !noteJson.success) { throw new Error(t('structuredViewBlock.convertNoteError')) } const createdNote = noteJson.data // Associate cell values to properties const propertiesPayload: Record = {} for (const [colId, propId] of Object.entries(columnMapping)) { propertiesPayload[propId] = row.values[colId] } if (Object.keys(propertiesPayload).length > 0) { await fetch(`/api/notes/${createdNote.id}/properties`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: propertiesPayload }) }) } } // 5. Update block attributes to point to the new notebook updateAttributes({ notebookId: newNotebook.id, isLocal: false, localColumnsJson: '[]', localRowsJson: '[]' }) await refreshNotebooks() toast.success(t('structuredViewBlock.convertSuccess')) setShowConvertModal(false) } catch (e) { console.error(e) toast.error(e instanceof Error ? e.message : t('structuredViewBlock.convertGenericError')) } finally { setConverting(false) } } // Dynamic colors for select pill badges const getOptionColor = useCallback((text: string) => { if (!text) return { style: { backgroundColor: 'rgba(128, 128, 128, 0.1)', color: '#888' } } let hash = 0 for (let i = 0; i < text.length; i++) { hash = text.charCodeAt(i) + ((hash << 5) - hash) } const h = Math.abs(hash) % 360 return { style: { backgroundColor: `hsla(${h}, 45%, 50%, 0.12)`, color: `hsla(${h}, 75%, 60%, 1)`, border: `1px solid hsla(${h}, 50%, 50%, 0.2)` } } }, []) // Fetch echo connections for a local row by search (simulated similarity) const handleToggleLocalEcho = async (rowId: string, titleVal: string) => { if (activeEchoRowId === rowId) { setActiveEchoRowId(null) setEchoConnections([]) setEchoError(null) return } setActiveEchoRowId(rowId) setEchoLoading(true) setEchoConnections([]) setEchoError(null) 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] } try { const q = (titleVal || '').trim() if (!q) { setEchoConnections([]) setEchoError(t('structuredViewBlock.echoNameRequired')) setEchoLoading(false) return } const res = await fetch(`/api/notes?search=${encodeURIComponent(q)}&limit=5`) const json = await res.json() let data = (json.success && Array.isArray(json.data)) ? json.data : [] if (data.length === 0) { // Fallback to searching significant words if full title matches nothing 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)) { data = jsonFallback.data } } } if (data.length === 0) { setEchoError(t('structuredViewBlock.echoNoMatch', { query: q })) } else { setEchoConnections(data.map((n: any, idx: number) => ({ noteId: n.id, title: n.title || t('structuredViewBlock.untitled'), similarity: Math.round((0.88 - idx * 0.04) * 100), updatedAt: n.updatedAt, isTextMatch: true }))) } } catch (e) { console.error(e) setEchoError(t('structuredViewBlock.echoSearchError')) } finally { setEchoLoading(false) } } // Real-time analytics calculation for local/ notebook modes const analyticsData = useMemo(() => { if (isLocal) { if (localRows.length === 0) return null const total = localRows.length const checkboxStats: Array<{ colName: string; percentage: number; count: number }> = [] localColumns.forEach(col => { if (col.type === 'checkbox') { const checked = localRows.filter(r => !!r.values[col.id]).length checkboxStats.push({ colName: col.name, percentage: Math.round((checked / total) * 100), count: checked }) } }) const selectStats: Array<{ colName: string; distribution: Record }> = [] localColumns.forEach(col => { if (col.type === 'select') { const dist: Record = {} localRows.forEach(r => { const val = r.values[col.id] || '' if (val) { dist[val] = (dist[val] || 0) + 1 } }) selectStats.push({ colName: col.name, distribution: dist }) } }) return { total, checkboxStats, selectStats } } else { if (notes.length === 0 || !schemaHook.schema) return null const total = notes.length const props = schemaHook.schema.properties || [] const checkboxStats: Array<{ colName: string; percentage: number; count: number }> = [] const selectStats: Array<{ colName: string; distribution: Record }> = [] props.forEach(p => { if (p.type === 'checkbox') { let checked = 0 notes.forEach(n => { const noteVals = schemaHook.noteValues[n.id] || {} if (noteVals[p.id] === true || noteVals[p.id] === 'true') { checked++ } }) checkboxStats.push({ colName: p.name, percentage: Math.round((checked / total) * 100), count: checked }) } else if (p.type === 'select') { const dist: Record = {} notes.forEach(n => { const noteVals = schemaHook.noteValues[n.id] || {} const val = (noteVals[p.id] as string) || '' if (val) { dist[val] = (dist[val] || 0) + 1 } }) selectStats.push({ colName: p.name, distribution: dist }) } }) return { total, checkboxStats, selectStats } } }, [isLocal, localRows, localColumns, notes, schemaHook.schema, schemaHook.noteValues]) const renderAnalyticsPanel = () => { if (!analyticsData || !showAnalytics) return null const { total, checkboxStats, selectStats } = analyticsData const hasCheckboxStats = checkboxStats.length > 0 const hasSelectStats = selectStats.some(s => Object.keys(s.distribution).length > 0) if (total === 0 || (!hasCheckboxStats && !hasSelectStats)) { return (
{t('structuredViewBlock.analyticsNoData')}
) } return (
{t('structuredViewBlock.analyticsTitle')}
{t('structuredViewBlock.analyticsTotalRows')}: {total}
{/* Checkboxes completion rates */} {hasCheckboxStats && (
{t('structuredViewBlock.analyticsCompletion')}
{checkboxStats.map(stat => (
{stat.colName} {stat.percentage}% ({stat.count}/{total})
))}
)} {/* Select dropdowns distribution */} {hasSelectStats && (
{t('structuredViewBlock.analyticsDistribution')}
{selectStats.map(stat => { const distKeys = Object.keys(stat.distribution) if (distKeys.length === 0) return null return (
{stat.colName}
{distKeys.map(val => { const count = distKeys ? stat.distribution[val] : 0 const pct = Math.round((count / total) * 100) const badgeStyle = getOptionColor(val).style return (
{val} ({count} - {pct}%)
) })}
) })}
)}
) } const isRTL = language === 'fa' || language === 'ar' // ==================================================== // LOCAL DATABASE RENDERING (Mode A) // ==================================================== if (isLocal) { return (
{/* Header toolbar */}
{t('structuredViewBlock.localDbTitle')}
{/* Analytics toggle */} {/* Convert button */} {/* Switch to notebook view */}
{/* Analytics Insights Panel */} {renderAnalyticsPanel()} {/* Modal for conversion */} {showConvertModal && (

{t('structuredViewBlock.convertModalTitle')}

{t('structuredViewBlock.convertModalDesc')}

setNewNotebookName(e.target.value)} className="w-full rounded-lg border border-border bg-background px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary" />
)} {/* Interactive Local Table */}
{localColumns.map((col) => ( ))} {localRows.map((row) => ( {/* Delete and Echo row handlers */} {/* Render editable values for each column */} {localColumns.map((col) => { const cellVal = row.values[col.id]; const badgeStyle = col.type === 'select' && cellVal ? getOptionColor(cellVal).style : undefined; return ( ); })} {/* Memory Echo Collapsible detail row */} {activeEchoRowId === row.id && ( )} ))}
{/* Editable column header name */} updateLocalColumnName(col.id, e.target.value)} onKeyDown={(e) => e.stopPropagation()} className="bg-transparent border-0 outline-none font-bold text-[10px] uppercase tracking-widest text-muted-foreground focus:text-foreground focus:bg-background/50 rounded px-1 w-24 transition-colors" /> {/* Column type badge/selector */} {col.id !== 'col-title' && ( )} {/* Display select options configurations inline */} {col.type === 'select' && ( updateLocalColumnOptions(col.id, e.target.value)} onKeyDown={(e) => e.stopPropagation()} className="text-[9px] text-muted-foreground bg-transparent border-b border-dashed border-border/60 outline-none focus:border-foreground max-w-[120px] px-1" /> )} {/* Delete column button */} {col.id !== 'col-title' && ( )}
{col.type === 'checkbox' ? (
updateLocalCellValue(row.id, col.id, e.target.checked)} onKeyDown={(e) => e.stopPropagation()} className="rounded border-border text-primary focus:ring-primary cursor-pointer w-4 h-4" />
) : col.type === 'select' ? (
) : ( updateLocalCellValue(row.id, col.id, e.target.value)} onKeyDown={(e) => { e.stopPropagation() if (e.key === 'Enter') { e.preventDefault() addLocalRow() } }} className="w-full bg-transparent border-0 border-b border-border/10 focus:border-primary/40 focus:bg-background/20 outline-none px-2 py-1 text-xs text-foreground/90 transition-all rounded font-medium" /> )}
{t('structuredViewBlock.echoPopoverTitle')}
{echoLoading ? (
{t('structuredViewBlock.echoLoading')}
) : echoError ? (

{echoError}

{t('structuredViewBlock.echoUpgradeText')}
) : echoConnections.length === 0 ? (

{t('structuredViewBlock.noEchoFound')}

{t('structuredViewBlock.echoUpgradeText')}
) : (
{echoConnections.map((conn) => (
{conn.isTextMatch ? t('structuredViewBlock.keywordMatch') : `${conn.similarity}%`}
))}
)}
{localRows.length === 0 && (

{t('structuredViewBlock.emptyTable')}

)} {/* Add row button */}
); } // ==================================================== // CARNET LINKED VIEW RENDERING (Mode B) // ==================================================== // Scenario 3: No notebook associated - show interactive notebook selector if (!effectiveNotebookId) { return (
{t('structuredViewBlock.selectNotebook')}

{t('structuredViewBlock.noNotebookDesc')}

{t('structuredViewBlock.or')}
) } // Loading state const isLoading = schemaHook.loading || notesLoading if (isLoading) { return (
) } // Error state const hasError = schemaHook.error || notesError if (hasError) { return (
{schemaHook.error || notesError}
) } // Scenario 2: Notebook has no schema configured if (!schemaHook.schema) { return (
{currentNotebook?.name || t('structuredViewBlock.insertLabel')}

{t('structuredViewBlock.noSchema')}

) } // Normal view rendering return (
{/* Mini toolbar */}
{currentNotebook?.icon ? ( {currentNotebook.icon} ) : (
)} {currentNotebook?.name}
{/* View selector buttons */}
{/* Analytics toggle */} {/* Open Notebook link */} {/* Switch to local database option */} {/* Analytics Insights Panel */} {renderAnalyticsPanel()} {/* Structured views content wrapper */}
{}} onSetKanbanGroupProperty={() => {}} onDeleteProperty={async (propertyId) => { await schemaHook.deleteProperty(propertyId) }} />
) }