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:
73
memento-note/lib/ai/tools/document-search.tool.ts
Normal file
73
memento-note/lib/ai/tools/document-search.tool.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { toolRegistry } from './registry'
|
||||
import { embeddingService } from '@/lib/ai/services/embedding.service'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
toolRegistry.register({
|
||||
name: 'document_search',
|
||||
description: 'Search within PDF documents attached to notes. Returns relevant passages with page numbers and source document info.',
|
||||
isInternal: true,
|
||||
buildTool: (ctx) =>
|
||||
tool({
|
||||
description: `Search within PDF documents attached to the user's notes.
|
||||
Returns matching passages with page numbers, chunk content, and the source note/document info.
|
||||
Use this when the user asks about specific documents, PDFs, or attached files.`,
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('The search query to find relevant passages in documents'),
|
||||
noteId: z.string().optional().describe('Optional: restrict search to attachments of a specific note'),
|
||||
limit: z.number().optional().describe('Max results to return (default 5)').default(5),
|
||||
}),
|
||||
execute: async ({ query, noteId, limit = 5 }) => {
|
||||
try {
|
||||
const queryEmbedding = await embeddingService.generateEmbedding(query)
|
||||
const vectorStr = embeddingService.toVectorString(queryEmbedding.embedding)
|
||||
|
||||
let noteFilter = ''
|
||||
const params: any[] = [vectorStr, limit, ctx.userId]
|
||||
|
||||
if (noteId) {
|
||||
noteFilter = `AND na."noteId" = $4`
|
||||
params.push(noteId)
|
||||
}
|
||||
|
||||
const results = await prisma.$queryRawUnsafe(
|
||||
`SELECT
|
||||
dc.id as "chunkId",
|
||||
dc.content,
|
||||
dc."pageNumber",
|
||||
dc."chunkIndex",
|
||||
na.id as "attachmentId",
|
||||
na."fileName",
|
||||
na."pageCount",
|
||||
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[]
|
||||
|
||||
if (!results.length) return { results: [], message: 'No matching documents found' }
|
||||
|
||||
return results.map(r => ({
|
||||
content: r.content.substring(0, 600),
|
||||
pageNumber: r.pageNumber,
|
||||
chunkIndex: r.chunkIndex,
|
||||
fileName: r.fileName,
|
||||
noteId: r.noteId,
|
||||
noteTitle: r.noteTitle || 'Untitled',
|
||||
}))
|
||||
} catch (e: any) {
|
||||
return { error: `Document search failed: ${e.message}` }
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -13,6 +13,8 @@ import './memory.tool'
|
||||
import './excalidraw.tool'
|
||||
import './pptx.tool'
|
||||
import './slides.tool'
|
||||
import './document-search.tool'
|
||||
import './task-extract.tool'
|
||||
|
||||
// Re-export registry
|
||||
export { toolRegistry, type ToolContext, type RegisteredTool } from './registry'
|
||||
|
||||
@@ -60,7 +60,7 @@ const PALETTES: Record<string, Theme> = {
|
||||
coastal_coral: { primary: '005f73', secondary: '0a9396', accent: 'ee9b00', light: 'e9f5f5', bg: 'ffffff' },
|
||||
vibrant_orange_mint: { primary: 'e05c00', secondary: '2ec4b6', accent: 'ff9f1c', light: 'edfaf9', bg: 'ffffff' },
|
||||
platinum_white_gold: { primary: '0a0a0a', secondary: '404040', accent: 'c9a84c', light: 'f5f5f0', bg: 'ffffff' },
|
||||
architectural_mono: { primary: '1C1C1C', secondary: '75B2D6', accent: 'D4A373', light: 'EDE9DF', bg: 'F2F0E9' },
|
||||
architectural_mono: { primary: '1C1C1C', secondary: 'A47148', accent: 'D4A373', light: 'EDE9DF', bg: 'F2F0E9' },
|
||||
minimal_silk: { primary: '212529', secondary: '6c757d', accent: 'dee2e6', light: 'f8f9fa', bg: 'ffffff' },
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class ToolRegistry {
|
||||
* When webOnly is true, only web tools are included (no note access).
|
||||
*/
|
||||
buildToolsForChat(ctx: ToolContext & { webOnly?: boolean }): Record<string, any> {
|
||||
const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read']
|
||||
const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read', 'document_search', 'task_extract']
|
||||
|
||||
// Add web tools only when user toggled web search AND config is present
|
||||
if (ctx.webSearch) {
|
||||
|
||||
106
memento-note/lib/ai/tools/task-extract.tool.ts
Normal file
106
memento-note/lib/ai/tools/task-extract.tool.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { toolRegistry } from './registry'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
toolRegistry.register({
|
||||
name: 'task_extract',
|
||||
description: 'Extract action items (TODOs) from notes in a notebook. Reads all notes, identifies tasks with assignees and deadlines, and creates a synthesis note.',
|
||||
isInternal: true,
|
||||
buildTool: (ctx) =>
|
||||
tool({
|
||||
description: 'Extract action items from notes in a notebook. Creates a new note with all identified tasks.',
|
||||
inputSchema: z.object({
|
||||
notebookId: z.string().optional().describe('Notebook ID to scan. If omitted, scans all user notes.'),
|
||||
noteIds: z.array(z.string()).optional().describe('Specific note IDs to scan instead of a whole notebook.'),
|
||||
locale: z.string().optional().describe('Language for the output (fr, en, es, de, etc.)'),
|
||||
}),
|
||||
execute: async ({ notebookId, noteIds, locale }) => {
|
||||
try {
|
||||
let where: any = { userId: ctx.userId, trashedAt: null }
|
||||
if (noteIds && noteIds.length > 0) {
|
||||
where.id = { in: noteIds }
|
||||
} else if (notebookId) {
|
||||
where.notebookId = notebookId
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where,
|
||||
select: { id: true, title: true, content: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 50,
|
||||
})
|
||||
|
||||
if (notes.length === 0) {
|
||||
return { error: 'No notes found to analyze' }
|
||||
}
|
||||
|
||||
const notesContext = notes.map(n =>
|
||||
`[ID: ${n.id}] "${n.title}":\n${(n.content || '').slice(0, 800)}`
|
||||
).join('\n\n---\n\n')
|
||||
|
||||
const lang = locale === 'fr' ? 'français' : locale === 'es' ? 'espagnol' : locale === 'de' ? 'allemand' : locale === 'it' ? 'italien' : locale === 'pt' ? 'portugais' : locale === 'nl' ? 'néerlandais' : locale === 'ru' ? 'russe' : locale === 'zh' ? 'chinois' : locale === 'ja' ? 'japonais' : locale === 'ar' ? 'arabe' : locale === 'fa' ? 'persan' : locale === 'hi' ? 'hindi' : 'English'
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getTagsProvider(config)
|
||||
|
||||
const prompt = `You are a task extraction specialist. Analyze the following notes and extract ALL action items, tasks, and TODOs.
|
||||
|
||||
For each task identified, provide:
|
||||
- **Task**: Clear, actionable description
|
||||
- **Source**: The note title where it was found
|
||||
- **Assignee**: If mentioned (otherwise "Unassigned")
|
||||
- **Deadline**: If mentioned (otherwise "No deadline")
|
||||
- **Priority**: High/Medium/Low based on urgency signals in the text
|
||||
- **Status**: If already completed or in-progress based on context
|
||||
|
||||
NOTES TO ANALYZE:
|
||||
${notesContext}
|
||||
|
||||
Respond in ${lang}. Structure the output as a clean Markdown document with:
|
||||
1. A summary paragraph
|
||||
2. Tasks grouped by priority (High → Medium → Low)
|
||||
3. A summary table at the end
|
||||
|
||||
Format each task as:
|
||||
### [Priority] Task Title
|
||||
- **Description**: ...
|
||||
- **Source note**: ...
|
||||
- **Assignee**: ...
|
||||
- **Deadline**: ...
|
||||
- **Status**: ...`
|
||||
|
||||
const result = await provider.generateText(prompt)
|
||||
|
||||
const summaryTitle = locale === 'fr'
|
||||
? `Action Items — ${new Date().toLocaleDateString('fr-FR')}`
|
||||
: `Action Items — ${new Date().toLocaleDateString('en-US')}`
|
||||
|
||||
const createdNote = await prisma.note.create({
|
||||
data: {
|
||||
title: summaryTitle,
|
||||
content: result,
|
||||
type: 'markdown',
|
||||
isMarkdown: true,
|
||||
autoGenerated: true,
|
||||
userId: ctx.userId,
|
||||
notebookId: notebookId || null,
|
||||
},
|
||||
select: { id: true, title: true },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
noteId: createdNote.id,
|
||||
title: createdNote.title,
|
||||
notesAnalyzed: notes.length,
|
||||
tasksNoteUrl: `/notes/${createdNote.id}`,
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { error: `Task extraction failed: ${e.message}` }
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user