Refactor Admin and Settings UI to Ethereal Precision aesthetic and improve note import/export functionality
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m4s

This commit is contained in:
Antigravity
2026-05-03 12:51:25 +00:00
parent 635e516616
commit b611ec874d
27 changed files with 1151 additions and 1081 deletions

View File

@@ -1,19 +0,0 @@
import { cn } from '@/lib/utils'
export interface AdminContentAreaProps {
children: React.ReactNode
className?: string
}
export function AdminContentArea({ children, className }: AdminContentAreaProps) {
return (
<main
className={cn(
'flex-1 overflow-y-auto bg-gray-50 dark:bg-zinc-950 p-6',
className
)}
>
{children}
</main>
)
}

View File

@@ -0,0 +1,71 @@
'use client'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Brain, Settings } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
export interface AdminNavProps {
className?: string
}
export interface NavItem {
titleKey: string
href: string
icon: React.ReactNode
}
const navItems: NavItem[] = [
{
titleKey: 'admin.sidebar.dashboard',
href: '/admin',
icon: <LayoutDashboard className="h-4 w-4" />,
},
{
titleKey: 'admin.sidebar.users',
href: '/admin/users',
icon: <Users className="h-4 w-4" />,
},
{
titleKey: 'admin.sidebar.aiManagement',
href: '/admin/ai',
icon: <Brain className="h-4 w-4" />,
},
{
titleKey: 'admin.sidebar.settings',
href: '/admin/settings',
icon: <Settings className="h-4 w-4" />,
},
]
export function AdminNav({ className }: AdminNavProps) {
const pathname = usePathname()
const { t } = useLanguage()
return (
<nav className={cn('flex items-center gap-1', className)}>
{navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/admin' && pathname?.startsWith(item.href + '/'))
return (
// <a> instead of <Link>: avoids Next.js RSC navigation transitions
// that trigger React Error #310 (React bug #33580) in production.
// Full-page reloads are acceptable for admin navigation.
<a
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-2 px-3 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap',
isActive
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
)}
>
{item.icon}
<span>{t(item.titleKey)}</span>
</a>
)
})}
</nav>
)
}

View File

@@ -1,79 +0,0 @@
'use client'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Brain, Settings } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
export interface AdminSidebarProps {
className?: string
}
export interface NavItem {
titleKey: string
href: string
icon: React.ReactNode
}
const navItems: NavItem[] = [
{
titleKey: 'admin.sidebar.dashboard',
href: '/admin',
icon: <LayoutDashboard className="h-5 w-5" />,
},
{
titleKey: 'admin.sidebar.users',
href: '/admin/users',
icon: <Users className="h-5 w-5" />,
},
{
titleKey: 'admin.sidebar.aiManagement',
href: '/admin/ai',
icon: <Brain className="h-5 w-5" />,
},
{
titleKey: 'admin.sidebar.settings',
href: '/admin/settings',
icon: <Settings className="h-5 w-5" />,
},
]
export function AdminSidebar({ className }: AdminSidebarProps) {
const pathname = usePathname()
const { t } = useLanguage()
return (
<aside
className={cn(
'w-64 bg-white dark:bg-zinc-900 border-r border-gray-200 dark:border-gray-800 p-4',
className
)}
>
<nav className="space-y-1">
{navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== '/admin' && pathname?.startsWith(item.href + '/'))
return (
// <a> instead of <Link>: avoids Next.js RSC navigation transitions
// that trigger React Error #310 (React bug #33580) in production.
// Full-page reloads are acceptable for admin navigation.
<a
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors',
'hover:bg-gray-100 dark:hover:bg-zinc-800',
isActive
? 'bg-gray-100 dark:bg-zinc-800 text-gray-900 dark:text-white font-semibold'
: 'text-gray-600 dark:text-gray-400'
)}
>
{item.icon}
<span>{t(item.titleKey)}</span>
</a>
)
})}
</nav>
</aside>
)
}

View File

