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

@@ -0,0 +1,292 @@
#!/usr/bin/env node
/**
* Merges missing i18n keys (admin fallback, brainstorm quota, landing page)
* into locale files. Contextual translations — not word-for-word.
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const localesDir = path.join(__dirname, '../locales')
function deepMerge(target, source) {
for (const key of Object.keys(source)) {
const sv = source[key]
if (sv && typeof sv === 'object' && !Array.isArray(sv)) {
if (!target[key] || typeof target[key] !== 'object') target[key] = {}
deepMerge(target[key], sv)
} else {
target[key] = sv
}
}
return target
}
function flatten(obj, prefix = '') {
const result = {}
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k
if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(result, flatten(v, key))
else result[key] = v
}
return result
}
const adminAiFallback = {
de: {
fallbackSectionTitle: 'Ausweich-Anbieter (optional)',
fallbackSectionDescription:
'Wird bei Anbieterfehlern automatisch genutzt (429, 5xx). Ein erneuter Versuch innerhalb von 1,5 s.',
fallbackProvider: 'Ausweich-Anbieter',
fallbackModel: 'Ausweich-Modell',
fallbackNone: 'Keiner (deaktiviert)',
fallbackModelPlaceholder: 'z. B. gpt-4o-mini',
},
es: {
fallbackSectionTitle: 'Proveedor de respaldo (opcional)',
fallbackSectionDescription:
'Se usa automáticamente ante errores del proveedor (429, 5xx). Un reintento en 1,5 s.',
fallbackProvider: 'Proveedor de respaldo',
fallbackModel: 'Modelo de respaldo',
fallbackNone: 'Ninguno (desactivado)',
fallbackModelPlaceholder: 'p. ej. gpt-4o-mini',
},
it: {
fallbackSectionTitle: 'Provider di riserva (opzionale)',
fallbackSectionDescription:
'Usato automaticamente in caso di errori del provider (429, 5xx). Un solo nuovo tentativo entro 1,5 s.',
fallbackProvider: 'Provider di riserva',
fallbackModel: 'Modello di riserva',
fallbackNone: 'Nessuno (disattivato)',
fallbackModelPlaceholder: 'es. gpt-4o-mini',
},
pt: {
fallbackSectionTitle: 'Provedor de contingência (opcional)',
fallbackSectionDescription:
'Usado automaticamente em erros do provedor (429, 5xx). Uma nova tentativa em 1,5 s.',
fallbackProvider: 'Provedor de contingência',
fallbackModel: 'Modelo de contingência',
fallbackNone: 'Nenhum (desativado)',
fallbackModelPlaceholder: 'ex.: gpt-4o-mini',
},
nl: {
fallbackSectionTitle: 'Fallback-provider (optioneel)',
fallbackSectionDescription:
'Wordt automatisch gebruikt bij providerfouten (429, 5xx). Eén nieuwe poging binnen 1,5 s.',
fallbackProvider: 'Fallback-provider',
fallbackModel: 'Fallback-model',
fallbackNone: 'Geen (uitgeschakeld)',
fallbackModelPlaceholder: 'bijv. gpt-4o-mini',
},
pl: {
fallbackSectionTitle: 'Zapasowy dostawca (opcjonalnie)',
fallbackSectionDescription:
'Używany automatycznie przy błędach dostawcy (429, 5xx). Jedna ponowna próba w 1,5 s.',
fallbackProvider: 'Zapasowy dostawca',
fallbackModel: 'Zapasowy model',
fallbackNone: 'Brak (wyłączone)',
fallbackModelPlaceholder: 'np. gpt-4o-mini',
},
ru: {
fallbackSectionTitle: 'Резервный провайдер (необязательно)',
fallbackSectionDescription:
'Используется автоматически при ошибках провайдера (429, 5xx). Одна повторная попытка за 1,5 с.',
fallbackProvider: 'Резервный провайдер',
fallbackModel: 'Резервная модель',
fallbackNone: 'Нет (отключено)',
fallbackModelPlaceholder: 'напр. gpt-4o-mini',
},
ar: {
fallbackSectionTitle: 'مزود احتياطي (اختياري)',
fallbackSectionDescription:
'يُستخدم تلقائياً عند أخطاء المزود (429، 5xx). محاولة واحدة خلال 1,5 ثانية.',
fallbackProvider: 'مزود احتياطي',
fallbackModel: 'نموذج احتياطي',
fallbackNone: 'لا شيء (معطّل)',
fallbackModelPlaceholder: 'مثال: gpt-4o-mini',
},
fa: {
fallbackSectionTitle: 'ارائه‌دهنده پشتیبان (اختیاری)',
fallbackSectionDescription:
'در صورت خطای ارائه‌دهنده (429، 5xx) به‌صورت خودکار استفاده می‌شود. یک تلاش مجدد در ۱,۵ ثانیه.',
fallbackProvider: 'ارائه‌دهنده پشتیبان',
fallbackModel: 'مدل پشتیبان',
fallbackNone: 'هیچ (غیرفعال)',
fallbackModelPlaceholder: 'مثلاً gpt-4o-mini',
},
hi: {
fallbackSectionTitle: 'फ़ॉलबैक प्रदाता (वैकल्पिक)',
fallbackSectionDescription:
'प्रदाता त्रुटियों (429, 5xx) पर स्वतः उपयोग। 1.5 सेकंड में एक पुनः प्रयास।',
fallbackProvider: 'फ़ॉलबैक प्रदाता',
fallbackModel: 'फ़ॉलबैक मॉडल',
fallbackNone: 'कोई नहीं (अक्षम)',
fallbackModelPlaceholder: 'उदा. gpt-4o-mini',
},
ja: {
fallbackSectionTitle: 'フォールバックプロバイダー(任意)',
fallbackSectionDescription:
'プロバイダーエラー時429、5xxに自動使用。1.5秒以内に1回再試行。',
fallbackProvider: 'フォールバックプロバイダー',
fallbackModel: 'フォールバックモデル',
fallbackNone: 'なし(無効)',
fallbackModelPlaceholder: '例: gpt-4o-mini',
},
ko: {
fallbackSectionTitle: '대체 제공업체(선택)',
fallbackSectionDescription:
'제공업체 오류(429, 5xx) 시 자동 사용. 1.5초 이내 1회 재시도.',
fallbackProvider: '대체 제공업체',
fallbackModel: '대체 모델',
fallbackNone: '없음(비활성화)',
fallbackModelPlaceholder: '예: gpt-4o-mini',
},
zh: {
fallbackSectionTitle: '备用提供商(可选)',
fallbackSectionDescription: '提供商出错时429、5xx自动启用。1.5 秒内重试一次。',
fallbackProvider: '备用提供商',
fallbackModel: '备用模型',
fallbackNone: '无(已禁用)',
fallbackModelPlaceholder: '例如 gpt-4o-mini',
},
}
const brainstormQuota = {
de: {
quotaGuest:
'Der Gastgeber der Sitzung hat sein KI-Kontingent aufgebraucht. Bitte ihn, seinen Tarif zu erweitern.',
quotaHost:
'Sie haben Ihr KI-Kontingent für dieses Brainstorming erreicht. Wechseln Sie den Tarif, um fortzufahren.',
},
es: {
quotaGuest:
'El anfitrión de la sesión ha alcanzado su límite de IA. Pídele que mejore su plan.',
quotaHost:
'Has alcanzado tu límite de IA para este brainstorm. Mejora tu plan para continuar.',
},
it: {
quotaGuest:
"L'host della sessione ha raggiunto il limite IA. Chiedigli di aggiornare il piano.",
quotaHost:
'Hai raggiunto il limite IA per questo brainstorm. Passa a un piano superiore per continuare.',
},
pt: {
quotaGuest:
'O anfitrião da sessão atingiu o limite de IA. Peça-lhe para atualizar o plano.',
quotaHost:
'Atingiu o limite de IA deste brainstorm. Atualize o plano para continuar.',
},
nl: {
quotaGuest:
'De sessiehost heeft zijn AI-limiet bereikt. Vraag om een upgrade van het abonnement.',
quotaHost:
'Je hebt je AI-limiet voor deze brainstorm bereikt. Upgrade je abonnement om door te gaan.',
},
pl: {
quotaGuest:
'Gospodarz sesji wyczerpał limit AI. Poproś go o ulepszenie planu.',
quotaHost:
'Osiągnąłeś limit AI dla tego brainstormu. Ulepsz plan, aby kontynuować.',
},
ru: {
quotaGuest:
'Организатор сессии исчерпал лимит ИИ. Попросите его обновить тариф.',
quotaHost:
'Вы исчерпали лимит ИИ для этого мозгового штурма. Обновите тариф, чтобы продолжить.',
},
ar: {
quotaGuest:
'استنفد مضيف الجلسة حدّ الذكاء الاصطناعي. اطلب منه ترقية خطته.',
quotaHost: 'لقد وصلت إلى حدّ الذكاء الاصطناعي لهذه الجلسة. رقِّ خطتك للمتابعة.',
},
fa: {
quotaGuest:
'میزبان جلسه به سقف هوش مصنوعی رسیده. از او بخواهید طرحش را ارتقا دهد.',
quotaHost:
'به سقف هوش مصنوعی این طوفان فکری رسیدید. برای ادامه، طرح خود را ارتقا دهید.',
},
hi: {
quotaGuest:
'सत्र के होस्ट की AI सीमा समाप्त हो गई है। उनसे अपना प्लान अपग्रेड करने को कहें।',
quotaHost:
'इस ब्रेनस्टॉर्म के लिए आपकी AI सीमा समाप्त हो गई है। जारी रखने के लिए प्लान अपग्रेड करें।',
},
ja: {
quotaGuest:
'セッションのホストがAI利用上限に達しました。プランのアップグレードを依頼してください。',
quotaHost:
'このブレインストームのAI上限に達しました。続けるにはプランをアップグレードしてください。',
},
ko: {
quotaGuest: '세션 호스트의 AI 한도에 도달했습니다. 플랜 업그레이드를 요청하세요.',
quotaHost:
'이 브레인스토밍의 AI 한도에 도달했습니다. 계속하려면 플랜을 업그레이드하세요.',
},
zh: {
quotaGuest: '会话主持人已达到 AI 额度上限。请让对方升级套餐。',
quotaHost: '您已达到此头脑风暴的 AI 额度上限。升级套餐以继续。',
},
}
// Landing blocks — load from generated JSON (keeps this script maintainable)
const landingPatchesPath = path.join(__dirname, 'i18n-landing-patches.json')
if (!fs.existsSync(landingPatchesPath)) {
console.error('Missing i18n-landing-patches.json — run generate-landing-patches first')
process.exit(1)
}
const landingPatches = JSON.parse(fs.readFileSync(landingPatchesPath, 'utf8'))
const TARGET_LANGS = [
'ar',
'de',
'es',
'fa',
'hi',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'ru',
'zh',
]
for (const lang of TARGET_LANGS) {
const filePath = path.join(localesDir, `${lang}.json`)
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
if (adminAiFallback[lang]) {
if (!data.admin) data.admin = {}
if (!data.admin.ai) data.admin.ai = {}
Object.assign(data.admin.ai, adminAiFallback[lang])
}
if (brainstormQuota[lang]) {
if (!data.brainstorm) data.brainstorm = {}
Object.assign(data.brainstorm, brainstormQuota[lang])
}
if (landingPatches[lang]) {
if (!data.landing) data.landing = {}
deepMerge(data.landing, landingPatches[lang])
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8')
console.log(`${lang}.json updated`)
}
// Verify
const en = flatten(JSON.parse(fs.readFileSync(path.join(localesDir, 'en.json'), 'utf8')))
const enKeys = Object.keys(en)
let ok = true
for (const lang of TARGET_LANGS) {
const loc = flatten(JSON.parse(fs.readFileSync(path.join(localesDir, `${lang}.json`), 'utf8')))
const missing = enKeys.filter((k) => !(k in loc))
if (missing.length) {
console.error(`${lang}: still ${missing.length} missing keys`)
ok = false
}
}
console.log(ok ? '\nAll locales complete.' : '\nSome keys still missing.')