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

- 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:
Antigravity
2026-05-16 18:50:34 +00:00
parent ee8e2bda59
commit 8c7ca69640
117 changed files with 11732 additions and 834 deletions

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service'
import { getAISettings } from '@/app/actions/ai-settings'
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
export async function POST(request: NextRequest) {
try {
@@ -52,9 +53,27 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
// Check quota
try {
await checkEntitlementOrThrow(session.user.id, 'reformulate')
} catch (err) {
if (err instanceof QuotaExceededError) {
const isTierLocked = err.currentQuota === 0
return NextResponse.json({
error: isTierLocked ? 'feature_locked' : 'quota_exceeded',
errorKey: isTierLocked ? 'ai.featureLocked' : 'ai.quotaExceeded',
upgradeTier: err.upgradeTier,
quotaExceeded: true,
}, { status: 402 })
}
throw err
}
// Use the ParagraphRefactorService
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language)
incrementUsageAsync(session.user.id, 'reformulate')
return NextResponse.json({
originalText: result.original,
reformulatedText: result.refactored,

View File

@@ -67,7 +67,7 @@ async function getParentContext(
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL AND n.id NOT IN (${excludeList})
ORDER BY e.embedding <=> $2::vector
ORDER BY e.embedding::vector <=> $2::vector
LIMIT 5`,
hostUserId, vectorStr
) as any[]

View File

@@ -131,7 +131,7 @@ export async function POST(
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n.id IN (${idList}) AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $1::vector
ORDER BY e.embedding::vector <=> $1::vector
LIMIT 3`,
vectorStr
) as any[]
@@ -142,7 +142,7 @@ export async function POST(
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $2::vector
ORDER BY e.embedding::vector <=> $2::vector
LIMIT 3`,
aiUserId, vectorStr
) as any[]

View File

@@ -44,7 +44,7 @@ async function autoContextSearch(
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $2::vector
ORDER BY e.embedding::vector <=> $2::vector
LIMIT 8`,
userId, vectorStr
) as any[]

View File

@@ -146,7 +146,7 @@ export async function PUT(
})
// Revalidate to refresh UI
revalidatePath('/')
revalidatePath('/home')
return NextResponse.json({
success: true,
@@ -253,7 +253,7 @@ export async function DELETE(
})
// Revalidate to refresh UI
revalidatePath('/')
revalidatePath('/home')
return NextResponse.json({
success: true,

View File

@@ -6,7 +6,7 @@ export async function GET() {
name: "Memento Notes",
short_name: "Memento",
description: "A smart, local-first note taking app with AI capabilities.",
start_url: "/",
start_url: "/home",
display: "standalone",
background_color: "#F2F0E9",
theme_color: "#1C1C1C",

View File

@@ -98,7 +98,7 @@ export async function PATCH(
})
}
try { revalidatePath('/') } catch {}
try { revalidatePath('/home') } catch {}
return NextResponse.json({ success: true })
} catch (error) {
@@ -143,7 +143,7 @@ export async function DELETE(
await prisma.notebook.delete({ where: { id } })
try { revalidatePath('/') } catch {}
try { revalidatePath('/home') } catch {}
return NextResponse.json({
success: true,

View File

@@ -47,7 +47,7 @@ export async function POST(request: NextRequest) {
await prisma.$transaction(updates)
revalidatePath('/')
revalidatePath('/home')
return NextResponse.json({
success: true,

View File

@@ -105,7 +105,7 @@ export async function POST(request: NextRequest) {
}
})
try { revalidatePath('/') } catch {}
try { revalidatePath('/home') } catch {}
return NextResponse.json({
success: true,

View File

@@ -90,7 +90,7 @@ export async function POST(
console.error('[HISTORY] Failed to create snapshot after notebook move:', snapshotError)
}
// No revalidatePath('/') here — the client-side triggerRefresh() in
// No revalidatePath('/home') here — the client-side triggerRefresh() in
// notebooks-context.tsx handles the refresh. Avoiding server-side
// revalidation prevents a double-refresh (server + client).

View File

@@ -50,7 +50,7 @@ export async function POST(req: NextRequest) {
})
// Revalidate paths
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/data')
// Await cleanup in background (don't block response)

View File

@@ -222,7 +222,7 @@ export async function POST(req: NextRequest) {
}
}
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/data')
return NextResponse.json({ success: true, ...stats })

View File

@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { auth } from '@/auth';
import { getUserQuotas } from '@/lib/entitlements';
import { getUserQuotas, getEffectiveTier } from '@/lib/entitlements';
export async function GET() {
const session = await auth();
@@ -10,8 +10,11 @@ export async function GET() {
}
try {
const quotas = await getUserQuotas(session.user.id);
return NextResponse.json({ quotas });
const [quotas, tier] = await Promise.all([
getUserQuotas(session.user.id),
getEffectiveTier(session.user.id),
]);
return NextResponse.json({ quotas, tier });
} catch (error) {
console.error('[usage/current] Failed to fetch quotas:', error);
return NextResponse.json(

View File

@@ -8,6 +8,10 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if ((session.user as any)?.role !== 'ADMIN') {
return NextResponse.json({ users: [] })
}
const q = request.nextUrl.searchParams.get('q') || ''
if (q.length < 2) {
return NextResponse.json({ users: [] })