mirror of
https://github.com/m5stack/ESP-Claw.git
synced 2026-05-20 11:51:49 -07:00
1894 lines
62 KiB
JavaScript
1894 lines
62 KiB
JavaScript
/* ═══════════════════════════════════════════════════
|
||
i18n – extensible locale registry
|
||
To add a new language, append an entry to LOCALES
|
||
and add a matching key in `strings`.
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
const LOCALES = [
|
||
{ id: "en", label: "English" },
|
||
{ id: "zh-cn", label: "简体中文" },
|
||
];
|
||
|
||
const strings = {
|
||
en: {
|
||
docTitle: "ESP-Claw Settings",
|
||
pageTitle: "Device Settings",
|
||
pageSubtitle:
|
||
"Configure credentials, runtime settings, and manage FATFS storage on your ESP device.",
|
||
tabConfig: "Configuration",
|
||
tabMemory: "Memory",
|
||
tabFiles: "File Manager",
|
||
|
||
statusLoading: "Loading\u2026",
|
||
statusOnline: "Wi-Fi Connected",
|
||
statusOffline: "Wi-Fi Offline",
|
||
|
||
sectionWifi: "Wi-Fi Settings",
|
||
wifiSsid: "Wi-Fi SSID",
|
||
wifiPassword: "Wi-Fi Password",
|
||
|
||
sectionLlm: "LLM Settings",
|
||
llmProvider: "LLM Provider",
|
||
llmProviderOpenai: "OpenAI",
|
||
llmProviderQwen: "Qwen Compatible",
|
||
llmProviderDeepSeek: "DeepSeek",
|
||
llmProviderAnthropic: "Anthropic",
|
||
llmProviderCustom: "Custom",
|
||
llmApiKey: "API Key",
|
||
llmModel: "Model",
|
||
llmTimeout: "Timeout (ms)",
|
||
llmTimeoutPlaceholder: "120000",
|
||
llmAdvanced: "LLM Advanced Options",
|
||
llmBackend: "Backend Type",
|
||
llmBackendPlaceholder: "openai_compatible / anthropic / custom",
|
||
llmProfile: "Profile",
|
||
llmProfilePlaceholder: "openai / qwen_compatible / custom_openai_compatible / anthropic",
|
||
llmBaseUrl: "Base URL",
|
||
llmBaseUrlPlaceholder: "https://api.openai.com/v1",
|
||
llmAuthType: "Auth Type",
|
||
llmAuthTypePlaceholder: "bearer / api-key / none",
|
||
|
||
sectionIm: "Instant Messaging (IM)",
|
||
imWechatTitle: "WeChat",
|
||
imFeishuTitle: "Feishu",
|
||
wechatLoginDesc: "QR Code Sign-in",
|
||
qqAppId: "QQ App ID",
|
||
qqAppSecret: "QQ App Secret",
|
||
feishuAppId: "Feishu App ID",
|
||
feishuAppSecret: "Feishu App Secret",
|
||
tgBotToken: "Telegram Bot Token",
|
||
wechatToken: "WeChat Token",
|
||
wechatBaseUrl: "WeChat Base URL",
|
||
wechatBaseUrlPlaceholder: "https://ilinkai.weixin.qq.com",
|
||
wechatCdnBaseUrl: "WeChat CDN Base URL",
|
||
wechatCdnBaseUrlPlaceholder: "https://novac2c.cdn.weixin.qq.com/c2c",
|
||
wechatAccountId: "WeChat Account ID",
|
||
wechatAccountIdPlaceholder: "default",
|
||
wechatLogin: "WeChat Login",
|
||
wechatLoginGenerate: "Generate QR",
|
||
wechatLoginCancel: "Cancel",
|
||
wechatLoginStatus: "Status: idle",
|
||
wechatLoginNote:
|
||
"Generate a QR code here to obtain wechat_token automatically. The token is saved to NVS after confirmation. Restart the device to apply the new login.",
|
||
wechatLoginOpenLink: "Open login link",
|
||
|
||
sectionSearch: "Search (Optional)",
|
||
searchNote:
|
||
"If set, ESP-Claw can search online. Tip: Built-in weather skill also relies on online search.",
|
||
searchBraveKey: "Brave Search API Key",
|
||
searchTavilyKey: "Tavily API Key",
|
||
|
||
sectionCapabilities: "Capability Groups",
|
||
sectionAdvanced: "Advanced Settings",
|
||
enabledCapGroups: "Enabled Capability Groups",
|
||
enabledCapGroupsHelp:
|
||
"Choose which compiled capability groups can be used after boot, and which enabled groups stay visible to the LLM. Changes take effect after restart.",
|
||
capabilitySelectAll: "Select All",
|
||
capabilityClearAll: "Clear All",
|
||
capabilityLlmDefaults: "Restore LLM Set",
|
||
capabilityLlmClear: "Clear LLM",
|
||
capabilityLoading: "Loading capabilities...",
|
||
capabilityLoadError: "Failed to load capabilities",
|
||
capabilitySelectedSummary: "{enabled} enabled, {llm} LLM-visible, total {total}",
|
||
capabilityEnabled: "Enabled",
|
||
capabilityLlmAlwaysVisible: "LLM Always Visible",
|
||
enabledLuaModules: "Enabled Lua Modules",
|
||
enabledLuaModulesHelp:
|
||
"Choose which compiled Lua modules are registered when cap_lua starts. Changes take effect after restart.",
|
||
luaModuleSelectAll: "Select All",
|
||
luaModuleClearAll: "Clear All",
|
||
luaModuleLoading: "Loading Lua modules...",
|
||
luaModuleLoadError: "Failed to load Lua modules",
|
||
luaModuleSelectedSummary: "{enabled} enabled, total {total}",
|
||
luaModuleCapabilityRequired:
|
||
"Enable the cap_lua capability group above before Lua module settings can take effect.",
|
||
timezone: "Timezone",
|
||
timezonePlaceholder: "e.g. CST-8 (Beijing) or UTC0",
|
||
timezoneHelp:
|
||
"POSIX TZ string only. The sign is INVERTED relative to common UTC notation: Beijing (UTC+8) must be written as 'CST-8'; New York (UTC-5) is 'EST5'. Names like 'Asia/Shanghai' will NOT work — please convert manually.",
|
||
saveBtn: "Save Changes",
|
||
saveSuccess: "Settings saved",
|
||
saveError: "Failed to save settings",
|
||
memoryTitle: "Memory Files",
|
||
memoryDescription: "Manage the device memory files.",
|
||
memoryCardLongTerm: "Long-term Memory",
|
||
memoryCardSoul: "Soul",
|
||
memoryCardIdentity: "Identity",
|
||
memoryCardUserInfo: "User Info",
|
||
memoryMissing: "Long-term memory file does not exist yet.",
|
||
memoryRefreshAll: "Refresh All",
|
||
memoryRefresh: "Refresh",
|
||
memoryAuxMissing: "Memory file does not exist yet.",
|
||
memoryRefreshed: "Memory reloaded",
|
||
memoryLoadError: "Failed to load memory",
|
||
memorySaveError: "Failed to save memory",
|
||
configNote:
|
||
"Changes are stored in NVS. Choose an LLM provider first, then fill the shared API key and model fields. Restart the device after updating Wi-Fi, Feishu, core LLM, capability, or Lua module settings.",
|
||
|
||
fileRefresh: "Refresh",
|
||
fileUpDir: "Up One Level",
|
||
fileNewFolder: "New folder name",
|
||
fileCreateFolder: "Create Folder",
|
||
fileUploadPath: "/example.txt",
|
||
fileChoose: "Choose File",
|
||
fileNoFileSelected: "No file selected",
|
||
fileUpload: "Upload",
|
||
fileColName: "Name",
|
||
fileColType: "Type",
|
||
fileColSize: "Size",
|
||
fileColActions: "Actions",
|
||
fileTypeFolder: "Folder",
|
||
fileTypeFile: "File",
|
||
fileEmpty: "This folder is empty.",
|
||
fileOpen: "Open",
|
||
fileEdit: "Edit",
|
||
filePreview: "Preview",
|
||
fileDownload: "Download",
|
||
fileDelete: "Delete",
|
||
fileDevMode: "Dev Mode",
|
||
fileDevModeRequired: "Enable Dev Mode to modify files.",
|
||
fileDevModeConfirm:
|
||
"After entering Dev Mode, you can upload, modify, or delete files. Incorrect changes may cause the system to fail, reboot endlessly, or behave unexpectedly. Please proceed carefully.",
|
||
fileDeleteConfirm: "Delete {path}?",
|
||
fileUploadComplete: "Upload completed",
|
||
fileFolderCreated: "Folder created",
|
||
fileDeleteComplete: "Delete completed",
|
||
fileEditorTitle: "Edit File",
|
||
fileEditorRefresh: "Refresh",
|
||
fileEditorClose: "Close",
|
||
fileEditorSave: "Save",
|
||
fileEditorSaved: "File saved",
|
||
fileEditorLoadError: "Failed to load file",
|
||
fileEditorSaveError: "Failed to save file",
|
||
fileSelectAndPath:
|
||
"Select a file and provide a target path that starts with /.",
|
||
fileFolderNameRequired: "Enter a folder name.",
|
||
},
|
||
|
||
"zh-cn": {
|
||
docTitle: "ESP-Claw 设置",
|
||
pageTitle: "设备设置",
|
||
pageSubtitle:
|
||
"配置凭据、运行时设置,并管理 ESP 设备上的 FATFS 存储。",
|
||
tabConfig: "配置管理",
|
||
tabMemory: "记忆管理",
|
||
tabFiles: "文件管理",
|
||
|
||
statusLoading: "加载中\u2026",
|
||
statusOnline: "Wi-Fi 已连接",
|
||
statusOffline: "Wi-Fi 离线",
|
||
|
||
sectionWifi: "Wi-Fi 设置",
|
||
wifiSsid: "Wi-Fi 名称 (SSID)",
|
||
wifiPassword: "Wi-Fi 密码",
|
||
|
||
sectionLlm: "LLM 设置",
|
||
llmProvider: "LLM 提供商",
|
||
llmProviderOpenai: "OpenAI",
|
||
llmProviderQwen: "Qwen 兼容",
|
||
llmProviderDeepSeek: "DeepSeek",
|
||
llmProviderAnthropic: "Anthropic",
|
||
llmProviderCustom: "自定义",
|
||
llmApiKey: "API 密钥",
|
||
llmModel: "模型",
|
||
llmTimeout: "超时 (毫秒)",
|
||
llmTimeoutPlaceholder: "120000",
|
||
llmAdvanced: "LLM 高级选项",
|
||
llmBackend: "后端类型",
|
||
llmBackendPlaceholder: "openai_compatible / anthropic / custom",
|
||
llmProfile: "配置文件",
|
||
llmProfilePlaceholder: "openai / qwen_compatible / custom_openai_compatible / anthropic",
|
||
llmBaseUrl: "Base URL",
|
||
llmBaseUrlPlaceholder: "https://api.openai.com/v1",
|
||
llmAuthType: "认证类型",
|
||
llmAuthTypePlaceholder: "bearer / api-key / none",
|
||
|
||
sectionIm: "即时通讯 (IM)",
|
||
imWechatTitle: "微信",
|
||
imFeishuTitle: "飞书",
|
||
wechatLoginDesc: "二维码登录",
|
||
qqAppId: "QQ App ID",
|
||
qqAppSecret: "QQ App Secret",
|
||
feishuAppId: "飞书 App ID",
|
||
feishuAppSecret: "飞书 App Secret",
|
||
tgBotToken: "Telegram Bot Token",
|
||
wechatToken: "微信 Token",
|
||
wechatBaseUrl: "微信 Base URL",
|
||
wechatBaseUrlPlaceholder: "https://ilinkai.weixin.qq.com",
|
||
wechatCdnBaseUrl: "微信 CDN Base URL",
|
||
wechatCdnBaseUrlPlaceholder: "https://novac2c.cdn.weixin.qq.com/c2c",
|
||
wechatAccountId: "微信账号 ID",
|
||
wechatAccountIdPlaceholder: "默认",
|
||
wechatLogin: "微信登录",
|
||
wechatLoginGenerate: "生成二维码",
|
||
wechatLoginCancel: "取消",
|
||
wechatLoginStatus: "状态:空闲",
|
||
wechatLoginNote:
|
||
"在此生成二维码以自动获取 wechat_token。确认后令牌将保存至 NVS。重启设备以应用新的登录。",
|
||
wechatLoginOpenLink: "打开登录链接",
|
||
|
||
sectionSearch: "搜索(可选)",
|
||
searchNote:
|
||
"如填写,ESP-Claw 可在运行中检索在线资源。提示:系统自带的天气 Skill 也依赖在线搜索功能。",
|
||
searchBraveKey: "Brave Search API Key",
|
||
searchTavilyKey: "Tavily API Key",
|
||
|
||
sectionCapabilities: "Capability Group",
|
||
sectionAdvanced: "高级设置",
|
||
enabledCapGroups: "启用的 Capability Group",
|
||
enabledCapGroupsHelp:
|
||
"选择设备启动后允许使用哪些已编译 capability group,以及哪些启用后的 group 对 LLM 始终可见。修改后需重启生效。",
|
||
capabilitySelectAll: "全选",
|
||
capabilityClearAll: "清空",
|
||
capabilityLlmDefaults: "恢复 LLM 集合",
|
||
capabilityLlmClear: "清空 LLM",
|
||
capabilityLoading: "正在加载 capability...",
|
||
capabilityLoadError: "加载 capability 列表失败",
|
||
capabilitySelectedSummary: "已启用 {enabled},LLM 可见 {llm},总计 {total}",
|
||
capabilityEnabled: "启用",
|
||
capabilityLlmAlwaysVisible: "LLM 始终可见",
|
||
enabledLuaModules: "启用的 Lua 模块",
|
||
enabledLuaModulesHelp:
|
||
"选择 cap_lua 启动时要注册哪些已编译 Lua 模块。修改后需重启生效。",
|
||
luaModuleSelectAll: "全选",
|
||
luaModuleClearAll: "清空",
|
||
luaModuleLoading: "正在加载 Lua 模块...",
|
||
luaModuleLoadError: "加载 Lua 模块列表失败",
|
||
luaModuleSelectedSummary: "已启用 {enabled},总计 {total}",
|
||
luaModuleCapabilityRequired:
|
||
"需先在上方启用 cap_lua capability group,Lua 模块配置才会生效。",
|
||
timezone: "时区",
|
||
timezonePlaceholder: "例如 CST-8(北京时间)或 UTC0",
|
||
timezoneHelp:
|
||
"仅接受 POSIX TZ 字符串,符号与日常 UTC 表示相反。北京时间(UTC+8)应写作 “CST-8”,纽约(UTC-5)写作 “EST5”。不支持 “Asia/Shanghai” 这种 IANA 名称,请自行换算。",
|
||
saveBtn: "保存更改",
|
||
saveSuccess: "设置已保存",
|
||
saveError: "保存设置失败",
|
||
memoryTitle: "记忆文件",
|
||
memoryDescription: "管理记忆文件。",
|
||
memoryCardLongTerm: "长期记忆",
|
||
memoryCardSoul: "灵魂",
|
||
memoryCardIdentity: "身份",
|
||
memoryCardUserInfo: "用户信息",
|
||
memoryMissing: "长期记忆文件尚不存在。",
|
||
memoryRefreshAll: "全部刷新",
|
||
memoryRefresh: "刷新",
|
||
memoryAuxMissing: "记忆文件尚不存在。",
|
||
memoryRefreshed: "记忆已重新加载",
|
||
memoryLoadError: "加载记忆失败",
|
||
memorySaveError: "保存记忆失败",
|
||
configNote:
|
||
"更改存储在 NVS 中。请先选择 LLM 提供商,然后填写 API 密钥和模型字段。更新 Wi-Fi、飞书、核心 LLM、capability 或 Lua 模块设置后请重启设备。",
|
||
|
||
fileRefresh: "刷新",
|
||
fileUpDir: "返回上级",
|
||
fileNewFolder: "新文件夹名称",
|
||
fileCreateFolder: "创建文件夹",
|
||
fileUploadPath: "/example.txt",
|
||
fileChoose: "选择文件",
|
||
fileNoFileSelected: "未选择文件",
|
||
fileUpload: "上传",
|
||
fileColName: "名称",
|
||
fileColType: "类型",
|
||
fileColSize: "大小",
|
||
fileColActions: "操作",
|
||
fileTypeFolder: "文件夹",
|
||
fileTypeFile: "文件",
|
||
fileEmpty: "此文件夹为空。",
|
||
fileOpen: "打开",
|
||
fileEdit: "编辑",
|
||
filePreview: "预览",
|
||
fileDownload: "下载",
|
||
fileDelete: "删除",
|
||
fileDevMode: "开发者模式",
|
||
fileDevModeRequired: "请先启用开发者模式再修改文件。",
|
||
fileDevModeConfirm:
|
||
"进入开发者模式后,你可以上传、修改或删除文件。不正确的增删改可能导致系统无法正常运行、无限重启等问题。请注意合理修改。",
|
||
fileDeleteConfirm: "确定删除 {path}?",
|
||
fileUploadComplete: "上传完成",
|
||
fileFolderCreated: "文件夹已创建",
|
||
fileDeleteComplete: "删除完成",
|
||
fileEditorTitle: "编辑文件",
|
||
fileEditorRefresh: "刷新",
|
||
fileEditorClose: "退出",
|
||
fileEditorSave: "保存",
|
||
fileEditorSaved: "文件已保存",
|
||
fileEditorLoadError: "加载文件失败",
|
||
fileEditorSaveError: "保存文件失败",
|
||
fileSelectAndPath: "请选择文件并提供以 / 开头的目标路径。",
|
||
fileFolderNameRequired: "请输入文件夹名称。",
|
||
},
|
||
};
|
||
|
||
/* ── Current language & device state ── */
|
||
let currentLang = "en";
|
||
let wifiConnected = null; // null = unknown/loading, true/false = fetched
|
||
|
||
function t(key) {
|
||
const table = strings[currentLang] || strings["en"];
|
||
return table[key] ?? (strings["en"][key] || key);
|
||
}
|
||
|
||
function formatTemplate(key, vars) {
|
||
let value = t(key);
|
||
Object.entries(vars).forEach(([name, replacement]) => {
|
||
value = value.replaceAll(`{${name}}`, String(replacement));
|
||
});
|
||
return value;
|
||
}
|
||
|
||
function detectLang() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const fromUrl = params.get("lang");
|
||
if (fromUrl && strings[fromUrl]) return fromUrl;
|
||
|
||
const saved = localStorage.getItem("esp-claw-lang");
|
||
if (saved && strings[saved]) return saved;
|
||
|
||
const nav = (navigator.language || "").toLowerCase();
|
||
if (nav.startsWith("zh")) return "zh-cn";
|
||
return "en";
|
||
}
|
||
|
||
function applyI18n() {
|
||
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
||
el.textContent = t(el.dataset.i18n);
|
||
});
|
||
document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
|
||
el.placeholder = t(el.dataset.i18nPlaceholder);
|
||
});
|
||
document.querySelectorAll("[data-i18n-title]").forEach((el) => {
|
||
document.title = t(el.dataset.i18nTitle);
|
||
});
|
||
document.querySelectorAll("[data-i18n-option]").forEach((el) => {
|
||
el.textContent = t(el.dataset.i18nOption);
|
||
});
|
||
document.documentElement.lang = currentLang === "zh-cn" ? "zh-CN" : "en";
|
||
document.title = t("docTitle");
|
||
refreshStatusText();
|
||
}
|
||
|
||
function refreshStatusText() {
|
||
const el = document.getElementById("status-text");
|
||
if (wifiConnected === null) {
|
||
el.textContent = t("statusLoading");
|
||
} else {
|
||
el.textContent = wifiConnected ? t("statusOnline") : t("statusOffline");
|
||
}
|
||
}
|
||
|
||
function buildLocaleMenu() {
|
||
const menu = document.getElementById("locale-menu");
|
||
menu.innerHTML = "";
|
||
LOCALES.forEach((loc) => {
|
||
const li = document.createElement("li");
|
||
const a = document.createElement("a");
|
||
a.textContent = loc.label;
|
||
a.href = "#";
|
||
a.setAttribute("data-lang", loc.id);
|
||
if (loc.id === currentLang) a.classList.add("is-current");
|
||
a.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
setLang(loc.id);
|
||
document.getElementById("lang-switcher").removeAttribute("open");
|
||
});
|
||
li.appendChild(a);
|
||
menu.appendChild(li);
|
||
});
|
||
const label = LOCALES.find((l) => l.id === currentLang);
|
||
document.getElementById("current-locale-label").textContent =
|
||
label ? label.label : currentLang;
|
||
}
|
||
|
||
function setLang(lang) {
|
||
if (!strings[lang]) return;
|
||
currentLang = lang;
|
||
localStorage.setItem("esp-claw-lang", lang);
|
||
applyI18n();
|
||
buildLocaleMenu();
|
||
renderCapabilityCheckboxes();
|
||
renderFileRows(lastFileEntries);
|
||
renderMemoryState();
|
||
updateFileDevModeUI();
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
Tab switching
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
function initTabs() {
|
||
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
document
|
||
.querySelectorAll(".tab-btn")
|
||
.forEach((b) => b.classList.remove("active"));
|
||
document
|
||
.querySelectorAll(".tab-panel")
|
||
.forEach((p) => p.classList.remove("active"));
|
||
btn.classList.add("active");
|
||
const panel = document.getElementById("tab-" + btn.dataset.tab);
|
||
if (panel) panel.classList.add("active");
|
||
if (btn.dataset.tab === "memory") {
|
||
loadMemory().catch((err) =>
|
||
showBanner("memoryBanner", err.message || t("memoryLoadError"), true)
|
||
);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
Config – NVS fields
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
const configFields = [
|
||
"wifi_ssid",
|
||
"wifi_password",
|
||
"llm_api_key",
|
||
"llm_backend_type",
|
||
"llm_profile",
|
||
"llm_model",
|
||
"llm_base_url",
|
||
"llm_auth_type",
|
||
"llm_timeout_ms",
|
||
"qq_app_id",
|
||
"qq_app_secret",
|
||
"feishu_app_id",
|
||
"feishu_app_secret",
|
||
"tg_bot_token",
|
||
"wechat_token",
|
||
"wechat_base_url",
|
||
"wechat_cdn_base_url",
|
||
"wechat_account_id",
|
||
"search_brave_key",
|
||
"search_tavily_key",
|
||
"time_timezone",
|
||
];
|
||
|
||
const CAPABILITY_NONE_SENTINEL = "__none__";
|
||
let capabilityItems = [];
|
||
let pendingEnabledCapGroups = "";
|
||
let pendingLlmVisibleCapGroups = "";
|
||
let luaModuleItems = [];
|
||
let pendingEnabledLuaModules = "";
|
||
|
||
const llmProviderPresets = {
|
||
openai: {
|
||
llm_backend_type: "openai_compatible",
|
||
llm_profile: "openai",
|
||
llm_base_url: "https://api.openai.com/v1",
|
||
llm_auth_type: "bearer",
|
||
},
|
||
qwen: {
|
||
llm_backend_type: "openai_compatible",
|
||
llm_profile: "qwen_compatible",
|
||
llm_base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
llm_auth_type: "bearer",
|
||
},
|
||
deepseek: {
|
||
llm_backend_type: "openai_compatible",
|
||
llm_profile: "custom_openai_compatible",
|
||
llm_base_url: "https://api.deepseek.com",
|
||
llm_auth_type: "bearer",
|
||
},
|
||
anthropic: {
|
||
llm_backend_type: "anthropic",
|
||
llm_profile: "anthropic",
|
||
llm_base_url: "https://api.anthropic.com/v1",
|
||
llm_auth_type: "none",
|
||
},
|
||
};
|
||
|
||
const llmProviderDefaultModels = {
|
||
openai: "gpt-5.4",
|
||
qwen: "qwen3.6-plus",
|
||
deepseek: "deepseek-v4-pro",
|
||
anthropic: "claude-sonnet-4-6",
|
||
};
|
||
|
||
/* ── Banner helpers ── */
|
||
|
||
function showBanner(id, message, isError = false) {
|
||
const banner = document.getElementById(id);
|
||
banner.textContent = message;
|
||
banner.classList.remove("success", "error");
|
||
banner.classList.add(isError ? "error" : "success");
|
||
banner.classList.add("visible");
|
||
}
|
||
|
||
function hideBanner(id) {
|
||
const banner = document.getElementById(id);
|
||
banner.classList.remove("visible", "success", "error");
|
||
}
|
||
|
||
/* ── Config form read / fill ── */
|
||
|
||
function readConfigForm() {
|
||
const payload = {};
|
||
configFields.forEach((field) => {
|
||
const input = document.getElementById(field);
|
||
payload[field] = input ? input.value.trim() : "";
|
||
});
|
||
payload.enabled_cap_groups = serializeCapabilitySelection();
|
||
payload.llm_visible_cap_groups = serializeLlmVisibleSelection();
|
||
payload.enabled_lua_modules = serializeLuaModuleSelection();
|
||
return payload;
|
||
}
|
||
|
||
function fillConfigForm(data) {
|
||
configFields.forEach((field) => {
|
||
const input = document.getElementById(field);
|
||
if (input && typeof data[field] === "string") {
|
||
input.value = data[field];
|
||
}
|
||
});
|
||
pendingEnabledCapGroups =
|
||
typeof data.enabled_cap_groups === "string" ? data.enabled_cap_groups : "";
|
||
pendingLlmVisibleCapGroups =
|
||
typeof data.llm_visible_cap_groups === "string"
|
||
? data.llm_visible_cap_groups
|
||
: "";
|
||
pendingEnabledLuaModules =
|
||
typeof data.enabled_lua_modules === "string" ? data.enabled_lua_modules : "";
|
||
renderCapabilityCheckboxes();
|
||
renderLuaModuleCheckboxes();
|
||
syncProviderPreset();
|
||
}
|
||
|
||
function getSelectionSet(
|
||
serialized,
|
||
items,
|
||
idKey,
|
||
selectAllIfEmpty,
|
||
defaultPredicate = null
|
||
) {
|
||
const selected = new Set();
|
||
const raw = (serialized || "").trim();
|
||
|
||
if (!raw) {
|
||
items.forEach((item) => {
|
||
if (selectAllIfEmpty || (defaultPredicate && defaultPredicate(item))) {
|
||
selected.add(item[idKey]);
|
||
}
|
||
});
|
||
return selected;
|
||
}
|
||
|
||
raw.split(",").forEach((token) => {
|
||
const itemId = token.trim();
|
||
if (!itemId || itemId === CAPABILITY_NONE_SENTINEL || itemId === "none") {
|
||
return;
|
||
}
|
||
selected.add(itemId);
|
||
});
|
||
return selected;
|
||
}
|
||
|
||
function getCapabilitySelectionSet(serialized, selectAllIfEmpty, useDefaultLlmIfEmpty) {
|
||
return getSelectionSet(
|
||
serialized,
|
||
capabilityItems,
|
||
"group_id",
|
||
selectAllIfEmpty,
|
||
useDefaultLlmIfEmpty ? (item) => item.default_llm_visible : null
|
||
);
|
||
}
|
||
|
||
function getLuaModuleSelectionSet(serialized, selectAllIfEmpty) {
|
||
return getSelectionSet(serialized, luaModuleItems, "module_id", selectAllIfEmpty);
|
||
}
|
||
|
||
function updateCapabilitySelectionSummary() {
|
||
const summary = document.getElementById("capabilitySelectionSummary");
|
||
const enabledCount = document.querySelectorAll(
|
||
'#capabilityCheckboxList input[data-role="enabled"]:checked'
|
||
).length;
|
||
const llmCount = document.querySelectorAll(
|
||
'#capabilityCheckboxList input[data-role="llm-visible"]:checked'
|
||
).length;
|
||
|
||
if (!summary) return;
|
||
if (!capabilityItems.length) {
|
||
summary.textContent = t("capabilityLoading");
|
||
return;
|
||
}
|
||
|
||
summary.textContent = formatTemplate("capabilitySelectedSummary", {
|
||
enabled: enabledCount,
|
||
llm: llmCount,
|
||
total: capabilityItems.length,
|
||
});
|
||
}
|
||
|
||
function isLuaCapabilityEnabled() {
|
||
return !!document.querySelector(
|
||
'#capabilityCheckboxList input[data-role="enabled"][value="cap_lua"]:checked'
|
||
);
|
||
}
|
||
|
||
function updateLuaModuleAvailability() {
|
||
const disabled = !isLuaCapabilityEnabled();
|
||
const list = document.getElementById("luaModuleCheckboxList");
|
||
const note = document.getElementById("luaModuleCapabilityNote");
|
||
const selectAllButton = document.getElementById("luaModuleSelectAllButton");
|
||
const clearAllButton = document.getElementById("luaModuleClearAllButton");
|
||
|
||
if (list) {
|
||
list.classList.toggle("is-disabled", disabled);
|
||
}
|
||
if (note) {
|
||
note.classList.toggle("hidden", !disabled);
|
||
}
|
||
[selectAllButton, clearAllButton].forEach((button) => {
|
||
if (button) {
|
||
button.disabled = disabled || !luaModuleItems.length;
|
||
}
|
||
});
|
||
document
|
||
.querySelectorAll('#luaModuleCheckboxList input[data-role="enabled"]')
|
||
.forEach((input) => {
|
||
input.disabled = disabled;
|
||
});
|
||
}
|
||
|
||
function updateLuaModuleSelectionSummary() {
|
||
const summary = document.getElementById("luaModuleSelectionSummary");
|
||
const enabledCount = document.querySelectorAll(
|
||
'#luaModuleCheckboxList input[data-role="enabled"]:checked'
|
||
).length;
|
||
|
||
if (!summary) return;
|
||
if (!luaModuleItems.length) {
|
||
summary.textContent = t("luaModuleLoading");
|
||
return;
|
||
}
|
||
|
||
summary.textContent = formatTemplate("luaModuleSelectedSummary", {
|
||
enabled: enabledCount,
|
||
total: luaModuleItems.length,
|
||
});
|
||
}
|
||
|
||
function renderCapabilityCheckboxes() {
|
||
const list = document.getElementById("capabilityCheckboxList");
|
||
if (!list) return;
|
||
|
||
list.innerHTML = "";
|
||
if (!capabilityItems.length) {
|
||
updateCapabilitySelectionSummary();
|
||
return;
|
||
}
|
||
|
||
const enabled = getCapabilitySelectionSet(pendingEnabledCapGroups, true, false);
|
||
const llmVisible = getCapabilitySelectionSet(
|
||
pendingLlmVisibleCapGroups,
|
||
false,
|
||
true
|
||
);
|
||
capabilityItems.forEach((item) => {
|
||
const label = document.createElement("label");
|
||
label.className = "capability-item";
|
||
|
||
const meta = document.createElement("div");
|
||
meta.className = "capability-meta";
|
||
|
||
const title = document.createElement("div");
|
||
title.className = "capability-title";
|
||
title.textContent = item.display_name || item.group_id;
|
||
|
||
const id = document.createElement("div");
|
||
id.className = "capability-id";
|
||
id.textContent = item.group_id;
|
||
|
||
meta.appendChild(title);
|
||
meta.appendChild(id);
|
||
|
||
const controls = document.createElement("div");
|
||
controls.className = "capability-controls";
|
||
|
||
const enabledToggle = document.createElement("label");
|
||
enabledToggle.className = "capability-toggle";
|
||
const enabledInput = document.createElement("input");
|
||
enabledInput.type = "checkbox";
|
||
enabledInput.value = item.group_id;
|
||
enabledInput.dataset.role = "enabled";
|
||
enabledInput.checked = enabled.has(item.group_id);
|
||
enabledToggle.appendChild(enabledInput);
|
||
enabledToggle.appendChild(document.createTextNode(t("capabilityEnabled")));
|
||
|
||
const llmToggle = document.createElement("label");
|
||
llmToggle.className = "capability-toggle";
|
||
const llmInput = document.createElement("input");
|
||
llmInput.type = "checkbox";
|
||
llmInput.value = item.group_id;
|
||
llmInput.dataset.role = "llm-visible";
|
||
llmInput.checked = enabled.has(item.group_id) && llmVisible.has(item.group_id);
|
||
llmInput.disabled = !enabledInput.checked;
|
||
llmToggle.appendChild(llmInput);
|
||
llmToggle.appendChild(
|
||
document.createTextNode(t("capabilityLlmAlwaysVisible"))
|
||
);
|
||
|
||
enabledInput.addEventListener("change", () => {
|
||
if (!enabledInput.checked) {
|
||
llmInput.checked = false;
|
||
llmInput.disabled = true;
|
||
} else {
|
||
llmInput.disabled = false;
|
||
}
|
||
updateCapabilitySelectionSummary();
|
||
updateLuaModuleAvailability();
|
||
});
|
||
llmInput.addEventListener("change", updateCapabilitySelectionSummary);
|
||
|
||
controls.appendChild(enabledToggle);
|
||
controls.appendChild(llmToggle);
|
||
meta.appendChild(controls);
|
||
|
||
label.appendChild(meta);
|
||
list.appendChild(label);
|
||
});
|
||
|
||
updateCapabilitySelectionSummary();
|
||
updateLuaModuleAvailability();
|
||
}
|
||
|
||
function serializeCapabilitySelection() {
|
||
const checked = Array.from(
|
||
document.querySelectorAll('#capabilityCheckboxList input[type="checkbox"]:checked')
|
||
).map((input) => input.value);
|
||
|
||
if (capabilityItems.length && checked.length === capabilityItems.length) {
|
||
return "";
|
||
}
|
||
if (checked.length === 0) {
|
||
return CAPABILITY_NONE_SENTINEL;
|
||
}
|
||
return checked.join(",");
|
||
}
|
||
|
||
function setAllCapabilityCheckboxes(checked) {
|
||
document
|
||
.querySelectorAll('#capabilityCheckboxList input[data-role="enabled"]')
|
||
.forEach((input) => {
|
||
input.checked = checked;
|
||
});
|
||
document
|
||
.querySelectorAll('#capabilityCheckboxList input[data-role="llm-visible"]')
|
||
.forEach((input) => {
|
||
input.disabled = !checked;
|
||
if (!checked) {
|
||
input.checked = false;
|
||
}
|
||
});
|
||
updateCapabilitySelectionSummary();
|
||
updateLuaModuleAvailability();
|
||
}
|
||
|
||
function serializeLlmVisibleSelection() {
|
||
const checked = Array.from(
|
||
document.querySelectorAll(
|
||
'#capabilityCheckboxList input[data-role="llm-visible"]:checked'
|
||
)
|
||
).map((input) => input.value);
|
||
const defaultSelected = capabilityItems
|
||
.filter((item) => item.default_llm_visible)
|
||
.map((item) => item.group_id);
|
||
|
||
if (
|
||
checked.length === defaultSelected.length &&
|
||
checked.every((groupId) => defaultSelected.includes(groupId))
|
||
) {
|
||
return "";
|
||
}
|
||
if (checked.length === 0) {
|
||
return CAPABILITY_NONE_SENTINEL;
|
||
}
|
||
return checked.join(",");
|
||
}
|
||
|
||
function setLlmVisibleSelection(mode) {
|
||
const enabled = new Set(
|
||
Array.from(
|
||
document.querySelectorAll('#capabilityCheckboxList input[data-role="enabled"]:checked')
|
||
).map((input) => input.value)
|
||
);
|
||
|
||
document
|
||
.querySelectorAll('#capabilityCheckboxList input[data-role="llm-visible"]')
|
||
.forEach((input) => {
|
||
const isEnabled = enabled.has(input.value);
|
||
input.disabled = !isEnabled;
|
||
if (!isEnabled) {
|
||
input.checked = false;
|
||
return;
|
||
}
|
||
if (mode === "defaults") {
|
||
const item = capabilityItems.find((entry) => entry.group_id === input.value);
|
||
input.checked = !!item?.default_llm_visible;
|
||
} else if (mode === "clear") {
|
||
input.checked = false;
|
||
}
|
||
});
|
||
updateCapabilitySelectionSummary();
|
||
}
|
||
|
||
function renderLuaModuleCheckboxes() {
|
||
const list = document.getElementById("luaModuleCheckboxList");
|
||
if (!list) return;
|
||
|
||
list.innerHTML = "";
|
||
if (!luaModuleItems.length) {
|
||
updateLuaModuleSelectionSummary();
|
||
updateLuaModuleAvailability();
|
||
return;
|
||
}
|
||
|
||
const enabled = getLuaModuleSelectionSet(pendingEnabledLuaModules, true);
|
||
luaModuleItems.forEach((item) => {
|
||
const label = document.createElement("label");
|
||
label.className = "capability-item";
|
||
|
||
const meta = document.createElement("div");
|
||
meta.className = "capability-meta";
|
||
|
||
const title = document.createElement("div");
|
||
title.className = "capability-title";
|
||
title.textContent = item.display_name || item.module_id;
|
||
|
||
const id = document.createElement("div");
|
||
id.className = "capability-id";
|
||
id.textContent = item.module_id;
|
||
|
||
const controls = document.createElement("div");
|
||
controls.className = "capability-controls";
|
||
|
||
const enabledToggle = document.createElement("label");
|
||
enabledToggle.className = "capability-toggle";
|
||
const enabledInput = document.createElement("input");
|
||
enabledInput.type = "checkbox";
|
||
enabledInput.value = item.module_id;
|
||
enabledInput.dataset.role = "enabled";
|
||
enabledInput.checked = enabled.has(item.module_id);
|
||
enabledInput.addEventListener("change", updateLuaModuleSelectionSummary);
|
||
enabledToggle.appendChild(enabledInput);
|
||
enabledToggle.appendChild(document.createTextNode(t("capabilityEnabled")));
|
||
|
||
controls.appendChild(enabledToggle);
|
||
meta.appendChild(title);
|
||
meta.appendChild(id);
|
||
meta.appendChild(controls);
|
||
label.appendChild(meta);
|
||
list.appendChild(label);
|
||
});
|
||
|
||
updateLuaModuleSelectionSummary();
|
||
updateLuaModuleAvailability();
|
||
}
|
||
|
||
function serializeLuaModuleSelection() {
|
||
if (!luaModuleItems.length) {
|
||
return pendingEnabledLuaModules || "";
|
||
}
|
||
|
||
const checked = Array.from(
|
||
document.querySelectorAll('#luaModuleCheckboxList input[data-role="enabled"]:checked')
|
||
).map((input) => input.value);
|
||
|
||
if (luaModuleItems.length && checked.length === luaModuleItems.length) {
|
||
return "";
|
||
}
|
||
if (checked.length === 0) {
|
||
return CAPABILITY_NONE_SENTINEL;
|
||
}
|
||
return checked.join(",");
|
||
}
|
||
|
||
function setAllLuaModuleCheckboxes(checked) {
|
||
const disabled = !isLuaCapabilityEnabled();
|
||
document
|
||
.querySelectorAll('#luaModuleCheckboxList input[data-role="enabled"]')
|
||
.forEach((input) => {
|
||
input.checked = checked;
|
||
input.disabled = disabled;
|
||
});
|
||
updateLuaModuleSelectionSummary();
|
||
}
|
||
|
||
/* ── LLM provider preset logic ── */
|
||
|
||
function detectProviderPreset() {
|
||
const backend = document.getElementById("llm_backend_type")?.value.trim();
|
||
const profile = document.getElementById("llm_profile")?.value.trim();
|
||
const baseUrl = document.getElementById("llm_base_url")?.value.trim();
|
||
const authType = document.getElementById("llm_auth_type")?.value.trim();
|
||
|
||
const match = Object.entries(llmProviderPresets).find(
|
||
([, preset]) =>
|
||
preset.llm_backend_type === backend &&
|
||
preset.llm_profile === profile &&
|
||
preset.llm_base_url === baseUrl &&
|
||
preset.llm_auth_type === authType
|
||
);
|
||
return match ? match[0] : "custom";
|
||
}
|
||
|
||
function syncProviderPreset() {
|
||
const select = document.getElementById("llm_provider_preset");
|
||
if (select) select.value = detectProviderPreset();
|
||
}
|
||
|
||
function applyProviderPreset(presetKey) {
|
||
const preset = llmProviderPresets[presetKey];
|
||
if (!preset) {
|
||
syncProviderPreset();
|
||
return;
|
||
}
|
||
Object.entries(preset).forEach(([field, value]) => {
|
||
const input = document.getElementById(field);
|
||
if (input) input.value = value;
|
||
});
|
||
const modelInput = document.getElementById("llm_model");
|
||
if (modelInput && llmProviderDefaultModels[presetKey]) {
|
||
modelInput.value = llmProviderDefaultModels[presetKey];
|
||
}
|
||
syncProviderPreset();
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
API calls
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const response = await fetch("/api/status", { cache: "no-store" });
|
||
const data = await response.json();
|
||
const badge = document.getElementById("status-badge");
|
||
const bar = document.getElementById("status-bar");
|
||
|
||
wifiConnected = !!data.wifi_connected;
|
||
if (wifiConnected) {
|
||
badge.classList.remove("offline");
|
||
badge.classList.add("online");
|
||
bar.classList.add("is-online");
|
||
} else {
|
||
badge.classList.remove("online");
|
||
badge.classList.add("offline");
|
||
bar.classList.remove("is-online");
|
||
}
|
||
refreshStatusText();
|
||
|
||
if (data.ip) {
|
||
document.getElementById("info-ip").textContent = "IP: " + data.ip;
|
||
document.getElementById("info-ip").style.display = "";
|
||
document.getElementById("meta-sep-ip").style.display = "";
|
||
}
|
||
if (data.storage_base_path) {
|
||
document.getElementById("info-storage").textContent =
|
||
"Storage: " + data.storage_base_path;
|
||
document.getElementById("info-storage").style.display = "";
|
||
document.getElementById("meta-sep-storage").style.display = "";
|
||
}
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message || t("statusLoading"), true);
|
||
}
|
||
}
|
||
|
||
async function loadConfig() {
|
||
hideBanner("configBanner");
|
||
const response = await fetch("/api/config", { cache: "no-store" });
|
||
if (!response.ok) throw new Error("Failed to load settings");
|
||
fillConfigForm(await response.json());
|
||
}
|
||
|
||
async function loadCapabilities() {
|
||
const response = await fetch("/api/capabilities", { cache: "no-store" });
|
||
if (!response.ok) throw new Error(t("capabilityLoadError"));
|
||
const data = await response.json();
|
||
capabilityItems = Array.isArray(data.items) ? data.items : [];
|
||
renderCapabilityCheckboxes();
|
||
}
|
||
|
||
async function loadLuaModules() {
|
||
const response = await fetch("/api/lua-modules", { cache: "no-store" });
|
||
if (!response.ok) throw new Error(t("luaModuleLoadError"));
|
||
const data = await response.json();
|
||
luaModuleItems = Array.isArray(data.items) ? data.items : [];
|
||
renderLuaModuleCheckboxes();
|
||
}
|
||
|
||
const WECHAT_FIELDS = [
|
||
"wechat_token", "wechat_base_url", "wechat_cdn_base_url", "wechat_account_id",
|
||
];
|
||
|
||
async function reloadWechatFields() {
|
||
const response = await fetch("/api/config", { cache: "no-store" });
|
||
if (!response.ok) return;
|
||
const data = await response.json();
|
||
WECHAT_FIELDS.forEach((field) => {
|
||
const input = document.getElementById(field);
|
||
if (input && typeof data[field] === "string") {
|
||
input.value = data[field];
|
||
}
|
||
});
|
||
}
|
||
|
||
async function saveConfig() {
|
||
const button = document.getElementById("saveConfigButton");
|
||
button.disabled = true;
|
||
hideBanner("configBanner");
|
||
|
||
try {
|
||
const response = await fetch("/api/config", {
|
||
method: "POST",
|
||
cache: "no-store",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(readConfigForm()),
|
||
});
|
||
const responseText = await response.text();
|
||
let result = {};
|
||
if (responseText) {
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
if (response.ok) throw parseError;
|
||
result = { error: responseText };
|
||
}
|
||
}
|
||
if (!response.ok)
|
||
throw new Error(result.error || t("saveError"));
|
||
showBanner("configBanner", result.message || t("saveSuccess"));
|
||
syncProviderPreset();
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message, true);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
WeChat Login
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
let wechatLoginPollTimer = null;
|
||
|
||
function stopWechatLoginPolling() {
|
||
if (wechatLoginPollTimer) {
|
||
clearTimeout(wechatLoginPollTimer);
|
||
wechatLoginPollTimer = null;
|
||
}
|
||
}
|
||
|
||
function renderWechatLoginStatus(data) {
|
||
const qrCanvas = document.getElementById("wechatLoginQr");
|
||
const qrLink = document.getElementById("wechatLoginQrLink");
|
||
const meta = document.getElementById("wechatLoginMeta");
|
||
|
||
if (data.qr_data_url) {
|
||
if (typeof window.renderWechatQr === "function") {
|
||
try {
|
||
window.renderWechatQr(qrCanvas, data.qr_data_url);
|
||
} catch (error) {
|
||
console.error("Failed to render WeChat QR:", error);
|
||
}
|
||
}
|
||
qrCanvas.classList.remove("hidden");
|
||
qrLink.href = data.qr_data_url;
|
||
qrLink.textContent = t("wechatLoginOpenLink");
|
||
qrLink.classList.remove("hidden");
|
||
} else {
|
||
const ctx = qrCanvas.getContext("2d");
|
||
if (ctx) {
|
||
ctx.clearRect(0, 0, qrCanvas.width, qrCanvas.height);
|
||
}
|
||
qrCanvas.classList.add("hidden");
|
||
qrLink.removeAttribute("href");
|
||
qrLink.textContent = "";
|
||
qrLink.classList.add("hidden");
|
||
}
|
||
|
||
meta.textContent = data.status
|
||
? (currentLang === "zh-cn" ? "状态:" : "Status: ") + data.status
|
||
: t("wechatLoginStatus");
|
||
|
||
if (data.message) {
|
||
showBanner("wechatLoginBanner", data.message);
|
||
} else {
|
||
hideBanner("wechatLoginBanner");
|
||
}
|
||
|
||
if (data.completed && data.persisted) {
|
||
reloadWechatFields().catch(() => {});
|
||
}
|
||
}
|
||
|
||
async function pollWechatLoginStatus() {
|
||
try {
|
||
const response = await fetch("/api/wechat/login/status", {
|
||
cache: "no-store",
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.error || "Failed to fetch WeChat login status");
|
||
renderWechatLoginStatus(data);
|
||
if (data.active || (data.completed && !data.persisted)) {
|
||
wechatLoginPollTimer = setTimeout(pollWechatLoginStatus, 1500);
|
||
} else {
|
||
stopWechatLoginPolling();
|
||
}
|
||
} catch (err) {
|
||
showBanner("wechatLoginBanner", err.message, true);
|
||
stopWechatLoginPolling();
|
||
}
|
||
}
|
||
|
||
async function startWechatLogin() {
|
||
const button = document.getElementById("wechatLoginStartButton");
|
||
button.disabled = true;
|
||
hideBanner("wechatLoginBanner");
|
||
|
||
try {
|
||
const response = await fetch("/api/wechat/login/start", {
|
||
method: "POST",
|
||
cache: "no-store",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
account_id: document.getElementById("wechat_account_id").value.trim(),
|
||
force: true,
|
||
}),
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.error || "Failed to start WeChat login");
|
||
renderWechatLoginStatus(data);
|
||
stopWechatLoginPolling();
|
||
wechatLoginPollTimer = setTimeout(pollWechatLoginStatus, 1000);
|
||
} catch (err) {
|
||
showBanner("wechatLoginBanner", err.message, true);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function cancelWechatLogin() {
|
||
try {
|
||
const response = await fetch("/api/wechat/login/cancel", {
|
||
method: "POST",
|
||
cache: "no-store",
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.error || "Failed to cancel WeChat login");
|
||
renderWechatLoginStatus({
|
||
status: currentLang === "zh-cn" ? "已取消" : "cancelled",
|
||
message:
|
||
data.message ||
|
||
(currentLang === "zh-cn" ? "已取消微信登录。" : "WeChat login cancelled."),
|
||
qr_data_url: "",
|
||
completed: false,
|
||
persisted: false,
|
||
});
|
||
stopWechatLoginPolling();
|
||
} catch (err) {
|
||
showBanner("wechatLoginBanner", err.message, true);
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
File Manager
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
let currentPath = "/";
|
||
let lastFileEntries = [];
|
||
let fileDevModeEnabled = false;
|
||
const EDITABLE_FILE_EXTENSIONS = [".md", ".json", ".txt", ".jsonb", ".jsonc", ".lua"];
|
||
const MEMORY_FILES = [
|
||
{
|
||
id: "memoryEditor",
|
||
missingId: "memoryMissingState",
|
||
path: "/memory/memory.md",
|
||
editable: false,
|
||
},
|
||
{ id: "soulEditor", missingId: "soulMissingState", path: "/memory/soul.md", editable: true },
|
||
{ id: "identityEditor", missingId: "identityMissingState", path: "/memory/identity.md", editable: true },
|
||
{ id: "userEditor", missingId: "userMissingState", path: "/memory/user.md", editable: true },
|
||
];
|
||
let memoryLoaded = false;
|
||
let memoryContents = {};
|
||
let memoryFilePresence = {};
|
||
let activeEditorPath = "";
|
||
let activeEditorSource = "";
|
||
|
||
function isFileDevModeEnabled() {
|
||
return fileDevModeEnabled;
|
||
}
|
||
|
||
function applyFileEditorAccessMode() {
|
||
const textarea = document.getElementById("fileEditorTextarea");
|
||
const saveButton = document.getElementById("saveFileEditorButton");
|
||
if (!textarea || !saveButton) return;
|
||
|
||
const previewOnly = activeEditorSource === "files" && !isFileDevModeEnabled();
|
||
textarea.readOnly = previewOnly;
|
||
saveButton.classList.toggle("hidden", previewOnly);
|
||
saveButton.disabled = previewOnly;
|
||
}
|
||
|
||
function updateFileDevModeUI() {
|
||
const createRow = document.getElementById("fileCreateRow");
|
||
const uploadRow = document.getElementById("fileUploadRow");
|
||
const toggleButton = document.getElementById("devModeButton");
|
||
const enabled = isFileDevModeEnabled();
|
||
|
||
if (createRow) {
|
||
createRow.classList.toggle("hidden", !enabled);
|
||
}
|
||
if (uploadRow) {
|
||
uploadRow.classList.toggle("hidden", !enabled);
|
||
}
|
||
if (toggleButton) {
|
||
toggleButton.classList.toggle("active", enabled);
|
||
toggleButton.setAttribute("aria-pressed", enabled ? "true" : "false");
|
||
}
|
||
|
||
applyFileEditorAccessMode();
|
||
renderFileRows(lastFileEntries);
|
||
}
|
||
|
||
function setFileDevMode(enabled) {
|
||
fileDevModeEnabled = !!enabled;
|
||
updateFileDevModeUI();
|
||
}
|
||
|
||
function humanSize(value) {
|
||
if (value < 1024) return value + " B";
|
||
if (value < 1024 * 1024) return (value / 1024).toFixed(1) + " KB";
|
||
return (value / (1024 * 1024)).toFixed(1) + " MB";
|
||
}
|
||
|
||
function parentPath(path) {
|
||
if (path === "/") return "/";
|
||
const parts = path.split("/").filter(Boolean);
|
||
parts.pop();
|
||
return parts.length ? "/" + parts.join("/") : "/";
|
||
}
|
||
|
||
function joinPath(base, name) {
|
||
return base === "/" ? "/" + name : base + "/" + name;
|
||
}
|
||
|
||
function isEditableFile(path) {
|
||
const lowerPath = (path || "").toLowerCase();
|
||
return EDITABLE_FILE_EXTENSIONS.some((ext) => lowerPath.endsWith(ext));
|
||
}
|
||
|
||
function createActionSlot(node = null, placeholderText = "") {
|
||
const slot = document.createElement("span");
|
||
slot.className = "action-slot";
|
||
if (node) {
|
||
slot.appendChild(node);
|
||
} else {
|
||
slot.classList.add("placeholder");
|
||
slot.textContent = placeholderText;
|
||
}
|
||
return slot;
|
||
}
|
||
|
||
function renderFileRows(entries) {
|
||
lastFileEntries = entries;
|
||
const tbody = document.getElementById("fileTableBody");
|
||
tbody.innerHTML = "";
|
||
|
||
if (!entries.length) {
|
||
const row = document.createElement("tr");
|
||
const td = document.createElement("td");
|
||
td.setAttribute("colspan", "4");
|
||
td.textContent = t("fileEmpty");
|
||
td.style.color = "var(--text-muted)";
|
||
row.appendChild(td);
|
||
tbody.appendChild(row);
|
||
return;
|
||
}
|
||
|
||
entries
|
||
.sort(
|
||
(a, b) =>
|
||
Number(b.is_dir) - Number(a.is_dir) || a.name.localeCompare(b.name)
|
||
)
|
||
.forEach((entry) => {
|
||
const row = document.createElement("tr");
|
||
|
||
const tdName = document.createElement("td");
|
||
tdName.textContent = entry.name;
|
||
row.appendChild(tdName);
|
||
|
||
const tdType = document.createElement("td");
|
||
tdType.textContent = entry.is_dir ? t("fileTypeFolder") : t("fileTypeFile");
|
||
row.appendChild(tdType);
|
||
|
||
const tdSize = document.createElement("td");
|
||
tdSize.textContent = entry.is_dir ? "-" : humanSize(entry.size || 0);
|
||
row.appendChild(tdSize);
|
||
|
||
const tdActions = document.createElement("td");
|
||
tdActions.className = "actions";
|
||
let editControl = null;
|
||
let middleControl = null;
|
||
|
||
if (entry.is_dir) {
|
||
const openBtn = document.createElement("button");
|
||
openBtn.className = "link-btn";
|
||
openBtn.textContent = t("fileOpen");
|
||
openBtn.onclick = () => {
|
||
currentPath = entry.path;
|
||
loadFiles().catch((err) =>
|
||
showBanner("fileBanner", err.message, true)
|
||
);
|
||
};
|
||
middleControl = openBtn;
|
||
} else {
|
||
if (isEditableFile(entry.path)) {
|
||
const editBtn = document.createElement("button");
|
||
editBtn.className = "link-btn";
|
||
editBtn.textContent = isFileDevModeEnabled() ? t("fileEdit") : t("filePreview");
|
||
editBtn.onclick = () => {
|
||
openFileEditor(entry.path).catch((err) =>
|
||
showBanner("fileBanner", err.message || t("fileEditorLoadError"), true)
|
||
);
|
||
};
|
||
editControl = editBtn;
|
||
}
|
||
|
||
const dlLink = document.createElement("a");
|
||
dlLink.href = "/files" + entry.path;
|
||
dlLink.textContent = t("fileDownload");
|
||
dlLink.className = "link-btn";
|
||
dlLink.target = "_blank";
|
||
middleControl = dlLink;
|
||
}
|
||
|
||
const delBtn = document.createElement("button");
|
||
delBtn.className = "link-btn danger";
|
||
delBtn.textContent = t("fileDelete");
|
||
delBtn.disabled = !isFileDevModeEnabled();
|
||
delBtn.onclick = async () => {
|
||
if (!isFileDevModeEnabled()) return;
|
||
const msg = t("fileDeleteConfirm").replace("{path}", entry.path);
|
||
if (!window.confirm(msg)) return;
|
||
await deletePath(entry.path);
|
||
};
|
||
const editPlaceholder = entry.is_dir ? t("fileTypeFolder") : t("fileEdit");
|
||
tdActions.appendChild(createActionSlot(editControl, editPlaceholder));
|
||
tdActions.appendChild(createActionSlot(middleControl));
|
||
tdActions.appendChild(createActionSlot(delBtn));
|
||
|
||
row.appendChild(tdActions);
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
async function loadFiles() {
|
||
hideBanner("fileBanner");
|
||
document.getElementById("currentPath").textContent = currentPath;
|
||
|
||
const response = await fetch(
|
||
"/api/files?path=" + encodeURIComponent(currentPath),
|
||
{ cache: "no-store" }
|
||
);
|
||
if (!response.ok) throw new Error((await response.text()) || "Failed to load file list");
|
||
|
||
const data = await response.json();
|
||
currentPath = data.path || "/";
|
||
document.getElementById("currentPath").textContent = currentPath;
|
||
renderFileRows(data.entries || []);
|
||
}
|
||
|
||
async function uploadFile() {
|
||
if (!isFileDevModeEnabled()) {
|
||
showBanner("fileBanner", t("fileDevModeRequired"), true);
|
||
return;
|
||
}
|
||
|
||
const pathInput = document.getElementById("uploadPathInput");
|
||
const fileInput = document.getElementById("uploadFileInput");
|
||
const button = document.getElementById("uploadButton");
|
||
const file = fileInput.files[0];
|
||
const relativePath =
|
||
pathInput.value.trim() || (file ? joinPath(currentPath, file.name) : "");
|
||
|
||
if (!file || !relativePath.startsWith("/")) {
|
||
showBanner("fileBanner", t("fileSelectAndPath"), true);
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
hideBanner("fileBanner");
|
||
try {
|
||
const response = await fetch(
|
||
"/api/files/upload?path=" + encodeURIComponent(relativePath),
|
||
{ method: "POST", cache: "no-store", body: file }
|
||
);
|
||
if (!response.ok) throw new Error(await response.text());
|
||
pathInput.value = "";
|
||
fileInput.value = "";
|
||
document.getElementById("selectedFileName").textContent =
|
||
t("fileNoFileSelected");
|
||
showBanner("fileBanner", t("fileUploadComplete"));
|
||
await loadFiles();
|
||
} catch (err) {
|
||
showBanner("fileBanner", err.message || "Upload failed", true);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function createFolder() {
|
||
if (!isFileDevModeEnabled()) {
|
||
showBanner("fileBanner", t("fileDevModeRequired"), true);
|
||
return;
|
||
}
|
||
|
||
const input = document.getElementById("newFolderInput");
|
||
const name = input.value.trim();
|
||
if (!name) {
|
||
showBanner("fileBanner", t("fileFolderNameRequired"), true);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch("/api/files/mkdir", {
|
||
method: "POST",
|
||
cache: "no-store",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ path: joinPath(currentPath, name) }),
|
||
});
|
||
if (!response.ok) throw new Error(await response.text());
|
||
input.value = "";
|
||
showBanner("fileBanner", t("fileFolderCreated"));
|
||
await loadFiles();
|
||
} catch (err) {
|
||
showBanner("fileBanner", err.message || "Failed to create folder", true);
|
||
}
|
||
}
|
||
|
||
async function deletePath(path) {
|
||
if (!isFileDevModeEnabled()) {
|
||
showBanner("fileBanner", t("fileDevModeRequired"), true);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(
|
||
"/api/files?path=" + encodeURIComponent(path),
|
||
{ method: "DELETE", cache: "no-store" }
|
||
);
|
||
if (!response.ok) throw new Error(await response.text());
|
||
showBanner("fileBanner", t("fileDeleteComplete"));
|
||
await loadFiles();
|
||
} catch (err) {
|
||
showBanner("fileBanner", err.message || "Delete failed", true);
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
File Editor
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
function openFileEditorModal() {
|
||
document.getElementById("fileEditorModal").classList.remove("hidden");
|
||
document.body.style.overflow = "hidden";
|
||
}
|
||
|
||
function closeFileEditorModal() {
|
||
document.getElementById("fileEditorModal").classList.add("hidden");
|
||
document.body.style.overflow = "";
|
||
const shouldRefreshMemory = activeEditorSource === "memory";
|
||
activeEditorPath = "";
|
||
activeEditorSource = "";
|
||
hideBanner("fileEditorBanner");
|
||
if (shouldRefreshMemory) {
|
||
refreshMemory({ showSuccessBanner: false }).catch((err) =>
|
||
showBanner("memoryBanner", err.message || t("memoryLoadError"), true)
|
||
);
|
||
}
|
||
}
|
||
|
||
async function loadFileIntoEditor(path, options = {}) {
|
||
const { allowMissing = false } = options;
|
||
const response = await fetch("/files" + path, { cache: "no-store" });
|
||
if (!response.ok) {
|
||
if (allowMissing && response.status === 404) {
|
||
document.getElementById("fileEditorPath").textContent = path;
|
||
document.getElementById("fileEditorTextarea").value = "";
|
||
activeEditorPath = path;
|
||
return;
|
||
}
|
||
throw new Error((await response.text()) || t("fileEditorLoadError"));
|
||
}
|
||
|
||
const content = await response.text();
|
||
document.getElementById("fileEditorPath").textContent = path;
|
||
document.getElementById("fileEditorTextarea").value = content;
|
||
activeEditorPath = path;
|
||
}
|
||
|
||
async function openFileEditor(path) {
|
||
hideBanner("fileEditorBanner");
|
||
activeEditorSource = "files";
|
||
await loadFileIntoEditor(path);
|
||
applyFileEditorAccessMode();
|
||
openFileEditorModal();
|
||
}
|
||
|
||
async function openMemoryEditor(path) {
|
||
hideBanner("fileEditorBanner");
|
||
activeEditorSource = "memory";
|
||
await loadFileIntoEditor(path, { allowMissing: true });
|
||
applyFileEditorAccessMode();
|
||
openFileEditorModal();
|
||
}
|
||
|
||
async function refreshFileEditor() {
|
||
const button = document.getElementById("refreshFileEditorButton");
|
||
if (!activeEditorPath) return;
|
||
|
||
button.disabled = true;
|
||
hideBanner("fileEditorBanner");
|
||
try {
|
||
await loadFileIntoEditor(activeEditorPath, {
|
||
allowMissing: activeEditorSource === "memory",
|
||
});
|
||
} catch (err) {
|
||
showBanner("fileEditorBanner", err.message || t("fileEditorLoadError"), true);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function saveFileEditor() {
|
||
const button = document.getElementById("saveFileEditorButton");
|
||
const textarea = document.getElementById("fileEditorTextarea");
|
||
if (!activeEditorPath) return;
|
||
if (activeEditorSource === "files" && !isFileDevModeEnabled()) {
|
||
showBanner("fileEditorBanner", t("fileDevModeRequired"), true);
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
hideBanner("fileEditorBanner");
|
||
try {
|
||
const response = await fetch(
|
||
"/api/files/upload?path=" + encodeURIComponent(activeEditorPath),
|
||
{
|
||
method: "POST",
|
||
cache: "no-store",
|
||
body: new Blob([textarea.value], { type: "text/plain; charset=utf-8" }),
|
||
}
|
||
);
|
||
if (!response.ok) {
|
||
throw new Error((await response.text()) || t("fileEditorSaveError"));
|
||
}
|
||
showBanner("fileEditorBanner", t("fileEditorSaved"));
|
||
await loadFiles();
|
||
} catch (err) {
|
||
showBanner("fileEditorBanner", err.message || t("fileEditorSaveError"), true);
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
Memory Manager
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
function renderMemoryState() {
|
||
MEMORY_FILES.forEach((file) => {
|
||
const editor = document.getElementById(file.id);
|
||
const missing = file.missingId ? document.getElementById(file.missingId) : null;
|
||
const exists = !!memoryFilePresence[file.path];
|
||
const editButton = document.querySelector(`.memory-edit-btn[data-path="${file.path}"]`);
|
||
|
||
if (editor) {
|
||
editor.value = memoryContents[file.path] || "";
|
||
editor.readOnly = true;
|
||
editor.classList.toggle("hidden", !exists);
|
||
}
|
||
if (missing) {
|
||
missing.classList.toggle("hidden", exists);
|
||
}
|
||
if (editButton) {
|
||
const showEdit = !!file.editable && exists;
|
||
editButton.classList.toggle("hidden", !showEdit);
|
||
editButton.disabled = !showEdit;
|
||
}
|
||
});
|
||
}
|
||
|
||
async function loadMemory() {
|
||
hideBanner("memoryBanner");
|
||
|
||
const dirResponse = await fetch("/api/files?path=" + encodeURIComponent("/memory"), {
|
||
cache: "no-store",
|
||
});
|
||
|
||
if (!dirResponse.ok) {
|
||
if (dirResponse.status === 404) {
|
||
memoryContents = {};
|
||
memoryFilePresence = {};
|
||
MEMORY_FILES.forEach((file) => {
|
||
memoryContents[file.path] = "";
|
||
memoryFilePresence[file.path] = false;
|
||
});
|
||
memoryLoaded = true;
|
||
renderMemoryState();
|
||
return;
|
||
}
|
||
throw new Error((await dirResponse.text()) || t("memoryLoadError"));
|
||
}
|
||
|
||
const dirData = await dirResponse.json();
|
||
const entries = dirData.entries || [];
|
||
const entryNames = new Set(entries.map((entry) => entry.name));
|
||
memoryContents = {};
|
||
memoryFilePresence = {};
|
||
|
||
for (const file of MEMORY_FILES) {
|
||
const fileName = file.path.split("/").pop();
|
||
const exists = entryNames.has(fileName);
|
||
memoryFilePresence[file.path] = exists;
|
||
if (!exists) {
|
||
memoryContents[file.path] = "";
|
||
continue;
|
||
}
|
||
|
||
const response = await fetch("/files" + file.path, {
|
||
cache: "no-store",
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error((await response.text()) || t("memoryLoadError"));
|
||
}
|
||
memoryContents[file.path] = await response.text();
|
||
}
|
||
|
||
memoryLoaded = true;
|
||
renderMemoryState();
|
||
}
|
||
|
||
async function refreshMemory(options = {}) {
|
||
const { showSuccessBanner = true } = options;
|
||
const button = document.getElementById("refreshMemoryButton");
|
||
button.disabled = true;
|
||
hideBanner("memoryBanner");
|
||
|
||
try {
|
||
await loadMemory();
|
||
if (showSuccessBanner) {
|
||
showBanner("memoryBanner", t("memoryRefreshed"));
|
||
}
|
||
} catch (err) {
|
||
showBanner("memoryBanner", err.message || t("memoryLoadError"), true);
|
||
throw err;
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function refreshMemoryFile(path) {
|
||
hideBanner("memoryBanner");
|
||
|
||
const file = MEMORY_FILES.find((item) => item.path === path);
|
||
if (!file) return;
|
||
|
||
try {
|
||
const response = await fetch("/files" + file.path, { cache: "no-store" });
|
||
if (response.status === 404) {
|
||
memoryFilePresence[file.path] = false;
|
||
memoryContents[file.path] = "";
|
||
} else if (!response.ok) {
|
||
throw new Error((await response.text()) || t("memoryLoadError"));
|
||
} else {
|
||
memoryFilePresence[file.path] = true;
|
||
memoryContents[file.path] = await response.text();
|
||
}
|
||
renderMemoryState();
|
||
} catch (err) {
|
||
showBanner("memoryBanner", err.message || t("memoryLoadError"), true);
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
Event binding
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
function bindEvents() {
|
||
initTabs();
|
||
document
|
||
.getElementById("capabilitySelectAllButton")
|
||
.addEventListener("click", () => setAllCapabilityCheckboxes(true));
|
||
document
|
||
.getElementById("capabilityClearAllButton")
|
||
.addEventListener("click", () => setAllCapabilityCheckboxes(false));
|
||
document
|
||
.getElementById("capabilityLlmSelectDefaultsButton")
|
||
.addEventListener("click", () => setLlmVisibleSelection("defaults"));
|
||
document
|
||
.getElementById("capabilityLlmClearAllButton")
|
||
.addEventListener("click", () => setLlmVisibleSelection("clear"));
|
||
document
|
||
.getElementById("luaModuleSelectAllButton")
|
||
.addEventListener("click", () => setAllLuaModuleCheckboxes(true));
|
||
document
|
||
.getElementById("luaModuleClearAllButton")
|
||
.addEventListener("click", () => setAllLuaModuleCheckboxes(false));
|
||
|
||
document.getElementById("saveConfigButton").addEventListener("click", saveConfig);
|
||
|
||
document.getElementById("llm_provider_preset").addEventListener("change", (e) => {
|
||
applyProviderPreset(e.target.value);
|
||
});
|
||
|
||
["llm_backend_type", "llm_profile", "llm_base_url", "llm_auth_type"].forEach(
|
||
(field) => {
|
||
const input = document.getElementById(field);
|
||
if (input) input.addEventListener("input", syncProviderPreset);
|
||
}
|
||
);
|
||
|
||
document
|
||
.getElementById("wechatLoginStartButton")
|
||
.addEventListener("click", startWechatLogin);
|
||
document
|
||
.getElementById("wechatLoginCancelButton")
|
||
.addEventListener("click", cancelWechatLogin);
|
||
|
||
document.getElementById("refreshFilesButton").addEventListener("click", () =>
|
||
loadFiles().catch((err) => showBanner("fileBanner", err.message, true))
|
||
);
|
||
document.getElementById("devModeButton").addEventListener("click", () => {
|
||
const nextEnabled = !isFileDevModeEnabled();
|
||
if (nextEnabled && !window.confirm(t("fileDevModeConfirm"))) {
|
||
return;
|
||
}
|
||
setFileDevMode(nextEnabled);
|
||
});
|
||
document.getElementById("upDirButton").addEventListener("click", () => {
|
||
currentPath = parentPath(currentPath);
|
||
loadFiles().catch((err) => showBanner("fileBanner", err.message, true));
|
||
});
|
||
document.getElementById("uploadButton").addEventListener("click", uploadFile);
|
||
document.getElementById("refreshMemoryButton").addEventListener("click", refreshMemory);
|
||
document.querySelectorAll(".memory-refresh-btn").forEach((button) => {
|
||
button.addEventListener("click", () => refreshMemoryFile(button.dataset.path));
|
||
});
|
||
document.querySelectorAll(".memory-edit-btn").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
openMemoryEditor(button.dataset.path).catch((err) =>
|
||
showBanner("memoryBanner", err.message || t("fileEditorLoadError"), true)
|
||
);
|
||
});
|
||
});
|
||
document
|
||
.getElementById("closeFileEditorButton")
|
||
.addEventListener("click", closeFileEditorModal);
|
||
document
|
||
.getElementById("refreshFileEditorButton")
|
||
.addEventListener("click", refreshFileEditor);
|
||
document
|
||
.getElementById("saveFileEditorButton")
|
||
.addEventListener("click", saveFileEditor);
|
||
document.getElementById("fileEditorModal").addEventListener("click", (e) => {
|
||
if (e.target.id === "fileEditorModal") {
|
||
closeFileEditorModal();
|
||
}
|
||
});
|
||
document.getElementById("chooseFileButton").addEventListener("click", () => {
|
||
document.getElementById("uploadFileInput").click();
|
||
});
|
||
document.getElementById("createFolderButton").addEventListener("click", createFolder);
|
||
document.getElementById("uploadFileInput").addEventListener("change", (e) => {
|
||
const file = e.target.files[0];
|
||
const nameSpan = document.getElementById("selectedFileName");
|
||
if (file) {
|
||
document.getElementById("uploadPathInput").value = joinPath(
|
||
currentPath,
|
||
file.name
|
||
);
|
||
nameSpan.textContent = file.name;
|
||
} else {
|
||
nameSpan.textContent = t("fileNoFileSelected");
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
Bootstrap
|
||
═══════════════════════════════════════════════════ */
|
||
|
||
async function bootstrap() {
|
||
fileDevModeEnabled = false;
|
||
capabilityItems = [];
|
||
pendingEnabledCapGroups = "";
|
||
pendingLlmVisibleCapGroups = "";
|
||
luaModuleItems = [];
|
||
pendingEnabledLuaModules = "";
|
||
currentLang = detectLang();
|
||
applyI18n();
|
||
buildLocaleMenu();
|
||
bindEvents();
|
||
updateFileDevModeUI();
|
||
|
||
try {
|
||
await loadStatus();
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message || "Failed to load device status", true);
|
||
}
|
||
|
||
try {
|
||
await loadCapabilities();
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message || t("capabilityLoadError"), true);
|
||
}
|
||
|
||
try {
|
||
await loadLuaModules();
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message || t("luaModuleLoadError"), true);
|
||
}
|
||
|
||
try {
|
||
await loadConfig();
|
||
} catch (err) {
|
||
showBanner("configBanner", err.message || "Failed to load settings", true);
|
||
}
|
||
|
||
try {
|
||
await pollWechatLoginStatus();
|
||
} catch (err) {
|
||
showBanner(
|
||
"wechatLoginBanner",
|
||
err.message || "Failed to load WeChat login status",
|
||
true
|
||
);
|
||
}
|
||
|
||
try {
|
||
await loadFiles();
|
||
} catch (err) {
|
||
showBanner("fileBanner", err.message || "Failed to load file list", true);
|
||
}
|
||
|
||
if (document.querySelector('.tab-btn[data-tab="memory"]')?.classList.contains("active")) {
|
||
try {
|
||
await loadMemory();
|
||
} catch (err) {
|
||
showBanner("memoryBanner", err.message || t("memoryLoadError"), true);
|
||
}
|
||
} else {
|
||
renderMemoryState();
|
||
}
|
||
}
|
||
|
||
bootstrap().catch((err) => {
|
||
showBanner("configBanner", err.message || "Failed to initialize the page", true);
|
||
});
|