// ModelRouter Desktop GUI — renderer // Clash-Verge-style: the content area is a full-screen iframe loading the // real WebUI 1:1 (status / chat / keys / priority / sources / adapters). // The shell only adds the titlebar, settings overlay and tray integration. const state = { core: { running: false, baseUrl: "", port: 8787, profileDir: "", configFile: "", coreExists: true, }, settings: { port: 8787, autoStart: true, silentStart: false, minimizeToTray: true, }, theme: localStorage.getItem("mr-theme") || "light", loadedOnce: false, }; const $ = (s) => document.querySelector(s); function esc(s) { return String(s == null ? "" : s).replace( /[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ c ], ); } function toast(msg, isErr, ms) { const t = $("#toast"); t.textContent = msg; t.className = "show" + (isErr ? " err" : ""); clearTimeout(t._tm); t._tm = setTimeout(() => { t.className = ""; }, ms || 2600); } const frame = () => $("#webui-frame"); // ===== core status ===== async function refreshCore() { state.core = await window.modelrouter.core.state(); renderCoreStatus(); } function renderCoreStatus() { const core = state.core; const dot = $("#conn-dot"); dot.className = "status-dot" + (core.running ? " ok" : " bad"); dot.title = core.running ? "内核运行中" : "内核未运行"; } function showOffline() { frame().style.display = "none"; $("#offline").style.display = "flex"; $("#console-loading").style.display = "none"; } function showLoading() { frame().style.display = "none"; $("#offline").style.display = "none"; $("#console-loading").style.display = "flex"; } function loadFrame(url) { const f = frame(); f.style.display = "block"; $("#offline").style.display = "none"; $("#console-loading").style.display = "none"; f.src = url; state.loadedOnce = true; } async function ensureCore() { await refreshCore(); if (state.core.running) { loadFrame(state.core.baseUrl); } else { showOffline(); } } // ===== side rail toggles ===== function renderRail() { const auto = $("#rail-autostart-dot"); const sil = $("#rail-silent-dot"); if (auto) auto.classList.toggle("on", !!state.settings.autoStart); if (sil) sil.classList.toggle("on", !!state.settings.silentStart); } async function toggleAutoStart() { const next = !state.settings.autoStart; state.settings.autoStart = next; await window.modelrouter.settings.set({ autoStart: next }); renderRail(); toast(next ? "开机自启已开启" : "开机自启已关闭"); } async function toggleSilent() { const next = !state.settings.silentStart; state.settings.silentStart = next; await window.modelrouter.settings.set({ silentStart: next }); renderRail(); toast(next ? "静默启动已开启" : "静默启动已关闭"); } // ===== settings overlay ===== async function openSettings() { state.settings = await window.modelrouter.settings.get(); $("#set-port").value = state.settings.port ?? 8787; $("#set-autostart").checked = !!state.settings.autoStart; $("#set-silent").checked = !!state.settings.silentStart; $("#set-tray").checked = !!state.settings.minimizeToTray; $("#settings-overlay").style.display = "flex"; renderRail(); } function closeSettings() { $("#settings-overlay").style.display = "none"; } async function saveSettings() { const port = parseInt($("#set-port").value, 10); const autoStart = $("#set-autostart").checked; const silentStart = $("#set-silent").checked; const minimizeToTray = $("#set-tray").checked; await window.modelrouter.settings.set({ port, autoStart, silentStart, minimizeToTray, }); closeSettings(); toast("设置已保存"); await refreshCore(); if (state.core.running) { loadFrame(state.core.baseUrl); } } // ===== init ===== function init() { $("#tb-min").onclick = () => window.modelrouter.win.minimize(); $("#tb-max").onclick = () => window.modelrouter.win.toggleMaximize(); $("#tb-close").onclick = () => window.modelrouter.win.close(); $("#tb-settings").onclick = openSettings; $("#rail-settings").onclick = openSettings; $("#rail-autostart").onclick = toggleAutoStart; $("#rail-silent").onclick = toggleSilent; $("#rail-theme").onclick = () => { state.theme = state.theme === "dark" ? "light" : "dark"; applyTheme(); }; $("#set-close").onclick = closeSettings; $("#set-cancel").onclick = closeSettings; $("#set-save").onclick = saveSettings; $("#set-dir").onclick = () => window.modelrouter.shell.openPath(state.core.profileDir); $("#set-log").onclick = async () => { const p = await window.modelrouter.settings.logFile(); window.modelrouter.shell.openPath(p); }; $("#off-start").onclick = async () => { showLoading(); await window.modelrouter.core.start(); await refreshCore(); if (state.core.running) loadFrame(state.core.baseUrl); else showOffline(); }; frame().onload = () => refreshCore(); window.modelrouter.core.onState((d) => { state.core = Object.assign({}, state.core, d); renderCoreStatus(); if (d.running && !state.loadedOnce) loadFrame(state.core.baseUrl); if (!d.running) showOffline(); }); // Escape closes overlay document.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("#settings-overlay").style.display === "flex") closeSettings(); }); } async function boot() { init(); state.settings = await window.modelrouter.settings.get(); renderRail(); await ensureCore(); } boot();