import { Bot, RefreshCcw, Save } from 'lucide-react' import type { ReactNode } from 'react' import { startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { adminApi, ApiError } from '@/lib/api' import type { AdminSiteSettingsResponse, SiteSettingsPayload } from '@/lib/types' function Field({ label, hint, children, }: { label: string hint?: string children: ReactNode }) { return (
{children} {hint ?

{hint}

: null}
) } function toPayload(form: AdminSiteSettingsResponse): SiteSettingsPayload { return { siteName: form.site_name, siteShortName: form.site_short_name, siteUrl: form.site_url, siteTitle: form.site_title, siteDescription: form.site_description, heroTitle: form.hero_title, heroSubtitle: form.hero_subtitle, ownerName: form.owner_name, ownerTitle: form.owner_title, ownerBio: form.owner_bio, ownerAvatarUrl: form.owner_avatar_url, socialGithub: form.social_github, socialTwitter: form.social_twitter, socialEmail: form.social_email, location: form.location, techStack: form.tech_stack, aiEnabled: form.ai_enabled, aiProvider: form.ai_provider, aiApiBase: form.ai_api_base, aiApiKey: form.ai_api_key, aiChatModel: form.ai_chat_model, aiEmbeddingModel: form.ai_embedding_model, aiSystemPrompt: form.ai_system_prompt, aiTopK: form.ai_top_k, aiChunkSize: form.ai_chunk_size, } } export function SiteSettingsPage() { const [form, setForm] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [reindexing, setReindexing] = useState(false) const loadSettings = useCallback(async (showToast = false) => { try { const next = await adminApi.getSiteSettings() startTransition(() => { setForm(next) }) if (showToast) { toast.success('Site settings refreshed.') } } catch (error) { if (error instanceof ApiError && error.status === 401) { return } toast.error(error instanceof ApiError ? error.message : 'Unable to load site settings.') } finally { setLoading(false) } }, []) useEffect(() => { void loadSettings(false) }, [loadSettings]) const updateField = ( key: K, value: AdminSiteSettingsResponse[K], ) => { setForm((current) => (current ? { ...current, [key]: value } : current)) } const techStackValue = useMemo( () => (form?.tech_stack.length ? form.tech_stack.join('\n') : ''), [form?.tech_stack], ) if (loading || !form) { return (
) } return (
Site settings

Brand, profile, and AI controls

This page keeps the public brand, owner profile, and AI configuration aligned with the same backend data model the site already depends on.

Public identity Everything the public site reads for brand, hero copy, owner profile, and social metadata. updateField('site_name', event.target.value)} /> updateField('site_short_name', event.target.value)} /> updateField('site_url', event.target.value)} /> updateField('location', event.target.value)} /> updateField('site_title', event.target.value)} /> updateField('owner_title', event.target.value)} />