diff --git a/memento-note/components/brainstorm/ghost-cursor.tsx b/memento-note/components/brainstorm/ghost-cursor.tsx index 4dffc5d..d262d1c 100644 --- a/memento-note/components/brainstorm/ghost-cursor.tsx +++ b/memento-note/components/brainstorm/ghost-cursor.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useEffect, useRef } from 'react' -import { motion, AnimatePresence } from 'motion/react' interface GhostCursorProps { isActive: boolean @@ -10,12 +9,9 @@ interface GhostCursorProps { } export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorProps) { - const positionRef = useRef({ x: 0, y: 0 }) - const visibleRef = useRef(false) const elRef = useRef(null) const rafRef = useRef(null) - const targetRef = useRef({ x: 0, y: 0 }) - const initializedRef = useRef(false) + const posRef = useRef<{ x: number; y: number; visible: boolean }>({ x: 0, y: 0, visible: false }) const targetIdRef = useRef(targetId) targetIdRef.current = targetId const isActiveRef = useRef(isActive) @@ -23,52 +19,34 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro useEffect(() => { if (!isActive) { - visibleRef.current = false - initializedRef.current = false + posRef.current.visible = false if (elRef.current) elRef.current.style.opacity = '0' if (rafRef.current) cancelAnimationFrame(rafRef.current) return } - let angle = Math.random() * Math.PI * 2 - - const init = () => { - const container = containerRef.current - if (!container) { rafRef.current = requestAnimationFrame(init); return } - const rect = container.getBoundingClientRect() - if (rect.width === 0 || rect.height === 0) { rafRef.current = requestAnimationFrame(init); return } - - const cx = rect.width / 2 - const cy = rect.height / 2 - targetRef.current = { x: cx + (Math.random() - 0.5) * 200, y: cy + (Math.random() - 0.5) * 200 } - positionRef.current = { ...targetRef.current } - initializedRef.current = true - visibleRef.current = true - if (elRef.current) { - elRef.current.style.opacity = '1' - elRef.current.style.transform = `translate(${positionRef.current.x}px, ${positionRef.current.y}px)` - } - } - - rafRef.current = requestAnimationFrame(init) + let lastMove = 0 + const MOVE_INTERVAL = 120 const tick = () => { - if (!isActiveRef.current || !initializedRef.current) { + if (!isActiveRef.current) return + + const now = performance.now() + if (now - lastMove < MOVE_INTERVAL) { rafRef.current = requestAnimationFrame(tick) return } + lastMove = now const container = containerRef.current if (!container) { rafRef.current = requestAnimationFrame(tick); return } const containerRect = container.getBoundingClientRect() if (containerRect.width === 0 || containerRect.height === 0) { rafRef.current = requestAnimationFrame(tick); return } - const cx = containerRect.width / 2 - const cy = containerRect.height / 2 - let tx = targetRef.current.x - let ty = targetRef.current.y - const currentTargetId = targetIdRef.current + let tx: number | null = null + let ty: number | null = null + if (currentTargetId) { const nodeElement = container.querySelector(`[data-id="${currentTargetId}"]`) as HTMLElement | null if (nodeElement) { @@ -78,29 +56,34 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro } } - const prev = positionRef.current - const dx = tx - prev.x - const dy = ty - prev.y - const dist = Math.sqrt(dx * dx + dy * dy) - - if (!currentTargetId && dist < 20) { - angle = Math.random() * Math.PI * 2 - const radius = 150 + Math.random() * 200 - tx = cx + Math.cos(angle) * radius - ty = cy + Math.sin(angle) * radius - targetRef.current = { x: tx, y: ty } + if (tx === null || ty === null) { + if (posRef.current.visible) { + posRef.current.visible = false + if (elRef.current) elRef.current.style.opacity = '0' + } + rafRef.current = requestAnimationFrame(tick) + return } - const speed = currentTargetId ? 0.12 : 0.04 - let newX = prev.x + (tx - prev.x) * speed - let newY = prev.y + (ty - prev.y) * speed - if (isNaN(newX)) newX = cx - if (isNaN(newY)) newY = cy + const pos = posRef.current + const speed = 0.15 + const newX = pos.x + (tx - pos.x) * speed + const newY = pos.y + (ty - pos.y) * speed - positionRef.current = { x: newX, y: newY } - - if (elRef.current) { - elRef.current.style.transform = `translate(${newX}px, ${newY}px)` + if (!pos.visible) { + pos.x = tx + pos.y = ty + pos.visible = true + if (elRef.current) { + elRef.current.style.opacity = '1' + elRef.current.style.transform = `translate(${tx}px, ${ty}px)` + } + } else { + pos.x = isNaN(newX) ? tx : newX + pos.y = isNaN(newY) ? ty : newY + if (elRef.current) { + elRef.current.style.transform = `translate(${pos.x}px, ${pos.y}px)` + } } rafRef.current = requestAnimationFrame(tick) @@ -121,7 +104,7 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro left: 0, top: 0, opacity: 0, - transition: 'opacity 0.3s ease', + transition: 'opacity 0.4s ease, transform 0.15s ease-out', }} >
@@ -129,11 +112,10 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro
-
- AI ✦ + AI
diff --git a/memento-note/prisma/migrations/20260519120000_add_brainstorm_tables/migration.sql b/memento-note/prisma/migrations/20260519120000_add_brainstorm_tables/migration.sql new file mode 100644 index 0000000..a8ccecc --- /dev/null +++ b/memento-note/prisma/migrations/20260519120000_add_brainstorm_tables/migration.sql @@ -0,0 +1,159 @@ +CREATE TABLE "BrainstormSession" ( + "id" TEXT NOT NULL, + "seedIdea" TEXT NOT NULL, + "sourceNoteId" TEXT, + "contextNoteIds" TEXT, + "exportedNoteId" TEXT, + "userId" TEXT NOT NULL, + "inviteToken" TEXT, + "inviteExpiry" TIMESTAMP(3), + "liveblocksRoomId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "guestCanEdit" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BrainstormSession_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormIdea" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "waveNumber" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "connectionToSeed" TEXT, + "noveltyScore" INTEGER, + "parentIdeaId" TEXT, + "convertedToNoteId" TEXT, + "relatedNoteIds" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "positionX" DOUBLE PRECISION, + "positionY" DOUBLE PRECISION, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT, + "createdByType" TEXT DEFAULT 'ai', + + CONSTRAINT "BrainstormIdea_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormNoteRef" ( + "id" TEXT NOT NULL, + "ideaId" TEXT NOT NULL, + "noteId" TEXT, + "relation" TEXT NOT NULL, + "explanation" TEXT NOT NULL, + "verdict" TEXT NOT NULL DEFAULT 'unresolved', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "visibility" TEXT NOT NULL DEFAULT 'participants', + + CONSTRAINT "BrainstormNoteRef_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormParticipant" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'viewer', + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BrainstormParticipant_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormActivity" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT, + "action" TEXT NOT NULL, + "details" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BrainstormActivity_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormShare" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "sharedBy" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "permission" TEXT NOT NULL DEFAULT 'editor', + "notifiedAt" TIMESTAMP(3), + "respondedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BrainstormShare_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "BrainstormSnapshot" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "activityId" TEXT, + "step" INTEGER NOT NULL, + "label" TEXT, + "ideaGraph" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BrainstormSnapshot_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "BrainstormSession_inviteToken_key" ON "BrainstormSession"("inviteToken"); +CREATE INDEX "BrainstormSession_userId_idx" ON "BrainstormSession"("userId"); +CREATE INDEX "BrainstormSession_userId_createdAt_idx" ON "BrainstormSession"("userId", "createdAt"); +CREATE INDEX "BrainstormSession_inviteToken_idx" ON "BrainstormSession"("inviteToken"); +CREATE INDEX "BrainstormSession_isPublic_idx" ON "BrainstormSession"("isPublic"); + +CREATE INDEX "BrainstormIdea_sessionId_idx" ON "BrainstormIdea"("sessionId"); +CREATE INDEX "BrainstormIdea_waveNumber_idx" ON "BrainstormIdea"("waveNumber"); +CREATE INDEX "BrainstormIdea_status_idx" ON "BrainstormIdea"("status"); +CREATE INDEX "BrainstormIdea_parentIdeaId_idx" ON "BrainstormIdea"("parentIdeaId"); +CREATE INDEX "BrainstormIdea_sessionId_status_idx" ON "BrainstormIdea"("sessionId", "status"); +CREATE INDEX "BrainstormIdea_sessionId_waveNumber_createdAt_idx" ON "BrainstormIdea"("sessionId", "waveNumber", "createdAt"); + +CREATE INDEX "BrainstormNoteRef_ideaId_idx" ON "BrainstormNoteRef"("ideaId"); +CREATE INDEX "BrainstormNoteRef_noteId_idx" ON "BrainstormNoteRef"("noteId"); +CREATE INDEX "BrainstormNoteRef_noteId_relation_idx" ON "BrainstormNoteRef"("noteId", "relation"); +CREATE INDEX "BrainstormNoteRef_visibility_idx" ON "BrainstormNoteRef"("visibility"); + +CREATE UNIQUE INDEX "BrainstormParticipant_sessionId_userId_key" ON "BrainstormParticipant"("sessionId", "userId"); +CREATE INDEX "BrainstormParticipant_sessionId_idx" ON "BrainstormParticipant"("sessionId"); +CREATE INDEX "BrainstormParticipant_userId_idx" ON "BrainstormParticipant"("userId"); +CREATE INDEX "BrainstormParticipant_sessionId_userId_role_idx" ON "BrainstormParticipant"("sessionId", "userId", "role"); + +CREATE INDEX "BrainstormActivity_sessionId_createdAt_asc_idx" ON "BrainstormActivity"("sessionId", "createdAt"); +CREATE INDEX "BrainstormActivity_sessionId_createdAt_desc_idx" ON "BrainstormActivity"("sessionId", "createdAt" DESC); +CREATE INDEX "BrainstormActivity_sessionId_createdAt_idx" ON "BrainstormActivity"("sessionId", "createdAt"); + +CREATE UNIQUE INDEX "BrainstormShare_sessionId_userId_key" ON "BrainstormShare"("sessionId", "userId"); +CREATE INDEX "BrainstormShare_userId_idx" ON "BrainstormShare"("userId"); +CREATE INDEX "BrainstormShare_status_idx" ON "BrainstormShare"("status"); + +CREATE INDEX "BrainstormSnapshot_sessionId_step_idx" ON "BrainstormSnapshot"("sessionId", "step"); +CREATE INDEX "BrainstormSnapshot_sessionId_createdAt_idx" ON "BrainstormSnapshot"("sessionId", "createdAt"); +CREATE INDEX "BrainstormSnapshot_activityId_idx" ON "BrainstormSnapshot"("activityId"); + +ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_sourceNoteId_fkey" FOREIGN KEY ("sourceNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "BrainstormSession" ADD CONSTRAINT "BrainstormSession_exportedNoteId_fkey" FOREIGN KEY ("exportedNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_parentIdeaId_fkey" FOREIGN KEY ("parentIdeaId") REFERENCES "BrainstormIdea"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "BrainstormIdea" ADD CONSTRAINT "BrainstormIdea_convertedToNoteId_fkey" FOREIGN KEY ("convertedToNoteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "BrainstormNoteRef" ADD CONSTRAINT "BrainstormNoteRef_ideaId_fkey" FOREIGN KEY ("ideaId") REFERENCES "BrainstormIdea"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormNoteRef" ADD CONSTRAINT "BrainstormNoteRef_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "BrainstormParticipant" ADD CONSTRAINT "BrainstormParticipant_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormParticipant" ADD CONSTRAINT "BrainstormParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "BrainstormActivity" ADD CONSTRAINT "BrainstormActivity_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormActivity" ADD CONSTRAINT "BrainstormActivity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_sharedBy_fkey" FOREIGN KEY ("sharedBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BrainstormShare" ADD CONSTRAINT "BrainstormShare_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "BrainstormSnapshot" ADD CONSTRAINT "BrainstormSnapshot_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "BrainstormSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;