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:
@@ -25,6 +25,10 @@ import { deleteNote, toggleArchive, togglePin, updateNote } from '@/app/actions/
|
||||
import { ReminderDialog } from '@/components/reminder-dialog'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { toast } from 'sonner'
|
||||
import { fr } from 'date-fns/locale/fr'
|
||||
import { enUS } from 'date-fns/locale/en-US'
|
||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type NotesEditorialViewProps = {
|
||||
notes: Note[]
|
||||
@@ -33,13 +37,16 @@ type NotesEditorialViewProps = {
|
||||
onOpenHistory?: (note: Note) => void
|
||||
}
|
||||
|
||||
function formatNoteDate(date: Date | string): string {
|
||||
function formatNoteDate(date: Date | string, language: string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).toUpperCase()
|
||||
const locale = language === 'fr' ? fr : enUS
|
||||
if (language === 'fa') {
|
||||
return formatAbsoluteDateLocalized(d, language, 'd MMM yyyy', locale)
|
||||
}
|
||||
const month = d.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' })
|
||||
const day = d.getUTCDate()
|
||||
const year = d.getUTCFullYear()
|
||||
return `${month.toUpperCase()} ${day}, ${year}`
|
||||
}
|
||||
|
||||
function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
||||
@@ -113,27 +120,27 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuItem onClick={e => { e.stopPropagation(); onOpen(note) }}>
|
||||
<Pencil className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<Pencil className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{t('notes.open') || 'Ouvrir'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handlePin}>
|
||||
<Pin className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<Pin className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{note.isPinned ? (t('notes.unpin') || 'Désépingler') : (t('notes.pin') || 'Épingler')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleArchive}>
|
||||
<Archive className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<Archive className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{note.isArchived ? (t('notes.unarchive') || 'Désarchiver') : (t('notes.archive') || 'Archiver')}
|
||||
</DropdownMenuItem>
|
||||
{onOpenHistory && (
|
||||
<DropdownMenuItem onClick={e => { e.stopPropagation(); onOpenHistory(note) }}>
|
||||
<History className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<History className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{t('notes.history') || 'Historique'}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* Rappel */}
|
||||
<DropdownMenuItem onClick={e => { e.stopPropagation(); setShowReminder(true) }}>
|
||||
<Bell className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<Bell className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{note.reminder
|
||||
? (t('reminder.changeReminder') || 'Modifier le rappel')
|
||||
: (t('reminder.setReminder') || 'Définir un rappel')}
|
||||
@@ -142,17 +149,17 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
||||
{/* Déplacer vers un carnet */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger onClick={e => e.stopPropagation()}>
|
||||
<FolderOpen className="h-4 w-4 mr-2 text-foreground/50" />
|
||||
<FolderOpen className="h-4 w-4 me-2 text-foreground/50" />
|
||||
{t('notebookSuggestion.moveToNotebook') || 'Déplacer vers…'}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-52">
|
||||
<DropdownMenuItem onClick={e => { e.stopPropagation(); handleMoveToNotebook(null) }}>
|
||||
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold mr-2 shrink-0">N</span>
|
||||
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold me-2 shrink-0">N</span>
|
||||
{t('notebookSuggestion.generalNotes') || 'Notes générales'}
|
||||
</DropdownMenuItem>
|
||||
{notebooks.map((nb: any) => (
|
||||
<DropdownMenuItem key={nb.id} onClick={e => { e.stopPropagation(); handleMoveToNotebook(nb.id) }}>
|
||||
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold mr-2 shrink-0">{nb.name.charAt(0).toUpperCase()}</span>
|
||||
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold me-2 shrink-0">{nb.name.charAt(0).toUpperCase()}</span>
|
||||
{nb.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -161,7 +168,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleDelete} className="text-destructive focus:text-destructive focus:bg-destructive/10">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
<Trash2 className="h-4 w-4 me-2" />
|
||||
{t('notes.delete') || 'Supprimer'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -252,7 +259,7 @@ function EditorialThumbnail({
|
||||
type="button"
|
||||
aria-label={t('notes.generateIllustration') || 'Générer une illustration IA'}
|
||||
title={t('notes.generateIllustration') || 'Générer une illustration IA'}
|
||||
className="absolute bottom-2 right-2 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background/95 text-foreground shadow-card-rest backdrop-blur-sm transition-colors hover:bg-accent z-10 opacity-0 group-hover/thumb:opacity-100 md:opacity-100 focus-visible:opacity-100"
|
||||
className="absolute bottom-2 end-2 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background/95 text-foreground shadow-card-rest backdrop-blur-sm transition-colors hover:bg-accent z-10 opacity-0 group-hover/thumb:opacity-100 md:opacity-100 focus-visible:opacity-100"
|
||||
onClick={handleGenerateSvg}
|
||||
disabled={busy}
|
||||
>
|
||||
@@ -333,7 +340,7 @@ export function NotesEditorialView({
|
||||
notebookName,
|
||||
onOpenHistory,
|
||||
}: NotesEditorialViewProps) {
|
||||
const { t } = useLanguage()
|
||||
const { t, language } = useLanguage()
|
||||
const { data: session } = useSession()
|
||||
const { data: allLabels } = useLabelsQuery()
|
||||
const [aiIllustrationEnabled, setAiIllustrationEnabled] = useState(false)
|
||||
@@ -354,7 +361,8 @@ export function NotesEditorialView({
|
||||
{notes.map((note: Note, index: number) => {
|
||||
const title = getNoteDisplayTitle(note, t('notes.untitled') || 'Untitled')
|
||||
const excerpt = getNotePlainExcerpt(note)
|
||||
const dateStr = formatNoteDate(note.createdAt)
|
||||
const dateStr = formatNoteDate(note.createdAt, language)
|
||||
const editorialRtl = language === 'fa' || language === 'ar'
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
@@ -365,14 +373,31 @@ export function NotesEditorialView({
|
||||
className="space-y-4 group cursor-pointer relative pb-8"
|
||||
onClick={() => onOpen(note)}
|
||||
>
|
||||
{/* Date / breadcrumb */}
|
||||
<div className="note-date-badge">
|
||||
{notebookName ? `${notebookName} — ${dateStr}` : dateStr}
|
||||
{/* Date / breadcrumb — isolated bidi so Latin notebook name + Jalali date don’t reorder wrongly */}
|
||||
<div
|
||||
className={cn('note-date-badge', editorialRtl && 'note-date-badge--locale-rtl')}
|
||||
dir={editorialRtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{notebookName ? (
|
||||
<>
|
||||
<bdi className={cn(editorialRtl && 'uppercase tracking-[0.2em]')}>{notebookName}</bdi>
|
||||
<span className="mx-1.5 select-none text-muted-foreground/80" aria-hidden>
|
||||
—
|
||||
</span>
|
||||
<bdi dir="rtl" lang={editorialRtl ? 'fa' : undefined}>
|
||||
{dateStr}
|
||||
</bdi>
|
||||
</>
|
||||
) : (
|
||||
<bdi dir={editorialRtl ? 'rtl' : 'ltr'} lang={editorialRtl ? 'fa' : undefined}>
|
||||
{dateStr}
|
||||
</bdi>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions menu — absolutely positioned at top-right */}
|
||||
<div
|
||||
className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="absolute top-0 end-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<EditorialNoteMenu note={note} onOpen={onOpen} onOpenHistory={onOpenHistory} />
|
||||
|
||||
Reference in New Issue
Block a user