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
263 lines
10 KiB
TypeScript
263 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { updateAISettings } from '@/app/actions/ai-settings'
|
|
import { DemoModeToggle } from '@/components/demo-mode-toggle'
|
|
import { toast } from 'sonner'
|
|
import { Loader2, Sparkles, Brain, Languages, Tag, History, Wand2 } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { motion } from 'motion/react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface AISettingsPanelProps {
|
|
initialSettings: {
|
|
titleSuggestions: boolean
|
|
semanticSearch: boolean
|
|
paragraphRefactor: boolean
|
|
memoryEcho: boolean
|
|
memoryEchoFrequency: 'daily' | 'weekly' | 'custom'
|
|
aiProvider: 'auto' | 'openai' | 'ollama'
|
|
preferredLanguage: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
|
|
demoMode: boolean
|
|
languageDetection: boolean
|
|
autoLabeling: boolean
|
|
noteHistory: boolean
|
|
noteHistoryMode: 'manual' | 'auto'
|
|
}
|
|
}
|
|
|
|
export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
|
|
const [settings, setSettings] = useState(initialSettings)
|
|
const [isPending, setIsPending] = useState(false)
|
|
const { t } = useLanguage()
|
|
|
|
const handleToggle = async (feature: string, value: boolean) => {
|
|
setSettings(prev => ({ ...prev, [feature]: value }))
|
|
try {
|
|
setIsPending(true)
|
|
await updateAISettings({ [feature]: value })
|
|
toast.success(t('aiSettings.saved'))
|
|
} catch {
|
|
toast.error(t('aiSettings.error'))
|
|
setSettings(initialSettings)
|
|
} finally {
|
|
setIsPending(false)
|
|
}
|
|
}
|
|
|
|
const handleFrequencyChange = async (value: 'daily' | 'weekly' | 'custom') => {
|
|
setSettings(prev => ({ ...prev, memoryEchoFrequency: value }))
|
|
try {
|
|
setIsPending(true)
|
|
await updateAISettings({ memoryEchoFrequency: value })
|
|
toast.success(t('aiSettings.saved'))
|
|
} catch {
|
|
toast.error(t('aiSettings.error'))
|
|
setSettings(initialSettings)
|
|
} finally {
|
|
setIsPending(false)
|
|
}
|
|
}
|
|
|
|
const handleHistoryModeChange = async (value: 'manual' | 'auto') => {
|
|
setSettings(prev => ({ ...prev, noteHistoryMode: value }))
|
|
try {
|
|
setIsPending(true)
|
|
await updateAISettings({ noteHistoryMode: value })
|
|
toast.success(t('settings.settingsSaved'))
|
|
} catch {
|
|
toast.error(t('aiSettings.error'))
|
|
setSettings(initialSettings)
|
|
} finally {
|
|
setIsPending(false)
|
|
}
|
|
}
|
|
|
|
const handleDemoModeToggle = async (enabled: boolean) => {
|
|
setSettings(prev => ({ ...prev, demoMode: enabled }))
|
|
try {
|
|
setIsPending(true)
|
|
await updateAISettings({ demoMode: enabled })
|
|
} catch {
|
|
toast.error(t('aiSettings.error'))
|
|
setSettings(initialSettings)
|
|
throw new Error()
|
|
} finally {
|
|
setIsPending(false)
|
|
}
|
|
}
|
|
|
|
const features = [
|
|
{
|
|
key: 'titleSuggestions',
|
|
icon: Wand2,
|
|
name: t('aiSettings.titleSuggestions'),
|
|
description: t('aiSettings.titleSuggestionsDesc'),
|
|
value: settings.titleSuggestions,
|
|
},
|
|
{
|
|
key: 'paragraphRefactor',
|
|
icon: Sparkles,
|
|
name: t('aiSettings.aiNote'),
|
|
description: t('aiSettings.aiNoteDesc'),
|
|
value: settings.paragraphRefactor,
|
|
},
|
|
{
|
|
key: 'memoryEcho',
|
|
icon: Brain,
|
|
name: t('memoryEcho.title'),
|
|
description: t('memoryEcho.dailyInsight'),
|
|
value: settings.memoryEcho,
|
|
},
|
|
{
|
|
key: 'languageDetection',
|
|
icon: Languages,
|
|
name: t('aiSettings.languageDetection'),
|
|
description: t('aiSettings.languageDetectionDesc'),
|
|
value: settings.languageDetection ?? true,
|
|
},
|
|
{
|
|
key: 'autoLabeling',
|
|
icon: Tag,
|
|
name: t('aiSettings.autoLabeling'),
|
|
description: t('aiSettings.autoLabelingDesc'),
|
|
value: settings.autoLabeling ?? true,
|
|
},
|
|
{
|
|
key: 'noteHistory',
|
|
icon: History,
|
|
name: t('aiSettings.noteHistory'),
|
|
description: t('aiSettings.noteHistoryDesc'),
|
|
value: settings.noteHistory ?? false,
|
|
},
|
|
]
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="space-y-16 pb-20"
|
|
>
|
|
{isPending && (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
{t('aiSettings.saving')}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-10">
|
|
<div className="space-y-6">
|
|
<h4 className="text-sm font-bold text-ink border-b border-border/40 pb-4">
|
|
{t('aiSettings.features')}
|
|
</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
{features.map(({ key, icon: Icon, name, description, value }) => (
|
|
<div
|
|
key={key}
|
|
className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-6 flex items-center justify-between group hover:shadow-xl hover:shadow-brand-accent/5 transition-all duration-300"
|
|
>
|
|
<div className="flex items-center gap-5">
|
|
<div className={cn(
|
|
'p-3 bg-paper dark:bg-brand-accent/10 rounded-2xl border border-brand-accent/20 transition-all duration-300',
|
|
'group-hover:bg-brand-accent group-hover:text-white group-hover:scale-110',
|
|
'text-brand-accent'
|
|
)}>
|
|
<Icon size={18} />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h4 className="text-[13px] font-bold text-ink">{name}</h4>
|
|
<p className="text-[10px] text-concrete leading-relaxed pr-4 line-clamp-2">{description}</p>
|
|
</div>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer shrink-0 ml-4">
|
|
<input
|
|
type="checkbox"
|
|
className="sr-only peer"
|
|
checked={value}
|
|
onChange={() => handleToggle(key, !value)}
|
|
/>
|
|
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-brand-accent" />
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6">
|
|
{settings.memoryEcho && (
|
|
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-8 space-y-10">
|
|
<div className="space-y-1.5 text-left text-brand-accent">
|
|
<h4 className="text-sm font-bold">{t('aiSettings.frequency')}</h4>
|
|
<p className="text-[10px] opacity-60 uppercase tracking-wider font-semibold">
|
|
{t('aiSettings.frequencyDesc')}
|
|
</p>
|
|
</div>
|
|
<div className="space-y-6">
|
|
{(['daily', 'weekly'] as const).map((opt) => (
|
|
<label key={opt} className="flex items-center gap-4 cursor-pointer group">
|
|
<input
|
|
type="radio"
|
|
name="freq"
|
|
className="sr-only peer"
|
|
checked={settings.memoryEchoFrequency === opt}
|
|
onChange={() => handleFrequencyChange(opt)}
|
|
/>
|
|
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-brand-accent flex items-center justify-center p-0.5 transition-all">
|
|
<div className={cn(
|
|
'w-full h-full rounded-full transition-all',
|
|
settings.memoryEchoFrequency === opt ? 'bg-brand-accent' : 'bg-transparent'
|
|
)} />
|
|
</div>
|
|
<span className="text-sm font-medium text-ink group-hover:opacity-70 transition-opacity">
|
|
{opt === 'daily' ? t('aiSettings.frequencyDaily') : t('aiSettings.frequencyWeekly')}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{settings.noteHistory && (
|
|
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-8 space-y-10">
|
|
<div className="space-y-1.5 text-left text-brand-accent">
|
|
<h4 className="text-sm font-bold">{t('notes.historyMode')}</h4>
|
|
<p className="text-[10px] opacity-60 uppercase tracking-wider font-semibold">
|
|
{t('aiSettings.noteHistoryDesc')}
|
|
</p>
|
|
</div>
|
|
<div className="space-y-6">
|
|
{([
|
|
{ value: 'manual' as const, label: t('notes.historyModeManual'), desc: t('notes.historyModeManualDesc') },
|
|
{ value: 'auto' as const, label: t('notes.historyModeAuto'), desc: t('notes.historyModeAutoDesc') },
|
|
]).map((opt) => (
|
|
<label key={opt.value} className="flex items-center gap-4 cursor-pointer group">
|
|
<input
|
|
type="radio"
|
|
name="hist"
|
|
className="sr-only peer"
|
|
checked={(settings.noteHistoryMode ?? 'manual') === opt.value}
|
|
onChange={() => handleHistoryModeChange(opt.value)}
|
|
/>
|
|
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-brand-accent flex items-center justify-center p-0.5 transition-all">
|
|
<div className={cn(
|
|
'w-full h-full rounded-full transition-all',
|
|
(settings.noteHistoryMode ?? 'manual') === opt.value ? 'bg-brand-accent' : 'bg-transparent'
|
|
)} />
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
<p className="text-sm font-bold text-ink">{opt.label}</p>
|
|
<p className="text-[10px] text-concrete">{opt.desc}</p>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DemoModeToggle demoMode={settings.demoMode} onToggle={handleDemoModeToggle} />
|
|
</div>
|
|
</motion.div>
|
|
)
|
|
}
|