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
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user