@pheem49/mint 1.5.5 → 1.6.1
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.
- package/.codex +0 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/release.yml +79 -0
- package/Cargo.lock +5792 -0
- package/Cargo.toml +32 -0
- package/README.md +387 -353
- package/assets/icon.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +23 -0
- package/crates/mint-cli/src/agent.rs +851 -0
- package/crates/mint-cli/src/gmail.rs +216 -0
- package/crates/mint-cli/src/image.rs +142 -0
- package/crates/mint-cli/src/main.rs +2837 -0
- package/crates/mint-cli/src/mcp.rs +63 -0
- package/crates/mint-cli/src/onboard.rs +1149 -0
- package/crates/mint-cli/src/setup.rs +390 -0
- package/crates/mint-cli/src/skills.rs +8 -0
- package/crates/mint-cli/src/updater.rs +279 -0
- package/crates/mint-core/Cargo.toml +22 -0
- package/crates/mint-core/src/agent_loop.rs +94 -0
- package/crates/mint-core/src/api_server.rs +991 -0
- package/crates/mint-core/src/channels.rs +248 -0
- package/crates/mint-core/src/chat.rs +895 -0
- package/crates/mint-core/src/code_tools.rs +729 -0
- package/crates/mint-core/src/config.rs +368 -0
- package/crates/mint-core/src/files.rs +159 -0
- package/crates/mint-core/src/knowledge.rs +541 -0
- package/crates/mint-core/src/lib.rs +84 -0
- package/crates/mint-core/src/mcp.rs +273 -0
- package/crates/mint-core/src/memory.rs +673 -0
- package/crates/mint-core/src/orchestration.rs +2157 -0
- package/crates/mint-core/src/pictures.rs +314 -0
- package/crates/mint-core/src/plugins.rs +727 -0
- package/crates/mint-core/src/safety.rs +416 -0
- package/crates/mint-core/src/semantic.rs +254 -0
- package/crates/mint-core/src/shell.rs +317 -0
- package/crates/mint-core/src/skills.rs +71 -0
- package/crates/mint-core/src/symbols.rs +157 -0
- package/crates/mint-core/src/tasks.rs +308 -0
- package/crates/mint-core/src/tts.rs +92 -0
- package/crates/mint-core/src/weather.rs +93 -0
- package/crates/mint-core/src/web_search.rs +200 -0
- package/crates/mint-core/src/workflows.rs +81 -0
- package/crates/mint-core/tests/mcp_stdio.rs +45 -0
- package/crates/mint-core/tests/memory_persistence.rs +172 -0
- package/crates/mint-core/tests/pictures_storage.rs +14 -0
- package/crates/mint-core/tests/task_lifecycle.rs +87 -0
- package/package.json +35 -99
- package/src/bin/index.js +16 -0
- package/src/renderer/index-web.html +17 -0
- package/src/renderer/index.html +17 -0
- package/src/renderer/public/Live2DCubismCore.js +9 -0
- package/src/renderer/public/assets/icon.png +0 -0
- package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
- package/src/renderer/src/App.tsx +33 -0
- package/src/renderer/src/calculator.ts +47 -0
- package/src/renderer/src/components/ChatPanel.tsx +1598 -0
- package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
- package/src/renderer/src/components/Live2DStage.tsx +374 -0
- package/src/renderer/src/components/MintDashboard.tsx +950 -0
- package/src/renderer/src/components/ModelPanel.tsx +154 -0
- package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
- package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
- package/src/renderer/src/components/ScreenPicker.tsx +579 -0
- package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
- package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
- package/src/renderer/src/components/WidgetWindow.tsx +36 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
- package/src/{UI → renderer/src/css}/settings.css +69 -16
- package/src/renderer/src/css/spotlight.css +113 -0
- package/src/renderer/src/css/styles.css +3722 -0
- package/src/renderer/src/css/widget.css +185 -0
- package/src/renderer/src/env.d.ts +116 -0
- package/src/renderer/src/index.css +379 -0
- package/src/renderer/src/main.tsx +13 -0
- package/src/renderer/src/tauri.ts +996 -0
- package/src/renderer/src-web/App.tsx +25 -0
- package/src/renderer/src-web/calculator.ts +47 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
- package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
- package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
- package/src/renderer/src-web/css/settings.css +1100 -0
- package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
- package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
- package/src/{UI → renderer/src-web/css}/widget.css +2 -2
- package/src/renderer/src-web/env.d.ts +107 -0
- package/src/renderer/src-web/index.css +379 -0
- package/src/renderer/src-web/main.tsx +13 -0
- package/src/renderer/src-web/tauri.ts +983 -0
- package/tsconfig.json +30 -0
- package/vite.config.ts +33 -0
- package/vite.config.web.ts +51 -0
- package/GUIDE_TH.md +0 -125
- package/assets/Agent_Mint.png +0 -0
- package/assets/CLI_Screen.png +0 -0
- package/assets/Settings.png +0 -0
- package/benchmark_ai.js +0 -71
- package/install.ps1 +0 -64
- package/install.sh +0 -54
- package/main.js +0 -139
- package/mint-cli-logic.js +0 -3
- package/mint-cli.js +0 -410
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
- package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
- package/preload-picker.js +0 -11
- package/preload-settings.js +0 -11
- package/preload.js +0 -41
- package/scripts/install_linux_desktop_entry.js +0 -48
- package/src/AI_Brain/Gemini_API.js +0 -813
- package/src/AI_Brain/agent_orchestrator.js +0 -73
- package/src/AI_Brain/autonomous_brain.js +0 -179
- package/src/AI_Brain/behavior_memory.js +0 -135
- package/src/AI_Brain/headless_agent.js +0 -143
- package/src/AI_Brain/knowledge_base.js +0 -349
- package/src/AI_Brain/memory_store.js +0 -662
- package/src/AI_Brain/proactive_engine.js +0 -172
- package/src/AI_Brain/provider_adapter.js +0 -365
- package/src/Automation_Layer/browser_automation.js +0 -149
- package/src/Automation_Layer/file_operations.js +0 -286
- package/src/Automation_Layer/open_app.js +0 -85
- package/src/Automation_Layer/open_website.js +0 -38
- package/src/CLI/approval_handler.js +0 -47
- package/src/CLI/chat_router.js +0 -247
- package/src/CLI/chat_ui.js +0 -1159
- package/src/CLI/cli_colors.js +0 -115
- package/src/CLI/cli_formatters.js +0 -94
- package/src/CLI/code_agent.js +0 -1667
- package/src/CLI/code_session_memory.js +0 -62
- package/src/CLI/gmail_auth.js +0 -210
- package/src/CLI/image_input.js +0 -90
- package/src/CLI/intent_detectors.js +0 -181
- package/src/CLI/interactive_chat.js +0 -658
- package/src/CLI/list_features.js +0 -64
- package/src/CLI/onboarding.js +0 -416
- package/src/CLI/repo_summarizer.js +0 -282
- package/src/CLI/semantic_code_search.js +0 -312
- package/src/CLI/skill_manager.js +0 -41
- package/src/CLI/slash_command_handler.js +0 -418
- package/src/CLI/symbol_indexer.js +0 -231
- package/src/CLI/updater.js +0 -230
- package/src/CLI/workspace_manager.js +0 -90
- package/src/Channels/brave_search_bridge.js +0 -35
- package/src/Channels/discord_bridge.js +0 -66
- package/src/Channels/google_search_bridge.js +0 -38
- package/src/Channels/line_bridge.js +0 -60
- package/src/Channels/slack_bridge.js +0 -48
- package/src/Channels/telegram_bridge.js +0 -41
- package/src/Channels/whatsapp_bridge.js +0 -57
- package/src/Command_Parser/parser.js +0 -45
- package/src/Plugins/dev_tools.js +0 -41
- package/src/Plugins/discord.js +0 -20
- package/src/Plugins/docker.js +0 -47
- package/src/Plugins/gmail.js +0 -251
- package/src/Plugins/google_calendar.js +0 -252
- package/src/Plugins/mcp_manager.js +0 -95
- package/src/Plugins/notion.js +0 -256
- package/src/Plugins/obsidian.js +0 -54
- package/src/Plugins/plugin_manager.js +0 -81
- package/src/Plugins/spotify.js +0 -173
- package/src/Plugins/system_metrics.js +0 -31
- package/src/Plugins/system_monitor.js +0 -72
- package/src/System/action_executor.js +0 -178
- package/src/System/bridge_manager.js +0 -76
- package/src/System/chat_history_manager.js +0 -83
- package/src/System/config_manager.js +0 -194
- package/src/System/custom_workflows.js +0 -163
- package/src/System/daemon_manager.js +0 -67
- package/src/System/google_tts_urls.js +0 -51
- package/src/System/granular_automation.js +0 -157
- package/src/System/ipc_handlers.js +0 -332
- package/src/System/notifications.js +0 -23
- package/src/System/optional_require.js +0 -23
- package/src/System/picture_store.js +0 -109
- package/src/System/proactive_loop.js +0 -153
- package/src/System/safety_manager.js +0 -273
- package/src/System/sandbox_runner.js +0 -182
- package/src/System/screen_capture.js +0 -175
- package/src/System/smart_context.js +0 -227
- package/src/System/system_automation.js +0 -162
- package/src/System/system_events.js +0 -79
- package/src/System/system_info.js +0 -125
- package/src/System/task_manager.js +0 -222
- package/src/System/tool_registry.js +0 -293
- package/src/System/window_manager.js +0 -220
- package/src/UI/floating.css +0 -80
- package/src/UI/floating.html +0 -17
- package/src/UI/floating.js +0 -67
- package/src/UI/live2d_manager.js +0 -600
- package/src/UI/preload-floating.js +0 -7
- package/src/UI/preload-spotlight.js +0 -11
- package/src/UI/preload-widget.js +0 -5
- package/src/UI/proactive-glow.html +0 -42
- package/src/UI/renderer.js +0 -2127
- package/src/UI/screenPicker.html +0 -214
- package/src/UI/screenPicker.js +0 -262
- package/src/UI/settings.html +0 -577
- package/src/UI/settings.js +0 -770
- package/src/UI/spotlight.html +0 -23
- package/src/UI/spotlight.js +0 -185
- package/src/UI/widget.html +0 -29
- package/src/UI/widget.js +0 -10
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
package/src/UI/renderer.js
DELETED
|
@@ -1,2127 +0,0 @@
|
|
|
1
|
-
const chatContainer = document.getElementById('chat-container');
|
|
2
|
-
const chatForm = document.getElementById('chat-form');
|
|
3
|
-
const chatInput = document.getElementById('chat-input');
|
|
4
|
-
const closeBtn = document.getElementById('close-btn');
|
|
5
|
-
const maximizeBtn = document.getElementById('maximize-btn');
|
|
6
|
-
const minimizeBtn = document.getElementById('minimize-btn');
|
|
7
|
-
const clearBtn = document.getElementById('clear-btn');
|
|
8
|
-
const settingsBtn = document.getElementById('settings-btn');
|
|
9
|
-
const sidebarNewChatBtn = document.getElementById('sidebar-new-chat');
|
|
10
|
-
const sidebarSettingsBtn = document.getElementById('sidebar-settings');
|
|
11
|
-
const sidebarToggleBtn = document.getElementById('sidebar-toggle');
|
|
12
|
-
const appBody = document.querySelector('.app-body');
|
|
13
|
-
const sidebarChatBtn = document.getElementById('sidebar-chat-btn');
|
|
14
|
-
const sidebarPicturesBtn = document.getElementById('sidebar-pictures-btn');
|
|
15
|
-
const picturesLibrary = document.getElementById('pictures-library');
|
|
16
|
-
const picturesGrid = document.getElementById('pictures-grid');
|
|
17
|
-
const picturesEmpty = document.getElementById('pictures-empty');
|
|
18
|
-
const picturesCloseBtn = document.getElementById('pictures-close-btn');
|
|
19
|
-
const micBtn = document.getElementById('mic-btn');
|
|
20
|
-
const visionBtn = document.getElementById('vision-btn');
|
|
21
|
-
const chatProviderSelect = document.getElementById('chat-provider-select');
|
|
22
|
-
const imagePreviewContainer = document.getElementById('image-preview-container');
|
|
23
|
-
const imagePreview = document.getElementById('image-preview');
|
|
24
|
-
const removeImageBtn = document.getElementById('remove-image-btn');
|
|
25
|
-
const agentModeToggle = document.getElementById('agent-mode-toggle');
|
|
26
|
-
const modelMount = document.getElementById('model-mount');
|
|
27
|
-
const modelShell = document.getElementById('model-shell');
|
|
28
|
-
const modelStatus = document.getElementById('model-status');
|
|
29
|
-
const mintStatus = document.getElementById('mint-status');
|
|
30
|
-
const mintStatusLabel = document.getElementById('mint-status-label');
|
|
31
|
-
const modelActivityBadge = document.getElementById('model-activity-badge');
|
|
32
|
-
const startupLoading = document.getElementById('startup-loading');
|
|
33
|
-
const appContainer = document.querySelector('.app-container');
|
|
34
|
-
|
|
35
|
-
if (startupLoading) {
|
|
36
|
-
startupLoading.style.background = 'var(--bg-gradient)';
|
|
37
|
-
startupLoading.style.color = 'var(--text-muted)';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Proactive Assistant elements
|
|
41
|
-
const proactiveBar = document.getElementById('proactive-bar');
|
|
42
|
-
const proactiveMessage = document.getElementById('proactive-message');
|
|
43
|
-
const proactiveChips = document.getElementById('proactive-chips');
|
|
44
|
-
const proactiveDismissBtn = document.getElementById('proactive-dismiss-btn');
|
|
45
|
-
|
|
46
|
-
let currentBase64Image = null;
|
|
47
|
-
let enableVoiceReply = true;
|
|
48
|
-
let ttsProvider = 'google';
|
|
49
|
-
let ttsVolume = 1.0;
|
|
50
|
-
let ttsSpeed = 1.0;
|
|
51
|
-
let ttsPitch = 1.0;
|
|
52
|
-
let lastConversationLanguage = 'auto';
|
|
53
|
-
let mintActivityResetTimer = null;
|
|
54
|
-
let currentSettings = {};
|
|
55
|
-
|
|
56
|
-
const PROVIDER_PICKER_OPTIONS = [
|
|
57
|
-
['gemini', 'Gemini'],
|
|
58
|
-
['anthropic', 'Claude'],
|
|
59
|
-
['openai', 'OpenAI'],
|
|
60
|
-
['ollama', 'Ollama'],
|
|
61
|
-
['huggingface', 'Hugging Face'],
|
|
62
|
-
['local_openai', 'Local']
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
function buildProviderPicker(settings = currentSettings) {
|
|
66
|
-
if (!chatProviderSelect) return;
|
|
67
|
-
chatProviderSelect.textContent = '';
|
|
68
|
-
PROVIDER_PICKER_OPTIONS.forEach(([value, label]) => {
|
|
69
|
-
const option = document.createElement('option');
|
|
70
|
-
option.value = value;
|
|
71
|
-
option.textContent = label;
|
|
72
|
-
chatProviderSelect.appendChild(option);
|
|
73
|
-
});
|
|
74
|
-
chatProviderSelect.value = settings.aiProvider || 'gemini';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function syncAgentModeToggle(settings = currentSettings) {
|
|
78
|
-
if (!agentModeToggle) return;
|
|
79
|
-
agentModeToggle.checked = settings.assistantMode === 'agent';
|
|
80
|
-
agentModeToggle.closest('.smart-context-control')?.classList.toggle('is-active', agentModeToggle.checked);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function changeChatProvider(provider) {
|
|
84
|
-
if (!PROVIDER_PICKER_OPTIONS.some(([value]) => value === provider)) return;
|
|
85
|
-
const nextSettings = { ...currentSettings, aiProvider: provider };
|
|
86
|
-
chatProviderSelect.disabled = true;
|
|
87
|
-
try {
|
|
88
|
-
const result = await window.api.saveSettings(nextSettings);
|
|
89
|
-
if (!result || result.success !== false) {
|
|
90
|
-
currentSettings = nextSettings;
|
|
91
|
-
buildProviderPicker(currentSettings);
|
|
92
|
-
} else {
|
|
93
|
-
throw new Error(result.message || 'Unable to save provider setting');
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error('Failed to change provider:', error);
|
|
97
|
-
buildProviderPicker(currentSettings);
|
|
98
|
-
setMintActivity('error');
|
|
99
|
-
} finally {
|
|
100
|
-
chatProviderSelect.disabled = false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const MINT_ACTIVITY_STATES = {
|
|
105
|
-
idle: { label: 'Idle', title: 'Mint is idle' },
|
|
106
|
-
listening: { label: 'Listening', title: 'Mint is listening' },
|
|
107
|
-
thinking: { label: 'Thinking', title: 'Mint is thinking' },
|
|
108
|
-
speaking: { label: 'Speaking', title: 'Mint is speaking' },
|
|
109
|
-
error: { label: 'Error', title: 'Mint needs attention' }
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function setMintActivity(state, options = {}) {
|
|
113
|
-
const normalizedState = MINT_ACTIVITY_STATES[state] ? state : 'idle';
|
|
114
|
-
const meta = MINT_ACTIVITY_STATES[normalizedState];
|
|
115
|
-
if (mintActivityResetTimer) {
|
|
116
|
-
clearTimeout(mintActivityResetTimer);
|
|
117
|
-
mintActivityResetTimer = null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
[mintStatus, modelActivityBadge].forEach((element) => {
|
|
121
|
-
if (!element) return;
|
|
122
|
-
element.dataset.state = normalizedState;
|
|
123
|
-
element.title = meta.title;
|
|
124
|
-
const label = element.querySelector('.mint-status-label');
|
|
125
|
-
if (label) label.textContent = meta.label;
|
|
126
|
-
});
|
|
127
|
-
if (mintStatusLabel) mintStatusLabel.textContent = meta.label;
|
|
128
|
-
|
|
129
|
-
if (window.api && window.api.setAiState) {
|
|
130
|
-
window.api.setAiState(normalizedState);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (normalizedState === 'error' || options.resetAfter) {
|
|
134
|
-
mintActivityResetTimer = setTimeout(() => {
|
|
135
|
-
setMintActivity('idle');
|
|
136
|
-
}, options.resetAfter || 3500);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function detectConversationLanguage(text) {
|
|
141
|
-
const value = String(text || '');
|
|
142
|
-
if (/[\u0E00-\u0E7F]/.test(value)) return 'thai';
|
|
143
|
-
if (/[A-Za-z]/.test(value)) return 'english';
|
|
144
|
-
return 'auto';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function rememberConversationLanguage(text) {
|
|
148
|
-
const detected = detectConversationLanguage(text);
|
|
149
|
-
if (detected !== 'auto') {
|
|
150
|
-
lastConversationLanguage = detected;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function buildInteractionLanguageInstruction() {
|
|
155
|
-
if (lastConversationLanguage === 'thai') {
|
|
156
|
-
return 'Current conversation language: Thai. Reply in Thai. Do not reply in English just because this interaction instruction is written in English.';
|
|
157
|
-
}
|
|
158
|
-
if (lastConversationLanguage === 'english') {
|
|
159
|
-
return 'Current conversation language: English. Reply in English. Do not switch to Thai.';
|
|
160
|
-
}
|
|
161
|
-
return 'Infer the reply language from the recent conversation before this interaction instruction, not from the language of this instruction.';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// --- Theme Loading ---
|
|
165
|
-
function applyTheme(theme, accentColor, systemTextColor, config = {}) {
|
|
166
|
-
document.documentElement.setAttribute('data-theme', theme || 'dark');
|
|
167
|
-
const accent = accentColor || '#8f6cf5';
|
|
168
|
-
const defaultTextColor = theme === 'light' ? '#0f172a' : '#e8e8ea';
|
|
169
|
-
const textColor = (!systemTextColor || (theme === 'light' && systemTextColor === '#f8fafc'))
|
|
170
|
-
? defaultTextColor
|
|
171
|
-
: systemTextColor;
|
|
172
|
-
document.documentElement.style.setProperty('--accent', accent);
|
|
173
|
-
document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
|
|
174
|
-
document.documentElement.style.setProperty('--text-main', textColor);
|
|
175
|
-
|
|
176
|
-
// Dynamic UI Customizations
|
|
177
|
-
document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
|
|
178
|
-
document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
|
|
179
|
-
document.documentElement.style.fontSize = config.fontSize || '15px';
|
|
180
|
-
|
|
181
|
-
if (theme === 'custom') {
|
|
182
|
-
if (config.customBgStart && config.customBgEnd) {
|
|
183
|
-
const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
|
|
184
|
-
document.documentElement.style.setProperty('--bg-color', config.customBgStart);
|
|
185
|
-
document.documentElement.style.setProperty('--bg-gradient', gradient);
|
|
186
|
-
}
|
|
187
|
-
if (config.customPanelBg) {
|
|
188
|
-
const rgb = hexToRgb(config.customPanelBg);
|
|
189
|
-
document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
|
|
190
|
-
document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`);
|
|
191
|
-
document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`);
|
|
192
|
-
document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`);
|
|
193
|
-
document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`);
|
|
194
|
-
document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`);
|
|
195
|
-
document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
[
|
|
199
|
-
'--bg-color',
|
|
200
|
-
'--bg-gradient',
|
|
201
|
-
'--panel-bg',
|
|
202
|
-
'--panel-raised',
|
|
203
|
-
'--panel-soft',
|
|
204
|
-
'--chrome-bg',
|
|
205
|
-
'--surface-bg',
|
|
206
|
-
'--surface-strong',
|
|
207
|
-
'--input-bg'
|
|
208
|
-
].forEach(name => document.documentElement.style.removeProperty(name));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function hexToRgb(hex) {
|
|
213
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
214
|
-
return result ? {
|
|
215
|
-
r: parseInt(result[1], 16),
|
|
216
|
-
g: parseInt(result[2], 16),
|
|
217
|
-
b: parseInt(result[3], 16)
|
|
218
|
-
} : { r: 15, g: 23, b: 42 };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function loadTheme() {
|
|
222
|
-
try {
|
|
223
|
-
const config = await window.api.getSettings();
|
|
224
|
-
currentSettings = config || {};
|
|
225
|
-
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
226
|
-
enableVoiceReply = config.enableVoiceReply !== false;
|
|
227
|
-
ttsProvider = config.ttsProvider || 'google';
|
|
228
|
-
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
229
|
-
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
230
|
-
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
231
|
-
buildProviderPicker(currentSettings);
|
|
232
|
-
syncAgentModeToggle(currentSettings);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
applyTheme('dark', '#8b5cf6', '#f8fafc');
|
|
235
|
-
buildProviderPicker(currentSettings);
|
|
236
|
-
syncAgentModeToggle(currentSettings);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function lightenColor(hex, amount) {
|
|
241
|
-
const clean = hex.replace('#', '');
|
|
242
|
-
if (clean.length !== 6) return hex;
|
|
243
|
-
const num = parseInt(clean, 16);
|
|
244
|
-
const r = Math.min(255, (num >> 16) + amount);
|
|
245
|
-
const g = Math.min(255, ((num >> 8) & 0x00FF) + amount);
|
|
246
|
-
const b = Math.min(255, (num & 0x0000FF) + amount);
|
|
247
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// 🔔 Real-time theme sync from Settings window
|
|
251
|
-
window.api.onSettingsChanged((config) => {
|
|
252
|
-
currentSettings = config || currentSettings;
|
|
253
|
-
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
254
|
-
enableVoiceReply = config.enableVoiceReply !== false;
|
|
255
|
-
ttsProvider = config.ttsProvider || 'google';
|
|
256
|
-
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
257
|
-
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
258
|
-
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
259
|
-
buildProviderPicker(currentSettings);
|
|
260
|
-
syncAgentModeToggle(currentSettings);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
chatProviderSelect?.addEventListener('change', (event) => {
|
|
264
|
-
changeChatProvider(event.target.value);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
agentModeToggle?.addEventListener('change', async () => {
|
|
268
|
-
const nextSettings = {
|
|
269
|
-
...currentSettings,
|
|
270
|
-
assistantMode: agentModeToggle.checked ? 'agent' : 'chat'
|
|
271
|
-
};
|
|
272
|
-
agentModeToggle.disabled = true;
|
|
273
|
-
try {
|
|
274
|
-
const result = await window.api.saveSettings(nextSettings);
|
|
275
|
-
if (!result || result.success !== false) {
|
|
276
|
-
currentSettings = nextSettings;
|
|
277
|
-
} else {
|
|
278
|
-
throw new Error(result.message || 'Unable to save assistant mode');
|
|
279
|
-
}
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.error('Failed to change assistant mode:', error);
|
|
282
|
-
setMintActivity('error');
|
|
283
|
-
} finally {
|
|
284
|
-
syncAgentModeToggle(currentSettings);
|
|
285
|
-
agentModeToggle.disabled = false;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// --- Voice Input Setup ---
|
|
290
|
-
let mediaRecorder = null;
|
|
291
|
-
let audioChunks = [];
|
|
292
|
-
let speechRecognition = null;
|
|
293
|
-
let isSpeechStreaming = false;
|
|
294
|
-
let speechInterim = '';
|
|
295
|
-
let speechHadResult = false;
|
|
296
|
-
let speechFallbackTimer = null;
|
|
297
|
-
let voiceMode = null; // 'speech' | 'recorder' | null
|
|
298
|
-
let voiceSendQueue = Promise.resolve();
|
|
299
|
-
let speechPausedForReply = false;
|
|
300
|
-
let resumeSpeechAfterResponse = false;
|
|
301
|
-
const DEFAULT_PLACEHOLDER = "Type or speak a command...";
|
|
302
|
-
const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
303
|
-
|
|
304
|
-
function notifyAiIfNeeded() {
|
|
305
|
-
if (!window.api.notifyAiResponse) return;
|
|
306
|
-
if (!document.hasFocus() || document.hidden) {
|
|
307
|
-
window.api.notifyAiResponse();
|
|
308
|
-
} else if (window.api.clearAiNotifications) {
|
|
309
|
-
window.api.clearAiNotifications();
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function queueVoiceTextSend(text) {
|
|
314
|
-
const clean = (text || '').trim();
|
|
315
|
-
if (!clean) return;
|
|
316
|
-
voiceSendQueue = voiceSendQueue.then(() => sendTextMessage(clean, { allowSmartContext: false }));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function pauseSpeechForReply() {
|
|
320
|
-
if (!speechRecognition || !isSpeechStreaming) return;
|
|
321
|
-
resumeSpeechAfterResponse = true;
|
|
322
|
-
speechPausedForReply = true;
|
|
323
|
-
try {
|
|
324
|
-
speechRecognition.stop();
|
|
325
|
-
} catch (_) {}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function resumeSpeechIfNeeded() {
|
|
329
|
-
if (!speechRecognition || !isSpeechStreaming) {
|
|
330
|
-
resumeSpeechAfterResponse = false;
|
|
331
|
-
speechPausedForReply = false;
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (!resumeSpeechAfterResponse) return;
|
|
335
|
-
resumeSpeechAfterResponse = false;
|
|
336
|
-
speechPausedForReply = false;
|
|
337
|
-
try {
|
|
338
|
-
speechRecognition.start();
|
|
339
|
-
} catch (e) {
|
|
340
|
-
console.error("Speech recognition resume error:", e);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function setupSpeechRecognition() {
|
|
345
|
-
if (!SpeechRecognitionCtor) return;
|
|
346
|
-
speechRecognition = new SpeechRecognitionCtor();
|
|
347
|
-
speechRecognition.lang = 'th-TH';
|
|
348
|
-
speechRecognition.interimResults = true;
|
|
349
|
-
// Let the engine auto-stop on silence, then we restart if streaming is enabled.
|
|
350
|
-
speechRecognition.continuous = false;
|
|
351
|
-
|
|
352
|
-
speechRecognition.onstart = () => {
|
|
353
|
-
micBtn.classList.add('listening');
|
|
354
|
-
chatInput.placeholder = "Listening... (Click to stop)";
|
|
355
|
-
setMintActivity('listening');
|
|
356
|
-
speechHadResult = false;
|
|
357
|
-
if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
|
|
358
|
-
speechFallbackTimer = setTimeout(() => {
|
|
359
|
-
if (isSpeechStreaming && !speechHadResult) {
|
|
360
|
-
fallbackToMediaRecorder();
|
|
361
|
-
}
|
|
362
|
-
}, 1500);
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
speechRecognition.onresult = (event) => {
|
|
366
|
-
speechHadResult = true;
|
|
367
|
-
let interimTranscript = '';
|
|
368
|
-
let finalTranscript = '';
|
|
369
|
-
|
|
370
|
-
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
371
|
-
const result = event.results[i];
|
|
372
|
-
const transcript = result[0]?.transcript || '';
|
|
373
|
-
if (result.isFinal) {
|
|
374
|
-
finalTranscript += transcript;
|
|
375
|
-
} else {
|
|
376
|
-
interimTranscript += transcript;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (finalTranscript.trim()) {
|
|
381
|
-
const textToSend = finalTranscript.trim();
|
|
382
|
-
speechInterim = '';
|
|
383
|
-
chatInput.value = '';
|
|
384
|
-
pauseSpeechForReply();
|
|
385
|
-
queueVoiceTextSend(textToSend);
|
|
386
|
-
} else {
|
|
387
|
-
speechInterim = interimTranscript;
|
|
388
|
-
chatInput.value = speechInterim.trimStart();
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
speechRecognition.onerror = (err) => {
|
|
393
|
-
console.error("Speech recognition error:", err);
|
|
394
|
-
setMintActivity('error');
|
|
395
|
-
fallbackToMediaRecorder();
|
|
396
|
-
isSpeechStreaming = false;
|
|
397
|
-
resetMicUI();
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
speechRecognition.onend = () => {
|
|
401
|
-
if (speechFallbackTimer) {
|
|
402
|
-
clearTimeout(speechFallbackTimer);
|
|
403
|
-
speechFallbackTimer = null;
|
|
404
|
-
}
|
|
405
|
-
if (speechPausedForReply) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (isSpeechStreaming && !speechHadResult) {
|
|
409
|
-
fallbackToMediaRecorder();
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (isSpeechStreaming) {
|
|
413
|
-
try {
|
|
414
|
-
speechRecognition.start();
|
|
415
|
-
} catch (e) {
|
|
416
|
-
console.error("Speech recognition restart error:", e);
|
|
417
|
-
isSpeechStreaming = false;
|
|
418
|
-
resetMicUI();
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
resetMicUI();
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
async function setupMediaRecorder() {
|
|
427
|
-
try {
|
|
428
|
-
// Improved audio constraints for better quality and noise reduction
|
|
429
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
430
|
-
audio: {
|
|
431
|
-
echoCancellation: true,
|
|
432
|
-
noiseSuppression: true,
|
|
433
|
-
autoGainControl: true
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Check for supported MIME types
|
|
438
|
-
const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
|
|
439
|
-
mediaRecorder = new MediaRecorder(stream, { mimeType });
|
|
440
|
-
|
|
441
|
-
mediaRecorder.ondataavailable = (event) => {
|
|
442
|
-
if (event.data.size > 0) audioChunks.push(event.data);
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
mediaRecorder.onstop = async () => {
|
|
446
|
-
if (audioChunks.length === 0) {
|
|
447
|
-
resetMicUI();
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const audioBlob = new Blob(audioChunks, { type: mimeType });
|
|
452
|
-
audioChunks = [];
|
|
453
|
-
|
|
454
|
-
// Convert Blob to Base64
|
|
455
|
-
const reader = new FileReader();
|
|
456
|
-
reader.readAsDataURL(audioBlob);
|
|
457
|
-
reader.onloadend = async () => {
|
|
458
|
-
const base64Audio = reader.result;
|
|
459
|
-
// Send to Gemini
|
|
460
|
-
await sendVoiceMessage(base64Audio);
|
|
461
|
-
};
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
mediaRecorder.onstart = () => {
|
|
465
|
-
micBtn.classList.add('listening');
|
|
466
|
-
chatInput.placeholder = "Listening... (Click to stop)";
|
|
467
|
-
setMintActivity('listening');
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
} catch (err) {
|
|
471
|
-
console.error("Microphone access error:", err);
|
|
472
|
-
setMintActivity('error');
|
|
473
|
-
micBtn.style.display = 'none';
|
|
474
|
-
appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function resetMicUI() {
|
|
479
|
-
micBtn.classList.remove('listening');
|
|
480
|
-
chatInput.placeholder = DEFAULT_PLACEHOLDER;
|
|
481
|
-
if (voiceMode !== 'speech' && (!mediaRecorder || mediaRecorder.state === 'inactive')) {
|
|
482
|
-
setMintActivity('idle');
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async function sendVoiceMessage(base64Audio) {
|
|
487
|
-
showTyping();
|
|
488
|
-
chatInput.placeholder = "Processing voice...";
|
|
489
|
-
setMintActivity('thinking');
|
|
490
|
-
try {
|
|
491
|
-
// Send empty text, but include the audio
|
|
492
|
-
const response = await window.api.sendMessage("", null, base64Audio);
|
|
493
|
-
removeTyping();
|
|
494
|
-
|
|
495
|
-
// Show AI response
|
|
496
|
-
const msgDiv = await appendAiMessages(response.response, {
|
|
497
|
-
allowDelay: true,
|
|
498
|
-
timestamp: new Date().toISOString()
|
|
499
|
-
});
|
|
500
|
-
await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
|
|
501
|
-
notifyAiIfNeeded();
|
|
502
|
-
|
|
503
|
-
if (response.approval?.required) {
|
|
504
|
-
appendApprovalCard(msgDiv, response.approval);
|
|
505
|
-
} else if (response.action && response.action.type !== 'none') {
|
|
506
|
-
appendActionCard(msgDiv, response.action);
|
|
507
|
-
}
|
|
508
|
-
} catch (error) {
|
|
509
|
-
removeTyping();
|
|
510
|
-
setMintActivity('error');
|
|
511
|
-
appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
|
|
512
|
-
console.error(error);
|
|
513
|
-
resumeSpeechIfNeeded();
|
|
514
|
-
} finally {
|
|
515
|
-
resetMicUI();
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function fallbackToMediaRecorder() {
|
|
520
|
-
if (voiceMode === 'recorder') return;
|
|
521
|
-
isSpeechStreaming = false;
|
|
522
|
-
speechPausedForReply = false;
|
|
523
|
-
resumeSpeechAfterResponse = false;
|
|
524
|
-
voiceMode = 'recorder';
|
|
525
|
-
try {
|
|
526
|
-
if (speechRecognition) {
|
|
527
|
-
speechRecognition.stop();
|
|
528
|
-
}
|
|
529
|
-
} catch (_) {}
|
|
530
|
-
if (mediaRecorder && mediaRecorder.state === 'inactive') {
|
|
531
|
-
audioChunks = [];
|
|
532
|
-
mediaRecorder.start();
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Initialize voice input
|
|
537
|
-
setupMediaRecorder();
|
|
538
|
-
if (SpeechRecognitionCtor) {
|
|
539
|
-
setupSpeechRecognition();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
micBtn.addEventListener('click', (e) => {
|
|
543
|
-
e.preventDefault();
|
|
544
|
-
if (voiceMode === 'recorder') {
|
|
545
|
-
if (!mediaRecorder) return;
|
|
546
|
-
if (mediaRecorder.state === 'inactive') {
|
|
547
|
-
audioChunks = [];
|
|
548
|
-
mediaRecorder.start();
|
|
549
|
-
setMintActivity('listening');
|
|
550
|
-
} else {
|
|
551
|
-
mediaRecorder.stop();
|
|
552
|
-
setMintActivity('thinking');
|
|
553
|
-
voiceMode = null;
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (speechRecognition) {
|
|
559
|
-
if (!isSpeechStreaming) {
|
|
560
|
-
isSpeechStreaming = true;
|
|
561
|
-
voiceMode = 'speech';
|
|
562
|
-
speechInterim = '';
|
|
563
|
-
chatInput.value = '';
|
|
564
|
-
try {
|
|
565
|
-
speechRecognition.start();
|
|
566
|
-
} catch (err) {
|
|
567
|
-
console.error("Speech recognition start error:", err);
|
|
568
|
-
isSpeechStreaming = false;
|
|
569
|
-
resetMicUI();
|
|
570
|
-
}
|
|
571
|
-
} else {
|
|
572
|
-
isSpeechStreaming = false;
|
|
573
|
-
speechRecognition.stop();
|
|
574
|
-
voiceMode = null;
|
|
575
|
-
}
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (!mediaRecorder) return;
|
|
580
|
-
|
|
581
|
-
if (mediaRecorder.state === 'inactive') {
|
|
582
|
-
audioChunks = [];
|
|
583
|
-
mediaRecorder.start();
|
|
584
|
-
setMintActivity('listening');
|
|
585
|
-
} else {
|
|
586
|
-
mediaRecorder.stop();
|
|
587
|
-
setMintActivity('thinking');
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// --- Speech Synthesis Setup ---
|
|
592
|
-
let currentAudioPlayer = null;
|
|
593
|
-
|
|
594
|
-
function speakText(text, options = {}) {
|
|
595
|
-
setMintActivity('speaking');
|
|
596
|
-
const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
|
|
597
|
-
|
|
598
|
-
const wrappedOnEnd = () => {
|
|
599
|
-
if (window.Live2DManager) Live2DManager.stopLipSync();
|
|
600
|
-
onEnd();
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
return new Promise(async (resolve) => {
|
|
604
|
-
if (!enableVoiceReply) {
|
|
605
|
-
setMintActivity('idle');
|
|
606
|
-
wrappedOnEnd();
|
|
607
|
-
return resolve();
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Stop any currently playing audio
|
|
611
|
-
if (currentAudioPlayer) {
|
|
612
|
-
currentAudioPlayer.pause();
|
|
613
|
-
currentAudioPlayer.currentTime = 0;
|
|
614
|
-
currentAudioPlayer = null;
|
|
615
|
-
}
|
|
616
|
-
if (window.Live2DManager) Live2DManager.stopLipSync();
|
|
617
|
-
|
|
618
|
-
if ('speechSynthesis' in window) {
|
|
619
|
-
window.speechSynthesis.cancel();
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (!text || !text.trim()) {
|
|
623
|
-
setMintActivity('idle');
|
|
624
|
-
wrappedOnEnd();
|
|
625
|
-
return resolve();
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (window.Live2DManager) Live2DManager.startLipSync();
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
if (ttsProvider !== 'native') {
|
|
632
|
-
const urls = await window.api.getTtsUrls(text);
|
|
633
|
-
if (urls && urls.length > 0) {
|
|
634
|
-
let i = 0;
|
|
635
|
-
const playNext = () => {
|
|
636
|
-
if (i >= urls.length) {
|
|
637
|
-
setMintActivity('idle');
|
|
638
|
-
wrappedOnEnd();
|
|
639
|
-
return resolve();
|
|
640
|
-
}
|
|
641
|
-
const audio = new Audio(urls[i].url);
|
|
642
|
-
audio.volume = ttsVolume;
|
|
643
|
-
audio.playbackRate = ttsSpeed;
|
|
644
|
-
|
|
645
|
-
currentAudioPlayer = audio;
|
|
646
|
-
audio.onended = () => {
|
|
647
|
-
i++;
|
|
648
|
-
playNext();
|
|
649
|
-
};
|
|
650
|
-
audio.onerror = () => {
|
|
651
|
-
console.error("TTS Audio error", urls[i]);
|
|
652
|
-
i++;
|
|
653
|
-
playNext();
|
|
654
|
-
};
|
|
655
|
-
audio.play().catch(e => {
|
|
656
|
-
console.error("Audio playback prevented:", e);
|
|
657
|
-
fallbackSpeak(text, wrappedOnEnd, resolve);
|
|
658
|
-
});
|
|
659
|
-
};
|
|
660
|
-
playNext();
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
} catch (err) {
|
|
665
|
-
console.error("Cloud TTS Error, falling back to local:", err);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Fallback
|
|
669
|
-
fallbackSpeak(text, wrappedOnEnd, resolve);
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function fallbackSpeak(text, onEnd, resolve) {
|
|
674
|
-
if (!('speechSynthesis' in window)) {
|
|
675
|
-
setMintActivity('idle');
|
|
676
|
-
if (onEnd) onEnd();
|
|
677
|
-
resolve();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
window.speechSynthesis.cancel();
|
|
682
|
-
const utterance = new SpeechSynthesisUtterance(text);
|
|
683
|
-
utterance.lang = 'th-TH';
|
|
684
|
-
utterance.volume = ttsVolume;
|
|
685
|
-
utterance.rate = ttsSpeed;
|
|
686
|
-
utterance.pitch = ttsPitch;
|
|
687
|
-
|
|
688
|
-
let finished = false;
|
|
689
|
-
const done = () => {
|
|
690
|
-
if (finished) return;
|
|
691
|
-
finished = true;
|
|
692
|
-
setMintActivity('idle');
|
|
693
|
-
if (onEnd) onEnd();
|
|
694
|
-
resolve();
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
utterance.onend = done;
|
|
698
|
-
utterance.onerror = done;
|
|
699
|
-
window.speechSynthesis.speak(utterance);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Minimize window handler (hides to tray)
|
|
703
|
-
minimizeBtn.addEventListener('click', () => {
|
|
704
|
-
window.api.minimizeWindow();
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
// Close window handler (quits app)
|
|
708
|
-
closeBtn.addEventListener('click', () => {
|
|
709
|
-
window.api.quitApp();
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
maximizeBtn.addEventListener('click', () => {
|
|
713
|
-
window.api.maximizeWindow();
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Settings button
|
|
717
|
-
function openSettings() {
|
|
718
|
-
window.api.openSettings();
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
settingsBtn.addEventListener('click', openSettings);
|
|
722
|
-
sidebarSettingsBtn?.addEventListener('click', openSettings);
|
|
723
|
-
|
|
724
|
-
async function renderPicturesLibrary() {
|
|
725
|
-
if (!picturesGrid || !picturesEmpty) return;
|
|
726
|
-
picturesGrid.innerHTML = '';
|
|
727
|
-
|
|
728
|
-
const pictures = await window.api.listSavedPictures();
|
|
729
|
-
picturesEmpty.classList.toggle('is-hidden', pictures.length > 0);
|
|
730
|
-
|
|
731
|
-
for (const picture of pictures) {
|
|
732
|
-
const card = document.createElement('article');
|
|
733
|
-
card.className = 'picture-card';
|
|
734
|
-
|
|
735
|
-
const img = document.createElement('img');
|
|
736
|
-
img.src = picture.url;
|
|
737
|
-
img.alt = picture.filename || 'Saved picture';
|
|
738
|
-
img.loading = 'lazy';
|
|
739
|
-
|
|
740
|
-
const meta = document.createElement('div');
|
|
741
|
-
meta.className = 'picture-card-meta';
|
|
742
|
-
const date = picture.createdAt ? new Date(picture.createdAt).toLocaleString() : '';
|
|
743
|
-
meta.textContent = picture.message || date || picture.filename || 'Saved picture';
|
|
744
|
-
meta.title = [picture.filename, picture.message, date].filter(Boolean).join('\n');
|
|
745
|
-
|
|
746
|
-
card.appendChild(img);
|
|
747
|
-
card.appendChild(meta);
|
|
748
|
-
picturesGrid.appendChild(card);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async function openPicturesLibrary() {
|
|
753
|
-
if (!appBody || !picturesLibrary) return;
|
|
754
|
-
picturesLibrary.hidden = false;
|
|
755
|
-
requestAnimationFrame(() => {
|
|
756
|
-
appBody.classList.add('pictures-open');
|
|
757
|
-
});
|
|
758
|
-
sidebarChatBtn?.classList.remove('is-active');
|
|
759
|
-
sidebarPicturesBtn?.classList.add('is-active');
|
|
760
|
-
await renderPicturesLibrary();
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function closePicturesLibrary() {
|
|
764
|
-
if (!appBody || !picturesLibrary) return;
|
|
765
|
-
appBody.classList.remove('pictures-open');
|
|
766
|
-
setTimeout(() => {
|
|
767
|
-
if (!appBody.classList.contains('pictures-open')) {
|
|
768
|
-
picturesLibrary.hidden = true;
|
|
769
|
-
}
|
|
770
|
-
}, 240);
|
|
771
|
-
sidebarChatBtn?.classList.add('is-active');
|
|
772
|
-
sidebarPicturesBtn?.classList.remove('is-active');
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
sidebarChatBtn?.addEventListener('click', closePicturesLibrary);
|
|
776
|
-
sidebarPicturesBtn?.addEventListener('click', openPicturesLibrary);
|
|
777
|
-
picturesCloseBtn?.addEventListener('click', closePicturesLibrary);
|
|
778
|
-
|
|
779
|
-
function setSidebarCollapsed(isCollapsed) {
|
|
780
|
-
if (!appBody || !sidebarToggleBtn) return;
|
|
781
|
-
appBody.classList.toggle('sidebar-collapsed', isCollapsed);
|
|
782
|
-
sidebarToggleBtn.setAttribute('aria-expanded', String(!isCollapsed));
|
|
783
|
-
sidebarToggleBtn.setAttribute('aria-label', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
784
|
-
sidebarToggleBtn.setAttribute('title', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (appBody && sidebarToggleBtn) {
|
|
788
|
-
setSidebarCollapsed(true);
|
|
789
|
-
sidebarToggleBtn.addEventListener('click', () => {
|
|
790
|
-
setSidebarCollapsed(!appBody.classList.contains('sidebar-collapsed'));
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Throttle utility to prevent UI spam
|
|
795
|
-
function throttle(func, limit) {
|
|
796
|
-
let inThrottle;
|
|
797
|
-
return function() {
|
|
798
|
-
const args = arguments;
|
|
799
|
-
const context = this;
|
|
800
|
-
if (!inThrottle) {
|
|
801
|
-
func.apply(context, args);
|
|
802
|
-
inThrottle = true;
|
|
803
|
-
setTimeout(() => inThrottle = false, limit);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Vision system
|
|
809
|
-
visionBtn.addEventListener('click', throttle(async () => {
|
|
810
|
-
await window.api.startVision();
|
|
811
|
-
}, 1000));
|
|
812
|
-
|
|
813
|
-
window.api.onVisionReady((base64Image) => {
|
|
814
|
-
currentBase64Image = base64Image;
|
|
815
|
-
imagePreview.src = base64Image;
|
|
816
|
-
imagePreviewContainer.style.display = 'block';
|
|
817
|
-
chatInput.focus();
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
removeImageBtn.addEventListener('click', () => {
|
|
821
|
-
currentBase64Image = null;
|
|
822
|
-
imagePreview.src = '';
|
|
823
|
-
imagePreviewContainer.style.display = 'none';
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
function formatTime(isoString) {
|
|
827
|
-
if (!isoString) return '';
|
|
828
|
-
try {
|
|
829
|
-
const date = new Date(isoString);
|
|
830
|
-
return date.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
831
|
-
} catch (e) {
|
|
832
|
-
return '';
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function compactSmartContext(context) {
|
|
837
|
-
if (!context || typeof context !== 'object') return null;
|
|
838
|
-
const activeWindow = context.activeWindow || {};
|
|
839
|
-
const currentApp = context.currentApp || {};
|
|
840
|
-
const browser = context.browser || null;
|
|
841
|
-
return {
|
|
842
|
-
capturedAt: context.capturedAt,
|
|
843
|
-
platform: context.platform,
|
|
844
|
-
currentApp: currentApp.name || activeWindow.appName || activeWindow.processName || '',
|
|
845
|
-
processName: currentApp.processName || activeWindow.processName || '',
|
|
846
|
-
pid: currentApp.pid || activeWindow.pid || null,
|
|
847
|
-
activeWindowTitle: activeWindow.title || '',
|
|
848
|
-
browser: browser ? {
|
|
849
|
-
title: browser.title || '',
|
|
850
|
-
url: browser.url || '',
|
|
851
|
-
urlUnavailableReason: browser.urlUnavailableReason || ''
|
|
852
|
-
} : null,
|
|
853
|
-
selectedText: context.selectedText || '',
|
|
854
|
-
clipboardText: context.clipboardText || ''
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function appendSmartContextToMessage(message, context) {
|
|
859
|
-
const compact = compactSmartContext(context);
|
|
860
|
-
if (!compact) return message;
|
|
861
|
-
return [
|
|
862
|
-
message,
|
|
863
|
-
'',
|
|
864
|
-
'[SMART_CONTEXT]',
|
|
865
|
-
'Use this structured desktop context together with the attached screenshot. Do not mention it unless it helps answer the user.',
|
|
866
|
-
JSON.stringify(compact, null, 2),
|
|
867
|
-
'[/SMART_CONTEXT]'
|
|
868
|
-
].join('\n');
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function shouldShowAgentActivity(options = {}) {
|
|
872
|
-
return options.showAgentActivity !== false && currentSettings.assistantMode === 'agent';
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function createAgentActivityCard() {
|
|
876
|
-
const messageDiv = document.createElement('div');
|
|
877
|
-
messageDiv.classList.add('message', 'ai-message', 'agent-activity-message');
|
|
878
|
-
|
|
879
|
-
const bubble = document.createElement('div');
|
|
880
|
-
bubble.classList.add('message-bubble', 'agent-activity-card');
|
|
881
|
-
|
|
882
|
-
const header = document.createElement('div');
|
|
883
|
-
header.className = 'agent-activity-header';
|
|
884
|
-
const title = document.createElement('span');
|
|
885
|
-
title.textContent = 'Agent Activity';
|
|
886
|
-
const status = document.createElement('span');
|
|
887
|
-
status.className = 'agent-activity-status';
|
|
888
|
-
status.textContent = 'Running';
|
|
889
|
-
header.appendChild(title);
|
|
890
|
-
header.appendChild(status);
|
|
891
|
-
|
|
892
|
-
const list = document.createElement('div');
|
|
893
|
-
list.className = 'agent-activity-list';
|
|
894
|
-
|
|
895
|
-
bubble.appendChild(header);
|
|
896
|
-
bubble.appendChild(list);
|
|
897
|
-
messageDiv.appendChild(bubble);
|
|
898
|
-
chatContainer.appendChild(messageDiv);
|
|
899
|
-
scrollToBottom();
|
|
900
|
-
|
|
901
|
-
return {
|
|
902
|
-
element: messageDiv,
|
|
903
|
-
list,
|
|
904
|
-
status,
|
|
905
|
-
add(label, state = 'running', detail = '') {
|
|
906
|
-
const item = document.createElement('div');
|
|
907
|
-
item.className = 'agent-activity-item';
|
|
908
|
-
item.dataset.state = state;
|
|
909
|
-
|
|
910
|
-
const dot = document.createElement('span');
|
|
911
|
-
dot.className = 'agent-activity-dot';
|
|
912
|
-
|
|
913
|
-
const content = document.createElement('span');
|
|
914
|
-
content.className = 'agent-activity-text';
|
|
915
|
-
content.textContent = detail ? `${label}: ${detail}` : label;
|
|
916
|
-
|
|
917
|
-
item.appendChild(dot);
|
|
918
|
-
item.appendChild(content);
|
|
919
|
-
list.appendChild(item);
|
|
920
|
-
scrollToBottom();
|
|
921
|
-
return item;
|
|
922
|
-
},
|
|
923
|
-
update(item, state, label, detail = '') {
|
|
924
|
-
if (!item) return;
|
|
925
|
-
item.dataset.state = state;
|
|
926
|
-
const content = item.querySelector('.agent-activity-text');
|
|
927
|
-
if (content && label) {
|
|
928
|
-
content.textContent = detail ? `${label}: ${detail}` : label;
|
|
929
|
-
}
|
|
930
|
-
},
|
|
931
|
-
finish(state = 'done', label = 'Done') {
|
|
932
|
-
status.textContent = label;
|
|
933
|
-
status.dataset.state = state;
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function describeSmartContextActivity(context, hasScreenshot) {
|
|
939
|
-
const compact = compactSmartContext(context) || {};
|
|
940
|
-
const parts = [];
|
|
941
|
-
if (hasScreenshot) parts.push('screen');
|
|
942
|
-
if (compact.currentApp) parts.push(compact.currentApp);
|
|
943
|
-
if (compact.activeWindowTitle) parts.push(compact.activeWindowTitle);
|
|
944
|
-
if (compact.selectedText) parts.push('selected text');
|
|
945
|
-
if (compact.clipboardText) parts.push('clipboard');
|
|
946
|
-
return parts.slice(0, 3).join(' · ') || 'desktop context';
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function describeActionActivity(action) {
|
|
950
|
-
if (!action || action.type === 'none') return 'No desktop action';
|
|
951
|
-
const meta = getActionCardMeta(action);
|
|
952
|
-
return meta.detail ? `${meta.title} · ${meta.detail}` : meta.title;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Clear chat history
|
|
956
|
-
async function clearChatHistory(confirmMessage = 'Clear current chat history?') {
|
|
957
|
-
const shouldClear = window.confirm(confirmMessage);
|
|
958
|
-
if (!shouldClear) return;
|
|
959
|
-
|
|
960
|
-
closePicturesLibrary();
|
|
961
|
-
await window.api.resetChat();
|
|
962
|
-
// Remove all messages except the initial greeting
|
|
963
|
-
const messages = chatContainer.querySelectorAll('.message:not(.initial)');
|
|
964
|
-
messages.forEach(m => m.remove());
|
|
965
|
-
// Append a clear confirmation
|
|
966
|
-
appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
clearBtn.addEventListener('click', () => clearChatHistory('Clear current chat history?'));
|
|
970
|
-
sidebarNewChatBtn?.addEventListener('click', () => clearChatHistory('Start a new chat and clear current history?'));
|
|
971
|
-
|
|
972
|
-
function formatProviderInfo(providerInfo) {
|
|
973
|
-
if (!providerInfo || typeof providerInfo !== 'object') return '';
|
|
974
|
-
const provider = String(providerInfo.provider || '').trim();
|
|
975
|
-
const model = String(providerInfo.model || '').trim();
|
|
976
|
-
if (!provider && !model) return '';
|
|
977
|
-
return model ? `${provider || 'AI'} • ${model}` : provider;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function formatNumber(value) {
|
|
981
|
-
const number = Number(value) || 0;
|
|
982
|
-
return number.toLocaleString('en-US');
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function summarizeProviderUsage(providerInfo) {
|
|
986
|
-
const usage = Array.isArray(providerInfo?.usage) ? providerInfo.usage : [];
|
|
987
|
-
const selectedProvider = String(providerInfo?.provider || '').trim();
|
|
988
|
-
const selectedModel = String(providerInfo?.model || '').trim();
|
|
989
|
-
const row = usage.find(item =>
|
|
990
|
-
String(item.provider || '') === selectedProvider &&
|
|
991
|
-
String(item.model || '') === selectedModel
|
|
992
|
-
) || usage[0] || {};
|
|
993
|
-
|
|
994
|
-
return {
|
|
995
|
-
requests: Number(row.requests) || 0,
|
|
996
|
-
inputTokens: Number(row.inputTokens) || 0,
|
|
997
|
-
outputTokens: Number(row.outputTokens) || 0,
|
|
998
|
-
reasoningTokens: Number(row.reasoningTokens) || 0,
|
|
999
|
-
cacheReads: Number(row.cacheReads) || 0,
|
|
1000
|
-
totalTokens: Number(row.totalTokens) || 0
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function closeProviderPopover() {
|
|
1005
|
-
document.querySelectorAll('.provider-popover').forEach(popover => popover.remove());
|
|
1006
|
-
document.querySelectorAll('.provider-badge.is-open').forEach(badge => badge.classList.remove('is-open'));
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function createProviderRow(label, value) {
|
|
1010
|
-
const row = document.createElement('div');
|
|
1011
|
-
row.className = 'provider-popover-row';
|
|
1012
|
-
const labelEl = document.createElement('span');
|
|
1013
|
-
labelEl.textContent = label;
|
|
1014
|
-
const valueEl = document.createElement('strong');
|
|
1015
|
-
valueEl.textContent = value;
|
|
1016
|
-
row.appendChild(labelEl);
|
|
1017
|
-
row.appendChild(valueEl);
|
|
1018
|
-
return row;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function showProviderPopover(anchor, providerInfo) {
|
|
1022
|
-
closeProviderPopover();
|
|
1023
|
-
anchor.classList.add('is-open');
|
|
1024
|
-
|
|
1025
|
-
const provider = String(providerInfo?.provider || 'AI').trim();
|
|
1026
|
-
const model = String(providerInfo?.model || 'Unknown model').trim();
|
|
1027
|
-
const usage = summarizeProviderUsage(providerInfo);
|
|
1028
|
-
const popover = document.createElement('div');
|
|
1029
|
-
popover.className = 'provider-popover';
|
|
1030
|
-
|
|
1031
|
-
const title = document.createElement('div');
|
|
1032
|
-
title.className = 'provider-popover-title';
|
|
1033
|
-
title.textContent = 'Model details';
|
|
1034
|
-
popover.appendChild(title);
|
|
1035
|
-
|
|
1036
|
-
popover.appendChild(createProviderRow('Provider', provider));
|
|
1037
|
-
popover.appendChild(createProviderRow('Model', model));
|
|
1038
|
-
popover.appendChild(createProviderRow('Context tokens', formatNumber(usage.inputTokens)));
|
|
1039
|
-
popover.appendChild(createProviderRow('Output tokens', formatNumber(usage.outputTokens)));
|
|
1040
|
-
if (usage.reasoningTokens) {
|
|
1041
|
-
popover.appendChild(createProviderRow('Reasoning tokens', formatNumber(usage.reasoningTokens)));
|
|
1042
|
-
}
|
|
1043
|
-
popover.appendChild(createProviderRow('Total tokens', formatNumber(usage.totalTokens)));
|
|
1044
|
-
|
|
1045
|
-
const action = document.createElement('button');
|
|
1046
|
-
action.type = 'button';
|
|
1047
|
-
action.className = 'provider-popover-action';
|
|
1048
|
-
action.textContent = 'Change model in Settings';
|
|
1049
|
-
action.addEventListener('click', (event) => {
|
|
1050
|
-
event.stopPropagation();
|
|
1051
|
-
closeProviderPopover();
|
|
1052
|
-
if (window.api?.openSettings) window.api.openSettings();
|
|
1053
|
-
});
|
|
1054
|
-
popover.appendChild(action);
|
|
1055
|
-
|
|
1056
|
-
anchor.after(popover);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function splitListOutro(text) {
|
|
1060
|
-
const value = String(text || '').trim();
|
|
1061
|
-
const markers = [
|
|
1062
|
-
' คุณภีมอยาก',
|
|
1063
|
-
' อยากให้',
|
|
1064
|
-
' อยากดู',
|
|
1065
|
-
' บอกมิ้นท์',
|
|
1066
|
-
' Would you',
|
|
1067
|
-
' Do you want',
|
|
1068
|
-
' Tell me'
|
|
1069
|
-
];
|
|
1070
|
-
|
|
1071
|
-
for (const marker of markers) {
|
|
1072
|
-
const index = value.indexOf(marker);
|
|
1073
|
-
if (index > 60) {
|
|
1074
|
-
return {
|
|
1075
|
-
main: value.slice(0, index).trim(),
|
|
1076
|
-
outro: value.slice(index).trim()
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
return { main: value, outro: '' };
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function buildAiTextBlocks(text) {
|
|
1085
|
-
const normalized = normalizeAiText(text).replace(/\r\n/g, '\n').trim();
|
|
1086
|
-
if (!normalized) return [];
|
|
1087
|
-
|
|
1088
|
-
const readable = normalized
|
|
1089
|
-
.replace(/\s+(\d+)[.)]\s+/g, '\n$1. ')
|
|
1090
|
-
.replace(/\n{3,}/g, '\n\n');
|
|
1091
|
-
|
|
1092
|
-
const blocks = [];
|
|
1093
|
-
const lines = readable.split(/\n+/).map(line => line.trim()).filter(Boolean);
|
|
1094
|
-
|
|
1095
|
-
for (const line of lines) {
|
|
1096
|
-
const numbered = line.match(/^\d+[.)]\s+(.+)$/);
|
|
1097
|
-
const bullet = line.match(/^[-*•]\s+(.+)$/);
|
|
1098
|
-
|
|
1099
|
-
if (numbered || bullet) {
|
|
1100
|
-
const content = numbered ? numbered[1] : bullet[1];
|
|
1101
|
-
const { main, outro } = splitListOutro(content);
|
|
1102
|
-
blocks.push({ type: 'bullet', text: main });
|
|
1103
|
-
if (outro) blocks.push({ type: 'paragraph', text: outro });
|
|
1104
|
-
} else {
|
|
1105
|
-
blocks.push({ type: 'paragraph', text: line });
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return blocks;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function appendFormattedMessageText(bubble, text, sender) {
|
|
1113
|
-
if (sender !== 'ai') {
|
|
1114
|
-
const textSpan = document.createElement('span');
|
|
1115
|
-
textSpan.textContent = text;
|
|
1116
|
-
bubble.appendChild(textSpan);
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const blocks = buildAiTextBlocks(text);
|
|
1121
|
-
if (blocks.length === 0) return;
|
|
1122
|
-
|
|
1123
|
-
const wrapper = document.createElement('div');
|
|
1124
|
-
wrapper.classList.add('formatted-ai-text');
|
|
1125
|
-
|
|
1126
|
-
for (const block of blocks) {
|
|
1127
|
-
const item = document.createElement(block.type === 'bullet' ? 'div' : 'p');
|
|
1128
|
-
item.classList.add(block.type === 'bullet' ? 'ai-list-item' : 'ai-paragraph');
|
|
1129
|
-
|
|
1130
|
-
if (block.type === 'bullet') {
|
|
1131
|
-
const bullet = document.createElement('span');
|
|
1132
|
-
bullet.classList.add('ai-list-bullet');
|
|
1133
|
-
bullet.textContent = '•';
|
|
1134
|
-
const content = document.createElement('span');
|
|
1135
|
-
content.textContent = block.text;
|
|
1136
|
-
item.appendChild(bullet);
|
|
1137
|
-
item.appendChild(content);
|
|
1138
|
-
} else {
|
|
1139
|
-
item.textContent = block.text;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
wrapper.appendChild(item);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
bubble.appendChild(wrapper);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function appendMessage(text, sender, base64Image = null, timestamp = null, options = {}) {
|
|
1149
|
-
const messageDiv = document.createElement('div');
|
|
1150
|
-
messageDiv.classList.add('message', `${sender}-message`);
|
|
1151
|
-
|
|
1152
|
-
const bubbleWrapper = document.createElement('div');
|
|
1153
|
-
bubbleWrapper.classList.add('bubble-wrapper');
|
|
1154
|
-
|
|
1155
|
-
const bubble = document.createElement('div');
|
|
1156
|
-
bubble.classList.add('message-bubble');
|
|
1157
|
-
|
|
1158
|
-
if (base64Image && sender === 'user') {
|
|
1159
|
-
const img = document.createElement('img');
|
|
1160
|
-
img.src = base64Image;
|
|
1161
|
-
img.style.maxWidth = '100%';
|
|
1162
|
-
img.style.borderRadius = '4px';
|
|
1163
|
-
img.style.marginBottom = '8px';
|
|
1164
|
-
img.style.display = 'block';
|
|
1165
|
-
bubble.appendChild(img);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
if (text) {
|
|
1169
|
-
appendFormattedMessageText(bubble, text, sender);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
bubbleWrapper.appendChild(bubble);
|
|
1173
|
-
|
|
1174
|
-
const providerLabel = sender === 'ai' ? formatProviderInfo(options.providerInfo) : '';
|
|
1175
|
-
|
|
1176
|
-
// Add metadata
|
|
1177
|
-
if (timestamp || providerLabel) {
|
|
1178
|
-
const timeDiv = document.createElement('div');
|
|
1179
|
-
timeDiv.classList.add('message-time');
|
|
1180
|
-
if (providerLabel) {
|
|
1181
|
-
const providerButton = document.createElement('button');
|
|
1182
|
-
providerButton.type = 'button';
|
|
1183
|
-
providerButton.classList.add('provider-badge');
|
|
1184
|
-
providerButton.textContent = providerLabel;
|
|
1185
|
-
providerButton.title = 'View model details';
|
|
1186
|
-
providerButton.addEventListener('click', (event) => {
|
|
1187
|
-
event.stopPropagation();
|
|
1188
|
-
if (providerButton.classList.contains('is-open')) {
|
|
1189
|
-
closeProviderPopover();
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
showProviderPopover(providerButton, options.providerInfo);
|
|
1193
|
-
});
|
|
1194
|
-
timeDiv.appendChild(providerButton);
|
|
1195
|
-
}
|
|
1196
|
-
if (timestamp) {
|
|
1197
|
-
const timeSpan = document.createElement('span');
|
|
1198
|
-
timeSpan.textContent = formatTime(timestamp);
|
|
1199
|
-
timeDiv.appendChild(timeSpan);
|
|
1200
|
-
}
|
|
1201
|
-
bubbleWrapper.appendChild(timeDiv);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
messageDiv.appendChild(bubbleWrapper);
|
|
1205
|
-
chatContainer.appendChild(messageDiv);
|
|
1206
|
-
scrollToBottom();
|
|
1207
|
-
|
|
1208
|
-
return messageDiv; // Return it so we can append action cards if needed
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function normalizeAiText(input) {
|
|
1212
|
-
if (Array.isArray(input)) {
|
|
1213
|
-
return input
|
|
1214
|
-
.map((item) => (item == null ? '' : String(item).trim()))
|
|
1215
|
-
.filter(Boolean)
|
|
1216
|
-
.join('\n\n');
|
|
1217
|
-
}
|
|
1218
|
-
if (input == null) return '';
|
|
1219
|
-
return String(input);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
function splitAiMessages(text) {
|
|
1223
|
-
const normalized = normalizeAiText(text).trim();
|
|
1224
|
-
if (!normalized) return [];
|
|
1225
|
-
if (/(^|\s)\d+[.)]\s+/.test(normalized) || /(^|\n)\s*[-*•]\s+/.test(normalized)) {
|
|
1226
|
-
return [normalized];
|
|
1227
|
-
}
|
|
1228
|
-
const byBlankLine = normalized
|
|
1229
|
-
.split(/\n\s*\n/)
|
|
1230
|
-
.map((part) => part.trim())
|
|
1231
|
-
.filter(Boolean);
|
|
1232
|
-
if (byBlankLine.length > 1) return byBlankLine;
|
|
1233
|
-
return autoChunkAiText(normalized);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function sleep(ms) {
|
|
1237
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function estimateMessageDelay(text) {
|
|
1241
|
-
const base = 260;
|
|
1242
|
-
const perChar = 12;
|
|
1243
|
-
const jitter = Math.floor(Math.random() * 120);
|
|
1244
|
-
const scaled = base + Math.min(1200, text.length * perChar) + jitter;
|
|
1245
|
-
return Math.min(1600, scaled);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
async function appendAiMessages(text, options = {}) {
|
|
1249
|
-
const allowDelay = options.allowDelay !== false;
|
|
1250
|
-
const timestamp = options.timestamp || new Date().toISOString();
|
|
1251
|
-
const providerInfo = options.providerInfo || null;
|
|
1252
|
-
const parts = splitAiMessages(text);
|
|
1253
|
-
let lastDiv = null;
|
|
1254
|
-
|
|
1255
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
1256
|
-
if (allowDelay && index > 0) {
|
|
1257
|
-
showTyping();
|
|
1258
|
-
await sleep(estimateMessageDelay(parts[index]));
|
|
1259
|
-
removeTyping();
|
|
1260
|
-
}
|
|
1261
|
-
// Only show timestamp for the last bubble in a group if multiple
|
|
1262
|
-
const partTimestamp = (index === parts.length - 1) ? timestamp : null;
|
|
1263
|
-
const partProviderInfo = (index === parts.length - 1) ? providerInfo : null;
|
|
1264
|
-
lastDiv = appendMessage(parts[index], 'ai', null, partTimestamp, { providerInfo: partProviderInfo });
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
return lastDiv;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function autoChunkAiText(text) {
|
|
1271
|
-
const trimmed = text.trim();
|
|
1272
|
-
if (trimmed.length <= 120) return [trimmed];
|
|
1273
|
-
|
|
1274
|
-
const sentenceMatches = trimmed.match(/[^.!?…\n]+[.!?…]+|[^.!?…\n]+$/g);
|
|
1275
|
-
if (!sentenceMatches || sentenceMatches.length <= 1) return [trimmed];
|
|
1276
|
-
|
|
1277
|
-
const bubbles = [];
|
|
1278
|
-
let current = '';
|
|
1279
|
-
for (const sentence of sentenceMatches) {
|
|
1280
|
-
const next = current ? `${current} ${sentence}` : sentence;
|
|
1281
|
-
if (next.length > 180 && current) {
|
|
1282
|
-
bubbles.push(current.trim());
|
|
1283
|
-
current = sentence;
|
|
1284
|
-
} else {
|
|
1285
|
-
current = next;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (current.trim()) bubbles.push(current.trim());
|
|
1289
|
-
|
|
1290
|
-
if (bubbles.length > 3) {
|
|
1291
|
-
const merged = [bubbles[0], bubbles[1], bubbles.slice(2).join(' ').trim()];
|
|
1292
|
-
return merged.filter(Boolean);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return bubbles.length > 0 ? bubbles : [trimmed];
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function appendActionCard(messageDiv, action) {
|
|
1299
|
-
if (!messageDiv || !action || action.type === 'none') return;
|
|
1300
|
-
|
|
1301
|
-
const meta = getActionCardMeta(action);
|
|
1302
|
-
const card = document.createElement('div');
|
|
1303
|
-
card.classList.add('action-card');
|
|
1304
|
-
card.dataset.actionType = action.type || 'unknown';
|
|
1305
|
-
|
|
1306
|
-
const icon = document.createElement('span');
|
|
1307
|
-
icon.className = 'action-card-icon';
|
|
1308
|
-
icon.textContent = meta.icon;
|
|
1309
|
-
|
|
1310
|
-
const content = document.createElement('div');
|
|
1311
|
-
content.className = 'action-card-content';
|
|
1312
|
-
|
|
1313
|
-
const title = document.createElement('div');
|
|
1314
|
-
title.className = 'action-card-title';
|
|
1315
|
-
title.textContent = meta.title;
|
|
1316
|
-
content.appendChild(title);
|
|
1317
|
-
|
|
1318
|
-
if (meta.detail) {
|
|
1319
|
-
const detail = document.createElement('div');
|
|
1320
|
-
detail.className = 'action-card-detail';
|
|
1321
|
-
detail.textContent = meta.detail;
|
|
1322
|
-
content.appendChild(detail);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
card.appendChild(icon);
|
|
1326
|
-
card.appendChild(content);
|
|
1327
|
-
messageDiv.querySelector('.message-bubble')?.appendChild(card);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function getActionCardMeta(action) {
|
|
1331
|
-
const target = formatActionTarget(action);
|
|
1332
|
-
const type = action?.type || 'unknown';
|
|
1333
|
-
const targetOrFallback = target || 'No target';
|
|
1334
|
-
|
|
1335
|
-
const map = {
|
|
1336
|
-
open_url: ['🌐', 'Opened URL', target],
|
|
1337
|
-
search: ['🔍', 'Searched the web', target],
|
|
1338
|
-
open_app: ['🚀', 'Launched app', target],
|
|
1339
|
-
web_automation: ['🧭', 'Ran browser automation', target],
|
|
1340
|
-
create_folder: ['📁', 'Created folder', target],
|
|
1341
|
-
open_file: ['📄', 'Opened file', target],
|
|
1342
|
-
open_folder: ['📂', 'Opened folder', target],
|
|
1343
|
-
delete_file: ['🗑️', 'Deleted file', target],
|
|
1344
|
-
find_path: ['🔎', action.openAfter ? 'Found and opened path' : 'Found path', buildFindPathDetail(action)],
|
|
1345
|
-
clipboard_write: ['📋', 'Updated clipboard', target],
|
|
1346
|
-
learn_file: ['📚', 'Indexed file', target],
|
|
1347
|
-
learn_folder: ['📚', 'Indexed folder', target],
|
|
1348
|
-
system_info: ['💻', target ? 'Checked weather' : 'Checked system info', target],
|
|
1349
|
-
plugin: ['🔌', 'Ran plugin', target],
|
|
1350
|
-
mcp_tool: ['🧩', 'Called MCP tool', target],
|
|
1351
|
-
mouse_move: ['↗', 'Moved pointer', target],
|
|
1352
|
-
mouse_click: ['☝', 'Clicked screen', buildMouseDetail(action)],
|
|
1353
|
-
type_text: ['⌨', 'Typed text', target],
|
|
1354
|
-
key_tap: ['⌨', 'Pressed key', target],
|
|
1355
|
-
system_automation: ['⚙', 'Changed system setting', target]
|
|
1356
|
-
};
|
|
1357
|
-
|
|
1358
|
-
const [icon, title, detail] = map[type] || ['⚡', `Ran action: ${type}`, targetOrFallback];
|
|
1359
|
-
return { icon, title, detail };
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
function buildFindPathDetail(action) {
|
|
1363
|
-
const target = formatActionTarget(action);
|
|
1364
|
-
const typeLabel = action.pathType && action.pathType !== 'any' ? ` (${action.pathType})` : '';
|
|
1365
|
-
return target ? `${target}${typeLabel}` : typeLabel.trim();
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
function buildMouseDetail(action) {
|
|
1369
|
-
const point = formatActionTarget(action);
|
|
1370
|
-
const button = action.button ? `button ${action.button}` : 'left button';
|
|
1371
|
-
return point ? `${point} · ${button}` : button;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
function formatActionTarget(action) {
|
|
1375
|
-
if (!action || typeof action !== 'object') return '';
|
|
1376
|
-
if (action.server && action.target) return `${action.server}:${action.target}`;
|
|
1377
|
-
if (action.pluginName) return `${action.pluginName} ${action.target || ''}`.trim();
|
|
1378
|
-
if (action.target) return String(action.target);
|
|
1379
|
-
if (Number.isFinite(action.x) && Number.isFinite(action.y)) return `${action.x}, ${action.y}`;
|
|
1380
|
-
return '';
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
function getApprovalCopy(approval) {
|
|
1384
|
-
const action = approval?.action || {};
|
|
1385
|
-
const actionType = action.type || 'unknown';
|
|
1386
|
-
const target = formatActionTarget(action);
|
|
1387
|
-
const isDangerous = approval?.tier === 'dangerous';
|
|
1388
|
-
return {
|
|
1389
|
-
title: isDangerous ? 'Dangerous action requires approval' : 'Action requires approval',
|
|
1390
|
-
body: target ? `${actionType}: ${target}` : actionType,
|
|
1391
|
-
reason: approval?.reason || 'This action needs your permission before Mint can run it.',
|
|
1392
|
-
approveLabel: isDangerous ? 'Allow Dangerous Action' : 'Allow Action'
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
function appendApprovalCard(messageDiv, approval, activity = null) {
|
|
1397
|
-
if (!messageDiv || !approval?.action || !window.api?.executeApprovedAction) return;
|
|
1398
|
-
|
|
1399
|
-
const copy = getApprovalCopy(approval);
|
|
1400
|
-
const card = document.createElement('div');
|
|
1401
|
-
card.classList.add('action-card', 'approval-card');
|
|
1402
|
-
card.dataset.tier = approval.tier || 'approval';
|
|
1403
|
-
|
|
1404
|
-
const content = document.createElement('div');
|
|
1405
|
-
content.className = 'approval-card-content';
|
|
1406
|
-
|
|
1407
|
-
const title = document.createElement('div');
|
|
1408
|
-
title.className = 'approval-card-title';
|
|
1409
|
-
title.textContent = copy.title;
|
|
1410
|
-
|
|
1411
|
-
const body = document.createElement('div');
|
|
1412
|
-
body.className = 'approval-card-body';
|
|
1413
|
-
body.textContent = copy.body;
|
|
1414
|
-
|
|
1415
|
-
const reason = document.createElement('div');
|
|
1416
|
-
reason.className = 'approval-card-reason';
|
|
1417
|
-
reason.textContent = copy.reason;
|
|
1418
|
-
|
|
1419
|
-
content.appendChild(title);
|
|
1420
|
-
content.appendChild(body);
|
|
1421
|
-
content.appendChild(reason);
|
|
1422
|
-
|
|
1423
|
-
const actions = document.createElement('div');
|
|
1424
|
-
actions.className = 'approval-card-actions';
|
|
1425
|
-
|
|
1426
|
-
const approveBtn = document.createElement('button');
|
|
1427
|
-
approveBtn.type = 'button';
|
|
1428
|
-
approveBtn.className = 'approval-btn approval-btn-approve';
|
|
1429
|
-
approveBtn.textContent = copy.approveLabel;
|
|
1430
|
-
|
|
1431
|
-
const cancelBtn = document.createElement('button');
|
|
1432
|
-
cancelBtn.type = 'button';
|
|
1433
|
-
cancelBtn.className = 'approval-btn approval-btn-cancel';
|
|
1434
|
-
cancelBtn.textContent = 'Cancel';
|
|
1435
|
-
|
|
1436
|
-
const setDone = (message, state) => {
|
|
1437
|
-
approveBtn.disabled = true;
|
|
1438
|
-
cancelBtn.disabled = true;
|
|
1439
|
-
card.dataset.state = state;
|
|
1440
|
-
reason.textContent = message;
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
approveBtn.addEventListener('click', async () => {
|
|
1444
|
-
approveBtn.disabled = true;
|
|
1445
|
-
cancelBtn.disabled = true;
|
|
1446
|
-
reason.textContent = 'Running approved action...';
|
|
1447
|
-
const runStep = activity?.add('Running approved action', 'running', describeActionActivity(approval.action));
|
|
1448
|
-
setMintActivity('thinking');
|
|
1449
|
-
|
|
1450
|
-
try {
|
|
1451
|
-
const result = await window.api.executeApprovedAction(approval.action);
|
|
1452
|
-
if (!result || result.success === false) {
|
|
1453
|
-
setDone(result?.message || 'Action failed.', 'error');
|
|
1454
|
-
activity?.update(runStep, 'error', 'Action failed', result?.message || '');
|
|
1455
|
-
activity?.finish('error', 'Failed');
|
|
1456
|
-
setMintActivity('error');
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
setDone(result.message || 'Action completed.', 'approved');
|
|
1461
|
-
activity?.update(runStep, 'done', 'Action completed', result.message || describeActionActivity(approval.action));
|
|
1462
|
-
activity?.finish('done', 'Completed');
|
|
1463
|
-
setMintActivity('idle');
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
console.error('[Approval] Failed to execute action:', error);
|
|
1466
|
-
setDone(error.message || 'Action failed.', 'error');
|
|
1467
|
-
activity?.update(runStep, 'error', 'Action failed', error.message || '');
|
|
1468
|
-
activity?.finish('error', 'Failed');
|
|
1469
|
-
setMintActivity('error');
|
|
1470
|
-
}
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
cancelBtn.addEventListener('click', () => {
|
|
1474
|
-
setDone('Cancelled by user.', 'cancelled');
|
|
1475
|
-
activity?.add('Approval cancelled', 'cancelled');
|
|
1476
|
-
activity?.finish('cancelled', 'Cancelled');
|
|
1477
|
-
setMintActivity('idle');
|
|
1478
|
-
});
|
|
1479
|
-
|
|
1480
|
-
actions.appendChild(approveBtn);
|
|
1481
|
-
actions.appendChild(cancelBtn);
|
|
1482
|
-
card.appendChild(content);
|
|
1483
|
-
card.appendChild(actions);
|
|
1484
|
-
|
|
1485
|
-
messageDiv.querySelector('.message-bubble')?.appendChild(card);
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
function showTyping() {
|
|
1489
|
-
const typingDiv = document.createElement('div');
|
|
1490
|
-
typingDiv.classList.add('message', 'ai-message', 'typing-message');
|
|
1491
|
-
typingDiv.id = 'typing-indicator';
|
|
1492
|
-
|
|
1493
|
-
const indicator = document.createElement('div');
|
|
1494
|
-
indicator.classList.add('typing-indicator');
|
|
1495
|
-
indicator.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
|
|
1496
|
-
|
|
1497
|
-
typingDiv.appendChild(indicator);
|
|
1498
|
-
chatContainer.appendChild(typingDiv);
|
|
1499
|
-
scrollToBottom();
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function removeTyping() {
|
|
1503
|
-
const typingDiv = document.getElementById('typing-indicator');
|
|
1504
|
-
if (typingDiv) {
|
|
1505
|
-
typingDiv.remove();
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
function scrollToBottom() {
|
|
1510
|
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
function loadScript(src) {
|
|
1514
|
-
return new Promise((resolve, reject) => {
|
|
1515
|
-
if (document.querySelector(`script[src="${src}"]`)) {
|
|
1516
|
-
resolve();
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
const script = document.createElement('script');
|
|
1520
|
-
script.src = src;
|
|
1521
|
-
script.onload = resolve;
|
|
1522
|
-
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
|
1523
|
-
document.body.appendChild(script);
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function hideStartupLoading() {
|
|
1528
|
-
appContainer?.classList.remove('is-loading');
|
|
1529
|
-
if (!startupLoading) return;
|
|
1530
|
-
startupLoading.classList.add('is-hidden');
|
|
1531
|
-
setTimeout(() => startupLoading.remove(), 400);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
async function loadLive2DWhenIdle() {
|
|
1535
|
-
if (!modelMount || window.Live2DManager) {
|
|
1536
|
-
hideStartupLoading();
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
try {
|
|
1540
|
-
await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
|
|
1541
|
-
await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
|
|
1542
|
-
await loadScript('../../node_modules/pixi-live2d-display/dist/cubism4.min.js');
|
|
1543
|
-
await loadScript('live2d_manager.js');
|
|
1544
|
-
if (window.Live2DManager) {
|
|
1545
|
-
await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
|
|
1546
|
-
applyModelPanelControlState();
|
|
1547
|
-
}
|
|
1548
|
-
} catch (err) {
|
|
1549
|
-
console.error('[Live2D] Deferred load failed:', err);
|
|
1550
|
-
if (modelStatus) {
|
|
1551
|
-
modelStatus.classList.add('is-error');
|
|
1552
|
-
modelStatus.textContent = 'Live2D model unavailable.';
|
|
1553
|
-
}
|
|
1554
|
-
} finally {
|
|
1555
|
-
hideStartupLoading();
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
async function loadChatHistory() {
|
|
1560
|
-
try {
|
|
1561
|
-
const history = await window.api.getChatHistory();
|
|
1562
|
-
const initial = chatContainer.querySelector('.message.initial');
|
|
1563
|
-
|
|
1564
|
-
if (!Array.isArray(history) || history.length === 0) {
|
|
1565
|
-
if (initial) {
|
|
1566
|
-
initial.style.display = 'flex';
|
|
1567
|
-
initial.style.opacity = '1';
|
|
1568
|
-
}
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
if (initial) {
|
|
1573
|
-
initial.remove();
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
for (const item of history) {
|
|
1577
|
-
if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
|
|
1578
|
-
const sender = item.sender === 'user' ? 'user' : 'ai';
|
|
1579
|
-
if (sender === 'user' && !String(item.text).startsWith('Model interaction:')) {
|
|
1580
|
-
rememberConversationLanguage(item.text);
|
|
1581
|
-
}
|
|
1582
|
-
appendMessage(item.text, sender, null, item.timestamp, {
|
|
1583
|
-
providerInfo: sender === 'ai' ? item.providerInfo : null
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
console.error('Failed to load chat history:', error);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
async function sendTextMessage(text, options = {}) {
|
|
1592
|
-
const cleanText = (text || '').trim();
|
|
1593
|
-
const allowSmartContext = options.allowSmartContext !== false;
|
|
1594
|
-
const includePendingImage = options.includePendingImage !== false;
|
|
1595
|
-
const displayText = options.displayText !== undefined ? options.displayText : cleanText;
|
|
1596
|
-
const trackLanguage = options.trackLanguage !== false;
|
|
1597
|
-
|
|
1598
|
-
// We can send either a text message, an image, or both.
|
|
1599
|
-
if (!cleanText && (!includePendingImage || !currentBase64Image)) return;
|
|
1600
|
-
|
|
1601
|
-
// Cache the image for sending and UI, then clear
|
|
1602
|
-
let imageToSend = includePendingImage ? currentBase64Image : null;
|
|
1603
|
-
|
|
1604
|
-
// Clear input & UI for explicit images
|
|
1605
|
-
chatInput.value = '';
|
|
1606
|
-
if (includePendingImage) {
|
|
1607
|
-
currentBase64Image = null;
|
|
1608
|
-
imagePreviewContainer.style.display = 'none';
|
|
1609
|
-
imagePreview.src = '';
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
const now = new Date().toISOString();
|
|
1613
|
-
|
|
1614
|
-
// Show user message (with explicit image if available)
|
|
1615
|
-
appendMessage(displayText, 'user', imageToSend, now);
|
|
1616
|
-
if (trackLanguage) {
|
|
1617
|
-
rememberConversationLanguage(displayText || cleanText);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
const activity = shouldShowAgentActivity(options) ? createAgentActivityCard() : null;
|
|
1621
|
-
const contextStep = activity?.add('Preparing desktop context', 'running');
|
|
1622
|
-
|
|
1623
|
-
// Show typing early so user knows we are processing
|
|
1624
|
-
showTyping();
|
|
1625
|
-
setMintActivity('thinking');
|
|
1626
|
-
|
|
1627
|
-
let messageToSend = cleanText;
|
|
1628
|
-
|
|
1629
|
-
// Check Smart Context Toggle
|
|
1630
|
-
const smartToggle = document.getElementById('smart-context-toggle');
|
|
1631
|
-
if (allowSmartContext && smartToggle && smartToggle.checked && !imageToSend) {
|
|
1632
|
-
try {
|
|
1633
|
-
const [silentCapture, smartContext] = await Promise.all([
|
|
1634
|
-
window.api.captureSilentScreen(),
|
|
1635
|
-
window.api.getSmartContext ? window.api.getSmartContext() : Promise.resolve(null)
|
|
1636
|
-
]);
|
|
1637
|
-
if (silentCapture) {
|
|
1638
|
-
// Set imageToSend so it gets sent to the API, but we already appended the chat bubble
|
|
1639
|
-
imageToSend = silentCapture;
|
|
1640
|
-
}
|
|
1641
|
-
if (smartContext) {
|
|
1642
|
-
messageToSend = appendSmartContextToMessage(cleanText, smartContext);
|
|
1643
|
-
}
|
|
1644
|
-
if (activity && contextStep) {
|
|
1645
|
-
activity.update(
|
|
1646
|
-
contextStep,
|
|
1647
|
-
'done',
|
|
1648
|
-
'Read Smart Context',
|
|
1649
|
-
describeSmartContextActivity(smartContext, Boolean(silentCapture))
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
} catch (err) {
|
|
1653
|
-
console.error("Smart Context capture failed:", err);
|
|
1654
|
-
activity?.update(contextStep, 'error', 'Smart Context unavailable', err.message || '');
|
|
1655
|
-
}
|
|
1656
|
-
} else if (activity && contextStep) {
|
|
1657
|
-
activity.update(contextStep, 'skipped', 'Smart Context skipped', imageToSend ? 'image already attached' : 'toggle is off');
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
// Hide proactive bar if user is actively typing a message
|
|
1661
|
-
hideProactiveBar();
|
|
1662
|
-
const modelStep = activity?.add('Waiting for model response', 'running');
|
|
1663
|
-
|
|
1664
|
-
try {
|
|
1665
|
-
// Send to main process (text, image, audio=null)
|
|
1666
|
-
const response = await window.api.sendMessage(messageToSend, imageToSend, null);
|
|
1667
|
-
removeTyping();
|
|
1668
|
-
activity?.update(modelStep, 'done', 'Model response received');
|
|
1669
|
-
|
|
1670
|
-
if (typeof response.response !== 'string') {
|
|
1671
|
-
response.response = normalizeAiText(response.response);
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// Handle system_info action: fetch data and append to AI message
|
|
1675
|
-
if (response.action && response.action.type === 'system_info') {
|
|
1676
|
-
const infoStep = activity?.add('Running local info action', 'running', describeActionActivity(response.action));
|
|
1677
|
-
const city = (response.action.target || '').trim();
|
|
1678
|
-
// Only treat as weather if city looks like a real location name (not blank, not 'date', not 'time')
|
|
1679
|
-
const weatherKeywords = ['date', 'time', 'วัน', 'เวลา', 'today', 'now'];
|
|
1680
|
-
const isWeather = city && !weatherKeywords.some(k => city.toLowerCase().includes(k));
|
|
1681
|
-
|
|
1682
|
-
if (isWeather) {
|
|
1683
|
-
// Weather query
|
|
1684
|
-
const weather = await window.api.getWeather(city);
|
|
1685
|
-
response.response += `\n\n🌡️ ${weather.data}`;
|
|
1686
|
-
activity?.update(infoStep, 'done', 'Weather info added', city);
|
|
1687
|
-
} else {
|
|
1688
|
-
// General system info (date, time, RAM, CPU)
|
|
1689
|
-
const info = await window.api.getSystemInfo();
|
|
1690
|
-
const machine = info.machine && info.machine.display ? `\n🖥️ รุ่นเครื่อง: ${info.machine.display}` : '';
|
|
1691
|
-
const distro = info.distro ? `\nระบบ: ${info.distro}` : '';
|
|
1692
|
-
response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}${machine}${distro}\n💻 CPU: ${info.cpu.model} (${info.cpu.cores} คอร์)\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
|
|
1693
|
-
activity?.update(infoStep, 'done', 'System info added');
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// Show AI response
|
|
1698
|
-
const msgDiv = await appendAiMessages(response.response, {
|
|
1699
|
-
allowDelay: true,
|
|
1700
|
-
timestamp: response.timestamp,
|
|
1701
|
-
providerInfo: response.providerInfo
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
// Speak AI response
|
|
1705
|
-
await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
|
|
1706
|
-
notifyAiIfNeeded();
|
|
1707
|
-
|
|
1708
|
-
// Append action card if applicable
|
|
1709
|
-
if (response.approval?.required) {
|
|
1710
|
-
activity?.add('Selected action', 'approval', describeActionActivity(response.approval.action));
|
|
1711
|
-
activity?.add('Waiting for approval', 'running', response.approval.reason || '');
|
|
1712
|
-
activity?.finish('waiting', 'Waiting');
|
|
1713
|
-
appendApprovalCard(msgDiv, response.approval, activity);
|
|
1714
|
-
} else if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
|
|
1715
|
-
activity?.add('Selected action', 'done', describeActionActivity(response.action));
|
|
1716
|
-
appendActionCard(msgDiv, response.action);
|
|
1717
|
-
activity?.finish('done', 'Completed');
|
|
1718
|
-
} else if (response.action && response.action.type === 'system_info') {
|
|
1719
|
-
activity?.add('Selected action', 'done', describeActionActivity(response.action));
|
|
1720
|
-
activity?.finish('done', 'Completed');
|
|
1721
|
-
} else {
|
|
1722
|
-
activity?.add('No desktop action selected', 'done');
|
|
1723
|
-
activity?.finish('done', 'Completed');
|
|
1724
|
-
}
|
|
1725
|
-
} catch (error) {
|
|
1726
|
-
removeTyping();
|
|
1727
|
-
setMintActivity('error');
|
|
1728
|
-
activity?.update(modelStep, 'error', 'Model request failed', error.message || '');
|
|
1729
|
-
activity?.finish('error', 'Failed');
|
|
1730
|
-
appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
|
|
1731
|
-
console.error(error);
|
|
1732
|
-
resumeSpeechIfNeeded();
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
chatForm.addEventListener('submit', throttle(async (e) => {
|
|
1737
|
-
e.preventDefault();
|
|
1738
|
-
const text = chatInput.value.trim();
|
|
1739
|
-
await sendTextMessage(text);
|
|
1740
|
-
}, 500));
|
|
1741
|
-
|
|
1742
|
-
window.addEventListener('live2d-model-interaction', async (event) => {
|
|
1743
|
-
const prompt = event?.detail?.prompt;
|
|
1744
|
-
if (!prompt) return;
|
|
1745
|
-
setMintActivity('thinking');
|
|
1746
|
-
const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
|
|
1747
|
-
const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
|
|
1748
|
-
await sendTextMessage(interactionPrompt, {
|
|
1749
|
-
allowSmartContext: false,
|
|
1750
|
-
includePendingImage: false,
|
|
1751
|
-
trackLanguage: false,
|
|
1752
|
-
displayText: `${displayPrefix}: ${event.detail.label || event.detail.region || 'Interaction'}`
|
|
1753
|
-
});
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
|
-
// --- Image Paste and Drag-n-Drop Support ---
|
|
1757
|
-
function handleImageFile(file) {
|
|
1758
|
-
if (!file || !file.type.startsWith('image/')) return;
|
|
1759
|
-
const reader = new FileReader();
|
|
1760
|
-
reader.onload = (e) => {
|
|
1761
|
-
currentBase64Image = e.target.result;
|
|
1762
|
-
imagePreview.src = currentBase64Image;
|
|
1763
|
-
imagePreviewContainer.style.display = 'block';
|
|
1764
|
-
chatInput.focus();
|
|
1765
|
-
};
|
|
1766
|
-
reader.readAsDataURL(file);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// Paste Event
|
|
1770
|
-
chatInput.addEventListener('paste', (e) => {
|
|
1771
|
-
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
|
1772
|
-
for (let index in items) {
|
|
1773
|
-
const item = items[index];
|
|
1774
|
-
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
|
1775
|
-
const blob = item.getAsFile();
|
|
1776
|
-
handleImageFile(blob);
|
|
1777
|
-
break; // Handle only the first image
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
// Drag and Drop Events (on the whole chat form/input area)
|
|
1783
|
-
const inputArea = document.querySelector('.input-area');
|
|
1784
|
-
|
|
1785
|
-
inputArea.addEventListener('dragover', (e) => {
|
|
1786
|
-
e.preventDefault();
|
|
1787
|
-
e.stopPropagation();
|
|
1788
|
-
inputArea.style.opacity = '0.7'; // Visual feedback
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
inputArea.addEventListener('dragleave', (e) => {
|
|
1792
|
-
e.preventDefault();
|
|
1793
|
-
e.stopPropagation();
|
|
1794
|
-
inputArea.style.opacity = '1';
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
|
-
inputArea.addEventListener('drop', (e) => {
|
|
1798
|
-
e.preventDefault();
|
|
1799
|
-
e.stopPropagation();
|
|
1800
|
-
inputArea.style.opacity = '1';
|
|
1801
|
-
|
|
1802
|
-
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
1803
|
-
handleImageFile(e.dataTransfer.files[0]);
|
|
1804
|
-
}
|
|
1805
|
-
});
|
|
1806
|
-
|
|
1807
|
-
// Focus input on load + init theme
|
|
1808
|
-
window.addEventListener('DOMContentLoaded', async () => {
|
|
1809
|
-
chatInput.focus();
|
|
1810
|
-
await loadTheme();
|
|
1811
|
-
setMintActivity('idle');
|
|
1812
|
-
await loadChatHistory();
|
|
1813
|
-
loadLive2DWhenIdle();
|
|
1814
|
-
});
|
|
1815
|
-
|
|
1816
|
-
// Proactive OS Notifications (Battery, Network, etc.)
|
|
1817
|
-
window.api.onProactiveNotification((data) => {
|
|
1818
|
-
if (!data || !data.message) return;
|
|
1819
|
-
appendMessage(data.message, 'ai');
|
|
1820
|
-
// Also speak the notification automatically
|
|
1821
|
-
speakText(data.message);
|
|
1822
|
-
});
|
|
1823
|
-
|
|
1824
|
-
window.addEventListener('focus', () => {
|
|
1825
|
-
if (window.api.clearAiNotifications) window.api.clearAiNotifications();
|
|
1826
|
-
});
|
|
1827
|
-
|
|
1828
|
-
document.addEventListener('click', closeProviderPopover);
|
|
1829
|
-
document.addEventListener('keydown', (event) => {
|
|
1830
|
-
if (event.key === 'Escape') closeProviderPopover();
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
// =====================
|
|
1834
|
-
// Proactive Smart Suggestion Engine
|
|
1835
|
-
// =====================
|
|
1836
|
-
|
|
1837
|
-
function showProactiveBar(data) {
|
|
1838
|
-
// Clear old chips
|
|
1839
|
-
proactiveChips.innerHTML = '';
|
|
1840
|
-
|
|
1841
|
-
// Set message
|
|
1842
|
-
proactiveMessage.textContent = data.message || '';
|
|
1843
|
-
|
|
1844
|
-
// Render each suggestion as a chip
|
|
1845
|
-
data.suggestions.forEach((item, index) => {
|
|
1846
|
-
const chip = document.createElement('button');
|
|
1847
|
-
chip.className = 'suggestion-chip';
|
|
1848
|
-
chip.textContent = item.label;
|
|
1849
|
-
chip.style.animationDelay = `${index * 60}ms`;
|
|
1850
|
-
|
|
1851
|
-
chip.addEventListener('click', async () => {
|
|
1852
|
-
hideProactiveBar();
|
|
1853
|
-
|
|
1854
|
-
if (window.api.recordBehavior) {
|
|
1855
|
-
window.api.recordBehavior(`User picked: ${item.label}`);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
showTyping();
|
|
1859
|
-
try {
|
|
1860
|
-
const result = await window.api.executeProactiveAction(item.action);
|
|
1861
|
-
removeTyping();
|
|
1862
|
-
const confirmText = result?.message || `เปิด ${item.label} แล้วค่ะ ✅`;
|
|
1863
|
-
const msgDiv = appendMessage(confirmText, 'ai');
|
|
1864
|
-
speakText(confirmText);
|
|
1865
|
-
if (item.action && item.action.type !== 'none') {
|
|
1866
|
-
appendActionCard(msgDiv, item.action);
|
|
1867
|
-
}
|
|
1868
|
-
} catch (err) {
|
|
1869
|
-
removeTyping();
|
|
1870
|
-
appendMessage('ขออภัยค่ะ เกิดข้อผิดพลาด', 'ai');
|
|
1871
|
-
console.error('[Chip] Error:', err);
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
proactiveChips.appendChild(chip);
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
// Show bar with animation reset
|
|
1879
|
-
proactiveBar.style.display = 'none';
|
|
1880
|
-
requestAnimationFrame(() => {
|
|
1881
|
-
proactiveBar.style.display = 'block';
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
function hideProactiveBar() {
|
|
1886
|
-
proactiveBar.style.display = 'none';
|
|
1887
|
-
proactiveChips.innerHTML = '';
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// Receive multi-suggestion data from main process
|
|
1891
|
-
window.api.onProactiveSuggestion((data) => {
|
|
1892
|
-
if (data && data.message && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
|
|
1893
|
-
showProactiveBar(data);
|
|
1894
|
-
notifyAiIfNeeded();
|
|
1895
|
-
}
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
// Dismiss button
|
|
1899
|
-
proactiveDismissBtn.addEventListener('click', () => {
|
|
1900
|
-
hideProactiveBar();
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
// Sync Smart Context toggle → start/stop proactive loop
|
|
1904
|
-
const smartContextToggle = document.getElementById('smart-context-toggle');
|
|
1905
|
-
if (smartContextToggle) {
|
|
1906
|
-
smartContextToggle.addEventListener('change', () => {
|
|
1907
|
-
window.api.toggleProactive(smartContextToggle.checked);
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
// Toggle Live2D Model visibility
|
|
1912
|
-
const toggleModelBtn = document.getElementById('toggle-model-btn');
|
|
1913
|
-
const assistantWorkspace = document.querySelector('.assistant-workspace');
|
|
1914
|
-
const modelLockBtn = document.getElementById('model-lock-btn');
|
|
1915
|
-
const modelScaleSlider = document.getElementById('model-scale-slider');
|
|
1916
|
-
const modelScaleValue = document.getElementById('model-scale-value');
|
|
1917
|
-
const modelScaleResetBtn = document.getElementById('model-scale-reset-btn');
|
|
1918
|
-
const modelBgBtn = document.getElementById('model-bg-btn');
|
|
1919
|
-
const layoutPresetBtns = document.querySelectorAll('.layout-preset-btn');
|
|
1920
|
-
|
|
1921
|
-
const modelBgStorageKey = 'mint-model-background';
|
|
1922
|
-
const modelScaleStorageKey = 'mint-model-scale';
|
|
1923
|
-
const modelPositionLockStorageKey = 'mint-model-position-locked';
|
|
1924
|
-
const workspaceLayoutStorageKey = 'mint-workspace-layout';
|
|
1925
|
-
const modelBgClasses = ['model-bg-default', 'model-bg-clear', 'model-bg-grid', 'model-bg-stage'];
|
|
1926
|
-
const modelBgLabels = ['Default background', 'Clear background', 'Grid background', 'Stage background'];
|
|
1927
|
-
const workspaceLayoutClasses = ['layout-chat'];
|
|
1928
|
-
const workspaceLayoutPresets = ['companion', 'chat'];
|
|
1929
|
-
|
|
1930
|
-
function setModelHidden(isHidden) {
|
|
1931
|
-
if (!assistantWorkspace || !toggleModelBtn) return;
|
|
1932
|
-
assistantWorkspace.classList.toggle('model-hidden', Boolean(isHidden));
|
|
1933
|
-
toggleModelBtn.classList.toggle('active', Boolean(isHidden));
|
|
1934
|
-
toggleModelBtn.setAttribute('aria-pressed', String(Boolean(isHidden)));
|
|
1935
|
-
localStorage.setItem('mint-model-hidden', String(Boolean(isHidden)));
|
|
1936
|
-
|
|
1937
|
-
if (!isHidden && window.Live2DManager && Live2DManager.model) {
|
|
1938
|
-
setTimeout(() => {
|
|
1939
|
-
window.dispatchEvent(new Event('resize'));
|
|
1940
|
-
if (typeof Live2DManager.fitModelToMount === 'function') {
|
|
1941
|
-
Live2DManager.fitModelToMount();
|
|
1942
|
-
}
|
|
1943
|
-
}, 450);
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
function setModelPositionLocked(isLocked) {
|
|
1948
|
-
const locked = Boolean(isLocked);
|
|
1949
|
-
localStorage.setItem(modelPositionLockStorageKey, String(locked));
|
|
1950
|
-
modelLockBtn?.classList.toggle('is-active', locked);
|
|
1951
|
-
modelLockBtn?.setAttribute('aria-pressed', String(locked));
|
|
1952
|
-
modelLockBtn?.setAttribute('title', locked ? 'Unlock model position' : 'Lock model position');
|
|
1953
|
-
if (window.Live2DManager) {
|
|
1954
|
-
Live2DManager.setPointerTrackingEnabled(!locked);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
function setModelBackground(index) {
|
|
1959
|
-
if (!modelShell) return;
|
|
1960
|
-
const normalized = ((Number(index) || 0) + modelBgClasses.length) % modelBgClasses.length;
|
|
1961
|
-
modelBgClasses.forEach(className => modelShell.classList.remove(className));
|
|
1962
|
-
if (normalized > 0) {
|
|
1963
|
-
modelShell.classList.add(modelBgClasses[normalized]);
|
|
1964
|
-
}
|
|
1965
|
-
localStorage.setItem(modelBgStorageKey, String(normalized));
|
|
1966
|
-
modelBgBtn?.setAttribute('title', modelBgLabels[normalized]);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function setModelScale(value) {
|
|
1970
|
-
const next = Math.max(78, Math.min(128, Number(value) || 100));
|
|
1971
|
-
localStorage.setItem(modelScaleStorageKey, String(next));
|
|
1972
|
-
if (modelScaleSlider) modelScaleSlider.value = String(next);
|
|
1973
|
-
if (modelScaleValue) modelScaleValue.textContent = `${(next / 100).toFixed(2)}x`;
|
|
1974
|
-
if (window.Live2DManager) {
|
|
1975
|
-
Live2DManager.setZoomMultiplier(next / 100);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
function applyModelPanelControlState() {
|
|
1980
|
-
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) === 'true');
|
|
1981
|
-
setModelBackground(Number(localStorage.getItem(modelBgStorageKey) || 0));
|
|
1982
|
-
setModelScale(Number(localStorage.getItem(modelScaleStorageKey) || 100));
|
|
1983
|
-
setWorkspaceLayout(localStorage.getItem(workspaceLayoutStorageKey) || 'companion');
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
function setWorkspaceLayout(layout) {
|
|
1987
|
-
if (!assistantWorkspace) return;
|
|
1988
|
-
const normalized = workspaceLayoutPresets.includes(layout) ? layout : 'companion';
|
|
1989
|
-
workspaceLayoutClasses.forEach(className => assistantWorkspace.classList.remove(className));
|
|
1990
|
-
if (normalized !== 'companion') {
|
|
1991
|
-
assistantWorkspace.classList.add(`layout-${normalized}`);
|
|
1992
|
-
}
|
|
1993
|
-
localStorage.setItem(workspaceLayoutStorageKey, normalized);
|
|
1994
|
-
layoutPresetBtns.forEach((button) => {
|
|
1995
|
-
const isActive = button.dataset.layoutPreset === normalized;
|
|
1996
|
-
button.classList.toggle('is-active', isActive);
|
|
1997
|
-
button.setAttribute('aria-pressed', String(isActive));
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
if (toggleModelBtn && assistantWorkspace) {
|
|
2002
|
-
toggleModelBtn.addEventListener('click', () => {
|
|
2003
|
-
setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
// Restore preference on load
|
|
2007
|
-
const savedModelHidden = localStorage.getItem('mint-model-hidden');
|
|
2008
|
-
const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
|
|
2009
|
-
if (savedHidden) {
|
|
2010
|
-
setModelHidden(true);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
modelLockBtn?.addEventListener('click', () => {
|
|
2015
|
-
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) !== 'true');
|
|
2016
|
-
});
|
|
2017
|
-
modelScaleSlider?.addEventListener('input', (event) => setModelScale(event.target.value));
|
|
2018
|
-
modelScaleResetBtn?.addEventListener('click', () => setModelScale(100));
|
|
2019
|
-
modelBgBtn?.addEventListener('click', () => {
|
|
2020
|
-
const current = Number(localStorage.getItem(modelBgStorageKey) || 0);
|
|
2021
|
-
setModelBackground(current + 1);
|
|
2022
|
-
});
|
|
2023
|
-
layoutPresetBtns.forEach((button) => {
|
|
2024
|
-
button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layoutPreset));
|
|
2025
|
-
});
|
|
2026
|
-
|
|
2027
|
-
applyModelPanelControlState();
|
|
2028
|
-
|
|
2029
|
-
// Cycle Shiroko's Expression
|
|
2030
|
-
const changeExpressionBtn = document.getElementById('change-expression-btn');
|
|
2031
|
-
if (changeExpressionBtn) {
|
|
2032
|
-
changeExpressionBtn.addEventListener('click', () => {
|
|
2033
|
-
if (window.Live2DManager) {
|
|
2034
|
-
Live2DManager.cycleExpression();
|
|
2035
|
-
}
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// Cycle Live2D accessories
|
|
2040
|
-
const accessoryStorageKey = 'mint-live2d-accessories';
|
|
2041
|
-
const accessoryCycleBtn = document.getElementById('accessory-cycle-btn');
|
|
2042
|
-
const accessoryCycleLabel = document.getElementById('accessory-cycle-label');
|
|
2043
|
-
const accessoryCycleOrder = [null, 'glasses', 'pen', 'cat'];
|
|
2044
|
-
const accessoryLabels = {
|
|
2045
|
-
glasses: 'Glasses',
|
|
2046
|
-
pen: 'Pen',
|
|
2047
|
-
cat: 'Cat'
|
|
2048
|
-
};
|
|
2049
|
-
let savedAccessories = {};
|
|
2050
|
-
try {
|
|
2051
|
-
savedAccessories = JSON.parse(localStorage.getItem(accessoryStorageKey) || '{}') || {};
|
|
2052
|
-
} catch (_) {
|
|
2053
|
-
savedAccessories = {};
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
const getSavedAccessoryId = () => accessoryCycleOrder.find(id => id && savedAccessories[id] === true) || null;
|
|
2057
|
-
|
|
2058
|
-
function updateAccessoryCycleButton(accessoryId) {
|
|
2059
|
-
if (!accessoryCycleBtn) return;
|
|
2060
|
-
const isActive = Boolean(accessoryId);
|
|
2061
|
-
const label = accessoryId ? accessoryLabels[accessoryId] : 'Accessory';
|
|
2062
|
-
accessoryCycleBtn.classList.toggle('active', isActive);
|
|
2063
|
-
accessoryCycleBtn.setAttribute('aria-pressed', String(isActive));
|
|
2064
|
-
accessoryCycleBtn.title = `Accessory: ${label}`;
|
|
2065
|
-
if (accessoryCycleLabel) accessoryCycleLabel.textContent = label;
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
let currentAccessoryId = getSavedAccessoryId();
|
|
2069
|
-
updateAccessoryCycleButton(currentAccessoryId);
|
|
2070
|
-
|
|
2071
|
-
if (accessoryCycleBtn) {
|
|
2072
|
-
accessoryCycleBtn.addEventListener('click', () => {
|
|
2073
|
-
const currentIndex = accessoryCycleOrder.indexOf(currentAccessoryId);
|
|
2074
|
-
currentAccessoryId = accessoryCycleOrder[(currentIndex + 1) % accessoryCycleOrder.length];
|
|
2075
|
-
updateAccessoryCycleButton(currentAccessoryId);
|
|
2076
|
-
|
|
2077
|
-
if (window.Live2DManager) {
|
|
2078
|
-
Live2DManager.setExclusiveAccessory(currentAccessoryId, true);
|
|
2079
|
-
} else {
|
|
2080
|
-
savedAccessories = {};
|
|
2081
|
-
if (currentAccessoryId) savedAccessories[currentAccessoryId] = true;
|
|
2082
|
-
localStorage.setItem(accessoryStorageKey, JSON.stringify(savedAccessories));
|
|
2083
|
-
}
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// Toggle Live2D model interaction
|
|
2088
|
-
const toggleInteractionBtn = document.getElementById('toggle-interaction-btn');
|
|
2089
|
-
if (toggleInteractionBtn) {
|
|
2090
|
-
const savedInteractionEnabled = localStorage.getItem('mint-model-interaction-enabled') !== 'false';
|
|
2091
|
-
toggleInteractionBtn.classList.toggle('active', savedInteractionEnabled);
|
|
2092
|
-
toggleInteractionBtn.setAttribute('aria-pressed', String(savedInteractionEnabled));
|
|
2093
|
-
if (window.Live2DManager) {
|
|
2094
|
-
Live2DManager.setInteractionEnabled(savedInteractionEnabled);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
toggleInteractionBtn.addEventListener('click', () => {
|
|
2098
|
-
const isEnabled = !toggleInteractionBtn.classList.contains('active');
|
|
2099
|
-
toggleInteractionBtn.classList.toggle('active', isEnabled);
|
|
2100
|
-
toggleInteractionBtn.setAttribute('aria-pressed', String(isEnabled));
|
|
2101
|
-
if (window.Live2DManager) {
|
|
2102
|
-
Live2DManager.setInteractionEnabled(isEnabled, true);
|
|
2103
|
-
} else {
|
|
2104
|
-
localStorage.setItem('mint-model-interaction-enabled', String(isEnabled));
|
|
2105
|
-
}
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
// Toggle Live2D interaction area guide
|
|
2110
|
-
const interactionGuideBtn = document.getElementById('interaction-guide-btn');
|
|
2111
|
-
if (interactionGuideBtn && modelShell) {
|
|
2112
|
-
const savedGuideVisible = localStorage.getItem('mint-interaction-guide-visible') === 'true';
|
|
2113
|
-
modelShell.classList.toggle('show-interaction-guide', savedGuideVisible);
|
|
2114
|
-
interactionGuideBtn.classList.toggle('active', savedGuideVisible);
|
|
2115
|
-
|
|
2116
|
-
interactionGuideBtn.addEventListener('click', () => {
|
|
2117
|
-
const isVisible = modelShell.classList.toggle('show-interaction-guide');
|
|
2118
|
-
interactionGuideBtn.classList.toggle('active', isVisible);
|
|
2119
|
-
localStorage.setItem('mint-interaction-guide-visible', String(isVisible));
|
|
2120
|
-
});
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// Spotlight integration
|
|
2124
|
-
window.api.onSpotlightToChat((query) => {
|
|
2125
|
-
chatInput.value = query;
|
|
2126
|
-
chatForm.dispatchEvent(new Event('submit'));
|
|
2127
|
-
});
|