"use client"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/shared/utils/cn"; import { APP_CONFIG, UPDATER_CONFIG } from "@/shared/constants/config"; import { MEDIA_PROVIDER_KINDS } from "@/shared/constants/providers"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import Button from "./Button"; import { ConfirmModal } from "./Modal"; // const VISIBLE_MEDIA_KINDS = ["embedding", "image", "imageToText", "tts", "stt", "webSearch", "webFetch", "video", "music"]; const VISIBLE_MEDIA_KINDS = ["embedding", "image", "tts", "stt"]; // Combined entry: webSearch + webFetch share one page at /dashboard/media-providers/web const COMBINED_WEB_ITEM = { id: "web", label: "Web Fetch & Search", icon: "travel_explore", href: "/dashboard/media-providers/web" }; const navItems = [ { href: "/dashboard/endpoint", label: "Endpoint", icon: "api" }, { href: "/dashboard/providers", label: "Providers", icon: "dns" }, // { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden { href: "/dashboard/combos", label: "Combos", icon: "layers" }, { href: "/dashboard/usage", label: "Usage", icon: "bar_chart" }, { href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" }, { href: "/dashboard/mitm", label: "MITM", icon: "security" }, { href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" }, ]; const debugItems = [ { href: "/dashboard/console-log", label: "Console Log", icon: "terminal" }, { href: "/dashboard/translator", label: "Translator", icon: "translate" }, ]; const systemItems = [ { href: "/dashboard/proxy-pools", label: "Proxy Pools", icon: "lan" }, { href: "/dashboard/skills", label: "Skills", icon: "extension" }, ]; export default function Sidebar({ onClose }) { const pathname = usePathname(); const [mediaOpen, setMediaOpen] = useState(false); const [showShutdownModal, setShowShutdownModal] = useState(false); const [isShuttingDown, setIsShuttingDown] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [showUpdateModal, setShowUpdateModal] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [updateStatus, setUpdateStatus] = useState(null); const [enableTranslator, setEnableTranslator] = useState(false); const { copied, copy } = useCopyToClipboard(2000); const INSTALL_CMD = UPDATER_CONFIG.installCmd; const STATUS_URL = `http://localhost:${UPDATER_CONFIG.statusPort}/update/status`; useEffect(() => { fetch("/api/settings") .then(res => res.json()) .then(data => { if (data.enableTranslator) setEnableTranslator(true); }) .catch(() => {}); }, []); // Lazy check for new npm version on mount useEffect(() => { fetch("/api/version") .then(res => res.json()) .then(data => { if (data.hasUpdate) setUpdateInfo(data); }) .catch(() => {}); }, []); const isActive = (href) => { if (href === "/dashboard/endpoint") { return pathname === "/dashboard" || pathname.startsWith("/dashboard/endpoint"); } return pathname.startsWith(href); }; const handleUpdate = async () => { setIsUpdating(true); setShowUpdateModal(false); try { const res = await fetch("/api/version/update", { method: "POST" }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.message || "Update failed. Please run the install command manually."); setIsUpdating(false); return; } setIsDisconnected(true); } catch (e) { setIsDisconnected(true); } }; // Poll updater status server while updating (Next server is dead, updater.js is alive) useEffect(() => { if (!isUpdating || !isDisconnected) return; let stopped = false; const tick = async () => { try { const res = await fetch(STATUS_URL, { cache: "no-store" }); if (res.ok) { const data = await res.json(); if (!stopped) setUpdateStatus(data); } } catch { /* updater not ready yet or finished */ } }; tick(); const id = setInterval(tick, UPDATER_CONFIG.statusPollIntervalMs); return () => { stopped = true; clearInterval(id); }; }, [isUpdating, isDisconnected, STATUS_URL]); const handleShutdown = async () => { setIsShuttingDown(true); try { await fetch("/api/shutdown", { method: "POST" }); } catch (e) { // Expected to fail as server shuts down; ignore error } setIsShuttingDown(false); setShowShutdownModal(false); setIsDisconnected(true); }; return ( <> {/* Shutdown Confirmation Modal */} setShowShutdownModal(false)} onConfirm={handleShutdown} title="Close Proxy" message="Are you sure you want to close the proxy server?" confirmText="Close" cancelText="Cancel" variant="danger" loading={isShuttingDown} /> {/* Update Confirmation Modal */} setShowUpdateModal(false)} onConfirm={handleUpdate} title="Update 9Router" message={`This will close 9Router and install v${updateInfo?.latestVersion || ""} in a separate window. Continue?`} confirmText="Update" cancelText="Cancel" variant="primary" loading={isUpdating} /> {/* Disconnected Overlay */} {isDisconnected && (
{isUpdating ? ( copy(INSTALL_CMD)} /> ) : (
power_off

Server Disconnected

The proxy server has been stopped.

)}
)} ); } Sidebar.propTypes = { onClose: PropTypes.func, }; function UpdateProgress({ status, latestVersion, installCmd, copied, onCopy }) { const phase = status?.phase || "connecting"; const done = status?.done === true; const success = status?.success === true; const attempt = status?.attempt || 0; const maxRetries = status?.maxRetries || 0; const logTail = status?.logTail || []; const errorMsg = status?.error; const steps = [ { key: "stopped", label: "Stopped 9Router server", state: "done" }, { key: "launched", label: "Launched background installer", state: status ? "done" : "active", }, { key: "waiting", label: "Waiting for app processes to exit", state: phase === "waitingForExit" ? "active" : (status && phase !== "starting" ? "done" : "pending"), }, { key: "installing", label: attempt > 1 ? `Installing v${latestVersion || "latest"} (attempt ${attempt}/${maxRetries})` : `Installing v${latestVersion || "latest"}`, state: done ? (success ? "done" : "error") : (phase === "installing" ? "active" : "pending"), }, { key: "finished", label: done && success ? "Installed — ready to restart" : "Waiting to finish", state: done && success ? "done" : (done && !success ? "error" : "pending"), }, ]; return (
{done && success ? "check_circle" : done && !success ? "error" : "progress_activity"}

{done && success ? "Update Completed" : done && !success ? "Update Failed" : "Updating 9Router"}

{done && success ? `Installed v${latestVersion || "latest"} successfully` : done && !success ? (errorMsg || "Installation failed") : `Installing v${latestVersion || "latest"} from npm...`}

{/* Timeline */}
    {steps.map((s) => (
  • {s.state === "done" ? "check_circle" : s.state === "error" ? "cancel" : s.state === "active" ? "radio_button_checked" : "radio_button_unchecked"} {s.label}
  • ))}
{/* Log tail */} {logTail.length > 0 && (
            {logTail.join("\n")}
          
)} {/* Actions */} {done && success ? (

Run 9router in your terminal to start the new version.

) : done && !success ? (

Run the install command manually:

) : (

This may take 30-60 seconds. Please don't close this window.

)}
); } UpdateProgress.propTypes = { status: PropTypes.object, latestVersion: PropTypes.string, installCmd: PropTypes.string.isRequired, copied: PropTypes.bool, onCopy: PropTypes.func.isRequired, };