diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 5ec301e3..e5e40642 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -1,13 +1,32 @@ "use client"; import { useState, useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; import { Card, Button, Toggle, Input } from "@/shared/components"; +import { ConfirmModal } from "@/shared/components/Modal"; +import LanguageSwitcher from "@/shared/components/LanguageSwitcher"; import { useTheme } from "@/shared/hooks/useTheme"; import { cn } from "@/shared/utils/cn"; import { APP_CONFIG } from "@/shared/constants/config"; +import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config"; +import { LOCALE_FLAGS } from "@/shared/constants/locales"; + +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); +} export default function ProfilePage() { + const router = useRouter(); const { theme, setTheme, isDark } = useTheme(); + const [locale, setLocale] = useState("en"); + const [langOpen, setLangOpen] = useState(false); + const [shutdownOpen, setShutdownOpen] = useState(false); + const [isShuttingDown, setIsShuttingDown] = useState(false); const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" }); const [loading, setLoading] = useState(true); const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); @@ -39,6 +58,10 @@ export default function ProfilePage() { const [proxyLoading, setProxyLoading] = useState(false); const [proxyTestLoading, setProxyTestLoading] = useState(false); + useEffect(() => { + setLocale(getLocaleFromCookie()); + }, [langOpen]); + useEffect(() => { fetch("/api/settings") .then((res) => res.json()) @@ -515,6 +538,29 @@ export default function ProfilePage() { const observabilityEnabled = settings.enableObservability === true; + const handleShutdown = async () => { + setIsShuttingDown(true); + try { + await fetch("/api/version/shutdown", { method: "POST" }); + } catch (e) { + // Expected to fail as server shuts down; ignore error + } + setIsShuttingDown(false); + setShutdownOpen(false); + }; + + const handleLogout = async () => { + try { + const res = await fetch("/api/auth/logout", { method: "POST" }); + if (res.ok) { + router.push("/login"); + router.refresh(); + } + } catch (err) { + console.error("Failed to logout:", err); + } + }; + return (
@@ -593,6 +639,24 @@ export default function ProfilePage() {
+ {/* Language */} + +
+
+ language +
+

Language

+
+ +
+ {/* Security */}
@@ -1024,12 +1088,53 @@ export default function ProfilePage() {
+ {/* Account actions */} +
+ + +
+ {/* App Info */}

{APP_CONFIG.name} v{APP_CONFIG.version}

Local Mode - All data stored on your machine

+ + { + setLangOpen(false); + setLocale(next); + }} + /> + setShutdownOpen(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} + /> ); } diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js index de1bc052..f8427aa2 100644 --- a/src/shared/components/Header.js +++ b/src/shared/components/Header.js @@ -6,6 +6,7 @@ import Link from "next/link"; import PropTypes from "prop-types"; import ProviderIcon from "@/shared/components/ProviderIcon"; import HeaderMenu from "@/shared/components/HeaderMenu"; +import HeaderLanguage from "@/shared/components/HeaderLanguage"; import ThemeToggle from "@/shared/components/ThemeToggle"; import DonateModal from "@/shared/components/DonateModal"; import { useHeaderSearchStore } from "@/store/headerSearchStore"; @@ -315,6 +316,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) { Donate + setDonateOpen(false)} /> diff --git a/src/shared/components/HeaderLanguage.js b/src/shared/components/HeaderLanguage.js new file mode 100644 index 00000000..0e24d3c7 --- /dev/null +++ b/src/shared/components/HeaderLanguage.js @@ -0,0 +1,46 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config"; +import { LOCALE_FLAGS } from "@/shared/constants/locales"; +import LanguageSwitcher from "./LanguageSwitcher"; + +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); +} + +export default function HeaderLanguage() { + const [open, setOpen] = useState(false); + const [locale, setLocale] = useState("en"); + + useEffect(() => { + setLocale(getLocaleFromCookie()); + }, [open]); + + return ( + <> + + + { + setOpen(false); + setLocale(next); + }} + /> + + ); +} diff --git a/src/shared/components/HeaderMenu.js b/src/shared/components/HeaderMenu.js index 1f36be12..54ba6763 100644 --- a/src/shared/components/HeaderMenu.js +++ b/src/shared/components/HeaderMenu.js @@ -2,56 +2,9 @@ import { useState, useEffect, useRef } from "react"; import PropTypes from "prop-types"; -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); -} +import { ConfirmModal } from "./Modal"; function MenuItem({ icon, label, onClick, trailing, danger }) { return ( @@ -83,15 +36,21 @@ MenuItem.propTypes = { 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 [shutdownOpen, setShutdownOpen] = useState(false); + const [isShuttingDown, setIsShuttingDown] = useState(false); const { toggleTheme, isDark } = useTheme(); const menuRef = useRef(null); - useEffect(() => { - setLocale(getLocaleFromCookie()); - }, [langOpen]); + const handleShutdown = async () => { + setIsShuttingDown(true); + try { + await fetch("/api/version/shutdown", { method: "POST" }); + } catch (e) { + // Expected to fail as server shuts down; ignore error + } + setIsShuttingDown(false); + setShutdownOpen(false); + }; useEffect(() => { const handleClickOutside = (e) => { @@ -125,21 +84,16 @@ export default function HeaderMenu({ onLogout }) { label="Change Log" onClick={() => { close(); setChangelogOpen(true); }} /> - { close(); setLangOpen(true); }} - /> { toggleTheme(); close(); }} /> { close(); setRemoteOpen(true); }} + icon="power_settings_new" + label="Shutdown" + danger + onClick={() => { close(); setShutdownOpen(true); }} /> setChangelogOpen(false)} /> - setRemoteOpen(false)} /> - { - setLangOpen(false) - setLocale(locale) - })} /> + setShutdownOpen(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} + /> ); } diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 4dc3e666..2008d1cb 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -10,6 +10,7 @@ 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"]; @@ -40,8 +41,7 @@ const systemItems = [ export default function Sidebar({ onClose }) { const pathname = usePathname(); const [mediaOpen, setMediaOpen] = useState(false); - const [showShutdownModal, setShowShutdownModal] = useState(false); - const [isShuttingDown, setIsShuttingDown] = useState(false); + const [showRemoteModal, setShowRemoteModal] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [showUpdateModal, setShowUpdateModal] = useState(false); @@ -106,18 +106,6 @@ export default function Sidebar({ onClose }) { // user runs the command manually in another terminal. - const handleShutdown = async () => { - setIsShuttingDown(true); - try { - await fetch("/api/version/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} - /> + {/* Remote Promo Modal */} + setShowRemoteModal(false)} /> {/* Update Confirmation Modal */}