'use client'; import { useState, useEffect } from 'react'; import axios from 'axios'; import toast from 'react-hot-toast'; import { motion } from 'framer-motion'; import { fadeUp, stagger } from '@/lib/animations'; import { User, ChartBar as BarChart3, Key, Shield, CircleNotch as LoaderCircle, FloppyDisk as Save, Sun, Envelope as Mail, Building as Building2, SignOut as LogOut, CaretRight as ChevronRight, Lightning as Zap, Trash as Trash2, Plus, Copy } from '@phosphor-icons/react'; import { useAuth } from '@/contexts/auth'; import { getSupabase } from '@/lib/supabase'; import { useRouter } from 'next/navigation'; import { getApiKeys, createApiKey, deleteApiKey } from '@/lib/api'; import type { ApiKey } from '@/lib/api'; import { CriticalButton, FlatInput, PageHeader } from '@/components/ui'; function getInitials(name?: string, email?: string): string { if (name) return name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase(); if (email) return email[0].toUpperCase(); return 'G'; } export default function SettingsPage() { const { user, isGuest, upgradeToAccount } = useAuth(); const router = useRouter(); const [fullName, setFullName] = useState(''); const [institution, setInstitution] = useState(''); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); const [jobsToday, setJobsToday] = useState(0); const [jobsTotal, setJobsTotal] = useState(0); const [dailyLimit, setDailyLimit] = useState(10); const [apiKeys, setApiKeys] = useState([]); const [keysLoading, setKeysLoading] = useState(false); const [creatingKey, setCreatingKey] = useState(false); const [newKeyValue, setNewKeyValue] = useState(null); const [keyNameInput, setKeyNameInput] = useState(''); useEffect(() => { const fetchData = async () => { try { const api = axios.create({ baseURL: '/api/backend' }); const supabase = getSupabase(); const { data: { session } } = await supabase.auth.getSession(); if (session?.access_token) api.defaults.headers.Authorization = `Bearer ${session.access_token}`; const [profileRes, countRes, jobsRes] = await Promise.all([ api.get('/api/profile'), api.get('/api/jobs/count'), api.get('/api/jobs'), ]); if (profileRes.data && !profileRes.data.error) { setFullName(profileRes.data.full_name || ''); setInstitution(profileRes.data.institution || ''); } setJobsToday(countRes.data.count || 0); setDailyLimit(countRes.data.limit || 10); setJobsTotal((jobsRes.data.jobs || []).length); } catch { /* guest users may 401 */ } finally { setLoading(false); } }; fetchData(); if (!isGuest) { setKeysLoading(true); getApiKeys().then(setApiKeys).catch(() => {}).finally(() => setKeysLoading(false)); } }, [isGuest]); const handleSave = async () => { setSaving(true); try { const api = axios.create({ baseURL: '/api/backend' }); const supabase = getSupabase(); const { data: { session } } = await supabase.auth.getSession(); if (session?.access_token) api.defaults.headers.Authorization = `Bearer ${session.access_token}`; await api.put('/api/profile', { full_name: fullName, institution }); toast.success('Profile updated'); } catch { toast.error('Failed to save settings'); } finally { setSaving(false); } }; const handleSignOut = async () => { const supabase = getSupabase(); await supabase.auth.signOut(); router.push('/auth'); }; const resolvedName = fullName || (user?.user_metadata?.full_name as string | undefined); const initials = getInitials(resolvedName, user?.email); const usagePercent = Math.min(Math.round((jobsToday / dailyLimit) * 100), 100); const memberSince = user?.created_at ? new Date(user.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) : null; return (
{/* ── Profile ── */}

Profile

{loading ? (
) : (
{/* Avatar identity row */}
{initials}

{resolvedName || (isGuest ? 'Guest User' : 'Synteny User')}

{user?.email ?? (isGuest ? 'No account' : '')}

{memberSince &&

Member since {memberSince}

} {isGuest &&

Guest session — data not persisted

}
{/* Email (read-only) */}
{/* Editable fields */}
setFullName(e.target.value)} placeholder="Your full name" className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-glass-border focus:border-accent-cyan/40 focus:ring-2 focus:ring-accent-cyan/10 outline-none transition text-sm text-text-primary bg-surface-1" />
setInstitution(e.target.value)} placeholder="University or lab" className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-glass-border focus:border-accent-cyan/40 focus:ring-2 focus:ring-accent-cyan/10 outline-none transition text-sm text-text-primary bg-surface-1" />
{isGuest ? (

Save your work

Create a free account to keep your analysis history, save settings, and never lose results.

upgradeToAccount()}> Sign in with Google
) : (
{!saving && } Save Profile
)}
)}
{/* ── Usage & Plan ── */}

Usage & Plan

{[ { label: 'Today', value: jobsToday, sub: `of ${dailyLimit} limit` }, { label: 'All time', value: jobsTotal, sub: 'total runs' }, { label: 'Plan', value: 'Free', sub: 'no credit card' }, ].map(stat => (

{stat.value}

{stat.label}

{stat.sub}

))}
{/* Usage bar */}
Daily analyses used {jobsToday} / {dailyLimit}
= 80 ? 'bg-accent-amber' : 'bg-accent-cyan'}`} initial={{ width: 0 }} animate={{ width: `${usagePercent}%` }} transition={{ duration: 0.9, ease: 'easeOut' }} />

Resets daily at midnight UTC

{/* Pro upsell stub */}

Need more capacity?

Pro plan with higher limits — coming soon

{/* ── Appearance ── */}

Appearance

Void Dark

Designed for long research sessions — reduced eye strain

Active

Additional themes will be available in a future update.

{/* ── API Access ── */}

API Access

{isGuest ? (

Sign in to access API keys

Create a free account to generate API keys for programmatic access.

) : (

Generate API keys to run Synteny pipelines from your scripts. Use the X-API-Key header to authenticate.

{/* Existing keys */} {keysLoading ? (
) : apiKeys.length > 0 ? (
{apiKeys.map(k => (

{k.name}

{k.key_prefix}...

{k.last_used_at ? `Used ${new Date(k.last_used_at).toLocaleDateString()}` : 'Never used'}
))}
) : (

No API keys yet.

)} {/* Generate new key */} {newKeyValue ? (

Key generated — copy it now. You won't see it again.

{newKeyValue}
) : (
setKeyNameInput(e.target.value)} placeholder="e.g. My script" className="flex-1 text-sm" /> { if (!keyNameInput.trim()) { toast.error('Enter a name'); return; } setCreatingKey(true); try { const res = await createApiKey(keyNameInput.trim()); setNewKeyValue(res.key); setApiKeys(prev => [...prev, { id: '', name: res.name, key_prefix: res.key_prefix, created_at: new Date().toISOString(), last_used_at: null }]); toast.success('API key created'); } catch { toast.error('Failed to create key'); } finally { setCreatingKey(false); } }} loading={creatingKey} > {!creatingKey && } Generate
)}
)}
{/* ── Data & Privacy ── */}

Data & Privacy

{[ 'Your uploaded sequences and results belong to you. We never train models on your data.', 'Cached query results are stored for 24 hours, keyed by input hash — not your user ID.', 'All AI outputs are labeled "AI-assisted research analysis — not for clinical or diagnostic use."', ].map((line, i) => (

{line}

))}
{/* ── Account Actions ── */}

{isGuest ? 'Get Started' : 'Account Actions'}

{isGuest ? (

Sign in with Google to save your results, access your history, and unlock higher daily limits.

upgradeToAccount()}> Sign in with Google
) : ( )}
); }