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:
130
memento-note/lib/brainstorm-collab.ts
Normal file
130
memento-note/lib/brainstorm-collab.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export async function verifyParticipant(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
requiredRole?: 'host' | 'editor' | 'viewer'
|
||||
): Promise<{ isParticipant: boolean; role: string }> {
|
||||
const participant = await prisma.brainstormParticipant.findFirst({
|
||||
where: { sessionId, userId },
|
||||
})
|
||||
|
||||
if (!participant) {
|
||||
return { isParticipant: false, role: 'none' }
|
||||
}
|
||||
|
||||
await prisma.brainstormParticipant.update({
|
||||
where: { id: participant.id },
|
||||
data: { lastSeenAt: new Date() },
|
||||
})
|
||||
|
||||
if (requiredRole === 'host' && participant.role !== 'host') {
|
||||
return { isParticipant: false, role: participant.role }
|
||||
}
|
||||
if (requiredRole === 'editor' && participant.role === 'viewer') {
|
||||
return { isParticipant: false, role: participant.role }
|
||||
}
|
||||
|
||||
return { isParticipant: true, role: participant.role }
|
||||
}
|
||||
|
||||
export async function logActivity(
|
||||
sessionId: string,
|
||||
action: string,
|
||||
userId?: string | null,
|
||||
details?: Record<string, any>
|
||||
) {
|
||||
await prisma.brainstormActivity.create({
|
||||
data: {
|
||||
sessionId,
|
||||
userId: userId || null,
|
||||
action,
|
||||
details: details ? JSON.stringify(details) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// [UPDATE - SÉCURITÉ] Résoudre l'userId et le périmètre de notes autorisé pour les appels IA.
|
||||
// - Hôte : accès complet à ses notes (publicNoteIds = null)
|
||||
// - Invité : restreint aux contextNoteIds publics de la session (publicNoteIds = string[] | [])
|
||||
export async function resolveAiContextUserId(
|
||||
sessionId: string,
|
||||
requestingUserId: string
|
||||
): Promise<{ aiUserId: string; isGuest: boolean; publicNoteIds: string[] | null }> {
|
||||
const session = await prisma.brainstormSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: {
|
||||
userId: true,
|
||||
contextNoteIds: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!session) throw new Error('Session not found')
|
||||
|
||||
const isHost = session.userId === requestingUserId
|
||||
if (isHost) {
|
||||
return { aiUserId: requestingUserId, isGuest: false, publicNoteIds: null }
|
||||
}
|
||||
|
||||
// Invité : on restreint aux contextNoteIds déclarés publics par l'hôte
|
||||
const publicNoteIds: string[] = session.contextNoteIds
|
||||
? (JSON.parse(session.contextNoteIds) as string[])
|
||||
: []
|
||||
|
||||
return {
|
||||
aiUserId: session.userId,
|
||||
isGuest: true,
|
||||
publicNoteIds: publicNoteIds.length > 0 ? publicNoteIds : [],
|
||||
}
|
||||
}
|
||||
|
||||
// [UPDATE - SÉCURITÉ] Sanitize les notes injectées dans un prompt IA pour un invité.
|
||||
// Tronque le contenu et masque les entités nommées (Prénom Nom) avec [Person].
|
||||
export function sanitizeNotesForGuest(
|
||||
notes: { id: string; title: string | null; summary: string }[]
|
||||
): { id: string; title: string; summary: string }[] {
|
||||
const namedEntityRe = /\b[A-ZÀ-Ü][a-zà-ü]+ [A-ZÀ-Ü][a-zà-ü]+\b/g
|
||||
return notes.map(n => ({
|
||||
id: n.id,
|
||||
title: (n.title || 'Note').replace(namedEntityRe, '[Person]'),
|
||||
summary: n.summary.slice(0, 80).replace(namedEntityRe, '[Person]') + '…',
|
||||
}))
|
||||
}
|
||||
|
||||
export async function captureSnapshot(
|
||||
sessionId: string,
|
||||
label: string,
|
||||
activityId?: string
|
||||
): Promise<void> {
|
||||
const ideas = await prisma.brainstormIdea.findMany({
|
||||
where: { sessionId, status: 'active' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
waveNumber: true,
|
||||
positionX: true,
|
||||
positionY: true,
|
||||
parentIdeaId: true,
|
||||
noveltyScore: true,
|
||||
createdByType: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
|
||||
})
|
||||
|
||||
const maxStep = await prisma.brainstormSnapshot.findFirst({
|
||||
where: { sessionId },
|
||||
orderBy: { step: 'desc' },
|
||||
select: { step: true },
|
||||
})
|
||||
|
||||
await prisma.brainstormSnapshot.create({
|
||||
data: {
|
||||
sessionId,
|
||||
activityId: activityId || null,
|
||||
step: (maxStep?.step || 0) + 1,
|
||||
label,
|
||||
ideaGraph: JSON.stringify(ideas),
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user