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:
107
memento-note/hooks/use-brainstorm-socket.ts
Normal file
107
memento-note/hooks/use-brainstorm-socket.ts
Normal 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 }
|
||||
}
|
||||
316
memento-note/hooks/use-brainstorm.ts
Normal file
316
memento-note/hooks/use-brainstorm.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user