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
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import prisma from '@/lib/prisma'
|
|
import { auth } from '@/auth'
|
|
import { verifyParticipant } from '@/lib/brainstorm-collab'
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ sessionId: string }> }
|
|
) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const { sessionId } = await params
|
|
|
|
const brainstormSession = await prisma.brainstormSession.findFirst({
|
|
where: {
|
|
id: sessionId,
|
|
OR: [
|
|
{ userId: session.user.id },
|
|
{ participants: { some: { userId: session.user.id } } },
|
|
],
|
|
},
|
|
include: {
|
|
ideas: {
|
|
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
|
|
include: {
|
|
noteRefs: {
|
|
include: {
|
|
note: { select: { id: true, title: true } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!brainstormSession) {
|
|
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
|
|
}
|
|
|
|
if (brainstormSession.exportedNoteId) {
|
|
const existingNote = await prisma.note.findUnique({
|
|
where: { id: brainstormSession.exportedNoteId },
|
|
})
|
|
if (existingNote) {
|
|
return NextResponse.json({ success: true, data: existingNote })
|
|
}
|
|
}
|
|
|
|
const waveLabels: Record<number, string> = {
|
|
1: '🔄 Variations',
|
|
2: '🔗 Analogies',
|
|
3: '💥 Disruptions',
|
|
}
|
|
|
|
const activeIdeas = brainstormSession.ideas.filter(i => i.status !== 'dismissed')
|
|
const convertedIdeas = brainstormSession.ideas.filter(i => i.status === 'converted')
|
|
const dismissedCount = brainstormSession.ideas.filter(i => i.status === 'dismissed').length
|
|
|
|
let markdown = `# Brainstorm: ${brainstormSession.seedIdea}\n\n`
|
|
markdown += `> Generated on ${brainstormSession.createdAt.toLocaleDateString()}\n\n`
|
|
markdown += `---\n\n`
|
|
markdown += `**Summary**: ${activeIdeas.length} active ideas, ${convertedIdeas.length} converted to notes, ${dismissedCount} dismissed.\n\n`
|
|
|
|
for (let wave = 1; wave <= 3; wave++) {
|
|
const waveIdeas = activeIdeas.filter(i => i.waveNumber === wave)
|
|
if (waveIdeas.length === 0) continue
|
|
|
|
markdown += `## ${waveLabels[wave] || `Wave ${wave}`}\n\n`
|
|
for (const idea of waveIdeas) {
|
|
const statusIcon = idea.status === 'converted' ? '✅' : idea.status === 'dismissed' ? '❌' : '💡'
|
|
markdown += `### ${statusIcon} ${idea.title}\n`
|
|
markdown += `${idea.description}\n\n`
|
|
markdown += `- **Connection**: ${idea.connectionToSeed || 'N/A'}\n`
|
|
markdown += `- **Novelty**: ${idea.noveltyScore || 'N/A'}/10\n`
|
|
|
|
const validRefs = (idea.noteRefs || []).filter(r => r.noteId && r.note)
|
|
if (validRefs.length > 0) {
|
|
markdown += `- **Origin**:\n`
|
|
for (const ref of validRefs) {
|
|
const relLabel = {
|
|
derived_from: 'Derived from',
|
|
opposes: 'Opposes',
|
|
extends: 'Extends',
|
|
synthesizes: 'Synthesizes',
|
|
transposes: 'Transposes',
|
|
}[ref.relation] || 'Related to'
|
|
markdown += ` - ${relLabel} [${ref.note?.title || 'Untitled'}](note:${ref.noteId}): ${ref.explanation}\n`
|
|
}
|
|
}
|
|
|
|
if (idea.convertedToNoteId) {
|
|
markdown += `- **→ Converted to note**: ${idea.convertedToNoteId}\n`
|
|
}
|
|
if (idea.parentIdeaId) {
|
|
const parent = brainstormSession.ideas.find(i => i.id === idea.parentIdeaId)
|
|
if (parent) {
|
|
markdown += `- **Parent idea**: ${parent.title}\n`
|
|
}
|
|
}
|
|
markdown += `\n`
|
|
}
|
|
}
|
|
|
|
const allReferencedNoteIds = new Set<string>()
|
|
for (const idea of brainstormSession.ideas) {
|
|
for (const ref of idea.noteRefs || []) {
|
|
if (ref.noteId) allReferencedNoteIds.add(ref.noteId)
|
|
}
|
|
}
|
|
|
|
if (allReferencedNoteIds.size > 0) {
|
|
markdown += `---\n\n## Notes sollicitées\n\n`
|
|
const refNotes = await prisma.note.findMany({
|
|
where: { id: { in: Array.from(allReferencedNoteIds) } },
|
|
select: { id: true, title: true },
|
|
})
|
|
for (const note of refNotes) {
|
|
const refsForNote = brainstormSession.ideas.flatMap(i =>
|
|
(i.noteRefs || []).filter(r => r.noteId === note.id)
|
|
)
|
|
const accepted = refsForNote.filter(r => r.verdict === 'accepted').length
|
|
const dismissed = refsForNote.filter(r => r.verdict === 'dismissed').length
|
|
const verdictStr = accepted > 0 ? `✅ ${accepted} idea(s) accepted` : dismissed > 0 ? `❌ all dismissed` : '⏳ unresolved'
|
|
markdown += `- [${note.title || 'Untitled'}](note:${note.id}) — ${verdictStr}\n`
|
|
}
|
|
markdown += `\n`
|
|
}
|
|
|
|
if (convertedIdeas.length > 0) {
|
|
markdown += `## Converted Notes\n\n`
|
|
const convertedNoteIds = convertedIdeas
|
|
.map(i => i.convertedToNoteId)
|
|
.filter(Boolean) as string[]
|
|
|
|
if (convertedNoteIds.length > 0) {
|
|
const notes = await prisma.note.findMany({
|
|
where: { id: { in: convertedNoteIds } },
|
|
select: { id: true, title: true },
|
|
})
|
|
for (const note of notes) {
|
|
markdown += `- [${note.title || 'Untitled'}](note:${note.id})\n`
|
|
}
|
|
}
|
|
}
|
|
|
|
const notebookName = brainstormSession.seedIdea.length > 40
|
|
? brainstormSession.seedIdea.substring(0, 40).trim() + '…'
|
|
: brainstormSession.seedIdea
|
|
|
|
let notebook = await prisma.notebook.findFirst({
|
|
where: { userId: session.user.id, name: notebookName, trashedAt: null },
|
|
})
|
|
|
|
if (!notebook) {
|
|
const notebookCount = await prisma.notebook.count({ where: { userId: session.user.id, trashedAt: null } })
|
|
notebook = await prisma.notebook.create({
|
|
data: {
|
|
userId: session.user.id,
|
|
name: notebookName,
|
|
order: notebookCount,
|
|
icon: 'wind',
|
|
},
|
|
})
|
|
}
|
|
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
userId: session.user.id,
|
|
title: `Synthèse: ${brainstormSession.seedIdea.slice(0, 50)}`,
|
|
content: markdown,
|
|
type: 'markdown',
|
|
labels: JSON.stringify(['brainstorm', 'export']),
|
|
notebookId: notebook.id,
|
|
},
|
|
})
|
|
|
|
await prisma.brainstormSession.update({
|
|
where: { id: sessionId },
|
|
data: { exportedNoteId: note.id },
|
|
})
|
|
|
|
return NextResponse.json({ success: true, data: { ...note, _notebookName: notebookName } }, { status: 201 })
|
|
} catch (error) {
|
|
console.error('Error exporting brainstorm:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to export brainstorm' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|