"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"; import NineRemotePromoModal from "./NineRemotePromoModal"; // 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 [showRemoteModal, setShowRemoteModal] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [showUpdateModal, setShowUpdateModal] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [shutdownCountdown, setShutdownCountdown] = useState(0); const [enableTranslator, setEnableTranslator] = useState(false); const { copied, copy } = useCopyToClipboard(2000); const INSTALL_CMD = UPDATER_CONFIG.installCmdLatest; 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); }; // Open manual update panel (no countdown yet — user must click Copy to trigger shutdown) const handleUpdate = () => { setShowUpdateModal(false); setIsUpdating(true); }; // Triggered by Copy button inside ManualUpdatePanel: copy + countdown + shutdown const handleCopyAndShutdown = async () => { try { await navigator.clipboard.writeText(INSTALL_CMD); } catch { /* clipboard blocked */ } copy(INSTALL_CMD); let remaining = UPDATER_CONFIG.shutdownCountdownSec; setShutdownCountdown(remaining); const timer = setInterval(() => { remaining -= 1; setShutdownCountdown(remaining); if (remaining <= 0) { clearInterval(timer); fetch("/api/version/shutdown", { method: "POST" }).catch(() => {}); setIsDisconnected(true); } }, 1000); }; const handleCancelUpdate = () => { setIsUpdating(false); setShutdownCountdown(0); }; // Note: legacy updater poll removed. New flow: copy install cmd + shutdown server, // user runs the command manually in another terminal. return ( <> {/* Remote Promo Modal */} setShowRemoteModal(false)} /> {/* Update Confirmation Modal */} setShowUpdateModal(false)} onConfirm={handleUpdate} title="Update 9Router" message={`Show install command for v${updateInfo?.latestVersion || ""}? You can copy it and shutdown to install manually.`} confirmText="Show Command" cancelText="Cancel" variant="primary" /> {/* Disconnected / Updating Overlay */} {(isDisconnected || isUpdating) && (
{isUpdating ? ( ) : (
power_off

Server Disconnected

The proxy server has been stopped.

)}
)} ); } Sidebar.propTypes = { onClose: PropTypes.func, }; function ManualUpdatePanel({ latestVersion, installCmd, copied, onCopyAndShutdown, onCancel, countdown, isDisconnected }) { const isCountingDown = countdown > 0; return (
content_copy

Update 9Router{latestVersion ? ` to v${latestVersion}` : ""}

{isDisconnected ? "Server stopped. Paste the command into a terminal to install." : isCountingDown ? `Command copied. Server will stop in ${countdown}s...` : "Click the button below to copy the install command and shutdown."}

Install command:

{installCmd}
  1. Click Copy & Shutdown below.
  2. Paste the command into your terminal and press Enter.
  3. Run 9router again after install.
{isDisconnected ? ( ) : (
)}
); } ManualUpdatePanel.propTypes = { latestVersion: PropTypes.string, installCmd: PropTypes.string.isRequired, copied: PropTypes.bool, onCopyAndShutdown: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, countdown: PropTypes.number, isDisconnected: PropTypes.bool, };