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
94 lines
2.7 KiB
TypeScript
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>
|
|
)
|
|
}
|