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

@@ -5,6 +5,7 @@ import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import bcrypt from 'bcryptjs'
import { z } from 'zod'
import { SubscriptionTier, SubscriptionStatus } from '@prisma/client'
// Schema pour la création d'utilisateur
const CreateUserSchema = z.object({
@@ -33,6 +34,13 @@ export async function getUsers() {
email: true,
role: true,
createdAt: true,
subscription: {
select: {
tier: true,
status: true,
currentPeriodEnd: true,
}
}
}
})
return users
@@ -118,3 +126,39 @@ export async function updateUserRole(userId: string, newRole: string) {
throw new Error('Failed to update role')
}
}
export async function updateUserSubscription(userId: string, tier: string) {
await checkAdmin()
const validTiers: string[] = ['BASIC', 'PRO', 'BUSINESS', 'ENTERPRISE']
if (!validTiers.includes(tier)) {
throw new Error('Invalid tier')
}
try {
const now = new Date()
const periodEnd = new Date(now)
periodEnd.setFullYear(periodEnd.getFullYear() + 1)
await prisma.subscription.upsert({
where: { userId },
update: {
tier: tier as SubscriptionTier,
status: 'ACTIVE',
currentPeriodStart: now,
currentPeriodEnd: periodEnd,
},
create: {
userId,
tier: tier as SubscriptionTier,
status: 'ACTIVE' as SubscriptionStatus,
currentPeriodStart: now,
currentPeriodEnd: periodEnd,
},
})
revalidatePath('/admin')
return { success: true }
} catch (error) {
throw new Error('Failed to update subscription')
}
}

View File

@@ -11,7 +11,7 @@ export async function authenticate(
await signIn('credentials', {
email: formData.get('email'),
password: formData.get('password'),
redirectTo: '/',
redirectTo: '/home',
});
} catch (error) {
if (error instanceof AuthError) {

View File

@@ -128,7 +128,7 @@ export async function respondToBrainstormShare(
})
}
revalidatePath('/')
revalidatePath('/home')
return { success: true }
}
@@ -193,6 +193,6 @@ export async function removeBrainstormShare(sessionId: string) {
data: { status: 'removed' },
})
revalidatePath('/')
revalidatePath('/home')
return { success: true }
}

View File

