@bonginkan/maria-lite 8.0.2 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -403,6 +403,7 @@ var init_helpers = __esm({
403
403
  "dev-adviser": "user-check",
404
404
  "dev-decision": "compass",
405
405
  estimate: "ruler",
406
+ analytics: "bar-chart-big",
406
407
  // CXO Agents
407
408
  ceo: "crown",
408
409
  coo: "target",
@@ -498,6 +499,7 @@ var init_helpers = __esm({
498
499
  config: "settings",
499
500
  "notification-router": "bell-ring",
500
501
  events: "radio",
502
+ vui: "mic",
501
503
  "task-manager": "activity",
502
504
  // Games
503
505
  minesweeper: "bomb",
@@ -660,7 +662,7 @@ var init_state = __esm({
660
662
  label: "Daily Intelligence",
661
663
  emoji: "\u{1F4CA}",
662
664
  serverCategories: ["daily"],
663
- commandIds: ["daily-report", "daily-news", "daily-papers", "daily-insights", "weekly-report", "discord-setup", "billing-pl"]
665
+ commandIds: ["daily-report", "daily-news", "daily-papers", "daily-insights", "weekly-report", "discord-setup", "billing-pl", "analytics"]
664
666
  },
665
667
  {
666
668
  id: "gtm",
@@ -767,7 +769,7 @@ var init_state = __esm({
767
769
  label: "System",
768
770
  emoji: "\u2699\uFE0F",
769
771
  serverCategories: [],
770
- commandIds: ["help", "version", "gui", "desktop", "login", "logout", "account", "vup", "schedule", "config", "notification-router", "events"]
772
+ commandIds: ["help", "version", "gui", "desktop", "login", "logout", "account", "vup", "schedule", "config", "notification-router", "events", "vui"]
771
773
  }
772
774
  ];
773
775
  state = {
@@ -2428,6 +2430,54 @@ var init_uni_editor = __esm({
2428
2430
  }
2429
2431
  });
2430
2432
 
2433
+ // services/desktop/client/modules/desktop-upload.ts
2434
+ function normalizeUploadRelativePath(rawPath, fallbackName) {
2435
+ const parts = String(rawPath || "").replace(/\\/g, "/").split("/").map((part) => part.trim()).filter((part) => part && part !== "." && part !== "..");
2436
+ if (parts.length === 0) {
2437
+ return { dir: "", filename: fallbackName };
2438
+ }
2439
+ const filename = parts.pop() || fallbackName;
2440
+ return { dir: parts.join("/"), filename };
2441
+ }
2442
+ function joinDesktopDirs(...parts) {
2443
+ return parts.map((part) => String(part || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
2444
+ }
2445
+ async function uploadFileToDesktopAtWithPath(dir, file, relativePath = "") {
2446
+ const normalized = normalizeUploadRelativePath(relativePath, file.name || "upload.bin");
2447
+ const targetDir = joinDesktopDirs(dir, normalized.dir);
2448
+ const formData = new FormData();
2449
+ formData.append("file", file, normalized.filename);
2450
+ if (targetDir) formData.append("dir", targetDir);
2451
+ try {
2452
+ const res = await api("/api/desktop/upload", { method: "POST", body: formData });
2453
+ if (!res.ok) {
2454
+ const body = await res.text().catch(() => "");
2455
+ console.error(`[upload] failed for ${file.name}: HTTP ${res.status}`, body);
2456
+ return "";
2457
+ }
2458
+ const json = await res.json().catch(() => null);
2459
+ if (json?.ok && json.files?.[0]?.path) return json.files[0].path;
2460
+ return "";
2461
+ } catch (err) {
2462
+ console.error(`[upload] error for ${file.name}:`, err);
2463
+ return "";
2464
+ }
2465
+ }
2466
+ async function uploadFileListToDesktopAt(dir, files, options) {
2467
+ const out = [];
2468
+ for (const file of files) {
2469
+ const relativePath = options?.preserveRelativePaths ? file.webkitRelativePath || file.name : file.name;
2470
+ const uploaded = await uploadFileToDesktopAtWithPath(dir, file, relativePath);
2471
+ if (uploaded) out.push(uploaded);
2472
+ }
2473
+ return out;
2474
+ }
2475
+ var init_desktop_upload = __esm({
2476
+ "services/desktop/client/modules/desktop-upload.ts"() {
2477
+ init_helpers();
2478
+ }
2479
+ });
2480
+
2431
2481
  // services/desktop/client/modules/file-manager.ts
2432
2482
  function fmHistoryPush(dir) {
2433
2483
  if (state.fmHistoryIdx < state.fmHistory.length - 1) {
@@ -2516,6 +2566,7 @@ function renderFileManagerContent(win) {
2516
2566
  html += '<button class="fm-refresh-btn" title="Refresh">\u21BB</button>';
2517
2567
  html += '<button class="fm-newdir-btn" title="New folder">\u{1F4C1}+</button>';
2518
2568
  html += '<label class="fm-upload-btn" title="Upload">\u2B06\uFE0F<input type="file" class="fm-upload-input" multiple style="display:none" /></label>';
2569
+ html += '<label class="fm-upload-btn" title="Upload folder">\u{1F4C1}\u2B06\uFE0F<input type="file" class="fm-upload-dir-input" webkitdirectory directory multiple style="display:none" /></label>';
2519
2570
  html += "</div>";
2520
2571
  html += "</div>";
2521
2572
  html += '<div class="fm-list">';
@@ -2632,9 +2683,19 @@ function attachFileManagerListeners(win) {
2632
2683
  uploadInput.addEventListener("change", () => {
2633
2684
  const files = Array.from(uploadInput.files || []);
2634
2685
  void (async () => {
2635
- for (const file of files) {
2636
- await uploadFileToDesktop(file);
2637
- }
2686
+ await uploadFileListToDesktopAt(state.currentDir, files);
2687
+ await loadDesktopFiles(state.currentDir);
2688
+ refreshFileManagerContent(win);
2689
+ renderDesktopFiles();
2690
+ })();
2691
+ });
2692
+ }
2693
+ const uploadDirInput = el.querySelector(".fm-upload-dir-input");
2694
+ if (uploadDirInput) {
2695
+ uploadDirInput.addEventListener("change", () => {
2696
+ const files = Array.from(uploadDirInput.files || []);
2697
+ void (async () => {
2698
+ await uploadFileListToDesktopAt(state.currentDir, files, { preserveRelativePaths: true });
2638
2699
  await loadDesktopFiles(state.currentDir);
2639
2700
  refreshFileManagerContent(win);
2640
2701
  renderDesktopFiles();
@@ -2733,6 +2794,7 @@ function showFileManagerContextMenu(x, y, name, kind, win) {
2733
2794
  } else {
2734
2795
  html += `<div class="ctx-item" data-action="fm-newdir"><span class="ctx-icon">\u{1F4C1}</span><span>New Folder</span></div>`;
2735
2796
  html += `<div class="ctx-item" data-action="fm-upload"><span class="ctx-icon">\u{1F4C4}</span><span>Upload File</span></div>`;
2797
+ html += `<div class="ctx-item" data-action="fm-upload-folder"><span class="ctx-icon">\u{1F4C1}</span><span>Upload Folder</span></div>`;
2736
2798
  html += '<div class="ctx-sep"></div>';
2737
2799
  html += `<div class="ctx-item" data-action="fm-refresh"><span class="ctx-icon">\u{1F504}</span><span>Refresh</span></div>`;
2738
2800
  }
@@ -2825,6 +2887,27 @@ function showFileManagerContextMenu(x, y, name, kind, win) {
2825
2887
  inp.click();
2826
2888
  break;
2827
2889
  }
2890
+ case "fm-upload-folder": {
2891
+ const inp = document.createElement("input");
2892
+ inp.type = "file";
2893
+ inp.multiple = true;
2894
+ inp.style.display = "none";
2895
+ inp.setAttribute("webkitdirectory", "");
2896
+ inp.setAttribute("directory", "");
2897
+ document.body.appendChild(inp);
2898
+ inp.addEventListener("change", () => {
2899
+ const files = Array.from(inp.files || []);
2900
+ void (async () => {
2901
+ await uploadFileListToDesktopAt(state.currentDir, files, { preserveRelativePaths: true });
2902
+ inp.remove();
2903
+ await loadDesktopFiles(state.currentDir);
2904
+ refreshFileManagerContent(win);
2905
+ renderDesktopFiles();
2906
+ })();
2907
+ });
2908
+ inp.click();
2909
+ break;
2910
+ }
2828
2911
  case "fm-refresh":
2829
2912
  void loadDesktopFiles(state.currentDir).then(() => {
2830
2913
  refreshFileManagerContent(win);
@@ -3668,25 +3751,6 @@ async function uploadFileToDesktop(file) {
3668
3751
  async function uploadFileToDesktopAt(dir, file) {
3669
3752
  await uploadFileToDesktopAtWithPath(dir, file);
3670
3753
  }
3671
- async function uploadFileToDesktopAtWithPath(dir, file) {
3672
- const formData = new FormData();
3673
- formData.append("file", file);
3674
- if (dir) formData.append("dir", dir);
3675
- try {
3676
- const res = await api("/api/desktop/upload", { method: "POST", body: formData });
3677
- if (!res.ok) {
3678
- const body = await res.text().catch(() => "");
3679
- console.error(`[upload] failed for ${file.name}: HTTP ${res.status}`, body);
3680
- return "";
3681
- }
3682
- const json = await res.json().catch(() => null);
3683
- if (json?.ok && json.files?.[0]?.path) return json.files[0].path;
3684
- return "";
3685
- } catch (err) {
3686
- console.error(`[upload] error for ${file.name}:`, err);
3687
- return "";
3688
- }
3689
- }
3690
3754
  async function createFolder(name) {
3691
3755
  await createFolderAt(state.currentDir, name);
3692
3756
  }
@@ -3728,6 +3792,7 @@ var init_file_manager = __esm({
3728
3792
  init_context_menu();
3729
3793
  init_slide_viewer();
3730
3794
  init_uni_editor();
3795
+ init_desktop_upload();
3731
3796
  OFFICE_BASE_CSS = `
3732
3797
  * { margin: 0; padding: 0; box-sizing: border-box; }
3733
3798
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1e293b; background: #f8fafc; }
@@ -3816,6 +3881,482 @@ function buildPreview(style, layout) {
3816
3881
  html += `</div>`;
3817
3882
  return html;
3818
3883
  }
3884
+ function fieldId(winId, key) {
3885
+ return `settings_${winId}_${key}`;
3886
+ }
3887
+ function fieldEl(winId, key) {
3888
+ return document.getElementById(fieldId(winId, key));
3889
+ }
3890
+ function renderOptionTags(options, selectedValue, emptyLabel) {
3891
+ if (!Array.isArray(options) || options.length === 0) {
3892
+ return `<option value="">${esc(emptyLabel)}</option>`;
3893
+ }
3894
+ return options.map((option) => `<option value="${esc(option.id)}"${option.id === selectedValue ? " selected" : ""}>${esc(option.label || option.id)}</option>`).join("");
3895
+ }
3896
+ function filteredAvatarModels(models, source) {
3897
+ return (models || []).filter((model) => model.source === source);
3898
+ }
3899
+ function selectedAvatarModelId(models, preferredId) {
3900
+ return models.find((model) => model.id === preferredId)?.id || models[0]?.id || "";
3901
+ }
3902
+ function renderVuiSettingsMarkup(winId, settings, meta) {
3903
+ const vui = settings || {};
3904
+ const tts = vui.tts || {};
3905
+ const avatar = vui.avatar || {};
3906
+ const vrmSource = avatar.vrm?.source === "custom" ? "custom" : "desktop";
3907
+ const vrmaSource = avatar.vrm?.animationSource === "custom" ? "custom" : "desktop";
3908
+ const live2dSource = avatar.live2d?.source === "custom" ? "custom" : "desktop";
3909
+ const vrmModels = filteredAvatarModels(meta.avatar?.vrm?.models, vrmSource);
3910
+ const vrmaModels = filteredAvatarModels(meta.avatar?.vrm?.animations, vrmaSource);
3911
+ const live2dModels = filteredAvatarModels(meta.avatar?.live2d?.models, live2dSource);
3912
+ const vrmSelected = selectedAvatarModelId(vrmModels, avatar.vrm?.selectedId || meta.avatar?.vrm?.current?.optionId || "");
3913
+ const vrmaSelected = selectedAvatarModelId(vrmaModels, avatar.vrm?.selectedAnimationId || meta.avatar?.vrm?.currentAnimation?.optionId || "");
3914
+ const live2dSelected = selectedAvatarModelId(live2dModels, avatar.live2d?.selectedId || meta.avatar?.live2d?.current?.optionId || "");
3915
+ const provider = tts.provider || "browser";
3916
+ const providerOptions = [
3917
+ { id: "browser", label: "Browser" },
3918
+ { id: "openai", label: meta.presets?.openai?.label || "OpenAI" },
3919
+ { id: "google", label: meta.presets?.google?.label || "Google" },
3920
+ { id: "elevenlabs", label: meta.presets?.elevenlabs?.label || "ElevenLabs" }
3921
+ ];
3922
+ const modelOptions = meta.presets?.[provider]?.models || [];
3923
+ const outputFormatOptions = meta.presets?.elevenlabs?.outputFormats || [];
3924
+ const outputFormat = tts.outputFormat || meta.presets?.elevenlabs?.defaults?.outputFormat || "mp3_44100_128";
3925
+ const elevenDefaults = meta.presets?.elevenlabs?.defaults || {};
3926
+ const hasPersonalElevenLabsApiKey = tts.hasPersonalElevenLabsApiKey === true;
3927
+ return [
3928
+ `<div class="uid-vui-stack">`,
3929
+ `<div class="uid-vui-note">Voice, language, and avatar defaults for /vui. Put personal VRM files under <code>~/.maria/desktop/vrm</code>, VRMA motion files under <code>~/.maria/desktop/vrma</code>, and Live2D models under <code>~/.maria/desktop/live2d</code>.</div>`,
3930
+ `<div class="uid-vui-grid">`,
3931
+ `<label class="uid-vui-field"><span>Recognition language</span><select id="${fieldId(winId, "vui-language")}">${renderOptionTags(meta.languages || [], vui.language || "ja-JP", "(no languages)")}</select></label>`,
3932
+ `<label class="uid-vui-field"><span>Default avatar mode</span><select id="${fieldId(winId, "avatar-default-mode")}">${renderOptionTags(meta.avatarModes || [], avatar.defaultMode || meta.avatar?.defaultMode || "orb", "(no modes)")}</select></label>`,
3933
+ `</div>`,
3934
+ `<div class="uid-vui-card">`,
3935
+ `<div class="uid-vui-card-title">Voice Reply</div>`,
3936
+ `<div class="uid-vui-grid">`,
3937
+ `<label class="uid-vui-toggle"><input id="${fieldId(winId, "tts-enabled")}" type="checkbox"${tts.enabled !== false ? " checked" : ""}><span>Enabled</span></label>`,
3938
+ `<label class="uid-vui-field"><span>Provider</span><select id="${fieldId(winId, "tts-provider")}">${renderOptionTags(providerOptions, provider, "(no providers)")}</select></label>`,
3939
+ `<label class="uid-vui-field" id="${fieldId(winId, "tts-model-wrap")}"><span>Model</span><select id="${fieldId(winId, "tts-model")}">${renderOptionTags(modelOptions, tts.model || "", "(no models)")}</select></label>`,
3940
+ `<label class="uid-vui-field" id="${fieldId(winId, "tts-language-wrap")}"><span>TTS language</span><select id="${fieldId(winId, "tts-language-code")}">${renderOptionTags(meta.languages || [], tts.languageCode || vui.language || "ja-JP", "(no languages)")}</select></label>`,
3941
+ `<label class="uid-vui-field" id="${fieldId(winId, "tts-voice-select-wrap")}"><span>Voice</span><select id="${fieldId(winId, "tts-voice-select")}"><option value="">Loading...</option></select></label>`,
3942
+ `<label class="uid-vui-field" id="${fieldId(winId, "tts-voice-custom-wrap")}"><span>Custom voice ID or share URL</span><input id="${fieldId(winId, "tts-voice-custom")}" type="text" value="" placeholder="Optional override for shared/private voices"></label>`,
3943
+ `<label class="uid-vui-field uid-vui-span"><span>Instructions</span><input id="${fieldId(winId, "tts-instructions")}" type="text" value="${esc(tts.instructions || "")}" placeholder="Optional speaking style instructions"></label>`,
3944
+ `</div>`,
3945
+ `<div id="${fieldId(winId, "elevenlabs-panel")}" class="uid-vui-grid uid-vui-subgrid">`,
3946
+ `<label class="uid-vui-field uid-vui-span"><span>Personal ElevenLabs API key (optional)</span><input id="${fieldId(winId, "tts-eleven-api-key")}" type="password" value="" autocomplete="off" placeholder="${esc(hasPersonalElevenLabsApiKey ? "Stored locally on this device. Enter a new key to replace it." : "Set your own key to use private/shared voices in your account.")}"></label>`,
3947
+ `<div class="uid-vui-hint uid-vui-span" id="${fieldId(winId, "tts-eleven-api-key-status")}">${esc(hasPersonalElevenLabsApiKey ? "A personal ElevenLabs API key is already stored locally on this device." : "Optional. Use this when a private or shared voice is not available from the shared MARIA server key.")}</div>`,
3948
+ `<label class="uid-vui-field"><span>Output format</span><select id="${fieldId(winId, "tts-eleven-output")}">${renderOptionTags(outputFormatOptions, outputFormat, "(no formats)")}</select></label>`,
3949
+ `<label class="uid-vui-field"><span>Stability</span><input id="${fieldId(winId, "tts-eleven-stability")}" type="number" min="0" max="1" step="0.01" value="${String(tts.elevenLabsSettings?.stability ?? elevenDefaults.stability ?? 0.5)}"></label>`,
3950
+ `<label class="uid-vui-field"><span>Similarity</span><input id="${fieldId(winId, "tts-eleven-similarity")}" type="number" min="0" max="1" step="0.01" value="${String(tts.elevenLabsSettings?.similarityBoost ?? elevenDefaults.similarityBoost ?? 0.75)}"></label>`,
3951
+ `<label class="uid-vui-field"><span>Style</span><input id="${fieldId(winId, "tts-eleven-style")}" type="number" min="0" max="1" step="0.01" value="${String(tts.elevenLabsSettings?.style ?? elevenDefaults.style ?? 0)}"></label>`,
3952
+ `<label class="uid-vui-field"><span>Speed</span><input id="${fieldId(winId, "tts-eleven-speed")}" type="number" min="0.7" max="1.2" step="0.01" value="${String(tts.elevenLabsSettings?.speed ?? elevenDefaults.speed ?? 1)}"></label>`,
3953
+ `<label class="uid-vui-toggle"><input id="${fieldId(winId, "tts-eleven-boost")}" type="checkbox"${tts.elevenLabsSettings?.useSpeakerBoost !== false ? " checked" : ""}><span>Speaker boost</span></label>`,
3954
+ `</div>`,
3955
+ `</div>`,
3956
+ `<div class="uid-vui-card">`,
3957
+ `<div class="uid-vui-card-title">VRM</div>`,
3958
+ `<div class="uid-vui-grid">`,
3959
+ `<label class="uid-vui-field"><span>Source</span><select id="${fieldId(winId, "vrm-source")}"><option value="desktop"${vrmSource === "desktop" ? " selected" : ""}>MARIA OS library</option><option value="custom"${vrmSource === "custom" ? " selected" : ""}>Custom path</option></select></label>`,
3960
+ `<label class="uid-vui-field" id="${fieldId(winId, "vrm-select-wrap")}"><span>Model</span><select id="${fieldId(winId, "vrm-select")}">${renderOptionTags(vrmModels, vrmSelected, "(no VRM models)")}</select></label>`,
3961
+ `<label class="uid-vui-field" id="${fieldId(winId, "vrm-custom-wrap")}"><span>Custom VRM path</span><input id="${fieldId(winId, "vrm-custom-path")}" type="text" value="${esc(avatar.vrm?.customPath || meta.avatar?.vrm?.current?.customPath || "")}" placeholder="/absolute/path/model.vrm"></label>`,
3962
+ `<label class="uid-vui-field"><span>Scale</span><input id="${fieldId(winId, "vrm-scale")}" type="number" min="0.5" max="3" step="0.05" value="${String(avatar.vrm?.scale ?? meta.avatar?.vrm?.scale ?? 1.5)}"></label>`,
3963
+ `<div class="uid-vui-actions uid-vui-span"><button type="button" class="uid-vui-btn" id="${fieldId(winId, "vrm-upload")}">Upload VRM</button><span class="uid-vui-hint">Copies selected .vrm files into <code>~/.maria/desktop/vrm</code>.</span></div>`,
3964
+ `<label class="uid-vui-field"><span>Idle / Motion source</span><select id="${fieldId(winId, "vrma-source")}"><option value="desktop"${vrmaSource === "desktop" ? " selected" : ""}>MARIA OS library</option><option value="custom"${vrmaSource === "custom" ? " selected" : ""}>Custom path</option></select></label>`,
3965
+ `<label class="uid-vui-field" id="${fieldId(winId, "vrma-select-wrap")}"><span>VRMA motion</span><select id="${fieldId(winId, "vrma-select")}">${renderOptionTags(vrmaModels, vrmaSelected, "(procedural fallback)")}</select></label>`,
3966
+ `<label class="uid-vui-field" id="${fieldId(winId, "vrma-custom-wrap")}"><span>Custom VRMA path</span><input id="${fieldId(winId, "vrma-custom-path")}" type="text" value="${esc(avatar.vrm?.customAnimationPath || meta.avatar?.vrm?.currentAnimation?.customPath || "")}" placeholder="/absolute/path/idle.vrma"></label>`,
3967
+ `<div class="uid-vui-actions uid-vui-span"><button type="button" class="uid-vui-btn" id="${fieldId(winId, "vrma-upload")}">Upload VRMA</button><span class="uid-vui-hint">Optional. When absent, MARIA OS falls back to procedural idle motion.</span></div>`,
3968
+ `</div>`,
3969
+ `</div>`,
3970
+ `<div class="uid-vui-card">`,
3971
+ `<div class="uid-vui-card-title">Live2D</div>`,
3972
+ `<div class="uid-vui-grid">`,
3973
+ `<label class="uid-vui-field"><span>Source</span><select id="${fieldId(winId, "live2d-source")}"><option value="desktop"${live2dSource === "desktop" ? " selected" : ""}>MARIA OS library</option><option value="custom"${live2dSource === "custom" ? " selected" : ""}>Custom directory</option></select></label>`,
3974
+ `<label class="uid-vui-field" id="${fieldId(winId, "live2d-select-wrap")}"><span>Model</span><select id="${fieldId(winId, "live2d-select")}">${renderOptionTags(live2dModels, live2dSelected, "(no Live2D models)")}</select></label>`,
3975
+ `<label class="uid-vui-field" id="${fieldId(winId, "live2d-custom-dir-wrap")}"><span>Custom model dir/file</span><input id="${fieldId(winId, "live2d-custom-dir")}" type="text" value="${esc(avatar.live2d?.customModelDir || meta.avatar?.live2d?.current?.customModelDir || "")}" placeholder="/absolute/path/modeldir or *.model3.json"></label>`,
3976
+ `<label class="uid-vui-field" id="${fieldId(winId, "live2d-custom-name-wrap")}"><span>Custom model name</span><input id="${fieldId(winId, "live2d-custom-name")}" type="text" value="${esc(avatar.live2d?.customModelName || meta.avatar?.live2d?.current?.customModelName || "")}" placeholder="Optional model name"></label>`,
3977
+ `<label class="uid-vui-field"><span>Scale</span><input id="${fieldId(winId, "live2d-scale")}" type="number" min="0.5" max="3" step="0.05" value="${String(avatar.live2d?.scale ?? meta.avatar?.live2d?.scale ?? 2.1)}"></label>`,
3978
+ `<label class="uid-vui-field"><span>Offset X</span><input id="${fieldId(winId, "live2d-offset-x")}" type="number" min="-4" max="4" step="0.05" value="${String(avatar.live2d?.offsetX ?? meta.avatar?.live2d?.offsetX ?? 0)}"></label>`,
3979
+ `<label class="uid-vui-field"><span>Offset Y</span><input id="${fieldId(winId, "live2d-offset-y")}" type="number" min="-4" max="4" step="0.05" value="${String(avatar.live2d?.offsetY ?? meta.avatar?.live2d?.offsetY ?? 1.1)}"></label>`,
3980
+ `<div class="uid-vui-actions uid-vui-span"><button type="button" class="uid-vui-btn" id="${fieldId(winId, "live2d-upload")}">Upload Live2D Folder</button><span class="uid-vui-hint">Preserves subdirectories under <code>~/.maria/desktop/live2d</code>.</span></div>`,
3981
+ `</div>`,
3982
+ `</div>`,
3983
+ `<div id="${fieldId(winId, "vui-status")}" class="uid-vui-status">Changes save automatically.</div>`,
3984
+ `</div>`
3985
+ ].join("");
3986
+ }
3987
+ function toFiniteNumber(raw, fallback) {
3988
+ const value = Number(raw);
3989
+ return Number.isFinite(value) ? value : fallback;
3990
+ }
3991
+ function notifyVuiSettingsChanged() {
3992
+ const detail = { updatedAt: Date.now() };
3993
+ try {
3994
+ localStorage.setItem("maria.desktop.vui.settings.updatedAt", String(detail.updatedAt));
3995
+ } catch {
3996
+ }
3997
+ try {
3998
+ const channel = new BroadcastChannel("maria.desktop.vui.settings");
3999
+ channel.postMessage(detail);
4000
+ channel.close();
4001
+ } catch {
4002
+ }
4003
+ try {
4004
+ window.dispatchEvent(new CustomEvent("maria:vui-settings-updated", { detail }));
4005
+ } catch {
4006
+ }
4007
+ }
4008
+ function pickLocalFiles(options) {
4009
+ return new Promise((resolve) => {
4010
+ const input = document.createElement("input");
4011
+ input.type = "file";
4012
+ input.style.display = "none";
4013
+ input.multiple = options?.multiple !== false;
4014
+ if (options?.accept) input.accept = options.accept;
4015
+ if (options?.directory) {
4016
+ input.setAttribute("webkitdirectory", "");
4017
+ input.setAttribute("directory", "");
4018
+ }
4019
+ document.body.appendChild(input);
4020
+ input.addEventListener("change", () => {
4021
+ const files = Array.from(input.files || []);
4022
+ input.remove();
4023
+ resolve(files);
4024
+ }, { once: true });
4025
+ input.click();
4026
+ });
4027
+ }
4028
+ async function loadVoiceOptionsForSettings(winId, meta, selectedVoice) {
4029
+ const provider = fieldEl(winId, "tts-provider")?.value || "browser";
4030
+ const model = fieldEl(winId, "tts-model")?.value || "";
4031
+ const languageCode = fieldEl(winId, "tts-language-code")?.value || "";
4032
+ const voiceSelectWrap = fieldEl(winId, "tts-voice-select-wrap");
4033
+ const voiceCustomWrap = fieldEl(winId, "tts-voice-custom-wrap");
4034
+ const voiceSelect = fieldEl(winId, "tts-voice-select");
4035
+ const voiceCustom = fieldEl(winId, "tts-voice-custom");
4036
+ if (!voiceSelectWrap || !voiceCustomWrap || !voiceSelect || !voiceCustom) return;
4037
+ if (provider === "browser") {
4038
+ voiceSelectWrap.style.display = "none";
4039
+ voiceCustomWrap.style.display = "none";
4040
+ return;
4041
+ }
4042
+ const presetVoices = meta.presets?.[provider]?.voices || [];
4043
+ let voices = presetVoices;
4044
+ try {
4045
+ const params = new URLSearchParams();
4046
+ params.set("provider", provider);
4047
+ if (model) params.set("model", model);
4048
+ if (languageCode) params.set("languageCode", languageCode);
4049
+ const result = await apiJson(`/api/vui/tts/voices?${params.toString()}`);
4050
+ if (Array.isArray(result.voices) && result.voices.length > 0) {
4051
+ voices = result.voices.map((voice) => ({
4052
+ id: voice.id,
4053
+ label: voice.label || voice.name || voice.id
4054
+ }));
4055
+ }
4056
+ } catch {
4057
+ }
4058
+ if (!voices.length) {
4059
+ voiceSelectWrap.style.display = "none";
4060
+ voiceCustomWrap.style.display = "grid";
4061
+ if (selectedVoice) voiceCustom.value = selectedVoice;
4062
+ return;
4063
+ }
4064
+ voiceSelect.innerHTML = renderOptionTags(voices, selectedVoice || voiceCustom.value || "", "(no voices)");
4065
+ voiceSelectWrap.style.display = "grid";
4066
+ voiceCustomWrap.style.display = "grid";
4067
+ if (selectedVoice && voices.some((voice) => voice.id === selectedVoice)) {
4068
+ voiceCustom.value = "";
4069
+ } else if (selectedVoice && !voices.some((voice) => voice.id === selectedVoice)) {
4070
+ voiceCustom.value = selectedVoice;
4071
+ }
4072
+ }
4073
+ function syncVuiSettingsVisibility(winId, meta) {
4074
+ const provider = fieldEl(winId, "tts-provider")?.value || "browser";
4075
+ const isBrowser = provider === "browser";
4076
+ const isGoogle = provider === "google";
4077
+ const isElevenLabs = provider === "elevenlabs";
4078
+ const ttsModelWrap = fieldEl(winId, "tts-model-wrap");
4079
+ const ttsLanguageWrap = fieldEl(winId, "tts-language-wrap");
4080
+ const ttsVoiceSelectWrap = fieldEl(winId, "tts-voice-select-wrap");
4081
+ const ttsVoiceCustomWrap = fieldEl(winId, "tts-voice-custom-wrap");
4082
+ const elevenPanel = fieldEl(winId, "elevenlabs-panel");
4083
+ if (ttsModelWrap) ttsModelWrap.style.display = isBrowser ? "none" : "grid";
4084
+ if (ttsLanguageWrap) ttsLanguageWrap.style.display = isGoogle ? "grid" : "none";
4085
+ if (ttsVoiceSelectWrap) ttsVoiceSelectWrap.style.display = isBrowser ? "none" : "grid";
4086
+ if (ttsVoiceCustomWrap) ttsVoiceCustomWrap.style.display = isBrowser ? "none" : "grid";
4087
+ if (elevenPanel) elevenPanel.style.display = isElevenLabs ? "grid" : "none";
4088
+ const modelOptions = meta.presets?.[provider]?.models || [];
4089
+ const modelSelect = fieldEl(winId, "tts-model");
4090
+ if (modelSelect) {
4091
+ const currentModel = modelSelect.value;
4092
+ modelSelect.innerHTML = renderOptionTags(modelOptions, currentModel, "(no models)");
4093
+ }
4094
+ const vrmSource = fieldEl(winId, "vrm-source")?.value || "desktop";
4095
+ const vrmaSource = fieldEl(winId, "vrma-source")?.value || "desktop";
4096
+ const live2dSource = fieldEl(winId, "live2d-source")?.value || "desktop";
4097
+ const vrmSelectWrap = fieldEl(winId, "vrm-select-wrap");
4098
+ const vrmCustomWrap = fieldEl(winId, "vrm-custom-wrap");
4099
+ const vrmaSelectWrap = fieldEl(winId, "vrma-select-wrap");
4100
+ const vrmaCustomWrap = fieldEl(winId, "vrma-custom-wrap");
4101
+ const live2dSelectWrap = fieldEl(winId, "live2d-select-wrap");
4102
+ const live2dCustomDirWrap = fieldEl(winId, "live2d-custom-dir-wrap");
4103
+ const live2dCustomNameWrap = fieldEl(winId, "live2d-custom-name-wrap");
4104
+ if (vrmSelectWrap) vrmSelectWrap.style.display = vrmSource === "custom" ? "none" : "grid";
4105
+ if (vrmCustomWrap) vrmCustomWrap.style.display = vrmSource === "custom" ? "grid" : "none";
4106
+ if (vrmaSelectWrap) vrmaSelectWrap.style.display = vrmaSource === "custom" ? "none" : "grid";
4107
+ if (vrmaCustomWrap) vrmaCustomWrap.style.display = vrmaSource === "custom" ? "grid" : "none";
4108
+ if (live2dSelectWrap) live2dSelectWrap.style.display = live2dSource === "custom" ? "none" : "grid";
4109
+ if (live2dCustomDirWrap) live2dCustomDirWrap.style.display = live2dSource === "custom" ? "grid" : "none";
4110
+ if (live2dCustomNameWrap) live2dCustomNameWrap.style.display = live2dSource === "custom" ? "grid" : "none";
4111
+ const vrmSelect = fieldEl(winId, "vrm-select");
4112
+ if (vrmSelect && vrmSource !== "custom") {
4113
+ const models = filteredAvatarModels(meta.avatar?.vrm?.models, vrmSource);
4114
+ vrmSelect.innerHTML = renderOptionTags(models, vrmSelect.value, "(no VRM models)");
4115
+ }
4116
+ const vrmaSelect = fieldEl(winId, "vrma-select");
4117
+ if (vrmaSelect && vrmaSource !== "custom") {
4118
+ const models = filteredAvatarModels(meta.avatar?.vrm?.animations, vrmaSource);
4119
+ vrmaSelect.innerHTML = renderOptionTags(models, vrmaSelect.value, "(procedural fallback)");
4120
+ }
4121
+ const live2dSelect = fieldEl(winId, "live2d-select");
4122
+ if (live2dSelect && live2dSource !== "custom") {
4123
+ const models = filteredAvatarModels(meta.avatar?.live2d?.models, live2dSource);
4124
+ live2dSelect.innerHTML = renderOptionTags(models, live2dSelect.value, "(no Live2D models)");
4125
+ }
4126
+ }
4127
+ async function hydrateVuiSettingsSection(winId) {
4128
+ const mount = document.getElementById(fieldId(winId, "vui-root"));
4129
+ if (!mount) return;
4130
+ mount.innerHTML = `<div class="uid-vui-note">Loading VUI settings...</div>`;
4131
+ try {
4132
+ const [settingsRes, metaRes] = await Promise.all([
4133
+ apiJson("/api/vui/settings"),
4134
+ apiJson("/api/vui/settings/meta")
4135
+ ]);
4136
+ mount.innerHTML = renderVuiSettingsMarkup(winId, settingsRes.vui, metaRes);
4137
+ const providerSelect = fieldEl(winId, "tts-provider");
4138
+ const modelSelect = fieldEl(winId, "tts-model");
4139
+ const languageSelect = fieldEl(winId, "tts-language-code");
4140
+ const vrmSourceSelect = fieldEl(winId, "vrm-source");
4141
+ const live2dSourceSelect = fieldEl(winId, "live2d-source");
4142
+ const statusEl = fieldEl(winId, "vui-status");
4143
+ let saveTimer = 0;
4144
+ let saveSerial = 0;
4145
+ const selectedVoiceValue = () => {
4146
+ const voiceSelect = fieldEl(winId, "tts-voice-select");
4147
+ const voiceCustom = fieldEl(winId, "tts-voice-custom");
4148
+ return (voiceCustom?.value || "").trim() || (voiceSelect?.value || "");
4149
+ };
4150
+ const setStatus = (text, state2 = "idle") => {
4151
+ if (!statusEl) return;
4152
+ statusEl.textContent = text;
4153
+ statusEl.dataset.state = state2;
4154
+ };
4155
+ const setElevenLabsApiKeyStatus = (stored) => {
4156
+ const input = fieldEl(winId, "tts-eleven-api-key");
4157
+ const status = fieldEl(winId, "tts-eleven-api-key-status");
4158
+ if (input) {
4159
+ input.value = "";
4160
+ input.placeholder = stored ? "Stored locally on this device. Enter a new key to replace it." : "Set your own key to use private/shared voices in your account.";
4161
+ }
4162
+ if (status) {
4163
+ status.textContent = stored ? "A personal ElevenLabs API key is stored locally on this device." : "Optional. Use this when a private or shared voice is not available from the shared MARIA server key.";
4164
+ }
4165
+ };
4166
+ const sync = async (selectedVoice) => {
4167
+ syncVuiSettingsVisibility(winId, metaRes);
4168
+ await loadVoiceOptionsForSettings(winId, metaRes, selectedVoice || settingsRes.vui?.tts?.voice || "");
4169
+ };
4170
+ const saveNow = async () => {
4171
+ const serial = ++saveSerial;
4172
+ setStatus("Saving\u2026", "saving");
4173
+ const voiceSelect = fieldEl(winId, "tts-voice-select");
4174
+ const voiceCustom = fieldEl(winId, "tts-voice-custom");
4175
+ const personalElevenLabsApiKeyRaw = (fieldEl(winId, "tts-eleven-api-key")?.value || "").trim();
4176
+ const customVoice = (voiceCustom?.value || "").trim();
4177
+ const selectedVoice = customVoice || (voiceSelect?.value || "");
4178
+ const providerValue = providerSelect?.value || "browser";
4179
+ const personalElevenLabsApiKey = providerValue === "elevenlabs" ? personalElevenLabsApiKeyRaw : "";
4180
+ const payload = {
4181
+ vui: {
4182
+ language: fieldEl(winId, "vui-language")?.value || "ja-JP",
4183
+ tts: {
4184
+ enabled: Boolean(fieldEl(winId, "tts-enabled")?.checked),
4185
+ provider: providerValue,
4186
+ voice: selectedVoice,
4187
+ model: modelSelect?.value || "",
4188
+ languageCode: languageSelect?.value || "",
4189
+ instructions: fieldEl(winId, "tts-instructions")?.value || "",
4190
+ outputFormat: fieldEl(winId, "tts-eleven-output")?.value || "",
4191
+ ...personalElevenLabsApiKey ? { personalElevenLabsApiKey } : {},
4192
+ elevenLabsSettings: {
4193
+ stability: toFiniteNumber(fieldEl(winId, "tts-eleven-stability")?.value || "", 0.5),
4194
+ similarityBoost: toFiniteNumber(fieldEl(winId, "tts-eleven-similarity")?.value || "", 0.75),
4195
+ style: toFiniteNumber(fieldEl(winId, "tts-eleven-style")?.value || "", 0),
4196
+ speed: toFiniteNumber(fieldEl(winId, "tts-eleven-speed")?.value || "", 1),
4197
+ useSpeakerBoost: Boolean(fieldEl(winId, "tts-eleven-boost")?.checked)
4198
+ }
4199
+ },
4200
+ avatar: {
4201
+ defaultMode: fieldEl(winId, "avatar-default-mode")?.value || "orb",
4202
+ vrm: {
4203
+ source: vrmSourceSelect?.value || "desktop",
4204
+ selectedId: fieldEl(winId, "vrm-select")?.value || "",
4205
+ customPath: fieldEl(winId, "vrm-custom-path")?.value || "",
4206
+ scale: toFiniteNumber(fieldEl(winId, "vrm-scale")?.value || "", 1.5),
4207
+ animationSource: fieldEl(winId, "vrma-source")?.value || "desktop",
4208
+ selectedAnimationId: fieldEl(winId, "vrma-select")?.value || "",
4209
+ customAnimationPath: fieldEl(winId, "vrma-custom-path")?.value || ""
4210
+ },
4211
+ live2d: {
4212
+ source: live2dSourceSelect?.value || "desktop",
4213
+ selectedId: fieldEl(winId, "live2d-select")?.value || "",
4214
+ customModelDir: fieldEl(winId, "live2d-custom-dir")?.value || "",
4215
+ customModelName: fieldEl(winId, "live2d-custom-name")?.value || "",
4216
+ scale: toFiniteNumber(fieldEl(winId, "live2d-scale")?.value || "", 2.1),
4217
+ offsetX: toFiniteNumber(fieldEl(winId, "live2d-offset-x")?.value || "", 0),
4218
+ offsetY: toFiniteNumber(fieldEl(winId, "live2d-offset-y")?.value || "", 1.1)
4219
+ }
4220
+ }
4221
+ }
4222
+ };
4223
+ try {
4224
+ await apiJson("/api/vui/settings", {
4225
+ method: "POST",
4226
+ headers: { "content-type": "application/json" },
4227
+ body: JSON.stringify(payload)
4228
+ });
4229
+ if (personalElevenLabsApiKey) {
4230
+ setElevenLabsApiKeyStatus(true);
4231
+ await loadVoiceOptionsForSettings(winId, metaRes, selectedVoice);
4232
+ }
4233
+ notifyVuiSettingsChanged();
4234
+ if (serial === saveSerial) setStatus("Saved.", "saved");
4235
+ } catch (error) {
4236
+ if (serial === saveSerial) {
4237
+ setStatus(`Save failed: ${error instanceof Error ? error.message : String(error)}`, "error");
4238
+ }
4239
+ }
4240
+ };
4241
+ const queueSave = (delayMs = 180) => {
4242
+ if (saveTimer) window.clearTimeout(saveTimer);
4243
+ saveTimer = window.setTimeout(() => {
4244
+ void saveNow();
4245
+ }, delayMs);
4246
+ };
4247
+ providerSelect?.addEventListener("change", () => {
4248
+ const selectedVoice = selectedVoiceValue();
4249
+ void sync(selectedVoice).then(() => queueSave(0));
4250
+ });
4251
+ modelSelect?.addEventListener("change", () => {
4252
+ const selectedVoice = selectedVoiceValue();
4253
+ void loadVoiceOptionsForSettings(winId, metaRes, selectedVoice).then(() => queueSave(0));
4254
+ });
4255
+ languageSelect?.addEventListener("change", () => {
4256
+ const selectedVoice = selectedVoiceValue();
4257
+ void loadVoiceOptionsForSettings(winId, metaRes, selectedVoice).then(() => queueSave(0));
4258
+ });
4259
+ fieldEl(winId, "tts-voice-select")?.addEventListener("change", () => {
4260
+ const custom = fieldEl(winId, "tts-voice-custom");
4261
+ if (custom) custom.value = "";
4262
+ queueSave(0);
4263
+ });
4264
+ vrmSourceSelect?.addEventListener("change", () => {
4265
+ syncVuiSettingsVisibility(winId, metaRes);
4266
+ queueSave(0);
4267
+ });
4268
+ fieldEl(winId, "vrma-source")?.addEventListener("change", () => {
4269
+ syncVuiSettingsVisibility(winId, metaRes);
4270
+ queueSave(0);
4271
+ });
4272
+ live2dSourceSelect?.addEventListener("change", () => {
4273
+ syncVuiSettingsVisibility(winId, metaRes);
4274
+ queueSave(0);
4275
+ });
4276
+ [
4277
+ "vui-language",
4278
+ "avatar-default-mode",
4279
+ "tts-enabled",
4280
+ "tts-eleven-output",
4281
+ "tts-eleven-boost",
4282
+ "vrm-select",
4283
+ "vrma-select",
4284
+ "live2d-select"
4285
+ ].forEach((key) => fieldEl(winId, key)?.addEventListener("change", () => queueSave(0)));
4286
+ [
4287
+ "tts-voice-custom",
4288
+ "tts-instructions",
4289
+ "tts-eleven-api-key",
4290
+ "tts-eleven-stability",
4291
+ "tts-eleven-similarity",
4292
+ "tts-eleven-style",
4293
+ "tts-eleven-speed",
4294
+ "vrm-custom-path",
4295
+ "vrma-custom-path",
4296
+ "vrm-scale",
4297
+ "live2d-custom-dir",
4298
+ "live2d-custom-name",
4299
+ "live2d-scale",
4300
+ "live2d-offset-x",
4301
+ "live2d-offset-y"
4302
+ ].forEach((key) => fieldEl(winId, key)?.addEventListener("input", () => queueSave(240)));
4303
+ fieldEl(winId, "vrm-upload")?.addEventListener("click", () => {
4304
+ void (async () => {
4305
+ const files = await pickLocalFiles({ accept: ".vrm", multiple: true });
4306
+ if (!files.length) return;
4307
+ setStatus("Uploading VRM files\u2026", "saving");
4308
+ const uploaded = [];
4309
+ for (const file of files) {
4310
+ const relPath = await uploadFileToDesktopAtWithPath("vrm", file);
4311
+ if (relPath) uploaded.push(relPath);
4312
+ }
4313
+ if (!uploaded.length) {
4314
+ setStatus("VRM upload failed.", "error");
4315
+ return;
4316
+ }
4317
+ await hydrateVuiSettingsSection(winId);
4318
+ })();
4319
+ });
4320
+ fieldEl(winId, "live2d-upload")?.addEventListener("click", () => {
4321
+ void (async () => {
4322
+ const files = await pickLocalFiles({ directory: true, multiple: true });
4323
+ if (!files.length) return;
4324
+ const hasRelativePath = files.some((file) => Boolean(file.webkitRelativePath && file.webkitRelativePath.includes("/")));
4325
+ if (!hasRelativePath) {
4326
+ setStatus("Folder upload is not supported in this browser.", "error");
4327
+ return;
4328
+ }
4329
+ setStatus("Uploading Live2D folder\u2026", "saving");
4330
+ const uploaded = await uploadFileListToDesktopAt("live2d", files, { preserveRelativePaths: true });
4331
+ if (!uploaded.length) {
4332
+ setStatus("Live2D folder upload failed.", "error");
4333
+ return;
4334
+ }
4335
+ await hydrateVuiSettingsSection(winId);
4336
+ })();
4337
+ });
4338
+ fieldEl(winId, "vrma-upload")?.addEventListener("click", () => {
4339
+ void (async () => {
4340
+ const files = await pickLocalFiles({ accept: ".vrma", multiple: true });
4341
+ if (!files.length) return;
4342
+ setStatus("Uploading VRMA files\u2026", "saving");
4343
+ const uploaded = [];
4344
+ for (const file of files) {
4345
+ const relPath = await uploadFileToDesktopAtWithPath("vrma", file);
4346
+ if (relPath) uploaded.push(relPath);
4347
+ }
4348
+ if (!uploaded.length) {
4349
+ setStatus("VRMA upload failed.", "error");
4350
+ return;
4351
+ }
4352
+ await hydrateVuiSettingsSection(winId);
4353
+ })();
4354
+ });
4355
+ await sync();
4356
+ } catch (error) {
4357
+ mount.innerHTML = `<div class="uid-vui-note" style="color:var(--danger);">Failed to load VUI settings: ${esc(error instanceof Error ? error.message : String(error))}</div>`;
4358
+ }
4359
+ }
3819
4360
  function updateAllWindowIcons() {
3820
4361
  const useLucide = state.iconStyle === "modern" || state.iconStyle === "minimal";
3821
4362
  for (const win of state.windows) {
@@ -3896,18 +4437,28 @@ function updateAllWindowIcons() {
3896
4437
  function openUiDesign() {
3897
4438
  const useLucide = state.iconStyle === "modern" || state.iconStyle === "minimal";
3898
4439
  const winIcon = useLucide ? lucideIcon(LUCIDE_SYSTEM_ICONS["ui-design"] || "paintbrush", "di-lucide") : "\u{1F3A8}";
4440
+ const viewportWidth = Math.max(640, window.innerWidth || 1280);
4441
+ const viewportHeight = Math.max(520, window.innerHeight || 900);
4442
+ const width = Math.min(720, Math.max(520, viewportWidth - 96));
4443
+ const height = Math.min(760, Math.max(520, viewportHeight - 140));
3899
4444
  const win = createWindow({
3900
4445
  appId: "__ui-design",
3901
- title: "UI Design",
4446
+ title: "Settings",
3902
4447
  icon: winIcon,
3903
4448
  content: "app",
3904
- width: 520,
3905
- height: 520
4449
+ width,
4450
+ height
3906
4451
  });
4452
+ win.x = Math.max(16, Math.min(win.x, viewportWidth - width - 16));
4453
+ win.y = Math.max(16, Math.min(win.y, viewportHeight - height - 64));
4454
+ const el = getWinEl(win.id);
4455
+ if (el) applyWindowStyles(el, win);
3907
4456
  renderUiDesignContent(win.id);
3908
4457
  }
3909
4458
  function renderUiDesignContent(winId) {
3910
4459
  const body = getWinBody(winId);
4460
+ body.style.overflowY = "auto";
4461
+ body.style.paddingRight = "4px";
3911
4462
  const curStyle = state.iconStyle;
3912
4463
  const curLayout = state.iconLayout;
3913
4464
  let html = '<div class="uid-container">';
@@ -3938,6 +4489,10 @@ function renderUiDesignContent(winId) {
3938
4489
  html += `<div class="uid-preview-wrap" id="uidPreview_${winId}">`;
3939
4490
  html += buildPreview(curStyle, curLayout);
3940
4491
  html += "</div></div>";
4492
+ html += '<div class="uid-section">';
4493
+ html += '<div class="uid-section-title">VUI</div>';
4494
+ html += `<div id="${fieldId(winId, "vui-root")}"><div style="font-size:12px; color:#667085;">Loading VUI settings...</div></div>`;
4495
+ html += "</div>";
3941
4496
  html += "</div>";
3942
4497
  body.innerHTML = html;
3943
4498
  body.querySelectorAll(".uid-option").forEach((btn) => {
@@ -3960,6 +4515,7 @@ function renderUiDesignContent(winId) {
3960
4515
  renderUiDesignContent(winId);
3961
4516
  });
3962
4517
  });
4518
+ void hydrateVuiSettingsSection(winId);
3963
4519
  }
3964
4520
  var _renderDesktopIcons, _renderStartMenu, _renderTaskbarPins, _renderTaskbarWindows, _refreshOpenFolderWindows, _renderDesktopFiles, _refreshAllFileManagerWindows, ICON_STYLES, ICON_LAYOUTS, LOGO_ASSETS;
3965
4521
  var init_ui_design = __esm({
@@ -3967,6 +4523,7 @@ var init_ui_design = __esm({
3967
4523
  init_helpers();
3968
4524
  init_state();
3969
4525
  init_window_manager();
4526
+ init_desktop_upload();
3970
4527
  _renderDesktopIcons = null;
3971
4528
  _renderStartMenu = null;
3972
4529
  _renderTaskbarPins = null;
@@ -17556,7 +18113,7 @@ function openCategoryFolder(folderId) {
17556
18113
  } else {
17557
18114
  settingsIcon = "\u{1F3A8}";
17558
18115
  }
17559
- html += `<div class="folder-app-item" data-app-id="__ui-design" title="Icon style &amp; layout settings">`;
18116
+ html += `<div class="folder-app-item" data-app-id="__ui-design" title="Desktop, VUI, and appearance settings">`;
17560
18117
  html += `<div class="folder-app-icon">${settingsIcon}</div>`;
17561
18118
  html += `<div class="folder-app-label">Settings</div>`;
17562
18119
  html += `</div>`;
@@ -17878,7 +18435,7 @@ function refreshOpenFolderWindows() {
17878
18435
  } else {
17879
18436
  settingsIcon = "\u{1F3A8}";
17880
18437
  }
17881
- html += `<div class="folder-app-item" data-app-id="__ui-design" title="Icon style &amp; layout settings">`;
18438
+ html += `<div class="folder-app-item" data-app-id="__ui-design" title="Desktop, VUI, and appearance settings">`;
17882
18439
  html += `<div class="folder-app-icon">${settingsIcon}</div>`;
17883
18440
  html += `<div class="folder-app-label">Settings</div>`;
17884
18441
  html += `</div>`;
@@ -18439,7 +18996,7 @@ function injectTaskbarDeps(deps) {
18439
18996
  function getInternalApps() {
18440
18997
  return [
18441
18998
  { id: "__file_manager", label: "Files", description: "File Manager", lucideIcon: LUCIDE_SYSTEM_ICONS["file-manager"] || "folder-open", emoji: "\u{1F4C2}", open: () => _openFileManager?.() },
18442
- { id: "__ui-design", label: "Settings", description: "Icon style & layout", lucideIcon: LUCIDE_SYSTEM_ICONS["ui-design"] || "paintbrush", emoji: "\u{1F3A8}", open: () => _openUiDesign?.() },
18999
+ { id: "__ui-design", label: "Settings", description: "Desktop, VUI, and appearance settings", lucideIcon: LUCIDE_SYSTEM_ICONS["ui-design"] || "paintbrush", emoji: "\u{1F3A8}", open: () => _openUiDesign?.() },
18443
19000
  { id: "__task_manager", label: "Concurrency", description: "View running jobs", lucideIcon: "activity", emoji: "\u{1F4CA}", open: () => _openTaskManager3?.() }
18444
19001
  ];
18445
19002
  }
@@ -19292,6 +19849,45 @@ function attachChatPanelListeners() {
19292
19849
  renderMessages();
19293
19850
  }
19294
19851
 
19852
+ // services/desktop/client/modules/vui-panel.ts
19853
+ var _panelOpen2 = false;
19854
+ var _iframeLoaded = false;
19855
+ function toggleVuiPanel() {
19856
+ if (_panelOpen2) closeVuiPanel();
19857
+ else openVuiPanel();
19858
+ }
19859
+ function openVuiPanel() {
19860
+ _panelOpen2 = true;
19861
+ const panel = document.getElementById("vuiPanel");
19862
+ if (panel) panel.classList.add("open");
19863
+ document.getElementById("desktop")?.classList.add("vui-panel-open");
19864
+ if (!_iframeLoaded) {
19865
+ const frame = document.getElementById("vuiFrame");
19866
+ if (frame) {
19867
+ frame.src = "/vui";
19868
+ _iframeLoaded = true;
19869
+ }
19870
+ }
19871
+ document.getElementById("vuiToggleBtn")?.classList.add("active");
19872
+ }
19873
+ function closeVuiPanel() {
19874
+ _panelOpen2 = false;
19875
+ const panel = document.getElementById("vuiPanel");
19876
+ if (panel) panel.classList.remove("open");
19877
+ document.getElementById("desktop")?.classList.remove("vui-panel-open");
19878
+ document.getElementById("vuiToggleBtn")?.classList.remove("active");
19879
+ }
19880
+ function attachVuiPanelListeners() {
19881
+ const closeBtn = document.getElementById("vuiPanelClose");
19882
+ if (closeBtn) closeBtn.addEventListener("click", closeVuiPanel);
19883
+ document.addEventListener("keydown", (e) => {
19884
+ if (e.altKey && e.code === "KeyV") {
19885
+ e.preventDefault();
19886
+ toggleVuiPanel();
19887
+ }
19888
+ });
19889
+ }
19890
+
19295
19891
  // services/desktop/client/desktop-client.ts
19296
19892
  init_dialogs();
19297
19893
  setUpdateRunBtnState(updateRunBtnState);
@@ -19469,6 +20065,10 @@ function attachGlobalListeners() {
19469
20065
  if (chatToggleBtn) {
19470
20066
  chatToggleBtn.addEventListener("click", () => toggleChatPanel());
19471
20067
  }
20068
+ const vuiToggleBtn = document.getElementById("vuiToggleBtn");
20069
+ if (vuiToggleBtn) {
20070
+ vuiToggleBtn.addEventListener("click", () => toggleVuiPanel());
20071
+ }
19472
20072
  document.addEventListener("click", (e) => {
19473
20073
  const target = e.target;
19474
20074
  if (state.startMenuOpen) {
@@ -19732,6 +20332,7 @@ async function boot() {
19732
20332
  attachDesktopDragDrop();
19733
20333
  attachDesktopIconDrag();
19734
20334
  attachChatPanelListeners();
20335
+ attachVuiPanelListeners();
19735
20336
  void requestNotificationPermission();
19736
20337
  }
19737
20338
  void boot();