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:
@@ -385,6 +385,85 @@ export class SemanticSearchService {
|
||||
await Promise.allSettled(batch.map(noteId => this.indexNote(noteId)))
|
||||
}
|
||||
}
|
||||
|
||||
async searchWithDocuments(
|
||||
userId: string,
|
||||
query: string,
|
||||
options?: SearchOptions & { noteId?: string; includeDocuments?: boolean }
|
||||
): Promise<(SearchResult & { source?: 'note' | 'document'; pageNumber?: number; fileName?: string })[]> {
|
||||
const includeDocuments = options?.includeDocuments !== false
|
||||
const noteResults = await this.searchAsUser(userId, query, options)
|
||||
|
||||
if (!includeDocuments) return noteResults
|
||||
|
||||
const queryEmbedding = await embeddingService.generateEmbedding(query)
|
||||
const vectorStr = embeddingService.toVectorString(queryEmbedding.embedding)
|
||||
|
||||
let noteFilter = ''
|
||||
const params: any[] = [vectorStr, 50, userId]
|
||||
|
||||
if (options?.noteId) {
|
||||
assertSafeId(options.noteId, 'noteId')
|
||||
params.push(options.noteId)
|
||||
noteFilter = `AND na."noteId" = $${params.length}`
|
||||
} else if (options?.notebookId) {
|
||||
assertSafeId(options.notebookId, 'notebookId')
|
||||
params.push(options.notebookId)
|
||||
noteFilter = `AND n."notebookId" = $${params.length}`
|
||||
}
|
||||
|
||||
const documentResults = await prisma.$queryRawUnsafe(
|
||||
`SELECT
|
||||
dc.content,
|
||||
dc."pageNumber",
|
||||
na."fileName",
|
||||
na."noteId",
|
||||
n.title as "noteTitle"
|
||||
FROM "DocumentChunk" dc
|
||||
JOIN "NoteAttachment" na ON na.id = dc."attachmentId"
|
||||
JOIN "Note" n ON n.id = na."noteId"
|
||||
WHERE dc."embedding" IS NOT NULL
|
||||
AND na.status = 'ready'
|
||||
AND n."trashedAt" IS NULL
|
||||
AND n."userId" = $3
|
||||
${noteFilter}
|
||||
ORDER BY dc."embedding" <=> $1::vector
|
||||
LIMIT $2`,
|
||||
...params
|
||||
) as any[]
|
||||
|
||||
const K = 60
|
||||
const fused = new Map<string, any>()
|
||||
|
||||
for (let i = 0; i < noteResults.length; i++) {
|
||||
const r = noteResults[i]
|
||||
fused.set(r.noteId, {
|
||||
...r,
|
||||
source: 'note',
|
||||
rrfScore: 1 / (K + i + 1),
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < documentResults.length; i++) {
|
||||
const r = documentResults[i]
|
||||
const key = `doc_${r.noteId}_${r.pageNumber}_${i}`
|
||||
fused.set(key, {
|
||||
noteId: r.noteId,
|
||||
title: `${r.noteTitle || 'Untitled'} → ${r.fileName} (p.${r.pageNumber})`,
|
||||
content: r.content.substring(0, 500),
|
||||
score: 0.5,
|
||||
matchType: 'related' as const,
|
||||
source: 'document',
|
||||
pageNumber: r.pageNumber,
|
||||
fileName: r.fileName,
|
||||
rrfScore: 1 / (K + i + 1),
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(fused.values())
|
||||
.sort((a, b) => b.rrfScore - a.rrfScore)
|
||||
.slice(0, options?.limit || 20)
|
||||
}
|
||||
}
|
||||
|
||||
export const semanticSearchService = new SemanticSearchService()
|
||||
|
||||
Reference in New Issue
Block a user