@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
|
@@ -0,0 +1,1598 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type DragEvent, type FormEvent, type KeyboardEvent, type RefObject } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
type AgentProgress,
|
|
4
|
+
type ChatResponse,
|
|
5
|
+
type RuntimeStatus,
|
|
6
|
+
getTtsUrls,
|
|
7
|
+
} from '../tauri'
|
|
8
|
+
|
|
9
|
+
const GEMINI_MODELS = [
|
|
10
|
+
'gemini-2.5-flash',
|
|
11
|
+
'gemini-2.5-pro',
|
|
12
|
+
'gemini-2.0-flash',
|
|
13
|
+
'gemini-1.5-flash',
|
|
14
|
+
'gemini-1.5-pro',
|
|
15
|
+
'gemini-3.1-flash-lite',
|
|
16
|
+
'gemini-3.1-flash-lite-preview'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const OPENAI_MODELS = [
|
|
20
|
+
'gpt-4o',
|
|
21
|
+
'gpt-4o-mini',
|
|
22
|
+
'o1',
|
|
23
|
+
'o3-mini',
|
|
24
|
+
'o1-preview',
|
|
25
|
+
'o1-mini',
|
|
26
|
+
'gpt-4-turbo'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const OPENROUTER_MODELS = [
|
|
30
|
+
'openai/gpt-4o-mini',
|
|
31
|
+
'openai/gpt-4o',
|
|
32
|
+
'anthropic/claude-3.5-sonnet',
|
|
33
|
+
'anthropic/claude-3.5-haiku',
|
|
34
|
+
'google/gemini-2.5-flash',
|
|
35
|
+
'meta-llama/llama-3.3-70b-instruct',
|
|
36
|
+
'mistralai/mistral-large'
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const DEEPSEEK_MODELS = [
|
|
40
|
+
'deepseek-v4-flash',
|
|
41
|
+
'deepseek-v4-pro',
|
|
42
|
+
'deepseek-chat',
|
|
43
|
+
'deepseek-reasoner'
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const ANTHROPIC_MODELS = [
|
|
47
|
+
'claude-3-7-sonnet-latest',
|
|
48
|
+
'claude-3-5-sonnet-latest',
|
|
49
|
+
'claude-3-5-haiku-latest',
|
|
50
|
+
'claude-3-opus-latest'
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const HF_MODELS = [
|
|
54
|
+
'meta-llama/Llama-3.3-70B-Instruct',
|
|
55
|
+
'meta-llama/Meta-Llama-3-8B-Instruct',
|
|
56
|
+
'meta-llama/Llama-3.2-3B-Instruct',
|
|
57
|
+
'Qwen/Qwen2.5-72B-Instruct',
|
|
58
|
+
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
59
|
+
'mistralai/Mistral-7B-Instruct-v0.3',
|
|
60
|
+
'google/gemma-2-9b-it'
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const LOCAL_MODELS = [
|
|
64
|
+
'local-model',
|
|
65
|
+
'Qwen/Qwen2.5-7B-Instruct-GGUF',
|
|
66
|
+
'meta-llama/Llama-3.2-3B-Instruct-GGUF',
|
|
67
|
+
'lmstudio-community/gemma-2-9b-it-GGUF'
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const OLLAMA_MODELS = [
|
|
71
|
+
'llama3:latest',
|
|
72
|
+
'llama3.1:latest',
|
|
73
|
+
'llama3.2:latest',
|
|
74
|
+
'gemma2:latest',
|
|
75
|
+
'mistral:latest',
|
|
76
|
+
'phi3:latest',
|
|
77
|
+
'qwen2.5:latest'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
interface ApprovalDetails {
|
|
81
|
+
title: string
|
|
82
|
+
body: string
|
|
83
|
+
reason?: string
|
|
84
|
+
isDangerous: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function badge(provider: string, model: string) {
|
|
88
|
+
return [provider, model].filter(Boolean).join(' / ')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function providerLabel(provider: string) {
|
|
92
|
+
switch (provider) {
|
|
93
|
+
case 'gemini':
|
|
94
|
+
return 'Gemini'
|
|
95
|
+
case 'openai':
|
|
96
|
+
return 'OpenAI'
|
|
97
|
+
case 'openrouter':
|
|
98
|
+
return 'OpenRouter'
|
|
99
|
+
case 'deepseek':
|
|
100
|
+
return 'DeepSeek'
|
|
101
|
+
case 'anthropic':
|
|
102
|
+
return 'Claude'
|
|
103
|
+
case 'huggingface':
|
|
104
|
+
return 'Hugging Face'
|
|
105
|
+
case 'local_openai':
|
|
106
|
+
return 'Local OpenAI'
|
|
107
|
+
case 'ollama':
|
|
108
|
+
return 'Ollama'
|
|
109
|
+
default:
|
|
110
|
+
return provider || 'Primary provider'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fallbackNotice(response: Pick<ChatResponse, 'provider' | 'fallbackProvider'> | null | undefined) {
|
|
115
|
+
if (!response?.fallbackProvider) return ''
|
|
116
|
+
return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface AgentActivity {
|
|
120
|
+
label: string
|
|
121
|
+
target: string
|
|
122
|
+
kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
|
|
123
|
+
state: 'active' | 'done' | 'error'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface AgentActivityView {
|
|
127
|
+
summary: string
|
|
128
|
+
items: AgentActivity[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function activityDetail(input: Record<string, unknown>, key: string) {
|
|
132
|
+
const value = input[key]
|
|
133
|
+
return typeof value === 'string' && value.trim() ? value : ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatActivityTarget(value: string) {
|
|
137
|
+
const compact = value.replace(/^\/home\/([^/]+)/, '~')
|
|
138
|
+
return compact || 'workspace'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function activityKind(action: string, target: string): AgentActivity['kind'] {
|
|
142
|
+
if (['search_code', 'semantic_search', 'knowledge_search', 'web_search', 'memory_recall'].includes(action)) return 'search'
|
|
143
|
+
if (['run_shell', 'verify'].includes(action)) return 'terminal'
|
|
144
|
+
if (['list_files', 'detect_project'].includes(action)) return 'folder'
|
|
145
|
+
if (['read_file', 'symbols', 'read_diagnostics', 'git_diff', 'apply_patch', 'write_file', 'note_write', 'view_image'].includes(action)) return 'file'
|
|
146
|
+
return target.includes('/') && !/\.[^/]+$/.test(target) ? 'folder' : 'tool'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function describeTool(action: string, input: Record<string, unknown>): AgentActivity {
|
|
150
|
+
const path = activityDetail(input, 'path')
|
|
151
|
+
const query = activityDetail(input, 'query')
|
|
152
|
+
const command = activityDetail(input, 'command')
|
|
153
|
+
const name = activityDetail(input, 'name')
|
|
154
|
+
const tool = activityDetail(input, 'tool')
|
|
155
|
+
const target = path || query || command || name || tool || action.replaceAll('_', ' ')
|
|
156
|
+
const labels: Record<string, string> = {
|
|
157
|
+
apply_patch: 'Applying patch',
|
|
158
|
+
ask_user: 'Asking user',
|
|
159
|
+
create_plan: 'Creating plan',
|
|
160
|
+
detect_project: 'Detecting project',
|
|
161
|
+
git_branch: 'Reading branch',
|
|
162
|
+
git_diff: 'Reading diff',
|
|
163
|
+
git_log: 'Reading log',
|
|
164
|
+
git_status: 'Reading git status',
|
|
165
|
+
knowledge_search: 'Searching knowledge',
|
|
166
|
+
list_files: 'Listing files',
|
|
167
|
+
list_tests: 'Listing tests',
|
|
168
|
+
mcp_tool: 'Calling MCP tool',
|
|
169
|
+
memory_recall: 'Recalling memory',
|
|
170
|
+
note_write: 'Writing note',
|
|
171
|
+
read_diagnostics: 'Reading diagnostics',
|
|
172
|
+
read_file: 'Reading file',
|
|
173
|
+
request_user_approval: 'Requesting approval',
|
|
174
|
+
run_plugin: 'Running plugin',
|
|
175
|
+
run_shell: 'Running command',
|
|
176
|
+
search_code: 'Searching code',
|
|
177
|
+
semantic_index: 'Indexing code',
|
|
178
|
+
semantic_search: 'Searching code',
|
|
179
|
+
symbols: 'Inspecting symbols',
|
|
180
|
+
update_plan: 'Updating plan',
|
|
181
|
+
verify: 'Verifying',
|
|
182
|
+
view_image: 'Viewing image',
|
|
183
|
+
web_search: 'Searching web',
|
|
184
|
+
write_file: 'Writing file',
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
label: labels[action] ?? 'Using tool',
|
|
188
|
+
target: formatActivityTarget(target),
|
|
189
|
+
kind: activityKind(action, target),
|
|
190
|
+
state: 'active',
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function activitySummary(items: AgentActivity[]) {
|
|
195
|
+
const files = new Set<string>()
|
|
196
|
+
const folders = new Set<string>()
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
if (item.kind === 'file') files.add(item.target)
|
|
199
|
+
if (item.kind === 'folder') folders.add(item.target)
|
|
200
|
+
}
|
|
201
|
+
const parts = [
|
|
202
|
+
files.size ? `${files.size} ${files.size === 1 ? 'file' : 'files'}` : '',
|
|
203
|
+
folders.size ? `${folders.size} ${folders.size === 1 ? 'folder' : 'folders'}` : '',
|
|
204
|
+
].filter(Boolean)
|
|
205
|
+
return parts.length ? `Exploring ${parts.join(', ')}` : 'Working through task'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
|
|
209
|
+
const activities: AgentActivity[] = []
|
|
210
|
+
for (const event of progress) {
|
|
211
|
+
if (event.type === 'ToolStart') {
|
|
212
|
+
activities.push(describeTool(event.data.action, event.data.input))
|
|
213
|
+
} else if (event.type === 'ToolEnd') {
|
|
214
|
+
for (let index = activities.length - 1; index >= 0; index -= 1) {
|
|
215
|
+
if (activities[index].state !== 'active') continue
|
|
216
|
+
activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
|
|
217
|
+
if (activities[index].state === 'error') activities[index].label = 'Failed'
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const items = activities.slice(-12)
|
|
223
|
+
return { summary: activitySummary(activities), items }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderAgentActivityTable(activityView: AgentActivityView) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="agent-activity-list">
|
|
229
|
+
<div className="agent-activity-table-head" aria-hidden="true">
|
|
230
|
+
<span>Tool</span>
|
|
231
|
+
<span />
|
|
232
|
+
<span>Target</span>
|
|
233
|
+
<span />
|
|
234
|
+
</div>
|
|
235
|
+
{activityView.items.map((activity, index) => (
|
|
236
|
+
<div className="agent-activity-item" data-kind={activity.kind} data-state={activity.state} key={`${index}-${activity.label}-${activity.target}`}>
|
|
237
|
+
<span className="agent-activity-label">{activity.label}</span>
|
|
238
|
+
<span className="agent-activity-icon" aria-hidden="true" />
|
|
239
|
+
<span className="agent-activity-text">{activity.target}</span>
|
|
240
|
+
<span className="agent-activity-chevron" aria-hidden="true">></span>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
248
|
+
if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
|
|
249
|
+
if (approval.WriteFile) return { title: 'Write File', body: `Path: ${approval.WriteFile.path}`, reason: approval.WriteFile.diff ? `Diff:\n${approval.WriteFile.diff}` : 'Writing new file content.', isDangerous: false }
|
|
250
|
+
if (approval.ApplyPatch) return { title: 'Apply Patch', body: `Path: ${approval.ApplyPatch.path}`, reason: approval.ApplyPatch.diff ? `Diff:\n${approval.ApplyPatch.diff}` : 'Applying code patch.', isDangerous: false }
|
|
251
|
+
if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
|
|
252
|
+
if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
|
|
253
|
+
if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
|
|
254
|
+
if (approval.McpTool) {
|
|
255
|
+
const { server, tool, arguments: args } = approval.McpTool
|
|
256
|
+
return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
|
|
257
|
+
}
|
|
258
|
+
if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
|
|
259
|
+
if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Approve to continue without a typed answer, or cancel to decline.', isDangerous: false }
|
|
260
|
+
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface ChatPanelProps {
|
|
264
|
+
interactions: any[]
|
|
265
|
+
sending: boolean
|
|
266
|
+
sendingMessage: string
|
|
267
|
+
sendingImageCount: number
|
|
268
|
+
streamedReply: string
|
|
269
|
+
streamedResponse: ChatResponse | null
|
|
270
|
+
agentProgress: AgentProgress[]
|
|
271
|
+
agentActivitySnapshots: Record<string, AgentProgress[]>
|
|
272
|
+
message: string
|
|
273
|
+
imageAttachments: Array<{ dataUri: string; name: string; previewDataUri?: string }>
|
|
274
|
+
documentName: string
|
|
275
|
+
pendingApproval: any | null
|
|
276
|
+
smartContext: boolean
|
|
277
|
+
agentMode: boolean
|
|
278
|
+
status: RuntimeStatus | null
|
|
279
|
+
workspacePath: string
|
|
280
|
+
chatEnd: RefObject<HTMLDivElement | null>
|
|
281
|
+
welcomeInteraction: any
|
|
282
|
+
onSubmit: (event: FormEvent<HTMLFormElement>) => void
|
|
283
|
+
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
284
|
+
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
285
|
+
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
286
|
+
onReadClipboardImage: () => Promise<boolean>
|
|
287
|
+
onSetMessage: (message: string) => void
|
|
288
|
+
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
289
|
+
onRemoveImage: (idx: number) => void
|
|
290
|
+
onRemoveDocument: () => void
|
|
291
|
+
onStartWebSearch: () => void
|
|
292
|
+
onCaptureScreen: () => void
|
|
293
|
+
onSetSmartContext: (enabled: boolean) => void
|
|
294
|
+
onSetAgentMode: (enabled: boolean) => void
|
|
295
|
+
onSetProvider: (provider: string) => void
|
|
296
|
+
onSelectWorkspace: () => void
|
|
297
|
+
onApproval: (approved: boolean, autoApproveSession?: boolean) => void
|
|
298
|
+
settingsConfig: any
|
|
299
|
+
onSetModel: (model: string) => void
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderFormattedMessage(text: string) {
|
|
303
|
+
const displayText = readableAssistantText(text)
|
|
304
|
+
if (!displayText) return null
|
|
305
|
+
|
|
306
|
+
const lines = displayText.split('\n')
|
|
307
|
+
return lines.map((line, lineIndex) => {
|
|
308
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
309
|
+
|
|
310
|
+
const formatInline = (str: string) => {
|
|
311
|
+
const parts = str.split(/\*\*([\s\S]*?)\*\*/g)
|
|
312
|
+
return parts.map((part, partIndex) => {
|
|
313
|
+
if (partIndex % 2 === 1) {
|
|
314
|
+
return (
|
|
315
|
+
<strong key={partIndex} className="chat-bold-highlight">
|
|
316
|
+
{part}
|
|
317
|
+
</strong>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
return part
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (headerMatch) {
|
|
325
|
+
const level = headerMatch[1].length
|
|
326
|
+
const content = headerMatch[2]
|
|
327
|
+
|
|
328
|
+
const style = {
|
|
329
|
+
fontWeight: 'bold',
|
|
330
|
+
display: 'block',
|
|
331
|
+
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
332
|
+
marginBottom: '6px',
|
|
333
|
+
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
334
|
+
color: 'var(--text-main)'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<span key={lineIndex} style={style}>
|
|
339
|
+
{formatInline(content)}
|
|
340
|
+
</span>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<Fragment key={lineIndex}>
|
|
346
|
+
{formatInline(line)}
|
|
347
|
+
{lineIndex < lines.length - 1 && '\n'}
|
|
348
|
+
</Fragment>
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function readableAssistantText(text: string) {
|
|
354
|
+
if (typeof text !== 'string') return ''
|
|
355
|
+
const trimmed = text.trim()
|
|
356
|
+
if (!trimmed.startsWith('{')) return text
|
|
357
|
+
try {
|
|
358
|
+
const value = JSON.parse(trimmed)
|
|
359
|
+
if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
|
|
360
|
+
return value.input.summary
|
|
361
|
+
}
|
|
362
|
+
if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
|
|
363
|
+
return value.finish.summary
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
return text
|
|
367
|
+
}
|
|
368
|
+
return text
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function renderSpeakerIcon(isSpeaking: boolean) {
|
|
372
|
+
if (isSpeaking) {
|
|
373
|
+
return (
|
|
374
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
375
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
376
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
377
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
378
|
+
</svg>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
return (
|
|
382
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
383
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
384
|
+
</svg>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function cleanSpeechText(text: string) {
|
|
389
|
+
return readableAssistantText(text)
|
|
390
|
+
.replace(/\*\*([\s\S]*?)\*\*/g, '$1')
|
|
391
|
+
.replace(/[*_`#]/g, '')
|
|
392
|
+
.trim()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function numericSetting(value: unknown, fallback: number) {
|
|
396
|
+
const numeric = Number(value)
|
|
397
|
+
return Number.isFinite(numeric) ? numeric : fallback
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
interface DiffHunk {
|
|
401
|
+
oldText: string
|
|
402
|
+
newText: string
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface FileChange {
|
|
406
|
+
path: string
|
|
407
|
+
created: boolean
|
|
408
|
+
additions: number
|
|
409
|
+
deletions: number
|
|
410
|
+
hunks: DiffHunk[]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
|
|
414
|
+
const changes = new Map<string, FileChange>()
|
|
415
|
+
let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
|
|
416
|
+
|
|
417
|
+
for (const event of progress || []) {
|
|
418
|
+
if (event.type === 'ToolStart') {
|
|
419
|
+
if (event.data.action === 'apply_patch') {
|
|
420
|
+
const patch = (event.data.input as any)?.patch
|
|
421
|
+
if (patch && typeof patch.path === 'string') {
|
|
422
|
+
let additions = 0
|
|
423
|
+
let deletions = 0
|
|
424
|
+
const hunksList: DiffHunk[] = []
|
|
425
|
+
const hunks = patch.hunks
|
|
426
|
+
if (Array.isArray(hunks)) {
|
|
427
|
+
for (const hunk of hunks) {
|
|
428
|
+
const oldText = hunk?.oldText || ''
|
|
429
|
+
const newText = hunk?.newText || ''
|
|
430
|
+
const oldLines = oldText ? oldText.split('\n').length : 0
|
|
431
|
+
const newLines = newText ? newText.split('\n').length : 0
|
|
432
|
+
deletions += oldLines
|
|
433
|
+
additions += newLines
|
|
434
|
+
hunksList.push({ oldText, newText })
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
activeEdit = {
|
|
438
|
+
action: 'apply_patch',
|
|
439
|
+
path: patch.path,
|
|
440
|
+
created: false,
|
|
441
|
+
additions,
|
|
442
|
+
deletions,
|
|
443
|
+
hunks: hunksList
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} else if (event.data.action === 'write_file') {
|
|
447
|
+
const path = (event.data.input as any)?.path
|
|
448
|
+
const fileContent = (event.data.input as any)?.file_content || ''
|
|
449
|
+
if (typeof path === 'string') {
|
|
450
|
+
const additions = fileContent ? fileContent.split('\n').length : 0
|
|
451
|
+
activeEdit = {
|
|
452
|
+
action: 'write_file',
|
|
453
|
+
path,
|
|
454
|
+
created: true,
|
|
455
|
+
additions,
|
|
456
|
+
deletions: 0,
|
|
457
|
+
hunks: [{ oldText: '', newText: fileContent }]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
activeEdit = null
|
|
462
|
+
}
|
|
463
|
+
} else if (event.type === 'ToolEnd') {
|
|
464
|
+
if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
|
|
465
|
+
const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
|
|
466
|
+
if (!isError) {
|
|
467
|
+
try {
|
|
468
|
+
const applied = JSON.parse(event.data.result)
|
|
469
|
+
const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
|
|
470
|
+
|
|
471
|
+
for (const path of appliedPaths) {
|
|
472
|
+
const existing = changes.get(path)
|
|
473
|
+
if (existing) {
|
|
474
|
+
existing.additions += activeEdit.additions
|
|
475
|
+
existing.deletions += activeEdit.deletions
|
|
476
|
+
existing.hunks.push(...activeEdit.hunks)
|
|
477
|
+
} else {
|
|
478
|
+
changes.set(path, {
|
|
479
|
+
path,
|
|
480
|
+
created: activeEdit.created,
|
|
481
|
+
additions: activeEdit.additions,
|
|
482
|
+
deletions: activeEdit.deletions,
|
|
483
|
+
hunks: [...activeEdit.hunks]
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch (e) {
|
|
488
|
+
const path = activeEdit.path
|
|
489
|
+
const existing = changes.get(path)
|
|
490
|
+
if (existing) {
|
|
491
|
+
existing.additions += activeEdit.additions
|
|
492
|
+
existing.deletions += activeEdit.deletions
|
|
493
|
+
existing.hunks.push(...activeEdit.hunks)
|
|
494
|
+
} else {
|
|
495
|
+
changes.set(path, {
|
|
496
|
+
path,
|
|
497
|
+
created: activeEdit.created,
|
|
498
|
+
additions: activeEdit.additions,
|
|
499
|
+
deletions: activeEdit.deletions,
|
|
500
|
+
hunks: [...activeEdit.hunks]
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
activeEdit = null
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return Array.from(changes.values())
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export default function ChatPanel({
|
|
514
|
+
interactions,
|
|
515
|
+
sending,
|
|
516
|
+
sendingMessage,
|
|
517
|
+
sendingImageCount,
|
|
518
|
+
streamedReply,
|
|
519
|
+
streamedResponse,
|
|
520
|
+
agentProgress,
|
|
521
|
+
agentActivitySnapshots,
|
|
522
|
+
message,
|
|
523
|
+
imageAttachments,
|
|
524
|
+
documentName,
|
|
525
|
+
pendingApproval,
|
|
526
|
+
smartContext,
|
|
527
|
+
agentMode,
|
|
528
|
+
status,
|
|
529
|
+
workspacePath,
|
|
530
|
+
chatEnd,
|
|
531
|
+
welcomeInteraction,
|
|
532
|
+
onSubmit,
|
|
533
|
+
onSelectImage,
|
|
534
|
+
onSelectDocument,
|
|
535
|
+
onPasteImage,
|
|
536
|
+
onReadClipboardImage,
|
|
537
|
+
onSetMessage,
|
|
538
|
+
onSendVoiceMessage,
|
|
539
|
+
onRemoveImage,
|
|
540
|
+
onRemoveDocument,
|
|
541
|
+
onStartWebSearch,
|
|
542
|
+
onCaptureScreen,
|
|
543
|
+
onSetSmartContext,
|
|
544
|
+
onSetAgentMode,
|
|
545
|
+
onSetProvider,
|
|
546
|
+
onSelectWorkspace,
|
|
547
|
+
onApproval,
|
|
548
|
+
settingsConfig,
|
|
549
|
+
onSetModel,
|
|
550
|
+
}: ChatPanelProps) {
|
|
551
|
+
const agentActivities = activitiesFrom(agentProgress)
|
|
552
|
+
const activeFallbackNotice = fallbackNotice(streamedResponse)
|
|
553
|
+
const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
|
|
554
|
+
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
555
|
+
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
556
|
+
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
557
|
+
const [isRecording, setIsRecording] = useState(false)
|
|
558
|
+
const [voiceMode, setVoiceMode] = useState(false)
|
|
559
|
+
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
560
|
+
const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
|
|
561
|
+
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
562
|
+
const toolMenuRef = useRef<HTMLDivElement | null>(null)
|
|
563
|
+
const recognitionRef = useRef<SpeechRecognition | null>(null)
|
|
564
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
|
565
|
+
const mediaStreamRef = useRef<MediaStream | null>(null)
|
|
566
|
+
const audioContextRef = useRef<AudioContext | null>(null)
|
|
567
|
+
const vadTimerRef = useRef<number | null>(null)
|
|
568
|
+
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
569
|
+
const speechRunRef = useRef(0)
|
|
570
|
+
const historyReadyRef = useRef(false)
|
|
571
|
+
const submittedDuringSessionRef = useRef(false)
|
|
572
|
+
const lastAutoSpokenIdRef = useRef<number | string | null>(null)
|
|
573
|
+
const voiceModeRef = useRef(false)
|
|
574
|
+
const sendingRef = useRef(false)
|
|
575
|
+
const voiceAwaitingResponseRef = useRef(false)
|
|
576
|
+
const speakingRef = useRef<string | null>(null)
|
|
577
|
+
const restartTimerRef = useRef<number | null>(null)
|
|
578
|
+
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
579
|
+
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
580
|
+
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
581
|
+
const voiceStatusLabel = voiceStatus === 'speaking' ? 'กำลังตอบ' : voiceStatus === 'thinking' ? 'กำลังคิด' : voiceStatus === 'listening' ? 'กำลังฟัง' : 'พร้อมฟัง'
|
|
582
|
+
|
|
583
|
+
const getAvailableModels = (provider: string) => {
|
|
584
|
+
switch (provider) {
|
|
585
|
+
case 'gemini':
|
|
586
|
+
return GEMINI_MODELS
|
|
587
|
+
case 'openai':
|
|
588
|
+
return OPENAI_MODELS
|
|
589
|
+
case 'openrouter':
|
|
590
|
+
return OPENROUTER_MODELS
|
|
591
|
+
case 'deepseek':
|
|
592
|
+
return DEEPSEEK_MODELS
|
|
593
|
+
case 'anthropic':
|
|
594
|
+
return ANTHROPIC_MODELS
|
|
595
|
+
case 'huggingface':
|
|
596
|
+
return HF_MODELS
|
|
597
|
+
case 'local_openai':
|
|
598
|
+
return LOCAL_MODELS
|
|
599
|
+
case 'ollama':
|
|
600
|
+
return OLLAMA_MODELS
|
|
601
|
+
default:
|
|
602
|
+
return []
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const activeProvider = status?.activeProvider ?? ''
|
|
607
|
+
const availableModels = getAvailableModels(activeProvider)
|
|
608
|
+
|
|
609
|
+
const getActiveModel = (provider: string) => {
|
|
610
|
+
if (!settingsConfig) return ''
|
|
611
|
+
switch (provider) {
|
|
612
|
+
case 'gemini':
|
|
613
|
+
return settingsConfig.geminiModel
|
|
614
|
+
case 'openai':
|
|
615
|
+
return settingsConfig.openaiModel
|
|
616
|
+
case 'openrouter':
|
|
617
|
+
return settingsConfig.openrouterModel
|
|
618
|
+
case 'deepseek':
|
|
619
|
+
return settingsConfig.deepseekModel
|
|
620
|
+
case 'anthropic':
|
|
621
|
+
return settingsConfig.anthropicModel
|
|
622
|
+
case 'huggingface':
|
|
623
|
+
return settingsConfig.hfModel
|
|
624
|
+
case 'local_openai':
|
|
625
|
+
return settingsConfig.localModelName
|
|
626
|
+
case 'ollama':
|
|
627
|
+
return settingsConfig.ollamaModel
|
|
628
|
+
default:
|
|
629
|
+
return ''
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const activeModel = getActiveModel(activeProvider)
|
|
633
|
+
const workspaceName = workspacePath
|
|
634
|
+
? workspacePath.split(/[\\/]/).filter(Boolean).pop() || workspacePath
|
|
635
|
+
: 'Select Project'
|
|
636
|
+
const submitOnEnter = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
637
|
+
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
638
|
+
event.preventDefault()
|
|
639
|
+
event.currentTarget.form?.requestSubmit()
|
|
640
|
+
}
|
|
641
|
+
const handleInputKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
642
|
+
submitOnEnter(event)
|
|
643
|
+
}
|
|
644
|
+
const clearRestartTimer = () => {
|
|
645
|
+
if (restartTimerRef.current === null) return
|
|
646
|
+
window.clearTimeout(restartTimerRef.current)
|
|
647
|
+
restartTimerRef.current = null
|
|
648
|
+
}
|
|
649
|
+
const clearVadTimer = () => {
|
|
650
|
+
if (vadTimerRef.current === null) return
|
|
651
|
+
window.clearInterval(vadTimerRef.current)
|
|
652
|
+
vadTimerRef.current = null
|
|
653
|
+
}
|
|
654
|
+
const stopRecognition = () => {
|
|
655
|
+
clearRestartTimer()
|
|
656
|
+
recognitionRef.current?.stop()
|
|
657
|
+
recognitionRef.current = null
|
|
658
|
+
clearVadTimer()
|
|
659
|
+
if (mediaRecorderRef.current?.state === 'recording') {
|
|
660
|
+
mediaRecorderRef.current.stop()
|
|
661
|
+
}
|
|
662
|
+
mediaRecorderRef.current = null
|
|
663
|
+
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
664
|
+
mediaStreamRef.current = null
|
|
665
|
+
audioContextRef.current?.close().catch(() => {})
|
|
666
|
+
audioContextRef.current = null
|
|
667
|
+
setIsRecording(false)
|
|
668
|
+
}
|
|
669
|
+
const scheduleVoiceListen = (delayMs = 350) => {
|
|
670
|
+
clearRestartTimer()
|
|
671
|
+
if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
672
|
+
restartTimerRef.current = window.setTimeout(() => {
|
|
673
|
+
restartTimerRef.current = null
|
|
674
|
+
startRecognition(true)
|
|
675
|
+
}, delayMs)
|
|
676
|
+
}
|
|
677
|
+
const cancelSpeech = () => {
|
|
678
|
+
speechRunRef.current += 1
|
|
679
|
+
audioRef.current?.pause()
|
|
680
|
+
audioRef.current = null
|
|
681
|
+
speakingRef.current = null
|
|
682
|
+
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
|
683
|
+
window.speechSynthesis.cancel()
|
|
684
|
+
}
|
|
685
|
+
setSpeakingText(null)
|
|
686
|
+
}
|
|
687
|
+
const speakNative = (text: string, displayText: string) => {
|
|
688
|
+
if (typeof window === 'undefined' || !window.speechSynthesis) {
|
|
689
|
+
setSpeakingText(null)
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
const hasThai = /[\u0e00-\u0e7f]/.test(text)
|
|
693
|
+
const utterance = new SpeechSynthesisUtterance(text)
|
|
694
|
+
utterance.lang = hasThai ? 'th-TH' : 'en-US'
|
|
695
|
+
utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
|
|
696
|
+
utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
|
|
697
|
+
utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
|
|
698
|
+
const voice = window.speechSynthesis.getVoices().find((item) => item.lang.startsWith(hasThai ? 'th' : 'en'))
|
|
699
|
+
if (voice) utterance.voice = voice
|
|
700
|
+
const finishSpeech = () => {
|
|
701
|
+
setSpeakingText((current) => (current === displayText ? null : current))
|
|
702
|
+
speakingRef.current = null
|
|
703
|
+
scheduleVoiceListen(900)
|
|
704
|
+
}
|
|
705
|
+
utterance.onend = finishSpeech
|
|
706
|
+
utterance.onerror = finishSpeech
|
|
707
|
+
speakingRef.current = displayText
|
|
708
|
+
setSpeakingText(displayText)
|
|
709
|
+
window.speechSynthesis.speak(utterance)
|
|
710
|
+
}
|
|
711
|
+
const playGoogleTts = async (text: string, displayText: string, runId: number) => {
|
|
712
|
+
const chunks = await getTtsUrls(text)
|
|
713
|
+
for (const chunk of chunks) {
|
|
714
|
+
if (speechRunRef.current !== runId) return
|
|
715
|
+
await new Promise<void>((resolve, reject) => {
|
|
716
|
+
const audio = new Audio(chunk.url)
|
|
717
|
+
audio.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
|
|
718
|
+
audio.playbackRate = Math.max(0.25, Math.min(4, numericSetting(settingsConfig?.ttsSpeed, 1)))
|
|
719
|
+
audioRef.current = audio
|
|
720
|
+
audio.onended = () => resolve()
|
|
721
|
+
audio.onpause = () => resolve()
|
|
722
|
+
audio.onerror = () => reject(new Error('Google TTS playback failed'))
|
|
723
|
+
audio.play().catch(reject)
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
if (speechRunRef.current === runId) {
|
|
727
|
+
setSpeakingText((current) => (current === displayText ? null : current))
|
|
728
|
+
speakingRef.current = null
|
|
729
|
+
scheduleVoiceListen(900)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const speak = (text: string) => {
|
|
733
|
+
const speechText = cleanSpeechText(text)
|
|
734
|
+
if (!speechText) return
|
|
735
|
+
if (speakingText === text) {
|
|
736
|
+
cancelSpeech()
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
cancelSpeech()
|
|
741
|
+
const runId = speechRunRef.current
|
|
742
|
+
speakingRef.current = text
|
|
743
|
+
setSpeakingText(text)
|
|
744
|
+
if (settingsConfig?.ttsProvider === 'google') {
|
|
745
|
+
playGoogleTts(speechText, text, runId).catch((error) => {
|
|
746
|
+
console.warn('Google TTS failed, falling back to native speech synthesis:', error)
|
|
747
|
+
if (speechRunRef.current === runId) speakNative(speechText, text)
|
|
748
|
+
})
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
speakNative(speechText, text)
|
|
752
|
+
}
|
|
753
|
+
const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
|
|
754
|
+
const reader = new FileReader()
|
|
755
|
+
reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
|
|
756
|
+
reader.onload = () => resolve(String(reader.result))
|
|
757
|
+
reader.readAsDataURL(blob)
|
|
758
|
+
})
|
|
759
|
+
const preferredAudioMimeType = () => {
|
|
760
|
+
const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
|
|
761
|
+
return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
|
|
762
|
+
}
|
|
763
|
+
const startAudioRecording = async (autoSend: boolean) => {
|
|
764
|
+
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
765
|
+
alert('ขออภัย ระบบนี้ไม่สามารถเข้าถึงไมค์หรืออัดเสียงใน WebView นี้ได้')
|
|
766
|
+
voiceModeRef.current = false
|
|
767
|
+
setVoiceMode(false)
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
773
|
+
const mimeType = preferredAudioMimeType()
|
|
774
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
|
|
775
|
+
const chunks: Blob[] = []
|
|
776
|
+
let heardVoice = false
|
|
777
|
+
let quietSince = 0
|
|
778
|
+
const startedAt = Date.now()
|
|
779
|
+
const audioContext = new AudioContext()
|
|
780
|
+
const source = audioContext.createMediaStreamSource(stream)
|
|
781
|
+
const analyser = audioContext.createAnalyser()
|
|
782
|
+
const samples = new Uint8Array(analyser.fftSize)
|
|
783
|
+
source.connect(analyser)
|
|
784
|
+
|
|
785
|
+
mediaStreamRef.current = stream
|
|
786
|
+
mediaRecorderRef.current = recorder
|
|
787
|
+
audioContextRef.current = audioContext
|
|
788
|
+
setVoiceTranscript('กำลังฟังเสียงจากไมค์')
|
|
789
|
+
|
|
790
|
+
recorder.ondataavailable = (event) => {
|
|
791
|
+
if (event.data.size > 0) chunks.push(event.data)
|
|
792
|
+
}
|
|
793
|
+
recorder.onstop = async () => {
|
|
794
|
+
clearVadTimer()
|
|
795
|
+
mediaRecorderRef.current = null
|
|
796
|
+
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
797
|
+
mediaStreamRef.current = null
|
|
798
|
+
audioContextRef.current?.close().catch(() => {})
|
|
799
|
+
audioContextRef.current = null
|
|
800
|
+
setIsRecording(false)
|
|
801
|
+
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
802
|
+
if (!heardVoice) {
|
|
803
|
+
setVoiceTranscript('ยังไม่ได้ยินเสียงพูด')
|
|
804
|
+
scheduleVoiceListen(700)
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
808
|
+
const audioDataUri = await blobToDataUri(blob)
|
|
809
|
+
setVoiceTranscript('ส่งเสียงให้ AI แล้ว')
|
|
810
|
+
voiceAwaitingResponseRef.current = true
|
|
811
|
+
setVoiceAwaitingResponse(true)
|
|
812
|
+
onSendVoiceMessage('', audioDataUri)
|
|
813
|
+
.catch((error) => console.error('Voice audio message failed', error))
|
|
814
|
+
.finally(() => {
|
|
815
|
+
voiceAwaitingResponseRef.current = false
|
|
816
|
+
setVoiceAwaitingResponse(false)
|
|
817
|
+
scheduleVoiceListen()
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
recorder.start()
|
|
822
|
+
setIsRecording(true)
|
|
823
|
+
vadTimerRef.current = window.setInterval(() => {
|
|
824
|
+
analyser.getByteTimeDomainData(samples)
|
|
825
|
+
let peak = 0
|
|
826
|
+
for (const sample of samples) {
|
|
827
|
+
peak = Math.max(peak, Math.abs(sample - 128))
|
|
828
|
+
}
|
|
829
|
+
const now = Date.now()
|
|
830
|
+
if (peak > 12) {
|
|
831
|
+
heardVoice = true
|
|
832
|
+
quietSince = 0
|
|
833
|
+
setVoiceTranscript('กำลังฟัง...')
|
|
834
|
+
} else if (heardVoice) {
|
|
835
|
+
quietSince = quietSince || now
|
|
836
|
+
}
|
|
837
|
+
const silenceElapsed = quietSince ? now - quietSince : 0
|
|
838
|
+
const totalElapsed = now - startedAt
|
|
839
|
+
if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
|
|
840
|
+
recorder.stop()
|
|
841
|
+
}
|
|
842
|
+
}, 120)
|
|
843
|
+
} catch (error) {
|
|
844
|
+
console.error('Failed to record microphone audio', error)
|
|
845
|
+
setIsRecording(false)
|
|
846
|
+
voiceModeRef.current = false
|
|
847
|
+
setVoiceMode(false)
|
|
848
|
+
alert('เปิดไมค์ไม่สำเร็จ กรุณาตรวจสิทธิ์ microphone ของแอป')
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const startRecognition = (autoSend: boolean) => {
|
|
852
|
+
if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
853
|
+
|
|
854
|
+
const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
855
|
+
if (!SpeechRecognitionApi) {
|
|
856
|
+
startAudioRecording(autoSend)
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
let sentTranscript = false
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
const recognition = new SpeechRecognitionApi()
|
|
864
|
+
recognition.continuous = false
|
|
865
|
+
recognition.interimResults = true
|
|
866
|
+
recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
|
|
867
|
+
recognition.onstart = () => setIsRecording(true)
|
|
868
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
869
|
+
let interimText = ''
|
|
870
|
+
let finalText = ''
|
|
871
|
+
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
872
|
+
const transcript = event.results[index]?.[0]?.transcript ?? ''
|
|
873
|
+
if (event.results[index]?.isFinal) finalText += transcript
|
|
874
|
+
else interimText += transcript
|
|
875
|
+
}
|
|
876
|
+
const displayText = (finalText || interimText).trim()
|
|
877
|
+
if (displayText) setVoiceTranscript(displayText)
|
|
878
|
+
const resultText = finalText.trim()
|
|
879
|
+
if (!resultText) return
|
|
880
|
+
sentTranscript = true
|
|
881
|
+
recognition.stop()
|
|
882
|
+
setVoiceTranscript(resultText)
|
|
883
|
+
if (autoSend) {
|
|
884
|
+
voiceAwaitingResponseRef.current = true
|
|
885
|
+
setVoiceAwaitingResponse(true)
|
|
886
|
+
onSendVoiceMessage(resultText)
|
|
887
|
+
.catch((error) => console.error('Voice message failed', error))
|
|
888
|
+
.finally(() => {
|
|
889
|
+
voiceAwaitingResponseRef.current = false
|
|
890
|
+
setVoiceAwaitingResponse(false)
|
|
891
|
+
scheduleVoiceListen()
|
|
892
|
+
})
|
|
893
|
+
} else {
|
|
894
|
+
onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
recognition.onerror = (event: Event) => {
|
|
898
|
+
console.error('Speech recognition error', event)
|
|
899
|
+
setIsRecording(false)
|
|
900
|
+
}
|
|
901
|
+
recognition.onend = () => {
|
|
902
|
+
recognitionRef.current = null
|
|
903
|
+
setIsRecording(false)
|
|
904
|
+
if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
|
|
905
|
+
}
|
|
906
|
+
recognitionRef.current = recognition
|
|
907
|
+
recognition.start()
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.error('Failed to start speech recognition', error)
|
|
910
|
+
recognitionRef.current = null
|
|
911
|
+
setIsRecording(false)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const toggleRecording = () => {
|
|
915
|
+
if (voiceMode) {
|
|
916
|
+
voiceModeRef.current = false
|
|
917
|
+
voiceAwaitingResponseRef.current = false
|
|
918
|
+
setVoiceMode(false)
|
|
919
|
+
setVoiceAwaitingResponse(false)
|
|
920
|
+
setVoiceTranscript('')
|
|
921
|
+
stopRecognition()
|
|
922
|
+
cancelSpeech()
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
voiceModeRef.current = true
|
|
927
|
+
setVoiceMode(true)
|
|
928
|
+
setVoiceAwaitingResponse(false)
|
|
929
|
+
setVoiceTranscript('')
|
|
930
|
+
startRecognition(true)
|
|
931
|
+
}
|
|
932
|
+
const resizeInput = (element: HTMLTextAreaElement) => {
|
|
933
|
+
element.style.height = 'auto'
|
|
934
|
+
element.style.height = `${Math.min(element.scrollHeight, 120)}px`
|
|
935
|
+
}
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
return () => {
|
|
938
|
+
clearRestartTimer()
|
|
939
|
+
cancelSpeech()
|
|
940
|
+
stopRecognition()
|
|
941
|
+
}
|
|
942
|
+
}, [])
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
voiceModeRef.current = voiceMode
|
|
945
|
+
if (!voiceMode) {
|
|
946
|
+
clearRestartTimer()
|
|
947
|
+
setVoiceTranscript('')
|
|
948
|
+
}
|
|
949
|
+
}, [voiceMode])
|
|
950
|
+
useEffect(() => {
|
|
951
|
+
sendingRef.current = sending
|
|
952
|
+
}, [sending])
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
voiceAwaitingResponseRef.current = voiceAwaitingResponse
|
|
955
|
+
}, [voiceAwaitingResponse])
|
|
956
|
+
useEffect(() => {
|
|
957
|
+
speakingRef.current = speakingText
|
|
958
|
+
}, [speakingText])
|
|
959
|
+
useEffect(() => {
|
|
960
|
+
if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
|
|
961
|
+
scheduleVoiceListen()
|
|
962
|
+
}, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (sending) submittedDuringSessionRef.current = true
|
|
965
|
+
}, [sending])
|
|
966
|
+
useEffect(() => {
|
|
967
|
+
if (interactions.length === 0) return
|
|
968
|
+
const latest = interactions[interactions.length - 1]
|
|
969
|
+
if (!historyReadyRef.current) {
|
|
970
|
+
historyReadyRef.current = true
|
|
971
|
+
if (!submittedDuringSessionRef.current) {
|
|
972
|
+
lastAutoSpokenIdRef.current = latest?.id ?? null
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (sending) return
|
|
977
|
+
if (!settingsConfig?.enableVoiceReply && !voiceMode) {
|
|
978
|
+
lastAutoSpokenIdRef.current = latest?.id ?? null
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
|
|
982
|
+
lastAutoSpokenIdRef.current = latest.id
|
|
983
|
+
speak(latest.aiText)
|
|
984
|
+
}, [interactions, sending, settingsConfig?.enableVoiceReply])
|
|
985
|
+
useEffect(() => {
|
|
986
|
+
if (!toolMenuOpen) return
|
|
987
|
+
const closeMenu = (event: MouseEvent) => {
|
|
988
|
+
if (toolMenuRef.current?.contains(event.target as Node)) return
|
|
989
|
+
setToolMenuOpen(false)
|
|
990
|
+
}
|
|
991
|
+
window.addEventListener('mousedown', closeMenu)
|
|
992
|
+
return () => window.removeEventListener('mousedown', closeMenu)
|
|
993
|
+
}, [toolMenuOpen])
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
const handleWindowPaste = (event: globalThis.ClipboardEvent) => {
|
|
996
|
+
if (!event.clipboardData) return
|
|
997
|
+
if (onPasteImage(event.clipboardData)) {
|
|
998
|
+
event.preventDefault()
|
|
999
|
+
event.stopPropagation()
|
|
1000
|
+
} else {
|
|
1001
|
+
window.setTimeout(() => void onReadClipboardImage(), 0)
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
window.addEventListener('paste', handleWindowPaste, true)
|
|
1005
|
+
return () => window.removeEventListener('paste', handleWindowPaste, true)
|
|
1006
|
+
}, [onPasteImage, onReadClipboardImage])
|
|
1007
|
+
useEffect(() => {
|
|
1008
|
+
const handleWindowKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
1009
|
+
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'v') return
|
|
1010
|
+
window.setTimeout(() => void onReadClipboardImage(), 0)
|
|
1011
|
+
}
|
|
1012
|
+
window.addEventListener('keydown', handleWindowKeyDown, true)
|
|
1013
|
+
return () => window.removeEventListener('keydown', handleWindowKeyDown, true)
|
|
1014
|
+
}, [onReadClipboardImage])
|
|
1015
|
+
useEffect(() => {
|
|
1016
|
+
if (message) return
|
|
1017
|
+
const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
|
|
1018
|
+
if (input) input.style.height = ''
|
|
1019
|
+
}, [message])
|
|
1020
|
+
|
|
1021
|
+
const openImagePicker = () => {
|
|
1022
|
+
setToolMenuOpen(false)
|
|
1023
|
+
document.getElementById('vision-file-input')?.click()
|
|
1024
|
+
}
|
|
1025
|
+
const openDocumentPicker = () => {
|
|
1026
|
+
setToolMenuOpen(false)
|
|
1027
|
+
document.getElementById('document-file-input')?.click()
|
|
1028
|
+
}
|
|
1029
|
+
const startWebSearch = () => {
|
|
1030
|
+
setToolMenuOpen(false)
|
|
1031
|
+
onStartWebSearch()
|
|
1032
|
+
}
|
|
1033
|
+
const appendWorkspaceReference = (reference: string) => {
|
|
1034
|
+
const trimmed = reference.trim()
|
|
1035
|
+
if (!trimmed) return
|
|
1036
|
+
onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed}` : trimmed)
|
|
1037
|
+
}
|
|
1038
|
+
const handleWorkspaceDrop = (event: DragEvent<HTMLElement>) => {
|
|
1039
|
+
const reference =
|
|
1040
|
+
event.dataTransfer.getData('application/x-mint-workspace-path') ||
|
|
1041
|
+
event.dataTransfer.getData('text/plain')
|
|
1042
|
+
if (!reference.trim().startsWith('@')) return
|
|
1043
|
+
event.preventDefault()
|
|
1044
|
+
appendWorkspaceReference(reference)
|
|
1045
|
+
}
|
|
1046
|
+
const isEmptyChat = interactions.length === 0 && !sending && !pendingApproval
|
|
1047
|
+
const renderCompletedActivity = (interaction: any) => {
|
|
1048
|
+
const interactionId = String(interaction.id)
|
|
1049
|
+
const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
|
|
1050
|
+
if (activityView.items.length === 0) return null
|
|
1051
|
+
const isOpen = Boolean(openActivityIds[interactionId])
|
|
1052
|
+
return (
|
|
1053
|
+
<div className="agent-activity-history">
|
|
1054
|
+
<button
|
|
1055
|
+
type="button"
|
|
1056
|
+
className="agent-activity-toggle"
|
|
1057
|
+
aria-expanded={isOpen}
|
|
1058
|
+
onClick={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
|
|
1059
|
+
>
|
|
1060
|
+
<span>{activityView.summary}</span>
|
|
1061
|
+
<span aria-hidden="true">{isOpen ? '^' : '>'}</span>
|
|
1062
|
+
</button>
|
|
1063
|
+
{isOpen && (
|
|
1064
|
+
<div className="agent-activity-card agent-activity-card-history">
|
|
1065
|
+
{renderAgentActivityTable(activityView)}
|
|
1066
|
+
</div>
|
|
1067
|
+
)}
|
|
1068
|
+
</div>
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const renderFileChanges = (interaction: any) => {
|
|
1073
|
+
const interactionId = String(interaction.id)
|
|
1074
|
+
const progress = agentActivitySnapshots[interactionId] ?? []
|
|
1075
|
+
const changes = parseFileChangesFromProgress(progress)
|
|
1076
|
+
if (changes.length === 0) return null
|
|
1077
|
+
|
|
1078
|
+
const totalAdditions = changes.reduce((sum, c) => sum + c.additions, 0)
|
|
1079
|
+
const totalDeletions = changes.reduce((sum, c) => sum + c.deletions, 0)
|
|
1080
|
+
const isOpen = Boolean(openReviewIds[interactionId])
|
|
1081
|
+
|
|
1082
|
+
return (
|
|
1083
|
+
<div className="file-changes-summary-container" style={{ marginBottom: '8px' }}>
|
|
1084
|
+
<button
|
|
1085
|
+
type="button"
|
|
1086
|
+
className="agent-activity-toggle"
|
|
1087
|
+
aria-expanded={isOpen}
|
|
1088
|
+
onClick={() => setOpenReviewIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
|
|
1089
|
+
style={{ display: 'flex', alignItems: 'center', gap: '6px', color: '#10b981', fontWeight: 500 }}
|
|
1090
|
+
>
|
|
1091
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '2px' }}>
|
|
1092
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
1093
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
1094
|
+
</svg>
|
|
1095
|
+
<span>
|
|
1096
|
+
{changes.length} {changes.length === 1 ? 'file' : 'files'} changed
|
|
1097
|
+
{totalAdditions > 0 && <span style={{ color: '#10b981', marginLeft: '6px' }}>+{totalAdditions}</span>}
|
|
1098
|
+
{totalDeletions > 0 && <span style={{ color: '#ef4444', marginLeft: '4px' }}>-{totalDeletions}</span>}
|
|
1099
|
+
</span>
|
|
1100
|
+
<span aria-hidden="true">{isOpen ? '^' : '>'}</span>
|
|
1101
|
+
</button>
|
|
1102
|
+
|
|
1103
|
+
{isOpen && (
|
|
1104
|
+
<div className="agent-activity-card" style={{ border: '1px solid rgba(16, 185, 129, 0.2)', borderRadius: '8px', padding: '10px', background: 'rgba(15, 23, 42, 0.6)', marginTop: '4px' }}>
|
|
1105
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
1106
|
+
{changes.map((change) => {
|
|
1107
|
+
const fileKey = `${interactionId}-${change.path}`
|
|
1108
|
+
const isDiffOpen = Boolean(openFileDiffs[fileKey])
|
|
1109
|
+
const fileName = change.path.split('/').pop() || change.path
|
|
1110
|
+
const dirPath = change.path.includes('/') ? change.path.substring(0, change.path.lastIndexOf('/')) : ''
|
|
1111
|
+
|
|
1112
|
+
return (
|
|
1113
|
+
<div key={change.path} style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)', paddingBottom: '4px' }}>
|
|
1114
|
+
<div
|
|
1115
|
+
onClick={() => setOpenFileDiffs((current) => ({ ...current, [fileKey]: !current[fileKey] }))}
|
|
1116
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', padding: '4px 6px', borderRadius: '4px', background: 'rgba(255, 255, 255, 0.02)' }}
|
|
1117
|
+
>
|
|
1118
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1119
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1120
|
+
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
1121
|
+
<polyline points="14 2 14 8 20 8" />
|
|
1122
|
+
</svg>
|
|
1123
|
+
<span style={{ fontSize: '0.82rem', fontWeight: 600, color: change.created ? '#10b981' : '#cbd5e1' }}>
|
|
1124
|
+
{fileName}
|
|
1125
|
+
{dirPath && <span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 400, marginLeft: '6px' }}>{dirPath}</span>}
|
|
1126
|
+
{change.created && <span style={{ fontSize: '0.7rem', color: '#10b981', marginLeft: '6px', padding: '1px 4px', border: '1px solid rgba(16, 185, 129, 0.3)', borderRadius: '3px', background: 'rgba(16, 185, 129, 0.1)' }}>new</span>}
|
|
1127
|
+
</span>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.76rem' }}>
|
|
1130
|
+
{change.additions > 0 && <span style={{ color: '#10b981' }}>+{change.additions}</span>}
|
|
1131
|
+
{change.deletions > 0 && <span style={{ color: '#ef4444' }}>-{change.deletions}</span>}
|
|
1132
|
+
<span style={{ color: '#64748b', transform: isDiffOpen ? 'rotate(90deg)' : 'none', display: 'inline-block', transition: 'transform 0.15s' }}>></span>
|
|
1133
|
+
</div>
|
|
1134
|
+
</div>
|
|
1135
|
+
|
|
1136
|
+
{isDiffOpen && (
|
|
1137
|
+
<div style={{ marginTop: '6px', background: '#0b0f19', borderRadius: '6px', padding: '8px', border: '1px solid rgba(255, 255, 255, 0.08)', overflowX: 'auto', maxHeight: '300px' }}>
|
|
1138
|
+
{change.hunks.map((hunk, hIdx) => (
|
|
1139
|
+
<div key={hIdx} style={{ fontSize: '0.74rem', fontFamily: 'monospace', lineHeight: '1.4', marginBottom: hIdx < change.hunks.length - 1 ? '10px' : 0 }}>
|
|
1140
|
+
{hunk.oldText && (
|
|
1141
|
+
<div style={{ background: 'rgba(239, 68, 68, 0.12)', borderLeft: '3px solid #ef4444', padding: '4px 6px', color: '#fca5a5', whiteSpace: 'pre-wrap' }}>
|
|
1142
|
+
{hunk.oldText.split('\n').map((line, lIdx) => (
|
|
1143
|
+
<div key={lIdx}>- {line}</div>
|
|
1144
|
+
))}
|
|
1145
|
+
</div>
|
|
1146
|
+
)}
|
|
1147
|
+
{hunk.newText && (
|
|
1148
|
+
<div style={{ background: 'rgba(16, 185, 129, 0.12)', borderLeft: '3px solid #10b981', padding: '4px 6px', color: '#a7f3d0', whiteSpace: 'pre-wrap' }}>
|
|
1149
|
+
{hunk.newText.split('\n').map((line, lIdx) => (
|
|
1150
|
+
<div key={lIdx}>+ {line}</div>
|
|
1151
|
+
))}
|
|
1152
|
+
</div>
|
|
1153
|
+
)}
|
|
1154
|
+
</div>
|
|
1155
|
+
))}
|
|
1156
|
+
</div>
|
|
1157
|
+
)}
|
|
1158
|
+
</div>
|
|
1159
|
+
)
|
|
1160
|
+
})}
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
)}
|
|
1164
|
+
</div>
|
|
1165
|
+
)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const renderActiveFileChanges = () => {
|
|
1169
|
+
const changes = parseFileChangesFromProgress(agentProgress)
|
|
1170
|
+
if (changes.length === 0) return null
|
|
1171
|
+
|
|
1172
|
+
const totalAdditions = changes.reduce((sum, c) => sum + c.additions, 0)
|
|
1173
|
+
const totalDeletions = changes.reduce((sum, c) => sum + c.deletions, 0)
|
|
1174
|
+
const isOpen = Boolean(openReviewIds['active-run'])
|
|
1175
|
+
|
|
1176
|
+
return (
|
|
1177
|
+
<div className="message ai-message agent-activity-message" style={{ marginTop: '4px', marginBottom: '8px' }}>
|
|
1178
|
+
<div className="agent-activity-card" style={{ border: '1px solid rgba(16, 185, 129, 0.25)', borderRadius: '8px', padding: '10px', background: 'rgba(15, 23, 42, 0.6)' }}>
|
|
1179
|
+
<button
|
|
1180
|
+
type="button"
|
|
1181
|
+
className="agent-activity-toggle"
|
|
1182
|
+
aria-expanded={isOpen}
|
|
1183
|
+
onClick={() => setOpenReviewIds((current) => ({ ...current, 'active-run': !current['active-run'] }))}
|
|
1184
|
+
style={{ display: 'flex', alignItems: 'center', gap: '6px', color: '#10b981', fontWeight: 500, border: 0, background: 'transparent', padding: 0 }}
|
|
1185
|
+
>
|
|
1186
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '2px' }}>
|
|
1187
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
1188
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
1189
|
+
</svg>
|
|
1190
|
+
<span>
|
|
1191
|
+
{changes.length} {changes.length === 1 ? 'file' : 'files'} changed in this run
|
|
1192
|
+
{totalAdditions > 0 && <span style={{ color: '#10b981', marginLeft: '6px' }}>+{totalAdditions}</span>}
|
|
1193
|
+
{totalDeletions > 0 && <span style={{ color: '#ef4444', marginLeft: '4px' }}>-{totalDeletions}</span>}
|
|
1194
|
+
</span>
|
|
1195
|
+
<span aria-hidden="true">{isOpen ? '^' : '>'}</span>
|
|
1196
|
+
</button>
|
|
1197
|
+
|
|
1198
|
+
{isOpen && (
|
|
1199
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '8px' }}>
|
|
1200
|
+
{changes.map((change) => {
|
|
1201
|
+
const fileKey = `active-${change.path}`
|
|
1202
|
+
const isDiffOpen = Boolean(openFileDiffs[fileKey])
|
|
1203
|
+
const fileName = change.path.split('/').pop() || change.path
|
|
1204
|
+
const dirPath = change.path.includes('/') ? change.path.substring(0, change.path.lastIndexOf('/')) : ''
|
|
1205
|
+
|
|
1206
|
+
return (
|
|
1207
|
+
<div key={change.path} style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)', paddingBottom: '4px' }}>
|
|
1208
|
+
<div
|
|
1209
|
+
onClick={() => setOpenFileDiffs((current) => ({ ...current, [fileKey]: !current[fileKey] }))}
|
|
1210
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', padding: '4px 6px', borderRadius: '4px', background: 'rgba(255, 255, 255, 0.02)' }}
|
|
1211
|
+
>
|
|
1212
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1213
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1214
|
+
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
1215
|
+
<polyline points="14 2 14 8 20 8" />
|
|
1216
|
+
</svg>
|
|
1217
|
+
<span style={{ fontSize: '0.82rem', fontWeight: 600, color: change.created ? '#10b981' : '#cbd5e1' }}>
|
|
1218
|
+
{fileName}
|
|
1219
|
+
{dirPath && <span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 400, marginLeft: '6px' }}>{dirPath}</span>}
|
|
1220
|
+
{change.created && <span style={{ fontSize: '0.7rem', color: '#10b981', marginLeft: '6px', padding: '1px 4px', border: '1px solid rgba(16, 185, 129, 0.3)', borderRadius: '3px', background: 'rgba(16, 185, 129, 0.1)' }}>new</span>}
|
|
1221
|
+
</span>
|
|
1222
|
+
</div>
|
|
1223
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.76rem' }}>
|
|
1224
|
+
{change.additions > 0 && <span style={{ color: '#10b981' }}>+{change.additions}</span>}
|
|
1225
|
+
{change.deletions > 0 && <span style={{ color: '#ef4444' }}>-{change.deletions}</span>}
|
|
1226
|
+
<span style={{ color: '#64748b', transform: isDiffOpen ? 'rotate(90deg)' : 'none', display: 'inline-block', transition: 'transform 0.15s' }}>></span>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
|
|
1230
|
+
{isDiffOpen && (
|
|
1231
|
+
<div style={{ marginTop: '6px', background: '#0b0f19', borderRadius: '6px', padding: '8px', border: '1px solid rgba(255, 255, 255, 0.08)', overflowX: 'auto', maxHeight: '300px' }}>
|
|
1232
|
+
{change.hunks.map((hunk, hunkIdx) => (
|
|
1233
|
+
<div key={hunkIdx} style={{ fontSize: '0.74rem', fontFamily: 'monospace', lineHeight: '1.4', marginBottom: hunkIdx < change.hunks.length - 1 ? '10px' : 0 }}>
|
|
1234
|
+
{hunk.oldText && (
|
|
1235
|
+
<div style={{ background: 'rgba(239, 68, 68, 0.12)', borderLeft: '3px solid #ef4444', padding: '4px 6px', color: '#fca5a5', whiteSpace: 'pre-wrap' }}>
|
|
1236
|
+
{hunk.oldText.split('\n').map((line, lIdx) => (
|
|
1237
|
+
<div key={lIdx}>- {line}</div>
|
|
1238
|
+
))}
|
|
1239
|
+
</div>
|
|
1240
|
+
)}
|
|
1241
|
+
{hunk.newText && (
|
|
1242
|
+
<div style={{ background: 'rgba(16, 185, 129, 0.12)', borderLeft: '3px solid #10b981', padding: '4px 6px', color: '#a7f3d0', whiteSpace: 'pre-wrap' }}>
|
|
1243
|
+
{hunk.newText.split('\n').map((line, lIdx) => (
|
|
1244
|
+
<div key={lIdx}>+ {line}</div>
|
|
1245
|
+
))}
|
|
1246
|
+
</div>
|
|
1247
|
+
)}
|
|
1248
|
+
</div>
|
|
1249
|
+
))}
|
|
1250
|
+
</div>
|
|
1251
|
+
)}
|
|
1252
|
+
</div>
|
|
1253
|
+
)
|
|
1254
|
+
})}
|
|
1255
|
+
</div>
|
|
1256
|
+
)}
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return (
|
|
1263
|
+
<section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
|
|
1264
|
+
<div className="chat-container">
|
|
1265
|
+
{interactions.map((interaction) => (
|
|
1266
|
+
<div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1267
|
+
{interaction.isSystemEvent ? (
|
|
1268
|
+
<div className="system-event" style={{ background: 'rgba(16, 185, 129, 0.06)', border: '1px solid rgba(16, 185, 129, 0.2)', borderRadius: '8px', padding: '10px 14px', color: '#a7f3d0', fontSize: '0.82rem', lineHeight: '1.45', alignSelf: 'stretch' }}>
|
|
1269
|
+
{interaction.userText}
|
|
1270
|
+
</div>
|
|
1271
|
+
) : interaction.userText && (
|
|
1272
|
+
<div className="message user-message">
|
|
1273
|
+
<div className="bubble-wrapper">
|
|
1274
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
1275
|
+
<div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
|
|
1276
|
+
</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
)}
|
|
1279
|
+
<div className="message ai-message">
|
|
1280
|
+
<div className="bubble-wrapper">
|
|
1281
|
+
{renderCompletedActivity(interaction)}
|
|
1282
|
+
{renderFileChanges(interaction)}
|
|
1283
|
+
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
1284
|
+
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1285
|
+
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
1286
|
+
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
1287
|
+
<span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
|
1288
|
+
<button
|
|
1289
|
+
type="button"
|
|
1290
|
+
className={`tts-btn ${speakingText === interaction.aiText ? 'is-speaking' : ''}`}
|
|
1291
|
+
onClick={() => speak(interaction.aiText)}
|
|
1292
|
+
title={speakingText === interaction.aiText ? 'หยุดอ่านออกเสียง' : 'อ่านออกเสียง'}
|
|
1293
|
+
>
|
|
1294
|
+
{renderSpeakerIcon(speakingText === interaction.aiText)}
|
|
1295
|
+
</button>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>
|
|
1300
|
+
))}
|
|
1301
|
+
|
|
1302
|
+
{sending && (
|
|
1303
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1304
|
+
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
|
|
1305
|
+
{agentMode && agentActivities.items.length > 0 && (
|
|
1306
|
+
<div className="message ai-message agent-activity-message">
|
|
1307
|
+
<div className="agent-activity-card">
|
|
1308
|
+
<div className="agent-activity-header">
|
|
1309
|
+
<span>{agentActivities.summary}</span>
|
|
1310
|
+
<span className="agent-activity-status" data-state={pendingApproval ? 'approval' : 'active'}>
|
|
1311
|
+
{pendingApproval ? 'Waiting for approval' : 'Working'}
|
|
1312
|
+
</span>
|
|
1313
|
+
</div>
|
|
1314
|
+
{renderAgentActivityTable(agentActivities)}
|
|
1315
|
+
</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
)}
|
|
1318
|
+
{renderActiveFileChanges()}
|
|
1319
|
+
<div className="message ai-message thinking-message">
|
|
1320
|
+
<div className="bubble-wrapper">
|
|
1321
|
+
<div className="message-bubble"><span>{streamedReply ? renderFormattedMessage(streamedReply) : 'Thinking...'}</span></div>
|
|
1322
|
+
{streamedResponse && (
|
|
1323
|
+
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1324
|
+
<button className="provider-badge">{badge(streamedResponse.provider, streamedResponse.model)}</button>
|
|
1325
|
+
{activeFallbackNotice && <span className="provider-fallback-notice">{activeFallbackNotice}</span>}
|
|
1326
|
+
{streamedReply && (
|
|
1327
|
+
<button
|
|
1328
|
+
type="button"
|
|
1329
|
+
className={`tts-btn ${speakingText === streamedReply ? 'is-speaking' : ''}`}
|
|
1330
|
+
onClick={() => speak(streamedReply)}
|
|
1331
|
+
title={speakingText === streamedReply ? 'หยุดอ่านออกเสียง' : 'อ่านออกเสียง'}
|
|
1332
|
+
>
|
|
1333
|
+
{renderSpeakerIcon(speakingText === streamedReply)}
|
|
1334
|
+
</button>
|
|
1335
|
+
)}
|
|
1336
|
+
</div>
|
|
1337
|
+
)}
|
|
1338
|
+
</div>
|
|
1339
|
+
</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
)}
|
|
1342
|
+
|
|
1343
|
+
{pendingApproval && (() => {
|
|
1344
|
+
const details = renderApprovalDetails(pendingApproval.approval)
|
|
1345
|
+
return (
|
|
1346
|
+
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1347
|
+
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1348
|
+
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1349
|
+
<div className="approval-card-content">
|
|
1350
|
+
<div className="approval-card-title">{details.title}</div>
|
|
1351
|
+
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1352
|
+
{details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>}
|
|
1353
|
+
</div>
|
|
1354
|
+
<div className="approval-card-actions">
|
|
1355
|
+
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
1356
|
+
<button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
|
|
1357
|
+
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
1358
|
+
</div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</div>
|
|
1361
|
+
</div>
|
|
1362
|
+
)
|
|
1363
|
+
})()}
|
|
1364
|
+
<div ref={chatEnd} />
|
|
1365
|
+
</div>
|
|
1366
|
+
|
|
1367
|
+
<div className="input-area">
|
|
1368
|
+
{isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
|
|
1369
|
+
<button type="button" className="workspace-select-btn" onClick={onSelectWorkspace}>
|
|
1370
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1371
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
1372
|
+
<path d="M3 6h7l2 2h9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>
|
|
1373
|
+
</svg>
|
|
1374
|
+
</span>
|
|
1375
|
+
<span>{workspaceName}</span>
|
|
1376
|
+
<span aria-hidden="true">⌄</span>
|
|
1377
|
+
</button>
|
|
1378
|
+
<div className="smart-context-bar">
|
|
1379
|
+
<div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1380
|
+
<label className="toggle-switch">
|
|
1381
|
+
<input type="checkbox" checked={smartContext} onChange={(event) => onSetSmartContext(event.target.checked)} />
|
|
1382
|
+
<span className="slider round" />
|
|
1383
|
+
</label>
|
|
1384
|
+
<span>Smart Context (Auto-Screen)</span>
|
|
1385
|
+
</div>
|
|
1386
|
+
<div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1387
|
+
<label className="toggle-switch">
|
|
1388
|
+
<input type="checkbox" checked={agentMode} onChange={(event) => onSetAgentMode(event.target.checked)} />
|
|
1389
|
+
<span className="slider round" />
|
|
1390
|
+
</label>
|
|
1391
|
+
<span>Agent Mode</span>
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
{voiceMode && (
|
|
1395
|
+
<div className="voice-mode-bar" data-state={voiceStatus}>
|
|
1396
|
+
<span className="voice-mode-dot" />
|
|
1397
|
+
<span>{voiceStatusLabel}</span>
|
|
1398
|
+
{voiceTranscript && <span className="voice-mode-transcript">{voiceTranscript}</span>}
|
|
1399
|
+
</div>
|
|
1400
|
+
)}
|
|
1401
|
+
|
|
1402
|
+
<form
|
|
1403
|
+
id="chat-form"
|
|
1404
|
+
onSubmit={onSubmit}
|
|
1405
|
+
onDragOver={(event) => {
|
|
1406
|
+
if (event.dataTransfer.types.includes('application/x-mint-workspace-path')) {
|
|
1407
|
+
event.preventDefault()
|
|
1408
|
+
event.dataTransfer.dropEffect = 'copy'
|
|
1409
|
+
}
|
|
1410
|
+
}}
|
|
1411
|
+
onDrop={handleWorkspaceDrop}
|
|
1412
|
+
onPaste={(event: ClipboardEvent<HTMLElement>) => {
|
|
1413
|
+
if (onPasteImage(event.clipboardData)) event.preventDefault()
|
|
1414
|
+
}}
|
|
1415
|
+
>
|
|
1416
|
+
{(imageAttachments.length > 0 || documentName) && (
|
|
1417
|
+
<div className="mint-attachment">
|
|
1418
|
+
{imageAttachments.map((attachment, idx) => (
|
|
1419
|
+
<div className="mint-image-attachment" key={idx}>
|
|
1420
|
+
<img className="mint-image-preview" src={attachment.previewDataUri || attachment.dataUri} alt={attachment.name || 'Image attachment'} />
|
|
1421
|
+
<button className="mint-attachment-remove" type="button" onClick={() => onRemoveImage(idx)} aria-label="Remove image">
|
|
1422
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1423
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
1424
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
1425
|
+
</svg>
|
|
1426
|
+
</button>
|
|
1427
|
+
</div>
|
|
1428
|
+
))}
|
|
1429
|
+
{documentName && (
|
|
1430
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
|
|
1431
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
|
|
1432
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1433
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
1434
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
1435
|
+
</svg>
|
|
1436
|
+
</span>
|
|
1437
|
+
<span style={{ fontSize: '0.76rem', color: 'var(--text-soft)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '220px' }}>{documentName}</span>
|
|
1438
|
+
<button type="button" onClick={onRemoveDocument} style={{ background: 'transparent', border: 0, color: '#ef4444', cursor: 'pointer', display: 'inline-flex', alignItems: 'center' }}>
|
|
1439
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1440
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
1441
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
1442
|
+
</svg>
|
|
1443
|
+
</button>
|
|
1444
|
+
</div>
|
|
1445
|
+
)}
|
|
1446
|
+
</div>
|
|
1447
|
+
)}
|
|
1448
|
+
<textarea
|
|
1449
|
+
id="chat-input"
|
|
1450
|
+
value={message}
|
|
1451
|
+
onChange={(event) => {
|
|
1452
|
+
resizeInput(event.currentTarget)
|
|
1453
|
+
onSetMessage(event.target.value)
|
|
1454
|
+
}}
|
|
1455
|
+
onKeyDown={handleInputKeyDown}
|
|
1456
|
+
onDrop={handleWorkspaceDrop}
|
|
1457
|
+
placeholder="Ask anything, @ to mention, / for actions"
|
|
1458
|
+
rows={1}
|
|
1459
|
+
/>
|
|
1460
|
+
<div className="chat-tool-menu-wrap" ref={toolMenuRef}>
|
|
1461
|
+
<button id="chat-tool-btn" type="button" aria-haspopup="menu" aria-expanded={toolMenuOpen} onClick={() => setToolMenuOpen((open) => !open)} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1462
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1463
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
1464
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
1465
|
+
</svg>
|
|
1466
|
+
</button>
|
|
1467
|
+
{toolMenuOpen && (
|
|
1468
|
+
<div className="chat-tool-menu" role="menu">
|
|
1469
|
+
<button type="button" role="menuitem" onClick={openImagePicker} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1470
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1471
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1472
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
1473
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
1474
|
+
<polyline points="21 15 16 10 5 21"></polyline>
|
|
1475
|
+
</svg>
|
|
1476
|
+
</span>
|
|
1477
|
+
<span>Add image</span>
|
|
1478
|
+
</button>
|
|
1479
|
+
<button type="button" role="menuitem" onClick={openDocumentPicker} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1480
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1481
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1482
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
1483
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
1484
|
+
</svg>
|
|
1485
|
+
</span>
|
|
1486
|
+
<span>Add file</span>
|
|
1487
|
+
</button>
|
|
1488
|
+
<button type="button" role="menuitem" onClick={startWebSearch} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1489
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
1490
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1491
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
1492
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
1493
|
+
</svg>
|
|
1494
|
+
</span>
|
|
1495
|
+
<span>Search web</span>
|
|
1496
|
+
</button>
|
|
1497
|
+
</div>
|
|
1498
|
+
)}
|
|
1499
|
+
</div>
|
|
1500
|
+
<input id="vision-file-input" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onChange={onSelectImage} style={{ display: 'none' }} />
|
|
1501
|
+
<input id="document-file-input" type="file" accept="application/pdf,.pdf" onChange={onSelectDocument} style={{ display: 'none' }} />
|
|
1502
|
+
<button id="screen-capture-btn" type="button" onClick={onCaptureScreen} aria-label="Capture screen">
|
|
1503
|
+
<span className="screen-capture-eye" aria-hidden="true" />
|
|
1504
|
+
</button>
|
|
1505
|
+
<div className="chat-provider-select" style={{ display: 'flex', gap: '4px', padding: 0, background: 'transparent', border: 0, width: '100%', height: '32px' }}>
|
|
1506
|
+
<select
|
|
1507
|
+
value={status?.activeProvider ?? ''}
|
|
1508
|
+
onChange={(event) => onSetProvider(event.target.value)}
|
|
1509
|
+
style={{
|
|
1510
|
+
flex: 1,
|
|
1511
|
+
minWidth: '65px',
|
|
1512
|
+
height: '100%',
|
|
1513
|
+
padding: '0 20px 0 6px',
|
|
1514
|
+
background: 'transparent',
|
|
1515
|
+
border: 0,
|
|
1516
|
+
color: 'var(--text-soft)',
|
|
1517
|
+
fontSize: '0.78rem',
|
|
1518
|
+
outline: 'none',
|
|
1519
|
+
cursor: 'pointer',
|
|
1520
|
+
fontFamily: 'inherit',
|
|
1521
|
+
textOverflow: 'ellipsis',
|
|
1522
|
+
whiteSpace: 'nowrap'
|
|
1523
|
+
}}
|
|
1524
|
+
>
|
|
1525
|
+
{status?.availableProviders.map((provider) => {
|
|
1526
|
+
let displayName = provider
|
|
1527
|
+
if (provider === 'gemini') displayName = 'Gemini'
|
|
1528
|
+
else if (provider === 'openai') displayName = 'OpenAI'
|
|
1529
|
+
else if (provider === 'openrouter') displayName = 'OpenRouter'
|
|
1530
|
+
else if (provider === 'deepseek') displayName = 'DeepSeek'
|
|
1531
|
+
else if (provider === 'anthropic') displayName = 'Claude'
|
|
1532
|
+
else if (provider === 'huggingface') displayName = 'HF'
|
|
1533
|
+
else if (provider === 'local_openai') displayName = 'Local'
|
|
1534
|
+
else if (provider === 'ollama') displayName = 'Ollama'
|
|
1535
|
+
return <option key={provider} value={provider}>{displayName}</option>
|
|
1536
|
+
})}
|
|
1537
|
+
</select>
|
|
1538
|
+
{availableModels.length > 0 && (
|
|
1539
|
+
<select
|
|
1540
|
+
value={activeModel}
|
|
1541
|
+
onChange={(event) => onSetModel(event.target.value)}
|
|
1542
|
+
style={{
|
|
1543
|
+
flex: 1.2,
|
|
1544
|
+
minWidth: '85px',
|
|
1545
|
+
height: '100%',
|
|
1546
|
+
padding: '0 20px 0 6px',
|
|
1547
|
+
background: 'transparent',
|
|
1548
|
+
border: 0,
|
|
1549
|
+
color: 'var(--text-soft)',
|
|
1550
|
+
fontSize: '0.78rem',
|
|
1551
|
+
outline: 'none',
|
|
1552
|
+
cursor: 'pointer',
|
|
1553
|
+
fontFamily: 'inherit',
|
|
1554
|
+
textOverflow: 'ellipsis',
|
|
1555
|
+
whiteSpace: 'nowrap'
|
|
1556
|
+
}}
|
|
1557
|
+
>
|
|
1558
|
+
{availableModels.map((model) => (
|
|
1559
|
+
<option key={model} value={model}>{model.split('/').pop()}</option>
|
|
1560
|
+
))}
|
|
1561
|
+
{!availableModels.includes(activeModel) && activeModel && (
|
|
1562
|
+
<option value={activeModel}>{activeModel.split('/').pop()}</option>
|
|
1563
|
+
)}
|
|
1564
|
+
</select>
|
|
1565
|
+
)}
|
|
1566
|
+
</div>
|
|
1567
|
+
<button
|
|
1568
|
+
id="mic-btn"
|
|
1569
|
+
className={`${isRecording ? 'is-recording' : ''} ${voiceMode ? 'voice-mode-active' : ''}`}
|
|
1570
|
+
type="button"
|
|
1571
|
+
onClick={toggleRecording}
|
|
1572
|
+
title={voiceMode ? 'ปิดโหมดสนทนาเสียง' : 'เปิดโหมดสนทนาเสียง'}
|
|
1573
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1574
|
+
>
|
|
1575
|
+
{isRecording ? (
|
|
1576
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1577
|
+
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
|
|
1578
|
+
</svg>
|
|
1579
|
+
) : (
|
|
1580
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1581
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
|
1582
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
|
1583
|
+
<line x1="12" y1="19" x2="12" y2="23"></line>
|
|
1584
|
+
<line x1="8" y1="23" x2="16" y2="23"></line>
|
|
1585
|
+
</svg>
|
|
1586
|
+
)}
|
|
1587
|
+
</button>
|
|
1588
|
+
<button id="send-btn" type="submit" disabled={sending || !canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1589
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1590
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
1591
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
1592
|
+
</svg>
|
|
1593
|
+
</button>
|
|
1594
|
+
</form>
|
|
1595
|
+
</div>
|
|
1596
|
+
</section>
|
|
1597
|
+
)
|
|
1598
|
+
}
|