import { useEffect, useMemo, useState, type ComponentType } from "react"; import { Check, ChevronDown, ChevronRight, Clipboard, Loader2, Plus, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { channelUiContribution } from "@/channel-plugins/registry"; import type { ChannelPluginConnectFlowProps } from "@/channel-plugins/types"; import { ToggleButton } from "@/components/settings/ToggleButton"; import { type ChannelProviderPreset, type ChannelSetupPresentation, } from "@/components/settings/channels/catalog"; import { CredentialForm, channelValuesForSubmit, defaultChannelFieldValues, } from "@/components/settings/channels/CredentialForm"; import { ChannelLogo, ChannelRuntimeError, ChannelStatusBadge, channelDescription, channelRequirements, channelSetup, channelStatusLabel, channelToggleChecked, localizedChannelDisplayName, } from "@/components/settings/channels/ChannelIdentity"; import { ChannelProviderPresets, ChannelSetupActions, ChannelSetupLinks, ChannelSetupSteps, ChannelValidationBadge, ChannelValidationChecks, ChannelValidationDetails, } from "@/components/settings/channels/ChannelSetupParts"; import { ChannelInstancesPanel } from "@/components/settings/channels/ChannelInstancesPanel"; import { Button } from "@/components/ui/button"; import { configureChannel, validateChannel, } from "@/lib/api"; import { copyTextToClipboard } from "@/lib/clipboard"; import type { ChannelValidationPayload, NanobotFeatureInfo, NanobotFeaturesPayload, } from "@/lib/types"; import { cn } from "@/lib/utils"; export function ChannelCatalogRow({ feature, selected, showBrandLogos, onSelect, }: { feature: NanobotFeatureInfo; selected: boolean; showBrandLogos: boolean; onSelect: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const displayName = localizedChannelDisplayName(feature, t); return ( ); } export function ChannelSetupPanel({ token, feature, actionKey, chatAppsDocsUrl, showBrandLogos, onAction, onFeaturesUpdate, }: { token: string; feature: NanobotFeatureInfo; actionKey: string | null; chatAppsDocsUrl?: string; showBrandLogos: boolean; onAction: (action: "enable" | "disable", name: string) => void; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; }) { const { t, i18n } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const displayName = localizedChannelDisplayName(feature, t); const [connectRequestId, setConnectRequestId] = useState(0); const uiContribution = channelUiContribution(feature.name, feature.webui); const PluginPanel = uiContribution?.Panel; if (PluginPanel) { return ( ); } if (feature.instances !== undefined) { return ( ); } const enableBusy = actionKey === `enable:${feature.name}`; const disableBusy = actionKey === `disable:${feature.name}`; const missingSupport = feature.enabled && !feature.installed; const alwaysEnabled = feature.capabilities?.includes("always_enabled") ?? false; const channelChecked = alwaysEnabled || channelToggleChecked(feature); const channelBusy = enableBusy || disableBusy; const setup = channelSetup(feature, i18n.resolvedLanguage ?? i18n.language); const needsSetupBeforeEnable = !channelChecked && feature.configured === false && !(uiContribution?.canConnectBeforeConfigured && setup.mode === "connect"); const channelToggleDisabled = alwaysEnabled || channelBusy || needsSetupBeforeEnable || (!feature.install_supported && !feature.installed && !feature.enabled); const installSupportLabel = tx("settings.nanobotFeatures.installSupport", "Install support"); const toggleAriaLabel = t("settings.channels.toggleChannel", { name: displayName, defaultValue: "{{name}} channel", }); return ( ); } function ChannelSetupSurface({ token, feature, setup, chatAppsDocsUrl, connectRequestId, ConnectFlow, onFeaturesUpdate, }: { token: string; feature: NanobotFeatureInfo; setup: ChannelSetupPresentation; chatAppsDocsUrl?: string; connectRequestId: number; ConnectFlow?: ComponentType; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const [notice, setNotice] = useState(null); const [saving, setSaving] = useState(false); const [validating, setValidating] = useState(false); const [validation, setValidation] = useState(null); const [visibleSecrets, setVisibleSecrets] = useState>({}); const [touchedFields, setTouchedFields] = useState>(() => new Set()); const configValuesKey = JSON.stringify(feature.config_values ?? {}); const configuredFields = useMemo( () => new Set(feature.configured_fields ?? []), [feature.configured_fields], ); const mode = setup.mode ?? "credentials"; const fields = setup.fields ?? []; const requiredFields = fields.filter((field) => !field.optional); const primaryFields = requiredFields.length ? requiredFields : fields.slice(0, 1); const optionalFields = fields.filter((field) => field.optional); const manualFields = setup.manualFields ?? []; const advancedFields = mode === "connect" ? manualFields : optionalFields; const editableFields = mode === "credentials" ? fields : mode === "connect" ? manualFields : []; const hasAdvanced = advancedFields.length > 0; const requirements = channelRequirements(feature, t); const summary = setup.summary ?? tx( "settings.channels.setupSummary", "Enable only turns on nanobot support. Add the platform credentials, then restart nanobot.", ); const [fieldValues, setFieldValues] = useState>(() => defaultChannelFieldValues(editableFields, feature.config_values), ); useEffect(() => { setNotice(null); setVisibleSecrets({}); setSaving(false); setValidating(false); setValidation(null); setTouchedFields(new Set()); setFieldValues(defaultChannelFieldValues(editableFields, feature.config_values)); }, [configValuesKey, feature.name]); const toggleSecret = (key: string) => { setVisibleSecrets((current) => ({ ...current, [key]: !current[key] })); }; const setFieldValue = (key: string, value: string) => { setFieldValues((current) => ({ ...current, [key]: value })); setTouchedFields((current) => new Set(current).add(key)); }; const applyPreset = (preset: ChannelProviderPreset) => { setFieldValues((current) => ({ ...current, ...preset.values })); setTouchedFields((current) => { const next = new Set(current); for (const key of Object.keys(preset.values)) next.add(key); return next; }); }; const copyCommand = () => { if (!setup.command) return; void copyTextToClipboard(setup.command).then((ok) => { setNotice( ok ? tx("settings.channels.commandCopied", "Command copied.") : tx("settings.channels.commandCopyFailed", "Could not copy command."), ); }); }; const saveCredentialSettings = async () => { setSaving(true); setValidating(true); setNotice(null); const values = channelValuesForSubmit(fields, fieldValues, touchedFields); try { const validationPayload = await validateChannel(token, feature.name, values); setValidation(validationPayload); if (!validationPayload.can_enable) { setNotice( validationPayload.message ?? tx("settings.channels.validationFailed", "Check the required setup before enabling."), ); return; } const payload = await configureChannel( token, feature.name, values, { enable: true }, ); if (payload.nanobot_features) { onFeaturesUpdate(payload.nanobot_features); } setNotice(tx("settings.channels.checkedAndEnabled", "Checked and enabled.")); } catch (err) { setNotice((err as Error).message); } finally { setSaving(false); setValidating(false); } }; const checkCurrentSettings = async () => { setValidating(true); setNotice(null); try { const payload = await validateChannel( token, feature.name, channelValuesForSubmit(fields, fieldValues, touchedFields), ); setValidation(payload); if (payload.message) setNotice(payload.message); } catch (err) { setNotice((err as Error).message); } finally { setValidating(false); } }; const primaryActionLabel = channelToggleChecked(feature) ? tx("settings.channels.checkConnection", "Check connection") : tx("settings.channels.checkAndEnable", "Check and enable"); return (
{ event.preventDefault(); if (mode === "credentials") void saveCredentialSettings(); }} >
{tx("settings.channels.requiredSetup", "Required setup")}
{mode !== "webui" ? ( ) : null} {mode === "webui" ? ( {tx("settings.channels.managedByWebui", "Managed by WebUI")} ) : null}

{requirements}

{summary}

{mode === "connect" && ConnectFlow ? ( ) : mode === "connect" ? ( <>
{setup.command ? ( ) : null}
{setup.command ? ( {setup.command} ) : null} ) : mode === "credentials" ? ( <> {setup.presets?.length ? ( ) : null} {primaryFields.length ? ( ) : null}
{feature.configured || validation ? ( ) : null}
) : null}
{notice ? (
{notice}
) : null} {setup.steps.length ? ( ) : null} {validation?.checks.length ? : null} {hasAdvanced ? (
{tx("settings.channels.advanced", "Advanced")} {advancedFields.length ? (
) : null}
) : null} ); }