fix: brainstorm infinite loop, ghost cursor, embedding ::vector cast, semantic search, billing stats, usage meter accordion
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
- Fix useBrainstormSocket: stable guestId via useRef, remove setState in cleanup - Fix GhostCursor: direct DOM manipulation via refs, no useState re-renders - Fix all SQL embedding queries: add ::vector cast on text columns - Fix embedding truncation to 15000 chars (under 8192 token limit) - Fix NoteEmbedding INSERT: remove non-existent updatedAt column - Fix billing page: show all quota stats in grid instead of single metric - Fix usage meter: accordion expand/collapse, per-feature detail - Fix semantic search: rebuild 103 note embeddings, ::vector cast on vectorSearch - Fix brainstorm expand/manual-idea/create: ::vector cast on embedding SQL
This commit is contained in:
@@ -213,7 +213,7 @@ export function BrainstormPage() {
|
||||
setConvertToast({ noteTitle: result.title || idea.title, noteId: result.id })
|
||||
setTimeout(() => {
|
||||
setConvertToast(null)
|
||||
router.push(`/?openNote=${result.id}`)
|
||||
router.push(`/home?openNote=${result.id}`)
|
||||
}, 2000)
|
||||
}
|
||||
} catch {}
|
||||
@@ -228,7 +228,7 @@ export function BrainstormPage() {
|
||||
setExportToast({ noteTitle: result.title || t('brainstorm.exportDefaultNoteTitle'), notebookName })
|
||||
setTimeout(() => {
|
||||
setExportToast(null)
|
||||
router.push(`/?openNote=${result.id}`)
|
||||
router.push(`/home?openNote=${result.id}`)
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
@@ -634,7 +634,7 @@ export function BrainstormPage() {
|
||||
</div>
|
||||
{ref.noteId && (
|
||||
<button
|
||||
onClick={() => router.push(`/?openNote=${ref.noteId}`)}
|
||||
onClick={() => router.push(`/home?openNote=${ref.noteId}`)}
|
||||
className="shrink-0 px-2 py-1 text-[9px] font-bold uppercase tracking-wider rounded-lg bg-foreground/5 hover:bg-foreground/10 text-muted-foreground hover:text-foreground transition-all"
|
||||
>
|
||||
{t('brainstorm.viewNote') || 'View'}
|
||||
|
||||
@@ -137,8 +137,8 @@ export function BrainstormShareDialog({
|
||||
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-foreground">
|
||||
<div className="w-7 h-7 rounded-lg bg-orange-500/10 flex items-center justify-center">
|
||||
<UserPlus size={14} className="text-orange-500" />
|
||||
<div className="w-7 h-7 rounded-lg bg-brand-accent/10 flex items-center justify-center">
|
||||
<UserPlus size={14} className="text-brand-accent" />
|
||||
</div>
|
||||
<span className="font-serif">{t('brainstorm.shareDialogTitle')}</span>
|
||||
</DialogTitle>
|
||||
@@ -159,7 +159,7 @@ export function BrainstormShareDialog({
|
||||
onFocus={() => results.length > 0 && setShowDropdown(true)}
|
||||
onBlur={() => setTimeout(() => setShowDropdown(false), 150)}
|
||||
placeholder={t('brainstorm.shareNameOrEmailPlaceholder')}
|
||||
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500/40 transition-all"
|
||||
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/40 transition-all"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -173,9 +173,9 @@ export function BrainstormShareDialog({
|
||||
e.preventDefault()
|
||||
selectUser(user)
|
||||
}}
|
||||
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-orange-500/5 transition-colors text-left"
|
||||
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-brand-accent/5 transition-colors text-left"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-orange-500/10 flex items-center justify-center text-xs font-bold text-orange-600 shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-accent/10 flex items-center justify-center text-xs font-bold text-brand-accent shrink-0">
|
||||
{(user.name || user.email).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -214,7 +214,7 @@ export function BrainstormShareDialog({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!query.trim() || isPending}
|
||||
className="w-full py-3 bg-orange-500 hover:bg-orange-600 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
|
||||
className="w-full py-3 bg-brand-accent hover:bg-brand-accent/90 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<UserPlus size={12} />
|
||||
{isPending ? t('brainstorm.shareSubmitting') : t('brainstorm.shareSubmit')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
|
||||
interface GhostCursorProps {
|
||||
@@ -10,114 +10,132 @@ interface GhostCursorProps {
|
||||
}
|
||||
|
||||
export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorProps) {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
const [visible, setVisible] = useState(false)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const positionRef = useRef({ x: 0, y: 0 })
|
||||
const visibleRef = useRef(false)
|
||||
const elRef = useRef<HTMLDivElement>(null)
|
||||
const rafRef = useRef<number | null>(null)
|
||||
const targetRef = useRef({ x: 0, y: 0 })
|
||||
const initializedRef = useRef(false)
|
||||
const targetIdRef = useRef(targetId)
|
||||
targetIdRef.current = targetId
|
||||
const isActiveRef = useRef(isActive)
|
||||
isActiveRef.current = isActive
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setVisible(false)
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
visibleRef.current = false
|
||||
initializedRef.current = false
|
||||
if (elRef.current) elRef.current.style.opacity = '0'
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
setVisible(true)
|
||||
|
||||
// Initialisation sécurisée
|
||||
const initialRect = container.getBoundingClientRect()
|
||||
const initialCx = initialRect.width > 0 ? initialRect.width / 2 : window.innerWidth / 2
|
||||
const initialCy = initialRect.height > 0 ? initialRect.height / 2 : window.innerHeight / 2
|
||||
|
||||
setPosition({
|
||||
x: initialCx + (Math.random() - 0.5) * 200,
|
||||
y: initialCy + (Math.random() - 0.5) * 200,
|
||||
})
|
||||
|
||||
let angle = Math.random() * Math.PI * 2
|
||||
let targetX = initialCx + Math.cos(angle) * 250
|
||||
let targetY = initialCy + Math.sin(angle) * 250
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
// Recalculer les dimensions à chaque tick pour s'adapter aux redimensionnements
|
||||
const currentContainer = containerRef.current
|
||||
if (!currentContainer) return
|
||||
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)
|
||||
|
||||
const tick = () => {
|
||||
if (!isActiveRef.current || !initializedRef.current) {
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
return
|
||||
}
|
||||
|
||||
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 containerRect = currentContainer.getBoundingClientRect()
|
||||
const cx = containerRect.width / 2
|
||||
const cy = containerRect.height / 2
|
||||
let tx = targetRef.current.x
|
||||
let ty = targetRef.current.y
|
||||
|
||||
let currentTargetX = targetX;
|
||||
let currentTargetY = targetY;
|
||||
|
||||
if (targetId) {
|
||||
const nodeElement = document.querySelector(`[data-id="${targetId}"]`);
|
||||
const currentTargetId = targetIdRef.current
|
||||
if (currentTargetId) {
|
||||
const nodeElement = container.querySelector(`[data-id="${currentTargetId}"]`) as HTMLElement | null
|
||||
if (nodeElement) {
|
||||
const nodeRect = nodeElement.getBoundingClientRect();
|
||||
currentTargetX = nodeRect.left - containerRect.left + nodeRect.width / 2;
|
||||
currentTargetY = nodeRect.top - containerRect.top + nodeRect.height / 2;
|
||||
const nodeRect = nodeElement.getBoundingClientRect()
|
||||
tx = nodeRect.left - containerRect.left + nodeRect.width / 2
|
||||
ty = nodeRect.top - containerRect.top + nodeRect.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(prev => {
|
||||
const dx = currentTargetX - prev.x
|
||||
const dy = currentTargetY - prev.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const prev = positionRef.current
|
||||
const dx = tx - prev.x
|
||||
const dy = ty - prev.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (!targetId && dist < 20) {
|
||||
angle = Math.random() * Math.PI * 2
|
||||
const radius = 150 + Math.random() * 200
|
||||
targetX = cx + Math.cos(angle) * radius
|
||||
targetY = cy + Math.sin(angle) * radius
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
const speed = targetId ? 0.15 : 0.06;
|
||||
|
||||
let newX = prev.x + dx * speed;
|
||||
let newY = prev.y + dy * speed;
|
||||
|
||||
// Protection Anti-NaN qui bloquait le curseur en haut à gauche
|
||||
if (isNaN(newX)) newX = cx;
|
||||
if (isNaN(newY)) newY = cy;
|
||||
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
|
||||
|
||||
return {
|
||||
x: newX,
|
||||
y: newY,
|
||||
}
|
||||
})
|
||||
}, 50)
|
||||
positionRef.current = { x: newX, y: newY }
|
||||
|
||||
if (elRef.current) {
|
||||
elRef.current.style.transform = `translate(${newX}px, ${newY}px)`
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
}, [isActive, containerRef, targetId])
|
||||
}, [isActive, containerRef])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
className="absolute pointer-events-none z-50"
|
||||
style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
|
||||
>
|
||||
<div className="relative">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill="#a78bfa" />
|
||||
</svg>
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-50" />
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-violet-500" />
|
||||
</div>
|
||||
<div className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg bg-gradient-to-r from-violet-500 to-purple-600">
|
||||
AI ✦
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div
|
||||
ref={elRef}
|
||||
className="absolute pointer-events-none z-50"
|
||||
style={{
|
||||
left: 0,
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill="#a78bfa" />
|
||||
</svg>
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-50" />
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-violet-500" />
|
||||
</div>
|
||||
<div className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg bg-gradient-to-r from-violet-500 to-purple-600">
|
||||
AI ✦
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user