2026-04-22 16:57:03 +08:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title data-i18n-title = "docTitle" > ESP-Claw Settings</ title >
< link rel = "stylesheet" href = "/styles.css" >
< script type = "importmap" >
{
"imports" : {
"lean-qr" : "/lean-qr.min.mjs"
}
}
</ script >
< script type = "module" >
import { generate } from "lean-qr" ;
window . renderWechatQr = ( canvas , content ) => {
if ( ! canvas || ! content ) {
return ;
}
const code = generate ( content );
code . toCanvas ( canvas );
};
</ script >
</ head >
< body >
< div class = "site-container" >
<!-- ── Navigation ── -->
< nav >
< a class = "nav-brand" href = "/" >
< svg class = "nav-logo" xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 686.46 191.26" >
< path fill = "currentColor"
d = "M274.61 59.98v13.2h-37.7v15.3h34.6v12.2h-34.6v17.5h38.5v13.2h-54.2v-71.4zm23.4 53.8q1.305 2.505 3.45 4.05c1.43 1.03 3.12 1.8 5.05 2.3q2.895.75 6 .75c1.4 0 2.9-.12 4.5-.35q2.4-.345 4.5-1.35c1.4-.67 2.57-1.58 3.5-2.75s1.4-2.65 1.4-4.45c0-1.93-.62-3.5-1.85-4.7s-2.85-2.2-4.85-3-4.27-1.5-6.8-2.1-5.1-1.27-7.7-2c-2.67-.67-5.27-1.48-7.8-2.45s-4.8-2.22-6.8-3.75-3.62-3.45-4.85-5.75-1.85-5.08-1.85-8.35c0-3.67.78-6.85 2.35-9.55q2.355-4.05 6.15-6.75c2.53-1.8 5.4-3.13 8.6-4s6.4-1.3 9.6-1.3c3.73 0 7.32.42 10.75 1.25q5.145 1.245 9.15 4.05c2.67 1.87 4.78 4.25 6.35 7.15s2.35 6.42 2.35 10.55h-15.2c-.13-2.13-.58-3.9-1.35-5.3s-1.78-2.5-3.05-3.3-2.72-1.37-4.35-1.7-3.42-.5-5.35-.5c-1.27 0-2.53.13-3.8.4s-2.42.73-3.45 1.4-1.88 1.5-2.55 2.5-1 2.27-1 3.8c0 1.4.27 2.53.8 3.4q.795 1.305 3.15 2.4c1.57.73 3.73 1.47 6.5 2.2s6.38 1.67 10.85 2.8c1.33.27 3.18.75 5.55 1.45s4.72 1.82 7.05 3.35 4.35 3.58 6.05 6.15 2.55 5.85 2.55 9.85c0 3.27-.63 6.3-1.9 9.1s-3.15 5.22-5.65 7.25c-2.5 2.04-5.6 3.62-9.3 4.75s-7.98 1.7-12.85 1.7c-3.93 0-7.75-.48-11.45-1.45-3.7-.96-6.97-2.48-9.8-4.55s-5.08-4.7-6.75-7.9q-2.505-4.8-2.4-11.4h15.2c0 2.4.43 4.43 1.3 6.1m85.1-53.8q6.705 0 11.4 1.95c3.13 1.3 5.68 3.02 7.65 5.15 1.96 2.13 3.4 4.57 4.3 7.3s1.35 5.57 1.35 8.5-.45 5.68-1.35 8.45-2.33 5.22-4.3 7.35-4.52 3.85-7.65 5.15q-4.695 1.95-11.4 1.95h-16.5v25.6h-15.7v-71.4zm-4.3 33.6c1.8 0 3.53-.13 5.2-.4s3.13-.78 4.4-1.55 2.28-1.85 3.05-3.25 1.15-3.23 1.15-5.5-.38-4.1-1.15-5.5-1.78-2.48-3.05-3.25-2.73-1.28-4.4-1.55-3.4-.4-5.2-.4h-12.2v21.4zm36.06 10.09h24.74v9.18h-24.74z" />
< path fill = "#e8362d"
d = "M487.08 78.23c-3.09-2.2-6.76-3.29-11.01-3.29-3.63 0-6.73.69-9.31 2.08s-4.7 3.25-6.38 5.59c-1.67 2.34-2.9 5.01-3.7 8.02a36.7 36.7 0 0 0-1.19 9.36c0 3.53.4 6.86 1.19 10.01s2.03 5.9 3.7 8.24 3.81 4.21 6.42 5.59c2.61 1.39 5.72 2.08 9.35 2.08 2.66 0 5.03-.45 7.1-1.34s3.85-2.14 5.36-3.73c1.5-1.59 2.68-3.48 3.53-5.68q1.275-3.3 1.53-7.11h8.08c-.79 7.8-3.43 13.87-7.9 18.21-4.48 4.33-10.6 6.5-18.36 6.5-4.7 0-8.81-.82-12.33-2.47-3.51-1.65-6.43-3.92-8.75-6.81s-4.07-6.3-5.23-10.23q-1.74-5.895-1.74-12.66c0-6.765.62-8.74 1.87-12.7s3.07-7.41 5.48-10.36 5.41-5.27 9.01-6.98c3.6-1.7 7.72-2.56 12.37-2.56 3.17 0 6.18.43 9.01 1.3s5.36 2.14 7.57 3.81c2.21 1.68 4.05 3.77 5.53 6.29 1.47 2.51 2.44 5.42 2.89 8.71h-8.08c-.91-4.39-2.9-7.69-5.99-9.88zm33.87-8.76v54.97h32.13v6.94h-40.21v-61.9h8.08zm65.71 0 23.71 61.9h-8.92l-6.63-18.64h-25.67l-6.8 18.64h-8.25l23.63-61.9h8.92zm5.61 36.33-10.12-28.87h-.17l-10.29 28.87h20.57zm69.53 25.58-13.86-52.02h-.17l-14.02 52.02h-8.42l-15.56-61.9h8.25l11.9 51.5h.17l13.6-51.5h8.76l13.43 51.5h.17l12.33-51.5h8.08l-16.24 61.9zM165.38 113v.06l-.04.54v.04c-1.29 20.81-10.26 39.52-24.09 53.36-15 15-35.69 24.26-58.57 24.26-9.69 0-18.97-1.66-27.6-4.71C24.43 175.72 2.07 147.36.03 113.53c-.01-.15-.03-.32-.03-.47 0-1.27.52-2.42 1.36-3.26s1.98-1.36 3.26-1.36h156.15c1.29 0 2.43.52 3.27 1.36.82.82 1.34 1.96 1.36 3.2zM55.07 37.15v52.87c0 1.27-.52 2.42-1.36 3.26-.82.84-1.98 1.36-3.26 1.36H6.65c-2.54 0-4.61-2.07-4.61-4.61 0-.31.03-.6.08-.88l.13-.49a82.4 82.4 0 0 1 21.86-38.77c7.03-7.03 15.3-12.8 24.47-16.95.03-.01.04-.01.07-.03.54-.24 1.16-.36 1.8-.36 1.27 0 2.43.52 3.26 1.36.84.82 1.36 1.98 1.36 3.26zm108.26 52.87c0 1.27-.52 2.42-1.36 3.26s-2 1.36-3.27 1.36H87.28c-2.54 0-4.61-2.07-4.61-4.61V4.61c0-1.27.52-2.43 1.36-3.27a4.645 4.645 0 0 1 5.06-.98c.14.06.27.13.41.2 14.45 6.65 27.52 15.83 38.64 26.96 15.44 15.44 27.14 34.65 33.58 56.13.03.06.04.13.06.18.46 1.5.88 3.01 1.27 4.53.03.1.06.2.07.31.01.03.01.06.03.08.03.13.07.25.08.38.07.29.1.6.1.91z" />
</ svg >
</ a >
< div class = "nav-right" >
< details class = "lang-switcher" id = "lang-switcher" >
< summary >
< svg class = "lang-icon" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2"
stroke-linecap = "round" stroke-linejoin = "round" >
< circle cx = "12" cy = "12" r = "10" />
< path d = "M2 12h20" />
< path d = "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</ svg >
< span id = "current-locale-label" > English</ span >
< svg class = "lang-chevron" width = "10" height = "10" viewBox = "0 0 10 10" fill = "none" >
< path d = "M2.5 4L5 6.5L7.5 4" stroke = "currentColor" stroke-width = "1.2" stroke-linecap = "round"
stroke-linejoin = "round" />
</ svg >
</ summary >
< ul class = "lang-switcher-menu" id = "locale-menu" ></ ul >
</ details >
</ div >
</ nav >
<!-- ── Page Header ── -->
< div class = "page-header" >
< h1 data-i18n = "pageTitle" > Device Settings</ h1 >
< p data-i18n = "pageSubtitle" > Configure credentials, runtime settings, and manage FATFS storage on your ESP device.
</ p >
</ div >
< main >
<!-- ── Status Bar ── -->
< div class = "status-bar" id = "status-bar" >
< div class = "status-info" >
< span class = "status-badge offline" id = "status-badge" >
< span class = "status-dot" ></ span >
< span id = "status-text" ></ span >
</ span >
< span class = "status-meta-sep" id = "meta-sep-ip" style = "display:none" > ·</ span >
< span class = "status-meta-item" id = "info-ip" style = "display:none" ></ span >
< span class = "status-meta-sep" id = "meta-sep-storage" style = "display:none" > ·</ span >
< span class = "status-meta-item" id = "info-storage" style = "display:none" ></ span >
</ div >
</ div >
<!-- ── Main Content Card ── -->
< div class = "main-content" >
<!-- Tabs -->
< div class = "tabs" >
< button type = "button" class = "tab-btn active" data-tab = "config" data-i18n = "tabConfig" > Configuration</ button >
< button type = "button" class = "tab-btn" data-tab = "memory" data-i18n = "tabMemory" > Memory</ button >
< button type = "button" class = "tab-btn" data-tab = "files" data-i18n = "tabFiles" > File Manager</ button >
</ div >
<!-- ═══ CONFIG TAB ═══ -->
< div class = "tab-panel active" id = "tab-config" >
< div class = "config-banner-wrap" >
< div id = "configBanner" class = "banner" ></ div >
</ div >
<!-- Wi-Fi Settings (default open) -->
< details class = "config-section" id = "section-wifi" open >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionWifi" > Wi-Fi Settings</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "wifi_ssid" data-i18n = "wifiSsid" > Wi-Fi SSID</ label >
< input type = "text" id = "wifi_ssid" name = "wifi_ssid" autocomplete = "off" >
</ div >
< div class = "form-group" >
< label for = "wifi_password" data-i18n = "wifiPassword" > Wi-Fi Password</ label >
< input type = "password" id = "wifi_password" name = "wifi_password" autocomplete = "off" >
</ div >
</ div >
</ div >
</ details >
<!-- LLM Settings (default open) -->
< details class = "config-section" id = "section-llm" open >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionLlm" > LLM Settings</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "llm_provider_preset" data-i18n = "llmProvider" > LLM Provider</ label >
< select id = "llm_provider_preset" name = "llm_provider_preset" >
< option value = "openai" data-i18n-option = "llmProviderOpenai" > OpenAI</ option >
< option value = "qwen" data-i18n-option = "llmProviderQwen" > Qwen Compatible</ option >
2026-04-27 15:04:14 +08:00
< option value = "deepseek" data-i18n-option = "llmProviderDeepSeek" > DeepSeek</ option >
2026-04-22 16:57:03 +08:00
< option value = "anthropic" data-i18n-option = "llmProviderAnthropic" > Anthropic</ option >
< option value = "custom" data-i18n-option = "llmProviderCustom" > Custom</ option >
</ select >
</ div >
< div class = "form-group" >
< label for = "llm_api_key" data-i18n = "llmApiKey" > API Key</ label >
< input type = "password" id = "llm_api_key" name = "llm_api_key" autocomplete = "off" >
</ div >
< div class = "form-group" >
< label for = "llm_model" data-i18n = "llmModel" > Model</ label >
< input type = "text" id = "llm_model" name = "llm_model" >
</ div >
< div class = "form-group" >
< label for = "llm_timeout_ms" data-i18n = "llmTimeout" > Timeout (ms)</ label >
< input type = "text" id = "llm_timeout_ms" name = "llm_timeout_ms" data-i18n-placeholder = "llmTimeoutPlaceholder" >
</ div >
</ div >
< details class = "advanced-section" style = "margin-top: 0.85rem;" >
< summary >
< span data-i18n = "llmAdvanced" > LLM Advanced Options</ span >
< svg class = "adv-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "advanced-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "llm_backend_type" data-i18n = "llmBackend" > Backend Type</ label >
< input type = "text" id = "llm_backend_type" name = "llm_backend_type" data-i18n-placeholder = "llmBackendPlaceholder" >
</ div >
< div class = "form-group" >
< label for = "llm_profile" data-i18n = "llmProfile" > Profile</ label >
< input type = "text" id = "llm_profile" name = "llm_profile" data-i18n-placeholder = "llmProfilePlaceholder" >
</ div >
< div class = "form-group" >
< label for = "llm_base_url" data-i18n = "llmBaseUrl" > Base URL</ label >
< input type = "url" id = "llm_base_url" name = "llm_base_url" data-i18n-placeholder = "llmBaseUrlPlaceholder" >
</ div >
< div class = "form-group" >
< label for = "llm_auth_type" data-i18n = "llmAuthType" > Auth Type</ label >
< input type = "text" id = "llm_auth_type" name = "llm_auth_type" data-i18n-placeholder = "llmAuthTypePlaceholder" >
</ div >
</ div >
</ div >
</ details >
</ div >
</ details >
<!-- IM Settings (default open) -->
< details class = "config-section" id = "section-im" open >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionIm" > Instant Messaging (IM)</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
<!-- WeChat (first, inputs collapsed by default) -->
< details class = "advanced-section" id = "im-wechat-section" >
< summary >
< span data-i18n = "imWechatTitle" > WeChat</ span >
< svg class = "adv-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "advanced-body" >
< div class = "wechat-login-panel" >
< div class = "wechat-login-header" >
< span class = "wechat-login-title" data-i18n = "wechatLoginDesc" > QR Code Sign-in</ span >
< div class = "wechat-login-actions" >
< button type = "button" class = "btn-secondary btn-sm" id = "wechatLoginStartButton" data-i18n = "wechatLoginGenerate" > Generate QR</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "wechatLoginCancelButton" data-i18n = "wechatLoginCancel" > Cancel</ button >
</ div >
</ div >
< div id = "wechatLoginBanner" class = "banner" ></ div >
< p id = "wechatLoginMeta" class = "section-note" data-i18n = "wechatLoginStatus" > Status: idle</ p >
< canvas id = "wechatLoginQr" class = "wechat-qr hidden" aria-label = "WeChat QR code" ></ canvas >
< p >< a id = "wechatLoginQrLink" class = "wechat-login-link hidden" target = "_blank" rel = "noopener noreferrer" data-i18n = "wechatLoginOpenLink" > Open login link</ a ></ p >
< div class = "form-grid" style = "margin-top: 0.85rem;" >
< div class = "form-group" >
< label for = "wechat_token" data-i18n = "wechatToken" > WeChat Token</ label >
< input type = "password" id = "wechat_token" name = "wechat_token" >
</ div >
< div class = "form-group" >
< label for = "wechat_account_id" data-i18n = "wechatAccountId" > WeChat Account ID</ label >
< input type = "text" id = "wechat_account_id" name = "wechat_account_id" data-i18n-placeholder = "wechatAccountIdPlaceholder" >
</ div >
< div class = "form-group" >
< label for = "wechat_base_url" data-i18n = "wechatBaseUrl" > WeChat Base URL</ label >
< input type = "text" id = "wechat_base_url" name = "wechat_base_url" data-i18n-placeholder = "wechatBaseUrlPlaceholder" >
</ div >
< div class = "form-group" >
< label for = "wechat_cdn_base_url" data-i18n = "wechatCdnBaseUrl" > WeChat CDN Base URL</ label >
< input type = "text" id = "wechat_cdn_base_url" name = "wechat_cdn_base_url" data-i18n-placeholder = "wechatCdnBaseUrlPlaceholder" >
</ div >
</ div >
< p class = "wechat-login-note" data-i18n = "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.</ p >
</ div >
</ div >
</ details >
<!-- QQ -->
< details class = "advanced-section" >
< summary >
< span > QQ</ span >
< svg class = "adv-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "advanced-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "qq_app_id" data-i18n = "qqAppId" > QQ App ID</ label >
< input type = "text" id = "qq_app_id" name = "qq_app_id" >
</ div >
< div class = "form-group" >
< label for = "qq_app_secret" data-i18n = "qqAppSecret" > QQ App Secret</ label >
< input type = "password" id = "qq_app_secret" name = "qq_app_secret" >
</ div >
</ div >
</ div >
</ details >
<!-- Feishu -->
< details class = "advanced-section" >
< summary >
< span data-i18n = "imFeishuTitle" > Feishu</ span >
< svg class = "adv-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "advanced-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "feishu_app_id" data-i18n = "feishuAppId" > Feishu App ID</ label >
< input type = "text" id = "feishu_app_id" name = "feishu_app_id" >
</ div >
< div class = "form-group" >
< label for = "feishu_app_secret" data-i18n = "feishuAppSecret" > Feishu App Secret</ label >
< input type = "password" id = "feishu_app_secret" name = "feishu_app_secret" >
</ div >
</ div >
</ div >
</ details >
<!-- Telegram -->
< details class = "advanced-section" >
< summary >
< span > Telegram</ span >
< svg class = "adv-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "advanced-body" >
< div class = "form-grid" >
< div class = "form-group full" >
< label for = "tg_bot_token" data-i18n = "tgBotToken" > Telegram Bot Token</ label >
< input type = "password" id = "tg_bot_token" name = "tg_bot_token" >
</ div >
</ div >
</ div >
</ details >
</ div >
</ details >
<!-- Search Settings (default open) -->
< details class = "config-section" id = "section-search" open >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionSearch" > Search (Optional)</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
< p class = "section-note" data-i18n = "searchNote" > If set, ESP-Claw can search online. Tip: Built-in weather skill also relies on online search.</ p >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "search_brave_key" data-i18n = "searchBraveKey" > Brave Search API Key</ label >
< input type = "password" id = "search_brave_key" name = "search_brave_key" autocomplete = "off" >
</ div >
< div class = "form-group" >
< label for = "search_tavily_key" data-i18n = "searchTavilyKey" > Tavily API Key</ label >
< input type = "password" id = "search_tavily_key" name = "search_tavily_key" autocomplete = "off" >
</ div >
</ div >
</ div >
</ details >
<!-- Capability Groups (default open) -->
< details class = "config-section" id = "section-capabilities" open >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionCapabilities" > Capability Groups</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
< div class = "capabilities-header" >
< label data-i18n = "enabledCapGroups" > Enabled Capability Groups</ label >
< div class = "capabilities-actions" >
< button type = "button" class = "btn-secondary btn-sm" id = "capabilitySelectAllButton" data-i18n = "capabilitySelectAll" > Select All</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "capabilityClearAllButton" data-i18n = "capabilityClearAll" > Clear All</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "capabilityLlmSelectDefaultsButton" data-i18n = "capabilityLlmDefaults" > LLM Defaults</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "capabilityLlmClearAllButton" data-i18n = "capabilityLlmClear" > Clear LLM</ button >
</ div >
</ div >
< div id = "capabilitySelectionSummary" class = "section-note" > Loading capabilities...</ div >
< div id = "capabilityCheckboxList" class = "capability-list" ></ div >
< small class = "form-hint" data-i18n = "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.</ small >
< div class = "capabilities-header" style = "margin-top: 1rem;" >
< label data-i18n = "enabledLuaModules" > Enabled Lua Modules</ label >
< div class = "capabilities-actions" >
< button type = "button" class = "btn-secondary btn-sm" id = "luaModuleSelectAllButton" data-i18n = "luaModuleSelectAll" > Select All</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "luaModuleClearAllButton" data-i18n = "luaModuleClearAll" > Clear All</ button >
</ div >
</ div >
< div id = "luaModuleSelectionSummary" class = "section-note" > Loading Lua modules...</ div >
< div id = "luaModuleCapabilityNote" class = "section-note hidden" data-i18n = "luaModuleCapabilityRequired" > Enable the cap_lua capability group above before Lua module settings can take effect.</ div >
< div id = "luaModuleCheckboxList" class = "capability-list" ></ div >
< small class = "form-hint" data-i18n = "enabledLuaModulesHelp" > Choose which compiled Lua modules are registered when cap_lua starts. Changes take effect after restart.</ small >
</ div >
</ details >
<!-- Advanced Settings (default collapsed) -->
< details class = "config-section" id = "section-advanced" >
< summary class = "section-header" >
< span class = "section-title" data-i18n = "sectionAdvanced" > Advanced Settings</ span >
< svg class = "section-chevron" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M6 9l6 6 6-6" /></ svg >
</ summary >
< div class = "section-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label for = "time_timezone" data-i18n = "timezone" > Timezone</ label >
< input type = "text" id = "time_timezone" name = "time_timezone" data-i18n-placeholder = "timezonePlaceholder" >
< small class = "form-hint" data-i18n = "timezoneHelp" > POSIX TZ string only. Sign is inverted: Beijing (UTC+8) must be written as 'CST-8'.</ small >
</ div >
</ div >
</ div >
</ details >
<!-- Save Actions -->
< div class = "config-actions" >
< button type = "button" class = "btn-primary" id = "saveConfigButton" data-i18n = "saveBtn" > Save Changes</ button >
< span class = "config-footer-note" data-i18n = "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.</ span >
</ div >
</ div >
<!-- ═══ MEMORY TAB ═══ -->
< div class = "tab-panel" id = "tab-memory" >
< div class = "memory-panel" >
< div id = "memoryBanner" class = "banner" ></ div >
< div class = "memory-header" >
< div >
< h2 class = "memory-title" data-i18n = "memoryTitle" > Memory Files</ h2 >
< p class = "memory-note" data-i18n = "memoryDescription" > Manage the device memory files.</ p >
</ div >
< div class = "memory-toolbar" >
< button type = "button" class = "btn-secondary btn-sm" id = "refreshMemoryButton" data-i18n = "memoryRefreshAll" > Refresh All</ button >
</ div >
</ div >
< div class = "memory-grid" >
< section class = "memory-card" >
< div class = "memory-card-header" >
< h3 class = "memory-card-title" data-i18n = "memoryCardLongTerm" > Long-term Memory</ h3 >
< div class = "memory-card-actions" >
< button type = "button" class = "btn-secondary btn-sm memory-refresh-btn" data-path = "/memory/memory.md" data-i18n = "memoryRefresh" > Refresh</ button >
</ div >
</ div >
< div id = "memoryMissingState" class = "memory-inline-empty hidden" >
< p class = "memory-note" data-i18n = "memoryMissing" > Long-term memory file does not exist yet.</ p >
</ div >
< textarea id = "memoryEditor" class = "memory-editor hidden" spellcheck = "false" ></ textarea >
</ section >
< section class = "memory-card" >
< div class = "memory-card-header" >
< h3 class = "memory-card-title" data-i18n = "memoryCardSoul" > Soul</ h3 >
< div class = "memory-card-actions" >
< button type = "button" class = "btn-secondary btn-sm memory-refresh-btn" data-path = "/memory/soul.md" data-i18n = "memoryRefresh" > Refresh</ button >
< button type = "button" class = "btn-secondary btn-sm memory-edit-btn" data-path = "/memory/soul.md" data-i18n = "fileEdit" > Edit</ button >
</ div >
</ div >
< div id = "soulMissingState" class = "memory-inline-empty hidden" >
< p class = "memory-note" data-i18n = "memoryAuxMissing" > Memory file does not exist yet.</ p >
</ div >
< textarea id = "soulEditor" class = "memory-editor memory-editor-sm hidden" spellcheck = "false" ></ textarea >
</ section >
< section class = "memory-card" >
< div class = "memory-card-header" >
< h3 class = "memory-card-title" data-i18n = "memoryCardIdentity" > Identity</ h3 >
< div class = "memory-card-actions" >
< button type = "button" class = "btn-secondary btn-sm memory-refresh-btn" data-path = "/memory/identity.md" data-i18n = "memoryRefresh" > Refresh</ button >
< button type = "button" class = "btn-secondary btn-sm memory-edit-btn" data-path = "/memory/identity.md" data-i18n = "fileEdit" > Edit</ button >
</ div >
</ div >
< div id = "identityMissingState" class = "memory-inline-empty hidden" >
< p class = "memory-note" data-i18n = "memoryAuxMissing" > Memory file does not exist yet.</ p >
</ div >
< textarea id = "identityEditor" class = "memory-editor memory-editor-sm hidden" spellcheck = "false" ></ textarea >
</ section >
< section class = "memory-card" >
< div class = "memory-card-header" >
< h3 class = "memory-card-title" data-i18n = "memoryCardUserInfo" > User Info</ h3 >
< div class = "memory-card-actions" >
< button type = "button" class = "btn-secondary btn-sm memory-refresh-btn" data-path = "/memory/user.md" data-i18n = "memoryRefresh" > Refresh</ button >
< button type = "button" class = "btn-secondary btn-sm memory-edit-btn" data-path = "/memory/user.md" data-i18n = "fileEdit" > Edit</ button >
</ div >
</ div >
< div id = "userMissingState" class = "memory-inline-empty hidden" >
< p class = "memory-note" data-i18n = "memoryAuxMissing" > Memory file does not exist yet.</ p >
</ div >
< textarea id = "userEditor" class = "memory-editor memory-editor-sm hidden" spellcheck = "false" ></ textarea >
</ section >
</ div >
</ div >
</ div >
<!-- ═══ FILES TAB ═══ -->
< div class = "tab-panel" id = "tab-files" >
< div class = "file-panel" >
< div id = "fileBanner" class = "banner" ></ div >
< div class = "file-toolbar" >
< button type = "button" class = "btn-secondary btn-sm" id = "upDirButton" data-i18n = "fileUpDir" > Up One
Level</ button >
< code class = "file-path" id = "currentPath" > /</ code >
< div class = "file-toolbar-right" >
< button type = "button" class = "btn-secondary btn-sm" id = "devModeButton"
data-i18n = "fileDevMode" > Dev Mode</ button >
< button type = "button" class = "btn-secondary btn-sm" id = "refreshFilesButton"
data-i18n = "fileRefresh" > Refresh</ button >
</ div >
</ div >
< div class = "file-create-row" id = "fileCreateRow" >
< input type = "text" id = "newFolderInput" data-i18n-placeholder = "fileNewFolder" >
< button type = "button" class = "btn-secondary btn-sm" id = "createFolderButton"
data-i18n = "fileCreateFolder" > Create Folder</ button >
</ div >
< div class = "file-upload-row" id = "fileUploadRow" >
< input type = "text" id = "uploadPathInput" data-i18n-placeholder = "fileUploadPath" >
< div class = "file-picker" >
< input type = "file" id = "uploadFileInput" >
< button type = "button" class = "btn-ghost btn-sm" id = "chooseFileButton" data-i18n = "fileChoose" > Choose
File</ button >
< span class = "file-name" id = "selectedFileName" data-i18n = "fileNoFileSelected" > No file selected</ span >
</ div >
< button type = "button" class = "btn-primary btn-sm" id = "uploadButton" data-i18n = "fileUpload" > Upload</ button >
</ div >
< div class = "table-wrap" >
< table >
< thead >
< tr >
< th data-i18n = "fileColName" > Name</ th >
< th data-i18n = "fileColType" > Type</ th >
< th data-i18n = "fileColSize" > Size</ th >
< th data-i18n = "fileColActions" > Actions</ th >
</ tr >
</ thead >
< tbody id = "fileTableBody" ></ tbody >
</ table >
</ div >
</ div >
</ div >
</ div > <!-- /.main-content -->
</ main >
< div id = "fileEditorModal" class = "modal-overlay hidden" >
< div class = "modal-dialog" >
< div class = "modal-header" >
< div >
< h2 class = "modal-title" data-i18n = "fileEditorTitle" > Edit File</ h2 >
< code class = "file-path" id = "fileEditorPath" > /</ code >
</ div >
< button type = "button" class = "btn-secondary btn-sm" id = "closeFileEditorButton" data-i18n = "fileEditorClose" > Close</ button >
</ div >
< div id = "fileEditorBanner" class = "banner" ></ div >
< textarea id = "fileEditorTextarea" class = "modal-editor" spellcheck = "false" ></ textarea >
< div class = "modal-actions" >
< button type = "button" class = "btn-secondary btn-sm" id = "refreshFileEditorButton" data-i18n = "fileEditorRefresh" > Refresh</ button >
< button type = "button" class = "btn-primary btn-sm" id = "saveFileEditorButton" data-i18n = "fileEditorSave" > Save</ button >
</ div >
</ div >
</ div >
<!-- ── Footer ── -->
< footer >
< span class = "footer-text" >
Copyright © 2026 < a href = "https://www.espressif.com" target = "_blank" rel = "noopener" >< span
class = "footer-highlight" > Espressif Systems</ span ></ a >
</ span >
</ footer >
</ div > <!-- /.site-container -->
< script src = "/app.js" ></ script >
</ body >
</ html >