import { useEffect, useRef, useState } from "react"; import { ChevronLeft, Loader2, Search } from "lucide-react"; import { useTranslation } from "react-i18next"; import { channelIsRunning, channelMatchesFilter, channelSearchText, localizedChannelDisplayName, type ChannelFilter, } from "@/components/settings/channels/ChannelIdentity"; import { ChannelCatalogRow, ChannelSetupPanel } from "@/components/settings/channels/ChannelSetupPanel"; import { DismissibleStatusMessage, RestartRequiredNotice, SETTINGS_SEARCH_INPUT_CLASS, } from "@/components/settings/shared/SettingsControls"; import { Input } from "@/components/ui/input"; import { useMediaQuery } from "@/hooks/useMediaQuery"; import type { NanobotFeaturesPayload } from "@/lib/types"; import { cn } from "@/lib/utils"; export function ChannelsSettings({ token, nanobotFeatures, loading, query, actionKey, chatAppsDocsUrl, showBrandLogos, error, requiresRestartPending, onQueryChange, onAction, onFeaturesUpdate, onDismissStatus, onRestart, isRestarting, }: { token: string; nanobotFeatures: NanobotFeaturesPayload | null; loading: boolean; query: string; actionKey: string | null; chatAppsDocsUrl?: string; showBrandLogos: boolean; error: string | null; requiresRestartPending: boolean; onQueryChange: (value: string) => void; onAction: (action: "enable" | "disable", name: string) => void; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; onDismissStatus: () => void; onRestart?: () => void; isRestarting?: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const normalizedQuery = query.trim().toLowerCase(); const [filter, setFilter] = useState("all"); const splitLayout = useMediaQuery("(min-width: 1280px)"); const containerRef = useRef(null); const compactDetailTopRef = useRef(null); const [compactDetailOpen, setCompactDetailOpen] = useState(false); const allChannels = (nanobotFeatures?.features ?? []) .filter((feature) => feature.type === "channel") .filter((feature) => feature.settings_visible !== false) .filter((feature) => !normalizedQuery || channelSearchText(feature, t).includes(normalizedQuery)) .sort((left, right) => { const rank = Number(!left.ready) - Number(!right.ready); return rank || localizedChannelDisplayName(left, t).localeCompare( localizedChannelDisplayName(right, t), ); }); const channels = allChannels.filter((feature) => channelMatchesFilter(feature, filter)); const [selectedChannelName, setSelectedChannelName] = useState(null); const selectedChannel = channels.find((feature) => feature.name === selectedChannelName) ?? channels[0] ?? null; const enabledCount = allChannels.filter(channelIsRunning).length; const offCount = Math.max(0, allChannels.length - enabledCount); const filterOptions: Array<{ value: ChannelFilter; label: string; count: number }> = [ { value: "all", label: tx("settings.channels.filterAll", "All"), count: allChannels.length }, { value: "on", label: tx("settings.channels.filterOn", "On"), count: enabledCount }, { value: "off", label: tx("settings.channels.filterOff", "Off"), count: offCount }, ]; const statusMessage = error; const statusIsError = true; useEffect(() => { if (!channels.length) { if (selectedChannelName !== null) setSelectedChannelName(null); setCompactDetailOpen(false); return; } if (!selectedChannelName || !channels.some((feature) => feature.name === selectedChannelName)) { setSelectedChannelName(channels[0].name); setCompactDetailOpen(false); } }, [channels, selectedChannelName]); useEffect(() => { if (splitLayout) return; const resetScroll = () => { let node = containerRef.current?.parentElement ?? null; while (node) { node.scrollTop = 0; node = node.parentElement; } if (compactDetailOpen) { compactDetailTopRef.current?.scrollIntoView?.({ block: "start" }); } }; resetScroll(); const frame = window.requestAnimationFrame(resetScroll); return () => window.cancelAnimationFrame(frame); }, [compactDetailOpen, selectedChannelName, splitLayout]); const openChannel = (name: string) => { setSelectedChannelName(name); if (!splitLayout) setCompactDetailOpen(true); }; const setupPanel = selectedChannel ? ( ) : null; const showingCompactDetail = !splitLayout && compactDetailOpen && selectedChannel !== null; return (
{!showingCompactDetail ? (
onQueryChange(event.target.value)} placeholder={tx("settings.channels.searchPlaceholder", "Search channels")} className={cn( "h-12 rounded-control pl-11 text-[15px]", SETTINGS_SEARCH_INPUT_CLASS, )} />
{filterOptions.map((option) => ( ))}
) : null} {statusMessage ? (
) : null} {requiresRestartPending ? (
) : null}
{loading && !nanobotFeatures ? (
{tx("settings.channels.loading", "Loading Channels...")}
) : channels.length ? splitLayout ? (
{channels.map((feature) => ( openChannel(feature.name)} /> ))}
{setupPanel}
) : showingCompactDetail ? (
{setupPanel}
) : (
{channels.map((feature) => ( openChannel(feature.name)} /> ))}
) : (
{tx("settings.channels.empty", "No channels match this filter.")}
)}
); }