@@ -3,7 +3,7 @@
import DOMPurify from 'isomorphic-dompurify'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { getAIProvider } from '@/lib/ai/factory'
import { getChatProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { getAISettings } from '@/app/actions/ai-settings'
import { revalidatePath } from 'next/cache'
@@ -58,18 +58,47 @@ export async function generateNoteIllustrationSvg(noteId: string): Promise<{ ok:
}
const config = await getSystemConfig()
const provider = getAIProvider(config)
const provider = getChatProvider(config)
const prompt = `Tu es un designer minimaliste. Produis UN SEUL document SVG valide pour une vignette de carte note.
Contraintes strictes:
- viewBox="0 0 224 168" (rapport 4:3), pas de width/height fixes en px sur la racine ou width="100%" height="100%"
- Style architectural / papier, 24 formes géométriques ou lignes, palette sobre (noir/gris/une couleur douce), pas de texte lisible
- AUCUN script, AUCUNE balise foreignObject, AUCUN lien externe, AUCUN attribut on*
- Réponds UNIQUEMENT avec le fragment SVG (commence par <svg ...> et finit par </svg>), sans markdown ni commentaire.
const prompt = `Create a small SVG thumbnail that VISUALLY REPRESENTS this note's topic.
Thème à suggérer visuellement (abstrait, pas littéral):
Titre: ${plainTitle || '(sans titre)'}
Extrait: ${plainBody.slice(0, 400)}`
OUTPUT: Only raw SVG markup. No markdown, no code fences, no comments. Start with <svg and end with </svg>.
SPECIFICATIONS:
- viewBox="0 0 224 168", NO fixed width/height attributes
- Maximum 1000 bytes
- Background: soft warm beige (#F5F0E8) or transparent
- Color palette (pick 2-3): warm charcoal (#2C2C2C), slate gray (#6B7280), soft sage (#A8B5A0), muted ochre (#C4A882), dusty rose (#C9A9A6), teal (#5F9EA0), burgundy (#8B4513)
- NO text, NO scripts, NO foreignObject, NO external links
CRITICAL: The illustration MUST be recognizably related to the topic.
Think of it like an ICON or PICTOGRAM for the title. Not abstract random shapes.
TOPIC: "${plainTitle || 'untitled'}"
How to illustrate this topic (pick the BEST match):
- If the topic is about CODE/DEV: Show angle brackets <>, curly braces {}, a terminal window shape, or circuit-like lines
- If the topic is about MUSIC: Show sound waves, musical notes shapes, or speaker icon
- If the topic is about FOOD/COOKING: Show a pot shape, utensils, or plate
- If the topic is about TRAVEL: Show a path/road, mountain peaks, or compass
- If the topic is about SCIENCE: Show atom orbits, flask/beaker, or molecule bonds
- If the topic is about BUSINESS/FINANCE: Show ascending chart lines, coins, or briefcase
- If the topic is about HEALTH: Show heart shape, pulse line, or leaf
- If the topic is about EDUCATION: Show book shape, graduation cap, or pencil
- If the topic is about NATURE: Show tree, mountain, water wave, or sun
- If the topic is about DESIGN/ART: Show palette, brush stroke, or frame
- If the topic is about PEOPLE/TEAM: Show overlapping circles, handshake, or connected nodes
- If the topic is about ARCHITECTURE: Show building outline, blueprint grid, or columns
- Otherwise: Extract the KEY CONCEPT from the title and draw its SIMPLEST iconic representation
TECHNICAL RULES:
- Use simple shapes: <circle>, <rect>, <line>, <path>, <ellipse>, <polygon>, <g>
- Keep it FLAT and MINIMAL — 2-4 elements max
- Use opacity for depth (0.3-0.8)
- The icon should be immediately recognizable even at small size
Additional context from the note:
${plainBody.slice(0, 200)}`
const raw = await provider.generateText(prompt)
const extracted = extractSvgSnippet(raw)
@@ -90,7 +119,7 @@ Extrait: ${plainBody.slice(0, 400)}`
},
})
revalidatePath('/')
revalidatePath('/home')
return { ok: true }
} catch (e) {
console.error('[note-illustration]', e)

View File

@@ -434,7 +434,7 @@ export async function restoreNoteVersion(noteId: string, historyEntryId: string)
},
})
revalidatePath('/')
revalidatePath('/home')
return parseNote(restored)
}
@@ -603,7 +603,7 @@ export async function createNote(data: {
if (!data.skipRevalidation) {
// Revalidate main page (handles both inbox and notebook views via query params)
revalidatePath('/')
revalidatePath('/home')
}
// Fire-and-forget: run AI operations in background without blocking the response
@@ -690,7 +690,7 @@ export async function createNote(data: {
const merged = [...new Set([...existingNames, ...appliedLabels])]
await syncNoteLabels(noteId, merged, notebookId ?? null, userId)
if (!data.skipRevalidation) {
revalidatePath('/')
revalidatePath('/home')
}
}
}
@@ -853,7 +853,7 @@ export async function updateNote(id: string, data: {
if (!options?.skipRevalidation) {
try { revalidatePath(`/note/${id}`) } catch {}
try { revalidatePath('/') } catch {}
try { revalidatePath('/home') } catch {}
}
if (isStructuralChange) {
@@ -895,7 +895,7 @@ export async function deleteNote(id: string, options?: { skipRevalidation?: bool
})
if (!options?.skipRevalidation) {
revalidatePath('/')
revalidatePath('/home')
}
return { success: true }
} catch (error) {
@@ -915,7 +915,7 @@ export async function trashNote(id: string, options?: { skipRevalidation?: boole
data: { trashedAt: new Date() }
})
if (!options?.skipRevalidation) {
revalidatePath('/')
revalidatePath('/home')
}
return { success: true }
} catch (error) {
@@ -933,7 +933,7 @@ export async function restoreNote(id: string) {
where: { id, userId: session.user.id },
data: { trashedAt: null }
})
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/trash')
return { success: true }
} catch (error) {
@@ -984,7 +984,7 @@ export async function permanentDeleteNote(id: string) {
await syncLabels(session.user.id, [])
revalidatePath('/trash')
revalidatePath('/')
revalidatePath('/home')
return { success: true }
} catch (error) {
console.error('Error permanently deleting note:', error)
@@ -1028,7 +1028,7 @@ export async function emptyTrash() {
await syncLabels(session.user.id, [])
revalidatePath('/trash')
revalidatePath('/')
revalidatePath('/home')
return { success: true }
} catch (error) {
console.error('Error emptying trash:', error)
@@ -1182,7 +1182,7 @@ export async function reorderNotes(draggedId: string, targetId: string) {
prisma.note.update({ where: { id: note.id }, data: { order: index } })
)
await prisma.$transaction(updates)
revalidatePath('/')
revalidatePath('/home')
return { success: true }
} catch (error) {
throw new Error('Failed to reorder notes')
@@ -1198,7 +1198,7 @@ export async function updateFullOrder(ids: string[]) {
prisma.note.update({ where: { id, userId }, data: { order: index } })
)
await prisma.$transaction(updates)
revalidatePath('/')
revalidatePath('/home')
return { success: true }
} catch (error) {
throw new Error('Failed to update order')
@@ -1314,7 +1314,7 @@ export async function cleanupAllOrphans() {
}
}
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings')
return {
success: true,
@@ -1487,7 +1487,7 @@ export async function dismissFromRecent(id: string) {
data: { dismissedFromRecent: true }
})
// revalidatePath('/') // Removed to prevent immediate refill of the list
// revalidatePath('/home') // Removed to prevent immediate refill of the list
return { success: true }
} catch (error) {
console.error('Error dismissing note from recent:', error)
@@ -1895,7 +1895,7 @@ export async function respondToShareRequest(shareId: string, action: 'accept' |
});
// Revalidate all relevant cache tags
revalidatePath('/');
revalidatePath('/home');
return { success: true, share: updatedShare };
} catch (error: any) {
@@ -1957,7 +1957,7 @@ export async function removeSharedNoteFromView(shareId: string) {
}
});
revalidatePath('/');
revalidatePath('/home');
return { success: true };
} catch (error: any) {
console.error('Error removing shared note from view:', error);
@@ -2018,7 +2018,7 @@ export async function leaveSharedNote(noteId: string) {
}
});
revalidatePath('/');
revalidatePath('/home');
return { success: true };
} catch (error: any) {
console.error('Error leaving shared note:', error);

View File

@@ -271,7 +271,7 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi
}
}
revalidatePath('/')
revalidatePath('/home')
return { success: true, created, moved }
} catch (err) {
console.error('[organize-notebook] Execute error:', err)

View File

@@ -96,7 +96,7 @@ export async function updateTheme(theme: string) {
where: { id: session.user.id },
data: { theme: normalized },
})
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/profile')
return { success: true }
} catch (error) {
@@ -123,7 +123,7 @@ export async function updateLanguage(language: string) {
// Note: The language will be applied on next page load
// The client component should handle updating localStorage and reloading
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/profile')
return { success: true, language }
} catch (error) {
@@ -168,7 +168,7 @@ export async function updateFontSize(fontSize: string) {
})
}
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/profile')
return { success: true, fontSize }
} catch (error) {
@@ -194,7 +194,7 @@ export async function updateShowRecentNotes(showRecentNotes: boolean) {
},
})
revalidatePath('/')
revalidatePath('/home')
revalidatePath('/settings/profile')
return { success: true, showRecentNotes }
} catch (error) {

View File

@@ -99,7 +99,7 @@ export async function applyTitleSuggestion(
console.error('[HISTORY] Failed to create snapshot after title suggestion:', snapshotError)
}
revalidatePath('/')
revalidatePath('/home')
revalidatePath(`/note/${noteId}`)
} catch (error) {
console.error('Error applying title suggestion:', error)