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 }
|
||||
}
|
||||
Reference in New Issue
Block a user