diff --git a/open-sse/utils/error.js b/open-sse/utils/error.js index b82dead3..df9afc98 100644 --- a/open-sse/utils/error.js +++ b/open-sse/utils/error.js @@ -126,7 +126,11 @@ export function unavailableResponse(statusCode, message, retryAfter, retryAfterH * @returns {string} Formatted error message */ export function formatProviderError(error, provider, model, statusCode) { - const code = statusCode || error.code || 'FETCH_FAILED'; + const code = statusCode || error.code || "FETCH_FAILED"; const message = error.message || "Unknown error"; - return `[${code}]: ${message}`; + // Expose low-level cause (e.g. UND_ERR_SOCKET, ECONNRESET, ETIMEDOUT) for diagnosing fetch failures + const causeCode = error.cause?.code; + const causeMsg = error.cause?.message; + const causeStr = causeCode || causeMsg ? ` (cause: ${[causeCode, causeMsg].filter(Boolean).join(": ")})` : ""; + return `[${code}]: ${message}${causeStr}`; } diff --git a/src/shared/components/HeaderMenu.js b/src/shared/components/HeaderMenu.js index 4a9ece46..bb44d8e6 100644 --- a/src/shared/components/HeaderMenu.js +++ b/src/shared/components/HeaderMenu.js @@ -2,16 +2,97 @@ import { useState, useEffect, useRef } from "react"; import PropTypes from "prop-types"; -import ThemeToggle from "./ThemeToggle"; -import LanguageSwitcher from "./LanguageSwitcher"; -import NineRemoteButton from "./NineRemoteButton"; +import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config"; +import { useTheme } from "@/shared/hooks/useTheme"; import ChangelogModal from "./ChangelogModal"; +import NineRemotePromoModal from "./NineRemotePromoModal"; +import LanguageSwitcher from "./LanguageSwitcher"; + +const LOCALE_INFO = { + "en": { name: "English", flag: "🇺🇸" }, + "vi": { name: "Tiếng Việt", flag: "🇻🇳" }, + "zh-CN": { name: "简体中文", flag: "🇨🇳" }, + "zh-TW": { name: "繁體中文", flag: "🇹🇼" }, + "ja": { name: "日本語", flag: "🇯🇵" }, + "pt-BR": { name: "Português (BR)", flag: "🇧🇷" }, + "pt-PT": { name: "Português (PT)", flag: "🇵🇹" }, + "ko": { name: "한국어", flag: "🇰🇷" }, + "es": { name: "Español", flag: "🇪🇸" }, + "de": { name: "Deutsch", flag: "🇩🇪" }, + "fr": { name: "Français", flag: "🇫🇷" }, + "he": { name: "עברית", flag: "🇮🇱" }, + "ar": { name: "العربية", flag: "🇸🇦" }, + "ru": { name: "Русский", flag: "🇷🇺" }, + "pl": { name: "Polski", flag: "🇵🇱" }, + "cs": { name: "Čeština", flag: "🇨🇿" }, + "nl": { name: "Nederlands", flag: "🇳🇱" }, + "tr": { name: "Türkçe", flag: "🇹🇷" }, + "uk": { name: "Українська", flag: "🇺🇦" }, + "tl": { name: "Tagalog", flag: "🇵🇭" }, + "id": { name: "Indonesia", flag: "🇮🇩" }, + "th": { name: "ไทย", flag: "🇹🇭" }, + "hi": { name: "हिन्दी", flag: "🇮🇳" }, + "bn": { name: "বাংলা", flag: "🇧🇩" }, + "ur": { name: "اردو", flag: "🇵🇰" }, + "ro": { name: "Română", flag: "🇷🇴" }, + "sv": { name: "Svenska", flag: "🇸🇪" }, + "it": { name: "Italiano", flag: "🇮🇹" }, + "el": { name: "Ελληνικά", flag: "🇬🇷" }, + "hu": { name: "Magyar", flag: "🇭🇺" }, + "fi": { name: "Suomi", flag: "🇫🇮" }, + "da": { name: "Dansk", flag: "🇩🇰" }, + "no": { name: "Norsk", flag: "🇳🇴" }, +}; + +function getLocaleFromCookie() { + if (typeof document === "undefined") return "en"; + const cookie = document.cookie + .split(";") + .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`)); + const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en"; + return normalizeLocale(value); +} + +function MenuItem({ icon, label, onClick, trailing, danger }) { + return ( + + ); +} + +MenuItem.propTypes = { + icon: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + trailing: PropTypes.node, + danger: PropTypes.bool, +}; export default function HeaderMenu({ onLogout }) { const [isOpen, setIsOpen] = useState(false); const [changelogOpen, setChangelogOpen] = useState(false); + const [remoteOpen, setRemoteOpen] = useState(false); + const [langOpen, setLangOpen] = useState(false); + const [locale, setLocale] = useState("en"); + const { toggleTheme, isDark } = useTheme(); const menuRef = useRef(null); + useEffect(() => { + setLocale(getLocaleFromCookie()); + }, [langOpen]); + useEffect(() => { const handleClickOutside = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) { @@ -24,10 +105,7 @@ export default function HeaderMenu({ onLogout }) { } }, [isOpen]); - const openChangelog = () => { - setIsOpen(false); - setChangelogOpen(true); - }; + const close = () => setIsOpen(false); return ( <> @@ -37,47 +115,45 @@ export default function HeaderMenu({ onLogout }) { className="flex items-center justify-center p-2 rounded-lg text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 transition-all" title="Menu" > - more_vert + grid_view {isOpen && (
- - -
- -
- -
- - Theme -
- - - -
- -
+ { close(); setChangelogOpen(true); }} + /> + { close(); setLangOpen(true); }} + /> + { toggleTheme(); close(); }} + /> + { close(); setRemoteOpen(true); }} + /> + { close(); onLogout(); }} + />
)} setChangelogOpen(false)} /> + setRemoteOpen(false)} /> + setLangOpen(false)} /> ); } diff --git a/src/shared/components/LanguageSwitcher.js b/src/shared/components/LanguageSwitcher.js index 5284688b..5e2f9019 100644 --- a/src/shared/components/LanguageSwitcher.js +++ b/src/shared/components/LanguageSwitcher.js @@ -54,12 +54,22 @@ const getLocaleInfo = (locale) => { return locales[locale] || { name: locale, flag: "🌐" }; }; -export default function LanguageSwitcher({ className = "" }) { +export default function LanguageSwitcher({ className = "", isOpen: controlledOpen, onClose, hideTrigger = false }) { const [locale, setLocale] = useState("en"); const [isPending, setIsPending] = useState(false); - const [isOpen, setIsOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const modalRef = useRef(null); + const isControlled = typeof controlledOpen === "boolean"; + const isOpen = isControlled ? controlledOpen : internalOpen; + const setIsOpen = (value) => { + if (isControlled) { + if (!value && onClose) onClose(); + } else { + setInternalOpen(value); + } + }; + useEffect(() => { setLocale(getLocaleFromCookie()); }, []); @@ -102,17 +112,19 @@ export default function LanguageSwitcher({ className = "" }) { return (
{/* Trigger button */} - + {!hideTrigger && ( + + )} {/* Portal modal - renders at document.body to avoid parent layout constraints */} {isOpen && createPortal(