feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s

- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
Antigravity
2026-05-16 12:59:30 +00:00
parent 1fcea6ed7d
commit bd495be965
2284 changed files with 395285 additions and 2327 deletions

View File

@@ -2,10 +2,20 @@ import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { verifyParticipant, logActivity, resolveAiContextUserId, captureSnapshot } from '@/lib/brainstorm-collab'
import {
billingOwnerFromSession,
verifyParticipant,
logActivity,
resolveAiContextUserId,
captureSnapshot,
} from '@/lib/brainstorm-collab'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import { getTagsProvider } from '@/lib/ai/factory'
import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user'
import { getSystemConfig } from '@/lib/config'
import {
reserveUsageOrThrow,
QuotaExceededError,
} from '@/lib/entitlements'
import { emitToSession } from '@/lib/socket-emit'
const manualSchema = z.object({
@@ -43,6 +53,11 @@ export async function POST(
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const { billingOwnerId, isGuestActor } = billingOwnerFromSession(
brainstormSession.userId,
session.user.id,
)
let wave = 1
let parentIdea: any = null
if (parentIdeaId) {
@@ -78,8 +93,25 @@ export async function POST(
},
})
// [UPDATE - SÉCURITÉ] Recherche vectorielle guest-safe
// Story 3.5: per-provider BYOK bypass for enrich
let enrichBlocked = false
try {
const config = await getSystemConfig()
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, billingOwnerId)
if (!willUseByok) {
await reserveUsageOrThrow(billingOwnerId, 'brainstorm_enrich')
}
} catch (err) {
if (err instanceof QuotaExceededError) {
enrichBlocked = true
} else {
console.error('[manual-idea] reserveUsage error:', err)
}
}
// [UPDATE - SÉCURITÉ] Recherche vectorielle guest-safe (skipped when host quota exhausted)
let relatedNoteIds: string[] = []
if (!enrichBlocked) {
try {
const { isGuest, publicNoteIds, aiUserId } = await resolveAiContextUserId(sessionId, session.user.id)
@@ -117,7 +149,10 @@ export async function POST(
}
relatedNoteIds = results.map((r: any) => r.id)
}
} catch {}
} catch (vectorErr) {
console.error('[manual-idea] vector context search failed:', vectorErr)
}
}
if (relatedNoteIds.length > 0) {
const notes = await prisma.note.findMany({
@@ -155,6 +190,16 @@ export async function POST(
const requestingUserId = session.user!.id
const enrichAsync = async () => {
if (enrichBlocked) {
await emitToSession(sessionId, 'idea:ai_failed', {
ideaId: idea.id,
reason: 'quota_exceeded',
isGuestActor,
billingOwnerId,
})
return
}
try {
// Notifier le room que l'IA traite ce nœud (bordure pulsante violet)
await emitToSession(sessionId, 'idea:ai_processing', {
@@ -163,7 +208,6 @@ export async function POST(
})
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const lang = locale === 'fr' ? 'French' : locale === 'es' ? 'Spanish' : locale === 'de' ? 'German' : locale === 'it' ? 'Italian' : locale === 'pt' ? 'Portuguese' : locale === 'ja' ? 'Japanese' : locale === 'ko' ? 'Korean' : locale === 'zh' ? 'Chinese' : locale === 'ar' ? 'Arabic' : "the user's language"
const enrichPrompt = `You are an idea enrichment assistant. Given a user's raw brainstorm idea and context, produce a JSON object with:
@@ -181,7 +225,12 @@ User's raw description: "${description || 'none provided'}"
Respond ONLY with the JSON object, no markdown.`
const raw = await provider.generateText(enrichPrompt)
const { result: raw } = await runLaneWithBillingUser(
'tags',
config,
billingOwnerId,
(provider) => provider.generateText(enrichPrompt),
)
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
const enriched = JSON.parse(cleaned)
@@ -195,7 +244,6 @@ Respond ONLY with the JSON object, no markdown.`
data: { title: enrichedTitle, description: enrichedDescription, connectionToSeed, noveltyScore },
})
// [UPDATE - TEMPS RÉEL] Notifier la complétion avec les données enrichies
await emitToSession(sessionId, 'idea:ai_completed', {
ideaId: idea.id,
title: enrichedTitle,