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

- 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:
Antigravity
2026-05-14 17:43:21 +00:00
parent 195e845f0a
commit 1fcea6ed7d
228 changed files with 57656 additions and 1059 deletions

View File

@@ -0,0 +1,107 @@
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'
export interface PresenceUser {
userId: string
name: string
cursor: { x: number; y: number } | null
color: string
}
export interface ActivityEvent {
action: string
userId: string
userName: string
details: any
}
const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001'
export function useBrainstormSocket(
sessionId: string | null,
userId: string | null,
userName: string | null,
onIdeaMoved?: (data: { ideaId: string; positionX: number; positionY: number }) => void
) {
const socketRef = useRef<Socket | null>(null)
const onIdeaMovedRef = useRef(onIdeaMoved)
onIdeaMovedRef.current = onIdeaMoved
const [others, setOthers] = useState<PresenceUser[]>([])
const [activities, setActivities] = useState<ActivityEvent[]>([])
const [aiProcessingNodeId, setAiProcessingNodeId] = useState<string | null>(null)
const effectiveUserId = userId || `guest_${Math.random().toString(36).slice(2, 10)}`
useEffect(() => {
if (!sessionId) return
const socket = io(SOCKET_URL, {
auth: {
userId: effectiveUserId,
sessionId,
name: userName || 'Guest',
isGuest: !userId,
},
transports: ['websocket'],
autoConnect: true,
})
socketRef.current = socket
socket.on('presence:update', (users: PresenceUser[]) => {
setOthers(users.filter(u => u.userId !== effectiveUserId))
})
socket.on('cursor:update', (data: { userId: string; cursor: { x: number; y: number } }) => {
setOthers(prev => prev.map(u =>
u.userId === data.userId ? { ...u, cursor: data.cursor } : u
))
})
socket.on('activity:new', (event: ActivityEvent) => {
setActivities(prev => [event, ...prev].slice(0, 50))
})
socket.on('idea:added', () => {})
socket.on('idea:dismissed', () => {})
socket.on('idea:moved', (data: { ideaId: string; positionX: number; positionY: number }) => {
onIdeaMovedRef.current?.(data)
})
socket.on('idea:ai_processing', (data: { ideaId: string }) => {
setAiProcessingNodeId(data.ideaId)
})
socket.on('idea:ai_completed', (data: { ideaId: string }) => {
setAiProcessingNodeId(prev => prev === data.ideaId ? null : prev)
})
socket.on('idea:ai_failed', (data: { ideaId: string }) => {
setAiProcessingNodeId(prev => prev === data.ideaId ? null : prev)
})
return () => {
socket.disconnect()
socketRef.current = null
setOthers([])
setAiProcessingNodeId(null)
}
}, [sessionId, effectiveUserId, userName])
const moveCursor = useCallback((cursor: { x: number; y: number } | null) => {
socketRef.current?.emit('cursor:move', cursor)
}, [])
const broadcastActivity = useCallback((action: string, details?: any) => {
socketRef.current?.emit('activity:new', {
action,
userId: effectiveUserId,
userName: userName || 'Guest',
details,
})
}, [effectiveUserId, userName])
return { others, activities, moveCursor, broadcastActivity, socketRef, aiProcessingNodeId }
}

View File

@@ -0,0 +1,316 @@
'use client'
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/lib/query-keys'
import type { BrainstormSession, BrainstormSessionListItem } from '@/types/brainstorm'
export interface CreateBrainstormResult {
session: BrainstormSession
contextSummary?: {
support: number
tension: number
extension: number
}
}
export function useBrainstormSessions() {
return useQuery({
queryKey: queryKeys.brainstormSessions(),
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
const res = await fetch('/api/brainstorm', { credentials: 'include' })
const data = await res.json()
return data.data || []
},
})
}
export function useSharedBrainstormSessions() {
return useQuery({
queryKey: queryKeys.brainstormSharedSessions(),
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
const res = await fetch('/api/brainstorm/shared', { credentials: 'include' })
const data = await res.json()
return data.data || []
},
})
}
export interface BrainstormSessionMeta {
role: 'owner' | 'editor' | 'viewer' | 'guest' | 'none'
canEdit: boolean
}
export interface BrainstormSessionResult {
session: BrainstormSession
meta?: BrainstormSessionMeta
}
export function useBrainstormSession(sessionId: string | null) {
return useQuery({
queryKey: queryKeys.brainstormSession(sessionId || ''),
queryFn: async (): Promise<BrainstormSessionResult | null> => {
if (!sessionId) return null
const res = await fetch(`/api/brainstorm/${sessionId}`, { credentials: 'include' })
const data = await res.json()
if (!data.data) return null
return {
session: data.data,
meta: data._meta || undefined,
}
},
enabled: !!sessionId,
})
}
export function useCreateBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: {
seedIdea: string
sourceNoteId?: string
contextNoteIds?: string[]
locale?: string
}): Promise<CreateBrainstormResult> => {
const res = await fetch('/api/brainstorm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to create brainstorm')
return { session: data.data, contextSummary: data.contextSummary }
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useExpandIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { ideaId: string; locale?: string }): Promise<BrainstormSession> => {
const res = await fetch(`/api/brainstorm/${sessionId}/expand`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to expand idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useDismissIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (ideaId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}/dismiss`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to dismiss idea')
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useConvertIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (ideaId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to convert idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useExportBrainstorm(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
const res = await fetch(`/api/brainstorm/${sessionId}/export`, {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to export brainstorm')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useFinalizeBrainstorm(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
const res = await fetch(`/api/brainstorm/${sessionId}/finalize`, {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to finalize brainstorm')
return data.impact
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useDeleteBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (sessionId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}`, {
method: 'DELETE',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to delete brainstorm')
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useInviteParticipant(sessionId: string) {
return useMutation({
mutationFn: async (input: { role: 'editor' | 'viewer'; expiresInHours?: number; email?: string }) => {
const res = await fetch(`/api/brainstorm/${sessionId}/invite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to create invite')
return data
},
})
}
export function useJoinBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (token: string) => {
const res = await fetch(`/api/brainstorm/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to join')
return data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useAddManualIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { title: string; description?: string; parentIdeaId?: string; locale?: string }) => {
const res = await fetch(`/api/brainstorm/${sessionId}/manual-idea`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to add idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useBrainstormActivity(sessionId: string | null) {
return useQuery({
queryKey: ['brainstorm', 'activity', sessionId],
queryFn: async () => {
if (!sessionId) return []
const res = await fetch(`/api/brainstorm/${sessionId}/activity`, { credentials: 'include' })
const data = await res.json()
return data.data || []
},
enabled: !!sessionId,
refetchInterval: 10000,
})
}
export function useUpdateBrainstormSettings(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { isPublic?: boolean; guestCanEdit?: boolean }) => {
const res = await fetch(`/api/brainstorm/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to update settings')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useBrainstormSnapshots(sessionId: string | null) {
return useQuery({
queryKey: ['brainstorm', 'snapshots', sessionId],
queryFn: async () => {
if (!sessionId) return []
const res = await fetch(`/api/brainstorm/${sessionId}/snapshots`, { credentials: 'include' })
const data = await res.json()
return data.data || []
},
enabled: !!sessionId,
})
}