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

@@ -3,11 +3,18 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import { BrainstormSession, BrainstormIdea } from '@/types/brainstorm'
import { useExpandIdea, useDismissIdea, useConvertIdea, useExportBrainstorm } from '@/hooks/use-brainstorm'
import {
useExpandIdea,
useDismissIdea,
useConvertIdea,
useExportBrainstorm,
brainstormQuotaMessageKey,
} from '@/hooks/use-brainstorm'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Sparkles, X, FileText, Download, ChevronDown } from 'lucide-react'
import { Sparkles, X, FileText, Download } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), {
ssr: false,
@@ -58,6 +65,7 @@ interface BrainstormCanvasProps {
}
export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
const { t } = useLanguage()
const fgRef = useRef<any>(null)
const [selectedIdea, setSelectedIdea] = useState<BrainstormIdea | null>(null)
const [isSheetOpen, setIsSheetOpen] = useState(false)
@@ -83,7 +91,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
sessionId: session.id,
waveNumber: 0,
title: session.seedIdea,
description: 'Original seed idea',
description: t('brainstorm.originalSeedDescription'),
connectionToSeed: null,
noveltyScore: null,
parentIdeaId: null,
@@ -121,7 +129,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
}
return { graphData: { nodes, links } }
}, [session])
}, [session, t])
const handleNodeClick = useCallback((node: any) => {
setSelectedIdea(node.idea)
@@ -131,12 +139,13 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
const handleExpand = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await expandIdea.mutateAsync(selectedIdea.id)
toast.success('Ideas expanded!')
} catch (err: any) {
toast.error(err.message || 'Failed to expand')
await expandIdea.mutateAsync({ ideaId: selectedIdea.id })
toast.success(t('brainstorm.toastExpandSuccess'))
} catch (err: unknown) {
const quotaKey = brainstormQuotaMessageKey(err)
toast.error(quotaKey ? t(quotaKey) : (err instanceof Error ? err.message : t('brainstorm.toastExpandFailed')))
}
}, [selectedIdea, expandIdea])
}, [selectedIdea, expandIdea, t])
const handleDismiss = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
@@ -144,30 +153,30 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
await dismissIdea.mutateAsync(selectedIdea.id)
setIsSheetOpen(false)
setSelectedIdea(null)
toast.success('Idea dismissed')
toast.success(t('brainstorm.toastDismissSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to dismiss')
toast.error(err.message || t('brainstorm.toastDismissFailed'))
}
}, [selectedIdea, dismissIdea])
}, [selectedIdea, dismissIdea, t])
const handleConvert = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await convertIdea.mutateAsync(selectedIdea.id)
toast.success('Idea converted to note!')
toast.success(t('brainstorm.toastConvertSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to convert')
toast.error(err.message || t('brainstorm.toastConvertFailed'))
}
}, [selectedIdea, convertIdea])
}, [selectedIdea, convertIdea, t])
const handleExport = useCallback(async () => {
try {
const note = await exportBrainstorm.mutateAsync()
toast.success('Exported as note!')
toast.success(t('brainstorm.toastExportNoteSuccess'))
} catch (err: any) {
toast.error(err.message || 'Failed to export')
toast.error(err.message || t('brainstorm.toastExportFailed'))
}
}, [exportBrainstorm])
}, [exportBrainstorm, t])
const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const label = node.name
@@ -211,12 +220,15 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
ctx.globalAlpha = 1
}, [])
const waveLegend = [
{ label: 'Seed', color: WAVE_COLORS[0] },
{ label: 'Variations', color: WAVE_COLORS[1] },
{ label: 'Analogies', color: WAVE_COLORS[2] },
{ label: 'Disruptions', color: WAVE_COLORS[3] },
]
const waveLegend = useMemo(
() => [
{ label: t('brainstorm.legendSeed'), color: WAVE_COLORS[0] },
{ label: t('brainstorm.legendVariations'), color: WAVE_COLORS[1] },
{ label: t('brainstorm.legendAnalogies'), color: WAVE_COLORS[2] },
{ label: t('brainstorm.legendDisruptions'), color: WAVE_COLORS[3] },
],
[t]
)
return (
<div className="relative w-full h-full bg-zinc-950 rounded-xl overflow-hidden">
@@ -264,7 +276,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
disabled={exportBrainstorm.isPending}
>
<Download size={14} className="mr-1" />
{exportBrainstorm.isPending ? 'Exporting...' : 'Export'}
{exportBrainstorm.isPending ? t('brainstorm.exporting') : t('brainstorm.export')}
</Button>
</div>
@@ -280,26 +292,26 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
<div className="mt-6 space-y-4">
{selectedIdea.description && selectedIdea.id !== 'seed' && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Description</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailDescription')}</p>
<p className="text-sm text-zinc-300">{selectedIdea.description}</p>
</div>
)}
{selectedIdea.connectionToSeed && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Connection</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailConnection')}</p>
<p className="text-sm text-zinc-300">{selectedIdea.connectionToSeed}</p>
</div>
)}
{selectedIdea.noveltyScore && (
{(selectedIdea.noveltyScore != null && selectedIdea.noveltyScore > 0) && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Novelty</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailNovelty')}</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-orange-500 via-memento-blue to-purple-500 rounded-full"
style={{ width: `${selectedIdea.noveltyScore * 10}%` }}
className="h-full bg-gradient-to-r from-orange-500 via-brand-accent to-purple-500 rounded-full"
style={{ width: `${Math.min(100, Math.max(0, selectedIdea.noveltyScore * 10))}%` }}
/>
</div>
<span className="text-sm text-zinc-400">{selectedIdea.noveltyScore}/10</span>
@@ -309,7 +321,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
{selectedIdea.waveNumber > 0 && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Wave</p>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">{t('brainstorm.ideaDetailWave')}</p>
<span
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
style={{
@@ -317,7 +329,11 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
color: WAVE_COLORS[selectedIdea.waveNumber],
}}
>
{selectedIdea.waveNumber === 1 ? 'Variation' : selectedIdea.waveNumber === 2 ? 'Analogy' : 'Disruption'}
{selectedIdea.waveNumber === 1
? t('brainstorm.waveFlavorVariation')
: selectedIdea.waveNumber === 2
? t('brainstorm.waveFlavorAnalogy')
: t('brainstorm.waveFlavorDisruption')}
</span>
</div>
)}
@@ -326,7 +342,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<p className="text-xs text-green-400 flex items-center gap-1">
<FileText size={12} />
Converted to note
{t('brainstorm.convertedToNoteStatus')}
</p>
</div>
)}
@@ -339,15 +355,15 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
>
<Sparkles size={14} className="mr-1" />
{expandIdea.isPending ? 'Generating...' : 'Dig Deeper'}
{expandIdea.isPending ? t('brainstorm.deepening') : t('brainstorm.deepen')}
</Button>
<Button
onClick={handleConvert}
disabled={convertIdea.isPending}
className="w-full bg-memento-blue hover:bg-blue-700 text-white"
className="w-full bg-brand-accent hover:bg-blue-700 text-white"
>
<FileText size={14} className="mr-1" />
{convertIdea.isPending ? 'Converting...' : 'Create Note'}
{convertIdea.isPending ? t('brainstorm.converting') : t('brainstorm.extract')}
</Button>
<Button
onClick={handleDismiss}
@@ -356,7 +372,7 @@ export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
className="w-full border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800"
>
<X size={14} className="mr-1" />
Dismiss
{t('brainstorm.dismiss')}
</Button>
</div>
)}