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

@@ -15,16 +15,24 @@ export interface EmbeddingResult {
export class EmbeddingService {
private readonly EMBEDDING_DIMENSION = 1536
private readonly MAX_CHARS = 15000
private truncateForEmbedding(text: string): string {
if (text.length <= this.MAX_CHARS) return text
return text.slice(0, this.MAX_CHARS)
}
async generateEmbedding(text: string): Promise<EmbeddingResult> {
if (!text || text.trim().length === 0) {
throw new Error('Cannot generate embedding for empty text')
}
const truncated = this.truncateForEmbedding(text)
try {
const config = await getSystemConfig()
const embedding = await withAiProviderFallback('embedding', config, (provider) =>
provider.getEmbeddings(text)
provider.getEmbeddings(truncated)
)
return {
@@ -41,7 +49,7 @@ export class EmbeddingService {
async generateBatchEmbeddings(texts: string[]): Promise<EmbeddingResult[]> {
if (!texts || texts.length === 0) return []
const validTexts = texts.filter(t => t && t.trim().length > 0)
const validTexts = texts.filter(t => t && t.trim().length > 0).map(t => this.truncateForEmbedding(t))
if (validTexts.length === 0) return []
try {

View File

@@ -77,14 +77,14 @@ export class MemoryEchoService {
for (const note of notesWithoutEmbeddings) {
if (!note.content || note.content.trim().length === 0) continue
try {
const embedding = await provider.getEmbeddings(note.content)
const embedding = await provider.getEmbeddings(note.content.slice(0, 15000))
if (embedding && embedding.length > 0) {
const vecStr = `[${embedding.join(',')}]`
await prisma.$executeRawUnsafe(
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2::vector, now(), now())
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt")
VALUES (gen_random_uuid(), $1, $2::vector, now())
ON CONFLICT ("noteId")
DO UPDATE SET "embedding" = $2::vector, "updatedAt" = now()`,
DO UPDATE SET "embedding" = $2::vector`,
note.id,
vecStr
)

View File

@@ -286,11 +286,15 @@ Only return the translated text, nothing else.`,
Keep the explanation clear, educational, and concise.`
}
const systemSuffix = mode === 'explain'
? `CRITICAL: Respond ONLY with your explanation in ${language}. No meta-commentary, no English. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
: `CRITICAL: Respond ONLY with the refactored text in ${language}. No explanations, no meta-commentary, no English. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
return `${instructions[mode]}
${content}
CRITICAL: Respond ONLY with the refactored text in ${language}. No explanations, no meta-commentary, no English. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
${systemSuffix}`
}
/**

View File

@@ -235,15 +235,15 @@ export class SemanticSearchService {
const rows: Array<{ noteId: string; similarity: number }> = await prisma.$queryRawUnsafe(
`SELECT n.id AS "noteId",
1 - (e."embedding" <=> ${vecParam}) AS similarity
1 - (e."embedding"::vector <=> ${vecParam}) AS similarity
FROM "Note" n
INNER JOIN "NoteEmbedding" e ON e."noteId" = n.id
WHERE n."trashedAt" IS NULL
AND n."isArchived" = false
${userClause}
${notebookClause}
AND 1 - (e."embedding" <=> ${vecParam}) >= ${thresholdParam}
ORDER BY e."embedding" <=> ${vecParam} ASC
AND 1 - (e."embedding"::vector <=> ${vecParam}) >= ${thresholdParam}
ORDER BY e."embedding"::vector <=> ${vecParam} ASC
LIMIT ${limitParam}`,
...params
)
@@ -356,10 +356,10 @@ export class SemanticSearchService {
const vecStr = embeddingService.toVectorString(embedding)
await prisma.$queryRawUnsafe(
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2::vector, now(), now())
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt")
VALUES (gen_random_uuid(), $1, $2::vector, now())
ON CONFLICT ("noteId")
DO UPDATE SET "embedding" = $2::vector, "updatedAt" = now()`,
DO UPDATE SET "embedding" = $2::vector`,
noteId,
vecStr
)
@@ -427,7 +427,7 @@ export class SemanticSearchService {
AND n."trashedAt" IS NULL
AND n."userId" = $3
${noteFilter}
ORDER BY dc."embedding" <=> $1::vector
ORDER BY dc."embedding"::vector <=> $1::vector
LIMIT $2`,
...params
) as any[]