@@ -1,15 +1,13 @@
'use client'
import { useState } from 'react'
import { Card } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { updateAISettings } from '@/app/actions/ai-settings'
import { DemoModeToggle } from '@/components/demo-mode-toggle'
import { toast } from 'sonner'
import { Loader2 } from 'lucide-react'
import { Loader2, Sparkles, Brain, Languages, Tag, History, Wand2 } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
interface AISettingsPanelProps {
@@ -35,17 +33,13 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
const { t } = useLanguage()
const handleToggle = async (feature: string, value: boolean) => {
// Optimistic update
setSettings(prev => ({ ...prev, [feature]: value }))
try {
setIsPending(true)
await updateAISettings({ [feature]: value })
toast.success(t('aiSettings.saved'))
} catch (error) {
console.error('Error updating setting:', error)
} catch {
toast.error(t('aiSettings.error'))
// Revert on error
setSettings(initialSettings)
} finally {
setIsPending(false)
@@ -54,31 +48,11 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
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 (error) {
console.error('Error updating frequency:', error)
toast.error(t('aiSettings.error'))
setSettings(initialSettings)
} finally {
setIsPending(false)
}
}
const handleLanguageChange = async (value: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl') => {
setSettings(prev => ({ ...prev, preferredLanguage: value }))
try {
setIsPending(true)
await updateAISettings({ preferredLanguage: value })
toast.success(t('aiSettings.saved'))
} catch (error) {
console.error('Error updating language:', error)
} catch {
toast.error(t('aiSettings.error'))
setSettings(initialSettings)
} finally {
@@ -88,179 +62,154 @@ export function AISettingsPanel({ initialSettings }: AISettingsPanelProps) {
const handleDemoModeToggle = async (enabled: boolean) => {
setSettings(prev => ({ ...prev, demoMode: enabled }))
try {
setIsPending(true)
await updateAISettings({ demoMode: enabled })
} catch (error) {
console.error('Error toggling demo mode:', error)
} catch {
toast.error(t('aiSettings.error'))
setSettings(initialSettings)
throw error
throw new Error()
} finally {
setIsPending(false)
}
}
const features = [
{
key: 'titleSuggestions',
icon: Wand2,
name: t('titleSuggestions.available').replace('💡 ', ''),
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 (
<div className="space-y-6">
<div className="space-y-8">
{isPending && (
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{t('aiSettings.saving')}
</div>
)}
{/* Feature Toggles */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">{t('aiSettings.features')}</h2>
<FeatureToggle
name={t('titleSuggestions.available').replace('💡 ', '')}
description={t('aiSettings.titleSuggestionsDesc')}
checked={settings.titleSuggestions}
onChange={(checked) => handleToggle('titleSuggestions', checked)}
/>
<FeatureToggle
name={t('aiSettings.aiNote')}
description={t('aiSettings.aiNoteDesc')}
checked={settings.paragraphRefactor}
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
/>
<FeatureToggle
name={t('memoryEcho.title')}
description={t('memoryEcho.dailyInsight')}
checked={settings.memoryEcho}
onChange={(checked) => handleToggle('memoryEcho', checked)}
/>
{settings.memoryEcho && (
<Card className="p-4 ml-6">
<Label htmlFor="frequency" className="text-sm font-medium">
{t('aiSettings.frequency')}
</Label>
<p className="text-xs text-gray-500 mb-3">
{t('aiSettings.frequencyDesc')}
</p>
<RadioGroup
value={settings.memoryEchoFrequency}
onValueChange={handleFrequencyChange}
{/* Feature toggles as cards */}
<div>
<h2 className="text-base font-semibold text-foreground mb-4">{t('aiSettings.features')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{features.map(({ key, icon: Icon, name, description, value }) => (
<div
key={key}
className="bg-card rounded-lg border border-border p-5 shadow-sm flex items-start justify-between gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="daily" id="daily" />
<Label htmlFor="daily" className="font-normal">
{t('aiSettings.frequencyDaily')}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="weekly" id="weekly" />
<Label htmlFor="weekly" className="font-normal">
{t('aiSettings.frequencyWeekly')}
</Label>
</div>
</RadioGroup>
</Card>
)}
{/* Language Detection Toggle */}
<FeatureToggle
name={t('aiSettings.languageDetection')}
description={t('aiSettings.languageDetectionDesc')}
checked={settings.languageDetection ?? true}
onChange={(checked) => handleToggle('languageDetection', checked)}
/>
{/* Auto Labeling Toggle */}
<FeatureToggle
name={t('aiSettings.autoLabeling')}
description={t('aiSettings.autoLabelingDesc')}
checked={settings.autoLabeling ?? true}
onChange={(checked) => handleToggle('autoLabeling', checked)}
/>
<FeatureToggle
name={t('aiSettings.noteHistory')}
description={t('aiSettings.noteHistoryDesc')}
checked={settings.noteHistory ?? false}
onChange={(checked) => handleToggle('noteHistory', checked)}
/>
{settings.noteHistory && (
<div className="space-y-2 rounded-lg border border-border/50 bg-muted/30 p-3">
<p className="text-sm font-medium">{t('notes.historyMode')}</p>
<RadioGroup
value={settings.noteHistoryMode ?? 'manual'}
onValueChange={(value) => {
const mode = value as 'manual' | 'auto'
setSettings((s) => ({ ...s, noteHistoryMode: mode }))
updateAISettings({ noteHistoryMode: mode }).then(() => {
toast.success(t('settings.settingsSaved'))
})
}}
className="space-y-2"
>
<div className="flex items-start gap-2">
<RadioGroupItem value="manual" id="history-manual" />
<div className="grid gap-0.5 leading-none">
<Label htmlFor="history-manual" className="text-sm font-medium">
{t('notes.historyModeManual')}
</Label>
<p className="text-xs text-muted-foreground">
{t('notes.historyModeManualDesc')}
</p>
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0 mt-0.5">
<Icon className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-foreground">{name}</p>
<p className="text-xs text-muted-foreground mt-0.5">{description}</p>
</div>
</div>
<div className="flex items-start gap-2">
<RadioGroupItem value="auto" id="history-auto" />
<div className="grid gap-0.5 leading-none">
<Label htmlFor="history-auto" className="text-sm font-medium">
{t('notes.historyModeAuto')}
</Label>
<p className="text-xs text-muted-foreground">
{t('notes.historyModeAutoDesc')}
</p>
</div>
</div>
</RadioGroup>
</div>
)}
{/* Demo Mode Toggle */}
<DemoModeToggle
demoMode={settings.demoMode}
onToggle={handleDemoModeToggle}
/>
<button
type="button"
role="switch"
aria-checked={value}
onClick={() => handleToggle(key, !value)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 mt-0.5 ${value ? 'bg-primary' : 'bg-muted-foreground/30'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${value ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
))}
</div>
</div>
{/* Memory Echo frequency — shown when enabled */}
{settings.memoryEcho && (
<div className="bg-card rounded-lg border border-border p-5 shadow-sm">
<h3 className="text-sm font-semibold text-foreground mb-1">{t('aiSettings.frequency')}</h3>
<p className="text-xs text-muted-foreground mb-4">{t('aiSettings.frequencyDesc')}</p>
<RadioGroup value={settings.memoryEchoFrequency} onValueChange={handleFrequencyChange} className="space-y-2">
{[
{ value: 'daily', label: t('aiSettings.frequencyDaily') },
{ value: 'weekly', label: t('aiSettings.frequencyWeekly') },
].map((opt) => (
<div key={opt.value} className="flex items-center space-x-2">
<RadioGroupItem value={opt.value} id={`freq-${opt.value}`} />
<Label htmlFor={`freq-${opt.value}`} className="font-normal text-sm cursor-pointer">{opt.label}</Label>
</div>
))}
</RadioGroup>
</div>
)}
{/* Note History mode — shown when enabled */}
{settings.noteHistory && (
<div className="bg-card rounded-lg border border-border p-5 shadow-sm">
<h3 className="text-sm font-semibold text-foreground mb-1">{t('notes.historyMode')}</h3>
<RadioGroup
value={settings.noteHistoryMode ?? 'manual'}
onValueChange={(value) => {
const mode = value as 'manual' | 'auto'
setSettings(s => ({ ...s, noteHistoryMode: mode }))
updateAISettings({ noteHistoryMode: mode }).then(() => toast.success(t('settings.settingsSaved')))
}}
className="space-y-3 mt-3"
>
{[
{ value: 'manual', label: t('notes.historyModeManual'), desc: t('notes.historyModeManualDesc') },
{ value: 'auto', label: t('notes.historyModeAuto'), desc: t('notes.historyModeAutoDesc') },
].map((opt) => (
<div key={opt.value} className="flex items-start gap-2">
<RadioGroupItem value={opt.value} id={`history-${opt.value}`} className="mt-0.5" />
<div>
<Label htmlFor={`history-${opt.value}`} className="text-sm font-medium cursor-pointer">{opt.label}</Label>
<p className="text-xs text-muted-foreground">{opt.desc}</p>
</div>
</div>
))}
</RadioGroup>
</div>
)}
{/* Demo Mode */}
<DemoModeToggle demoMode={settings.demoMode} onToggle={handleDemoModeToggle} />
</div>
)
}
interface FeatureToggleProps {
name: string
description: string
checked: boolean
onChange: (checked: boolean) => void
}
function FeatureToggle({ name, description, checked, onChange }: FeatureToggleProps) {
return (
<Card className="p-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-base font-medium">{name}</Label>
<p className="text-sm text-gray-500">{description}</p>
</div>
<Switch
checked={checked}
onCheckedChange={onChange}
disabled={false}
/>
</div>
</Card>
)
}

View File

@@ -1,7 +1,6 @@
'use client'
import { useState, useTransition } from 'react'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
@@ -117,59 +116,71 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
}
return (
<div className="space-y-6">
<div className="columns-1 lg:columns-2 gap-6 space-y-6">
{/* Section 1: What is MCP */}
<Card className="p-6">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-blue-500 mt-0.5 shrink-0" />
<div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden break-inside-avoid">
<div className="flex items-center gap-3 p-6 border-b border-border">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500 shrink-0">
<Info className="h-5 w-5" />
</div>
<div>
<h2 className="text-lg font-semibold">{t('mcpSettings.whatIsMcp.title')}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{t('mcpSettings.whatIsMcp.description')}
</p>
<a
href="https://modelcontextprotocol.io"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline mt-2"
>
{t('mcpSettings.whatIsMcp.learnMore')}
<ExternalLink className="h-3 w-3" />
</a>
<h2 className="font-semibold text-foreground">{t('mcpSettings.whatIsMcp.title')}</h2>
</div>
</div>
</Card>
<div className="p-6">
<p className="text-sm text-muted-foreground mt-1">
{t('mcpSettings.whatIsMcp.description')}
</p>
<a
href="https://modelcontextprotocol.io"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:underline mt-4"
>
{t('mcpSettings.whatIsMcp.learnMore')}
<ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
{/* Section 2: Server Status */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<Server className="h-5 w-5 shrink-0" />
<h2 className="text-lg font-semibold">{t('mcpSettings.serverStatus.title')}</h2>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<span className="text-gray-500">{t('mcpSettings.serverStatus.mode')}:</span>
<Badge variant="secondary">{serverStatus.mode.toUpperCase()}</Badge>
<div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden break-inside-avoid">
<div className="flex items-center gap-3 p-6 border-b border-border">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<Server className="h-5 w-5" />
</div>
<div>
<h2 className="font-semibold text-foreground">{t('mcpSettings.serverStatus.title')}</h2>
</div>
{serverStatus.mode === 'sse' && serverStatus.url && (
<div className="flex items-center gap-2">
<span className="text-gray-500">{t('mcpSettings.serverStatus.url')}:</span>
<code className="text-xs bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
{serverStatus.url}
</code>
</div>
)}
</div>
</Card>
<div className="p-6">
<div className="space-y-4 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">{t('mcpSettings.serverStatus.mode')}</span>
<Badge variant="secondary">{serverStatus.mode.toUpperCase()}</Badge>
</div>
{serverStatus.mode === 'sse' && serverStatus.url && (
<div className="space-y-1.5">
<span className="text-muted-foreground block">{t('mcpSettings.serverStatus.url')}</span>
<code className="text-xs bg-muted p-2 rounded block break-all font-mono">
{serverStatus.url}
</code>
</div>
)}
</div>
</div>
</div>
{/* Section 3: API Keys */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden break-inside-avoid">
<div className="flex items-center justify-between p-6 border-b border-border">
<div className="flex items-center gap-3">
<Key className="h-5 w-5 shrink-0" />
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<Key className="h-5 w-5" />
</div>
<div>
<h2 className="text-lg font-semibold">{t('mcpSettings.apiKeys.title')}</h2>
<p className="text-sm text-gray-500">
<h2 className="font-semibold text-foreground">{t('mcpSettings.apiKeys.title')}</h2>
<p className="text-sm text-muted-foreground">
{t('mcpSettings.apiKeys.description')}
</p>
</div>
@@ -188,28 +199,32 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
</Dialog>
</div>
{keys.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Key className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>{t('mcpSettings.apiKeys.empty')}</p>
</div>
) : (
<div className="space-y-3">
{keys.map(k => (
<KeyCard
key={k.shortId}
keyInfo={k}
onRevoke={handleRevoke}
onDelete={handleDelete}
isPending={isPending}
/>
))}
</div>
)}
</Card>
<div className="p-6">
{keys.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Key className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>{t('mcpSettings.apiKeys.empty')}</p>
</div>
) : (
<div className="space-y-3">
{keys.map(k => (
<KeyCard
key={k.shortId}
keyInfo={k}
onRevoke={handleRevoke}
onDelete={handleDelete}
isPending={isPending}
/>
))}
</div>
)}
</div>
</div>
{/* Section 4: Configuration Instructions */}
<ConfigInstructions serverStatus={serverStatus} />
<div className="break-inside-avoid">
<ConfigInstructions serverStatus={serverStatus} />
</div>
{/* Raw Key Display Dialog */}
<Dialog open={!!showRawKey} onOpenChange={(open) => { if (!open) setShowRawKey(null) }}>
@@ -222,9 +237,9 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
</DialogHeader>
<div className="space-y-3">
<div>
<Label className="text-xs text-gray-500">{rawKeyName}</Label>
<Label className="text-xs text-muted-foreground">{rawKeyName}</Label>
<div className="flex items-center gap-2 mt-1">
<code className="flex-1 text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded break-all font-mono">
<code className="flex-1 text-xs bg-muted p-3 rounded break-all font-mono">
{showRawKey}
</code>
<Button
@@ -330,7 +345,7 @@ function KeyCard({
}
return (
<div className="flex items-center justify-between p-4 rounded-lg border bg-gray-50 dark:bg-gray-900">
<div className="flex items-center justify-between p-4 rounded-lg border bg-muted/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{keyInfo.name}</span>
@@ -340,7 +355,7 @@ function KeyCard({
: t('mcpSettings.apiKeys.revoked')}
</Badge>
</div>
<div className="flex gap-4 text-xs text-gray-500">
<div className="flex gap-4 text-xs text-muted-foreground">
<span>
{t('mcpSettings.apiKeys.createdAt')}: {formatDate(keyInfo.createdAt)}
</span>
@@ -436,23 +451,25 @@ Transport: Streamable HTTP`,
]
return (
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<ExternalLink className="h-5 w-5 shrink-0" />
<div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden">
<div className="flex items-center gap-3 p-6 border-b border-border">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<ExternalLink className="h-5 w-5" />
</div>
<div>
<h2 className="text-lg font-semibold">
<h2 className="font-semibold text-foreground">
{t('mcpSettings.configInstructions.title')}
</h2>
<p className="text-sm text-gray-500">
<p className="text-sm text-muted-foreground">
{t('mcpSettings.configInstructions.description')}
</p>
</div>
</div>
<div className="space-y-2">
<div className="p-6 space-y-2">
{configs.map(cfg => (
<div key={cfg.id} className="border rounded-lg overflow-hidden">
<button
className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-muted transition-colors"
onClick={() => setExpanded(expanded === cfg.id ? null : cfg.id)}
>
<span className="font-medium text-sm">{cfg.title}</span>
@@ -464,8 +481,8 @@ Transport: Streamable HTTP`,
</button>
{expanded === cfg.id && (
<div className="px-4 pb-4">
<p className="text-sm text-gray-500 mb-2">{cfg.description}</p>
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto">
<p className="text-sm text-muted-foreground mb-2">{cfg.description}</p>
<pre className="text-xs bg-muted p-3 rounded overflow-x-auto">
<code>{cfg.snippet}</code>
</pre>
</div>
@@ -473,6 +490,6 @@ Transport: Streamable HTTP`,
</div>
))}
</div>
</Card>
</div>
)
}

View File

@@ -2,7 +2,7 @@
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Settings, Sparkles, Palette, User, Database, Info, Check, Key } from 'lucide-react'
import { Settings, Sparkles, Palette, User, Database, Info, Key } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
@@ -22,74 +22,32 @@ export function SettingsNav({ className }: SettingsNavProps) {
const { t } = useLanguage()
const sections: SettingsSection[] = [
{
id: 'general',
label: t('generalSettings.title'),
icon: <Settings className="h-5 w-5" />,
href: '/settings/general'
},
{
id: 'ai',
label: t('aiSettings.title'),
icon: <Sparkles className="h-5 w-5" />,
href: '/settings/ai'
},
{
id: 'appearance',
label: t('appearance.title'),
icon: <Palette className="h-5 w-5" />,
href: '/settings/appearance'
},
{
id: 'profile',
label: t('profile.title'),
icon: <User className="h-5 w-5" />,
href: '/settings/profile'
},
{
id: 'data',
label: t('dataManagement.title'),
icon: <Database className="h-5 w-5" />,
href: '/settings/data'
},
{
id: 'mcp',
label: t('mcpSettings.title'),
icon: <Key className="h-5 w-5" />,
href: '/settings/mcp'
},
{
id: 'about',
label: t('about.title'),
icon: <Info className="h-5 w-5" />,
href: '/settings/about'
}
{ id: 'general', label: t('generalSettings.title'), icon: <Settings className="h-4 w-4" />, href: '/settings/general' },
{ id: 'ai', label: t('aiSettings.title'), icon: <Sparkles className="h-4 w-4" />, href: '/settings/ai' },
{ id: 'appearance', label: t('appearance.title'), icon: <Palette className="h-4 w-4" />, href: '/settings/appearance' },
{ id: 'profile', label: t('profile.title'), icon: <User className="h-4 w-4" />, href: '/settings/profile' },
{ id: 'data', label: t('dataManagement.title'), icon: <Database className="h-4 w-4" />, href: '/settings/data' },
{ id: 'mcp', label: t('mcpSettings.title'), icon: <Key className="h-4 w-4" />, href: '/settings/mcp' },
{ id: 'about', label: t('about.title'), icon: <Info className="h-4 w-4" />, href: '/settings/about' },
]
const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/')
return (
<nav className={cn('space-y-1', className)}>
<nav className={cn('flex items-center gap-1', className)}>
{sections.map((section) => (
<Link
key={section.id}
href={section.href}
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg transition-colors',
'hover:bg-gray-100 dark:hover:bg-gray-800',
'flex items-center gap-2 px-3 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap',
isActive(section.href)
? 'bg-gray-100 dark:bg-gray-800 text-primary'
: 'text-gray-700 dark:text-gray-300'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
)}
>
{isActive(section.href) && (
<Check className="h-4 w-4 text-primary" />
)}
{!isActive(section.href) && (
<div className="w-4" />
)}
{section.icon}
<span className="font-medium">{section.label}</span>
<span>{section.label}</span>
</Link>
))}
</nav>