feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export - Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG) - Add document Q&A overlay with streaming chat and PDF preview - Add note attachments UI with status polling, grid layout, and auto-scroll - Add task extraction AI tool and agent executor improvements - Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings - Fix brainstorm 'Create Note' button: add success toast and redirect to created note - Fix memory echo notification infinite polling - Fix chat route to always include document_search tool - Add brainstorm i18n keys across all 14 locales - Add socket server for real-time brainstorm collaboration - Add hierarchical notebook selector and organize notebook dialog improvements - Add sidebar brainstorm section with session management - Update prisma schema with brainstorm tables, attachments, and document chunks
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Note } from '@/lib/types'
|
||||
import { format, formatDistanceToNow } from 'date-fns'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale/fr'
|
||||
import { enUS } from 'date-fns/locale/en-US'
|
||||
import { faIR } from 'date-fns/locale/fa-IR'
|
||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||
import { X, Info, Clock, Hash, Book, FileText, Calendar, Tag, ChevronRight, Trash2, RotateCcw, Loader2, Check, History as HistoryIcon } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -24,7 +26,9 @@ interface NoteDocumentInfoPanelProps {
|
||||
}
|
||||
|
||||
function getLocale(lang: string) {
|
||||
return lang === 'fr' ? fr : enUS
|
||||
if (lang === 'fr') return fr
|
||||
if (lang === 'fa') return faIR
|
||||
return enUS
|
||||
}
|
||||
|
||||
function wordCount(text: string) {
|
||||
@@ -35,13 +39,6 @@ function charCount(text: string) {
|
||||
return text.replace(/<[^>]+>/g, '').length
|
||||
}
|
||||
|
||||
const noteTypeLabel: Record<string, string> = {
|
||||
richtext: 'Rich Text',
|
||||
markdown: 'Markdown',
|
||||
text: 'Texte',
|
||||
checklist: 'Liste de tâches',
|
||||
}
|
||||
|
||||
export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }: NoteDocumentInfoPanelProps) {
|
||||
const { t, language } = useLanguage()
|
||||
const { notebooks } = useNotebooks()
|
||||
@@ -56,6 +53,16 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
const [isRestoring, setIsRestoring] = useState<string | null>(null)
|
||||
const locale = getLocale(language)
|
||||
|
||||
const displayNoteType = useMemo(() => {
|
||||
const map: Record<string, string> = {
|
||||
richtext: t('notes.noteTypes.richtext'),
|
||||
markdown: t('notes.noteTypes.markdown'),
|
||||
text: t('notes.noteTypes.text'),
|
||||
checklist: t('notes.noteTypes.checklist'),
|
||||
}
|
||||
return map[note.type] || note.type
|
||||
}, [t, note.type])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'versions' && historyEnabled) {
|
||||
loadHistory()
|
||||
@@ -75,7 +82,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
}
|
||||
|
||||
const handleDeleteVersion = async (entryId: string) => {
|
||||
if (!confirm('Supprimer cette version ?')) return
|
||||
if (!confirm(t('documentInfo.deleteVersionConfirm'))) return
|
||||
setIsDeleting(entryId)
|
||||
try {
|
||||
await deleteNoteHistoryEntry(note.id, entryId)
|
||||
@@ -131,7 +138,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
>
|
||||
{tab === 'info' && <Info className="h-3 w-3" />}
|
||||
{tab === 'versions' && <Clock className="h-3 w-3" />}
|
||||
{tab === 'info' ? 'Info' : 'Versions'}
|
||||
{tab === 'info' ? t('documentInfo.tabInfo') : t('documentInfo.tabVersions')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -153,11 +160,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="grid grid-cols-2 border-b border-border/30">
|
||||
<div className="flex flex-col items-center gap-1 py-6 border-r border-border/30">
|
||||
<span className="text-4xl font-bold font-memento-serif tabular-nums tracking-tight">{words}</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">mots</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">{t('documentInfo.wordsLabel')}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 py-6">
|
||||
<span className="text-4xl font-bold font-memento-serif tabular-nums tracking-tight">{chars}</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">caractères</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">{t('documentInfo.charactersLabel')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +173,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Book className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Carnet</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.notebookLabel')}</p>
|
||||
<p className="text-sm font-medium">{notebook.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,8 +182,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<FileText className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Type</p>
|
||||
<p className="text-sm font-medium">{noteTypeLabel[note.type] || note.type}</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.typeLabel')}</p>
|
||||
<p className="text-sm font-medium">{displayNoteType}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,8 +191,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Calendar className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Créée le</p>
|
||||
<p className="text-sm font-medium">{format(createdAt, 'd MMM yyyy', { locale })}</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.createdLabel')}</p>
|
||||
<p className="text-sm font-medium">{formatAbsoluteDateLocalized(createdAt, language, 'd MMM yyyy', locale)}</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{formatDistanceToNow(createdAt, { addSuffix: true, locale })}
|
||||
</p>
|
||||
@@ -197,8 +204,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Clock className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Modifiée</p>
|
||||
<p className="text-sm font-medium">{format(updatedAt, 'd MMM yyyy · HH:mm', { locale })}</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.modifiedLabel')}</p>
|
||||
<p className="text-sm font-medium">{formatAbsoluteDateLocalized(updatedAt, language, 'd MMM yyyy · HH:mm', locale)}</p>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{formatDistanceToNow(updatedAt, { addSuffix: true, locale })}
|
||||
</p>
|
||||
@@ -210,7 +217,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Tag className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-1.5">Labels</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-1.5">{t('documentInfo.labelsSection')}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(note.labels ?? []).map(label => (
|
||||
<LabelBadge key={label} label={label} />
|
||||
@@ -223,7 +230,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Hash className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">ID</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.idLabel')}</p>
|
||||
<p className="text-[11px] text-muted-foreground font-mono truncate">{note.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +244,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
{!historyEnabled ? (
|
||||
<div className="text-center py-6 space-y-3">
|
||||
<Clock className="h-8 w-8 text-muted-foreground/30 mx-auto" />
|
||||
<p className="text-sm text-muted-foreground">L'historique n'est pas activé pour cette note.</p>
|
||||
<p className="text-sm text-muted-foreground">{t('documentInfo.historyDisabled')}</p>
|
||||
<button
|
||||
className="text-xs px-4 py-2 rounded-lg bg-foreground text-background font-medium hover:opacity-80 transition-opacity"
|
||||
onClick={async () => {
|
||||
@@ -246,12 +253,12 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
setShowHistory(true)
|
||||
}}
|
||||
>
|
||||
Activer l'historique
|
||||
{t('documentInfo.enableHistory')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">Versions sauvegardées</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">{t('documentInfo.savedVersions')}</p>
|
||||
|
||||
{/* Save version button */}
|
||||
<button
|
||||
@@ -278,11 +285,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
}}
|
||||
>
|
||||
{isSavingVersion ? (
|
||||
<><Loader2 className="h-3.5 w-3.5 animate-spin" />Sauvegarde…</>
|
||||
<><Loader2 className="h-3.5 w-3.5 animate-spin" />{t('documentInfo.savingEllipsis')}</>
|
||||
) : versionSaved ? (
|
||||
<><Check className="h-3.5 w-3.5" /> Version sauvegardée !</>
|
||||
<><Check className="h-3.5 w-3.5" /> {t('documentInfo.versionSaved')}</>
|
||||
) : (
|
||||
<>Sauvegarder cette version</>
|
||||
<>{t('documentInfo.saveThisVersion')}</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -292,15 +299,15 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
{isLoadingHistory && historyEntries.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 opacity-40">
|
||||
<Loader2 className="h-6 w-6 animate-spin mb-2" />
|
||||
<p className="text-[10px] uppercase tracking-widest">Chargement...</p>
|
||||
<p className="text-[10px] uppercase tracking-widest">{t('documentInfo.loading')}</p>
|
||||
</div>
|
||||
) : historyEntries.length === 0 ? (
|
||||
<div className="text-center py-8 opacity-40 border border-dashed rounded-xl">
|
||||
<Clock className="h-6 w-6 mx-auto mb-2" />
|
||||
<p className="text-[10px] uppercase tracking-widest">Aucune version</p>
|
||||
<p className="text-[10px] uppercase tracking-widest">{t('documentInfo.noVersion')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative pl-6 space-y-6 before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-px before:bg-border/40">
|
||||
<div className="relative ps-6 space-y-6 before:absolute before:start-[11px] before:top-2 before:bottom-2 before:w-px before:bg-border/40">
|
||||
{historyEntries.map((entry, idx) => {
|
||||
const colors = ['#E2E8F0', '#ACB995', '#E9ECEF']
|
||||
const dotColor = colors[idx % colors.length]
|
||||
@@ -310,7 +317,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div key={entry.id} className="relative group">
|
||||
{/* Dot */}
|
||||
<div
|
||||
className="absolute -left-[19px] top-1.5 h-3 w-3 rounded-full border-2 border-background z-10 shadow-sm"
|
||||
className="absolute -start-[19px] top-1.5 h-3 w-3 rounded-full border-2 border-background z-10 shadow-sm"
|
||||
style={{ backgroundColor: dotColor }}
|
||||
/>
|
||||
|
||||
@@ -319,7 +326,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold font-mono">v{entry.version}</span>
|
||||
{isLatest && (
|
||||
<span className="text-[9px] px-1.5 py-0.5 rounded-md bg-primary/10 text-primary font-bold uppercase tracking-widest">Latest</span>
|
||||
<span className="text-[9px] px-1.5 py-0.5 rounded-md bg-primary/10 text-primary font-bold uppercase tracking-widest">{t('documentInfo.latestBadge')}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -328,7 +335,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
onClick={() => handleRestoreVersion(entry.id)}
|
||||
disabled={!!isRestoring || !!isDeleting}
|
||||
className="p-1.5 rounded-lg hover:bg-primary/10 text-muted-foreground hover:text-primary transition-colors"
|
||||
title="Restaurer"
|
||||
title={t('documentInfo.restoreTooltip')}
|
||||
>
|
||||
{isRestoring === entry.id ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCcw className="h-3 w-3" />}
|
||||
</button>
|
||||
@@ -336,7 +343,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
onClick={() => handleDeleteVersion(entry.id)}
|
||||
disabled={!!isRestoring || !!isDeleting}
|
||||
className="p-1.5 rounded-lg hover:bg-red-500/10 text-muted-foreground hover:text-red-500 transition-colors"
|
||||
title="Supprimer"
|
||||
title={t('documentInfo.deleteTooltip')}
|
||||
>
|
||||
{isDeleting === entry.id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
|
||||
</button>
|
||||
@@ -344,7 +351,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground font-medium">
|
||||
{format(new Date(entry.createdAt), 'd MMM · HH:mm', { locale })}
|
||||
{formatAbsoluteDateLocalized(new Date(entry.createdAt), language, 'd MMM · HH:mm', locale)}
|
||||
<span className="mx-1.5 opacity-30">·</span>
|
||||
{formatDistanceToNow(new Date(entry.createdAt), { addSuffix: true, locale })}
|
||||
</p>
|
||||
@@ -357,7 +364,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
|
||||
{/* Button to open the full modal (optional, but good to keep if user wants diff) */}
|
||||
<button
|
||||
className="w-full flex items-center justify-between p-3 rounded-xl border border-border/40 hover:bg-muted/50 transition-colors text-left group mt-4"
|
||||
className="w-full flex items-center justify-between p-3 rounded-xl border border-border/40 hover:bg-muted/50 transition-colors text-start group mt-4"
|
||||
onClick={() => setShowHistory(true)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -365,11 +372,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
<HistoryIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-wider">Mode Comparaison</p>
|
||||
<p className="text-[10px] text-muted-foreground">Comparer les versions côte à côte</p>
|
||||
<p className="text-xs font-bold uppercase tracking-wider">{t('documentInfo.comparisonMode')}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{t('documentInfo.comparisonSubtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground rtl:scale-x-[-1]" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user