import { agentNativePath } from "@agent-native/core/client/api-path"; import { ChangelogSettingsCard } from "@agent-native/core/client/changelog"; import { useActionQuery } from "@agent-native/core/client/hooks"; import { LanguagePicker, useT } from "@agent-native/core/client/i18n"; import { useOnboarding, type OnboardingMethod, type OnboardingStepStatus, } from "@agent-native/core/client/onboarding"; import { TeamPage } from "@agent-native/core/client/org"; import { AccountSettingsCard, SettingsTabsPage, useAgentSettingsTabs, useBuilderConnectFlow, useBuilderStatus, type SettingsSearchEntry, } from "@agent-native/core/client/settings"; import { CreativeContextSettingsLink } from "@agent-native/creative-context/client"; import { IconAlertCircle, IconCheck, IconChevronDown, IconCloudUpload, IconExternalLink, IconKey, IconLibraryPhoto, IconLoader2, IconPhoto, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState, type FormEvent, type ReactNode, } from "react"; import { PageShell } from "@/components/layout/PageShell"; 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 { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import changelog from "../../CHANGELOG.md?raw"; type ImageGenerationConfig = { builderEnabled?: boolean; builderConnected?: boolean; geminiConfigured?: boolean; openaiConfigured?: boolean; objectStorageConfigured?: boolean; configured?: boolean; lastIssue?: { message?: unknown; at?: unknown; } | null; }; type FormOnboardingMethod = Extract; export default function SettingsPage() { const t = useT(); const agentSettingsTabs = useAgentSettingsTabs(); const { data } = useActionQuery("list-libraries", { compact: true }) as { data?: { count?: number }; }; const generalSearchEntries = useMemo( () => [ { id: "assets-language", label: t("settings.languageTitle"), keywords: "language locale translation i18n", hash: "language", }, { id: "assets-generation-setup", label: t("settings.setupTitle"), keywords: "builder generation storage object storage api key gemini openai brand kit setup connect", hash: "asset-generation-setup", }, ], [t], ); return ( } teamLabel={t("team.title")} extraTabs={agentSettingsTabs} generalSearchEntries={generalSearchEntries} general={

{t("settings.connections")}

{t("settings.connectionsDescription")}

{t("settings.languageTitle")} {t("settings.languageDescription")}
} team={
} whatsNew={
} />
); } function AssetsSetupCard({ libraryCount }: { libraryCount: number }) { const t = useT(); const queryClient = useQueryClient(); const onboarding = useOnboarding(); const { status } = useBuilderStatus(); const [manualGenerationOpen, setManualGenerationOpen] = useState(false); const [manualStorageOpen, setManualStorageOpen] = useState(false); const { data: configData } = useActionQuery( "get-image-generation-config", {}, ) as { data?: ImageGenerationConfig }; const refreshSetup = async () => { await Promise.all([ onboarding.refresh(), queryClient.invalidateQueries({ queryKey: ["action", "get-image-generation-config"], }), ]); }; const flow = useBuilderConnectFlow({ trackingSource: "assets_settings_connections", trackingFlow: "image_generation", onConnected: refreshSetup, }); const generationStep = onboarding.steps.find( (step) => step.id === "image-generation", ); const storageStep = onboarding.steps.find( (step) => step.id === "image-storage", ); const builderEnabled = configData?.builderEnabled ?? true; const builderConfigured = flow.hasFetchedStatus ? flow.configured : !!status?.configured; const builderConnected = builderEnabled && (!!configData?.builderConnected || builderConfigured || !!flow.configured); const generationReady = builderConnected || configData?.configured === true || !!configData?.openaiConfigured || !!configData?.geminiConfigured || !!generationStep?.complete; const storageReady = builderConnected || !!configData?.objectStorageConfigured || !!storageStep?.complete; const setupIssue = flow.error ?? (typeof configData?.lastIssue?.message === "string" ? configData.lastIssue.message : null); const orgName = flow.orgName ?? status?.orgName ?? null; const readyCount = [generationReady, storageReady].filter(Boolean).length; return (
{t("settings.setupTitle")} {t("settings.setupDescription")}
{readyCount}/2 {t("settings.setupReady")}
} title="Builder" description={ builderConnected ? orgName ? `Connected to ${orgName}.` : t("settings.builderDescriptionReady") : builderEnabled ? t("settings.builderDescriptionManaged") : t("settings.builderDescriptionDisabled") } status={ {builderConnected ? t("settings.connected") : t("settings.optional")} } action={ builderEnabled ? ( ) : null } /> {setupIssue ? : null} } title={t("settings.generation")} description={generationSummary(configData, builderConnected, t)} status={ {generationReady ? t("settings.generationReady") : t("settings.generationNeedsSetup")} } action={ generationStep ? ( setManualGenerationOpen((open) => !open)} > {t("settings.manualKeys")} ) : null } /> {manualGenerationOpen && generationStep ? ( ) : null} } title={t("settings.storage")} description={ storageReady ? t("settings.storageReady") : t("settings.storageNeedsSetup") } status={ {storageReady ? t("settings.generationReady") : t("settings.generationNeedsSetup")} } action={ storageStep ? ( setManualStorageOpen((open) => !open)} > {t("settings.configure")} ) : null } /> {manualStorageOpen && storageStep ? ( ) : null} } title={t("settings.brandKits")} description={`${libraryCount} accessible ${ libraryCount === 1 ? "brand kit" : "brand kits" }.`} status={ {t("settings.available")} } />
); } function SettingsRow({ icon, title, description, status, action, }: { icon: ReactNode; title: string; description: string; status: ReactNode; action?: ReactNode; }) { return (
{icon}

{title}

{status}

{description}

{action ?
{action}
: null}
); } function StatusPill({ children, tone, }: { children: ReactNode; tone: "ready" | "attention" | "neutral"; }) { return ( {tone === "ready" ? : null} {children} ); } function DisclosureButton({ children, open, onClick, }: { children: ReactNode; open: boolean; onClick: () => void; }) { return ( ); } function ManualMethodPanel({ step, title, description, onSaved, }: { step: OnboardingStepStatus; title: string; description: string; onSaved: () => Promise; }) { const t = useT(); const methods = useMemo(() => step.methods.filter(isFormMethod), [step]); const [selectedId, setSelectedId] = useState(methods[0]?.id ?? ""); useEffect(() => { if (!methods.some((method) => method.id === selectedId)) { setSelectedId(methods[0]?.id ?? ""); } }, [methods, selectedId]); const selected = methods.find((method) => method.id === selectedId); return (

{title}

{description}

{methods.length > 1 ? (
) : null} {selected ? ( ) : (

{t("settings.noManualOptions")}

)}
); } function CredentialForm({ method, onSaved, }: { method: FormOnboardingMethod; onSaved: () => Promise; }) { const t = useT(); const [values, setValues] = useState>({}); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const fields = method.payload.fields; const submitLabel = fields.length > 1 ? t("settings.saveSettings") : t("settings.saveKey"); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setSaving(true); setError(null); try { const vars = fields .map((field) => ({ key: field.key, value: (values[field.key] ?? "").trim(), })) .filter((item) => item.value !== ""); if (!vars.length) { setError(t("settings.enterValueFirst")); return; } const response = await fetch(agentNativePath("/_agent-native/env-vars"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vars, scope: method.payload.writeScope ?? "workspace", }), }); if (!response.ok) { const data = (await response.json().catch(() => ({}))) as { error?: string; }; throw new Error(`${t("settings.saveFailed")}: ${response.status}`); } setValues({}); await onSaved(); } catch (err) { setError(err instanceof Error ? err.message : t("settings.saveFailed")); } finally { setSaving(false); } }; return (
{method.description ? (

{method.description}

) : null}
{fields.map((field) => { const id = `${method.id}-${field.key}`; return (
setValues((current) => ({ ...current, [field.key]: event.target.value, })) } className="mt-2" autoComplete="off" spellCheck={false} />
); })}
{error ? (

{error}

) : null}
); } function SetupIssueCallout({ message }: { message: string }) { return (

{message}

); } function isFormMethod( method: OnboardingMethod, ): method is FormOnboardingMethod { return method.kind === "form"; } function generationSummary( config: ImageGenerationConfig | undefined, builderConnected: boolean, t: (key: string, options?: Record) => string, ) { if (builderConnected) return t("settings.builderManaged"); const providers = [ config?.geminiConfigured ? "Gemini" : null, config?.openaiConfigured ? "OpenAI" : null, ].filter(Boolean); if (providers.length) { return t("settings.providerConfigured", { providers: providers.join(" and "), }); } if (config?.builderEnabled === false) { return t("settings.addGeminiOrOpenAI"); } return t("settings.addBuilderGeminiOrOpenAI"); }