Files
Antigravity 1fcea6ed7d
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
- 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
2026-05-14 17:43:21 +00:00

94 lines
2.7 KiB
TypeScript

'use client'
import React from 'react'
import { PresenceUser } from '@/hooks/use-brainstorm-socket'
function Cursor({ x, y, name, color }: { x: number; y: number; name: string; color: string }) {
return (
<div
className="absolute pointer-events-none z-50 transition-all duration-75"
style={{ transform: `translate(${x}px, ${y}px)` }}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill={color} />
</svg>
<div
className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg"
style={{ backgroundColor: color }}
>
{name}
</div>
</div>
)
}
export function LiveCursors({ others }: { others: PresenceUser[] }) {
return (
<div className="absolute inset-0 pointer-events-none z-50">
{others.map((user) => {
if (!user.cursor) return null
return (
<Cursor
key={user.userId}
x={user.cursor.x}
y={user.cursor.y}
name={user.name}
color={user.color}
/>
)
})}
</div>
)
}
export function useCursorTracking(
containerRef: React.RefObject<HTMLDivElement | null>,
moveCursor: (cursor: { x: number; y: number } | null) => void
) {
React.useEffect(() => {
const container = containerRef.current
if (!container) return
const handleMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect()
moveCursor({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
const handleLeave = () => {
moveCursor(null)
}
container.addEventListener('mousemove', handleMove)
container.addEventListener('mouseleave', handleLeave)
return () => {
container.removeEventListener('mousemove', handleMove)
container.removeEventListener('mouseleave', handleLeave)
}
}, [containerRef, moveCursor])
}
export function PresenceAvatars({ others }: { others: PresenceUser[] }) {
if (others.length === 0) return null
return (
<div className="flex items-center gap-1">
{others.slice(0, 4).map((user) => {
const initial = (user.name || '?').charAt(0).toUpperCase()
return (
<div
key={user.userId}
className="w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-bold text-white border-2 border-white dark:border-zinc-900 shadow-sm"
style={{ backgroundColor: user.color, marginLeft: '-6px' }}
title={user.name}
>
{initial}
</div>
)
})}
{others.length > 4 && (
<span className="text-[9px] text-muted-foreground ml-1">+{others.length - 4}</span>
)}
</div>
)
}