@pheem49/mint 1.8.0 → 1.8.2
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/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +20 -6
- package/Release_Note.md +129 -0
- package/assets/Mint_CLI.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +242 -54
- package/crates/mint-cli/src/main.rs +397 -74
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +64 -2
- package/crates/mint-core/src/browser.rs +456 -0
- package/crates/mint-core/src/channels.rs +1 -0
- package/crates/mint-core/src/chat.rs +3 -0
- package/crates/mint-core/src/config.rs +130 -1
- package/crates/mint-core/src/lib.rs +19 -0
- package/crates/mint-core/src/orchestration.rs +317 -12
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/tts.rs +3 -1
- package/crates/mint-core/src/workflows.rs +1 -22
- package/flatpak/com.pheem49.mint.desktop +9 -0
- package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
- package/flatpak/com.pheem49.mint.yaml +52 -0
- package/package.json +1 -1
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/shared-modules/CODEOWNERS +23 -0
- package/shared-modules/README.md +79 -0
- package/shared-modules/SDL/README.md +3 -0
- package/shared-modules/SDL/SDL-1.2.15.json +51 -0
- package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
- package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
- package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
- package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
- package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
- package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
- package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
- package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
- package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
- package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
- package/shared-modules/SDL/sdl12-compat.json +22 -0
- package/shared-modules/SDL2/SDL2_image.json +27 -0
- package/shared-modules/SDL2/SDL2_ttf.json +29 -0
- package/shared-modules/cld2/CMakeLists.txt +157 -0
- package/shared-modules/cld2/cld2.json +24 -0
- package/shared-modules/clutter/clutter.json +72 -0
- package/shared-modules/dbus-glib/dbus-glib.json +29 -0
- package/shared-modules/glew/glew.json +36 -0
- package/shared-modules/glu/glu-9.json +12 -0
- package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
- package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
- package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
- package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
- package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
- package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
- package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
- package/shared-modules/gtk2/gtk2.json +120 -0
- package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
- package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
- package/shared-modules/gtk2/readme.md +10 -0
- package/shared-modules/intltool/intltool-0.51.json +20 -0
- package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
- package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
- package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
- package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
- package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
- package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
- package/shared-modules/lame/lame-3.99.5.json +32 -0
- package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
- package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
- package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
- package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
- package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
- package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
- package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
- package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
- package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
- package/shared-modules/libcanberra/libcanberra.json +44 -0
- package/shared-modules/libmad/libmad.json +19 -0
- package/shared-modules/libsecret/libsecret.json +23 -0
- package/shared-modules/libsoup/libsoup-2.4.json +14 -0
- package/shared-modules/libusb/libusb.json +27 -0
- package/shared-modules/linux-audio/dssi.json +31 -0
- package/shared-modules/linux-audio/fftw3f-static.json +44 -0
- package/shared-modules/linux-audio/fftw3f.json +42 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
- package/shared-modules/linux-audio/fluidsynth2.json +19 -0
- package/shared-modules/linux-audio/ladspa.json +26 -0
- package/shared-modules/linux-audio/lash.json +35 -0
- package/shared-modules/linux-audio/libinstpatch.json +16 -0
- package/shared-modules/linux-audio/liblo-static.json +22 -0
- package/shared-modules/linux-audio/liblo.json +17 -0
- package/shared-modules/linux-audio/lilv-static.json +94 -0
- package/shared-modules/linux-audio/lilv.json +82 -0
- package/shared-modules/linux-audio/lrdf.json +57 -0
- package/shared-modules/linux-audio/lv2.json +21 -0
- package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
- package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
- package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
- package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
- package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
- package/shared-modules/linux-audio/stk.json +28 -0
- package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
- package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
- package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
- package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
- package/shared-modules/lua5.3/lua.pc.in +8 -0
- package/shared-modules/lua5.3/luaconf.patch +47 -0
- package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
- package/shared-modules/lua5.4/lua-5.4.json +41 -0
- package/shared-modules/lua5.4/lua.pc.in +8 -0
- package/shared-modules/lua5.4/luaconf.patch +34 -0
- package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
- package/shared-modules/luajit/luajit.json +30 -0
- package/shared-modules/lzo/lzo.json +27 -0
- package/shared-modules/mac/mac.json +18 -0
- package/shared-modules/physfs/physfs.json +26 -0
- package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
- package/shared-modules/pygame/cython-fixes.patch +34 -0
- package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
- package/shared-modules/pygame/glib.patch +25 -0
- package/shared-modules/pygame/pygame1-python2.json +157 -0
- package/shared-modules/pygtk/pygtk-c99.patch +15 -0
- package/shared-modules/pygtk/pygtk.json +117 -0
- package/shared-modules/pygtk/pygtk.patch +39 -0
- package/shared-modules/python2.7/python-2.7.json +48 -0
- package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
- package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
- package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
- package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
- package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
- package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
- package/shared-modules/update-checker.sh +50 -0
- package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
- package/shared-modules/vorbisgain/0002-errno.patch +33 -0
- package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
- package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
- package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
- package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
- package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
- package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
- package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
- package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
- package/shared-modules/vorbisgain/README.md +23 -0
- package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
- package/src/renderer/shared/agentProgress.ts +145 -0
- package/src/renderer/shared/calculator.ts +52 -0
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
- package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
- package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
- package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
- package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
- package/src/renderer/shared/constants/models.ts +91 -0
- package/src/renderer/shared/platform.ts +82 -0
- package/src/renderer/shared/types.ts +140 -0
- package/src/renderer/shared/utils/agentActivity.ts +187 -0
- package/src/renderer/shared/utils/approval.ts +1 -0
- package/src/renderer/shared/utils/approval.tsx +64 -0
- package/src/renderer/shared/utils/markdown.tsx +347 -0
- package/src/renderer/shared/utils/providers.ts +36 -0
- package/src/renderer/shared/utils/speech.ts +216 -0
- package/src/renderer/shared/utils/ui.ts +147 -0
- package/src/renderer/src/agentProgress.ts +11 -39
- package/src/renderer/src/calculator.ts +3 -46
- package/src/renderer/src/components/ChatPanel.tsx +230 -1202
- package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src/components/MintDashboard.tsx +63 -171
- package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
- package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +6 -0
- package/src/renderer/src/css/styles.css +148 -11
- package/src/renderer/src/index.css +12 -1
- package/src/renderer/src/tauri.ts +169 -143
- package/src/renderer/src-web/agentProgress.ts +11 -39
- package/src/renderer/src-web/calculator.ts +3 -46
- package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
- package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src-web/css/image-studio.css +6 -0
- package/src/renderer/src-web/css/styles.css +184 -12
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +204 -104
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +254 -59
- package/src-tauri/src/webhooks.rs +1 -0
- package/src-tauri/src/workflows.rs +57 -4
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type FormEvent, type KeyboardEvent, type RefObject, type DragEvent } from 'react'
|
|
2
|
-
import { hasAgentToolActivity, thoughtsFrom } from '../agentProgress'
|
|
2
|
+
import { hasAgentToolActivity, thoughtsFrom, parseFileChangesFromProgress } from '../agentProgress'
|
|
3
|
+
import {
|
|
4
|
+
GEMINI_MODELS,
|
|
5
|
+
OPENAI_MODELS,
|
|
6
|
+
ANTHROPIC_MODELS,
|
|
7
|
+
OPENROUTER_MODELS,
|
|
8
|
+
DEEPSEEK_MODELS,
|
|
9
|
+
HF_MODELS,
|
|
10
|
+
LOCAL_MODELS,
|
|
11
|
+
OLLAMA_MODELS,
|
|
12
|
+
} from '../../shared/constants/models'
|
|
13
|
+
import { badge, providerLabel, fallbackNotice } from '../../shared/utils/providers'
|
|
14
|
+
import { activitiesFrom, parseWebSearchSources, type AgentActivity, type AgentActivityView } from '../../shared/utils/agentActivity'
|
|
15
|
+
import { AgentActivityTable } from '../../shared/components/AgentActivityTable'
|
|
16
|
+
import { ChatCodeBlock } from '../../shared/components/ChatCodeBlock'
|
|
17
|
+
import { renderApprovalDetails, renderDiff, type ApprovalDetails } from '../../shared/utils/approval'
|
|
18
|
+
import { ApprovalCard } from '../../shared/components/ApprovalCard'
|
|
19
|
+
import { renderFormattedMessage, readableAssistantText, cleanSpeechText, renderSpeakerIcon } from '../../shared/utils/markdown'
|
|
20
|
+
import { ThinkingBlock } from '../../shared/components/ThinkingBlock'
|
|
21
|
+
import { AgentActivityDrawer } from '../../shared/components/AgentActivityDrawer'
|
|
22
|
+
import type { DiffHunk, FileChange } from '../../shared/types'
|
|
23
|
+
import { numericSetting } from '../../shared/utils/ui'
|
|
24
|
+
import { useSpeechToText } from '../../shared/utils/speech'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
3
28
|
import {
|
|
4
29
|
type AgentProgress,
|
|
5
30
|
type ChatResponse,
|
|
@@ -7,317 +32,6 @@ import {
|
|
|
7
32
|
getTtsUrls,
|
|
8
33
|
} from '../tauri'
|
|
9
34
|
|
|
10
|
-
const GEMINI_MODELS = [
|
|
11
|
-
'gemini-2.5-flash',
|
|
12
|
-
'gemini-2.5-pro',
|
|
13
|
-
'gemini-2.0-flash',
|
|
14
|
-
'gemini-1.5-flash',
|
|
15
|
-
'gemini-1.5-pro',
|
|
16
|
-
'gemini-3.1-flash-lite',
|
|
17
|
-
'gemini-3.1-flash-lite-preview'
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
const OPENAI_MODELS = [
|
|
21
|
-
'gpt-4o',
|
|
22
|
-
'gpt-4o-mini',
|
|
23
|
-
'o1',
|
|
24
|
-
'o3-mini',
|
|
25
|
-
'o1-preview',
|
|
26
|
-
'o1-mini',
|
|
27
|
-
'gpt-4-turbo'
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
const OPENROUTER_MODELS = [
|
|
31
|
-
'openai/gpt-4o-mini',
|
|
32
|
-
'openai/gpt-4o',
|
|
33
|
-
'anthropic/claude-3.5-sonnet',
|
|
34
|
-
'anthropic/claude-3.5-haiku',
|
|
35
|
-
'google/gemini-2.5-flash',
|
|
36
|
-
'meta-llama/llama-3.3-70b-instruct',
|
|
37
|
-
'mistralai/mistral-large'
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
const DEEPSEEK_MODELS = [
|
|
41
|
-
'deepseek-v4-flash',
|
|
42
|
-
'deepseek-v4-pro',
|
|
43
|
-
'deepseek-chat',
|
|
44
|
-
'deepseek-reasoner'
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
const ANTHROPIC_MODELS = [
|
|
48
|
-
'claude-3-7-sonnet-latest',
|
|
49
|
-
'claude-3-5-sonnet-latest',
|
|
50
|
-
'claude-3-5-haiku-latest',
|
|
51
|
-
'claude-3-opus-latest'
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
const HF_MODELS = [
|
|
55
|
-
'meta-llama/Llama-3.3-70B-Instruct',
|
|
56
|
-
'meta-llama/Meta-Llama-3-8B-Instruct',
|
|
57
|
-
'meta-llama/Llama-3.2-3B-Instruct',
|
|
58
|
-
'Qwen/Qwen2.5-72B-Instruct',
|
|
59
|
-
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
60
|
-
'mistralai/Mistral-7B-Instruct-v0.3',
|
|
61
|
-
'google/gemma-2-9b-it'
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
const LOCAL_MODELS = [
|
|
65
|
-
'local-model',
|
|
66
|
-
'Qwen/Qwen2.5-7B-Instruct-GGUF',
|
|
67
|
-
'meta-llama/Llama-3.2-3B-Instruct-GGUF',
|
|
68
|
-
'lmstudio-community/gemma-2-9b-it-GGUF'
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
const OLLAMA_MODELS = [
|
|
72
|
-
'llama3:latest',
|
|
73
|
-
'llama3.1:latest',
|
|
74
|
-
'llama3.2:latest',
|
|
75
|
-
'gemma2:latest',
|
|
76
|
-
'mistral:latest',
|
|
77
|
-
'phi3:latest',
|
|
78
|
-
'qwen2.5:latest'
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
interface ApprovalDetails {
|
|
82
|
-
title: string
|
|
83
|
-
body: string
|
|
84
|
-
reason?: string
|
|
85
|
-
isDangerous: boolean
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function badge(provider: string, model: string) {
|
|
89
|
-
return [provider, model].filter(Boolean).join(' / ')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function providerLabel(provider: string) {
|
|
93
|
-
switch (provider) {
|
|
94
|
-
case 'gemini':
|
|
95
|
-
return 'Gemini'
|
|
96
|
-
case 'openai':
|
|
97
|
-
return 'OpenAI'
|
|
98
|
-
case 'openrouter':
|
|
99
|
-
return 'OpenRouter'
|
|
100
|
-
case 'deepseek':
|
|
101
|
-
return 'DeepSeek'
|
|
102
|
-
case 'anthropic':
|
|
103
|
-
return 'Claude'
|
|
104
|
-
case 'huggingface':
|
|
105
|
-
return 'Hugging Face'
|
|
106
|
-
case 'local_openai':
|
|
107
|
-
return 'Local OpenAI'
|
|
108
|
-
case 'ollama':
|
|
109
|
-
return 'Ollama'
|
|
110
|
-
default:
|
|
111
|
-
return provider || 'Primary provider'
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function fallbackNotice(response: Pick<ChatResponse, 'provider' | 'fallbackProvider'> | null | undefined) {
|
|
116
|
-
if (!response?.fallbackProvider) return ''
|
|
117
|
-
return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface AgentActivity {
|
|
121
|
-
label: string
|
|
122
|
-
target: string
|
|
123
|
-
kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
|
|
124
|
-
state: 'active' | 'done' | 'error'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interface AgentActivityView {
|
|
128
|
-
summary: string
|
|
129
|
-
items: AgentActivity[]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function activityDetail(input: Record<string, unknown>, key: string) {
|
|
133
|
-
const value = input[key]
|
|
134
|
-
return typeof value === 'string' && value.trim() ? value : ''
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function formatActivityTarget(value: string) {
|
|
138
|
-
const compact = value.replace(/^\/home\/([^/]+)/, '~')
|
|
139
|
-
return compact || 'workspace'
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function activityKind(action: string, target: string): AgentActivity['kind'] {
|
|
143
|
-
if (['search_code', 'semantic_search', 'knowledge_search', 'web_search', 'memory_recall'].includes(action)) return 'search'
|
|
144
|
-
if (['run_shell', 'verify'].includes(action)) return 'terminal'
|
|
145
|
-
if (['list_files', 'detect_project'].includes(action)) return 'folder'
|
|
146
|
-
if (['read_file', 'symbols', 'read_diagnostics', 'git_diff', 'apply_patch', 'write_file', 'note_write', 'view_image'].includes(action)) return 'file'
|
|
147
|
-
return target.includes('/') && !/\.[^/]+$/.test(target) ? 'folder' : 'tool'
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function describeTool(action: string, input: Record<string, unknown>): AgentActivity {
|
|
151
|
-
const path = activityDetail(input, 'path')
|
|
152
|
-
const query = activityDetail(input, 'query')
|
|
153
|
-
const command = activityDetail(input, 'command')
|
|
154
|
-
const name = activityDetail(input, 'name')
|
|
155
|
-
const tool = activityDetail(input, 'tool')
|
|
156
|
-
const target = path || query || command || name || tool || action.replaceAll('_', ' ')
|
|
157
|
-
const labels: Record<string, string> = {
|
|
158
|
-
apply_patch: 'Applying patch',
|
|
159
|
-
ask_user: 'Asking user',
|
|
160
|
-
create_plan: 'Creating plan',
|
|
161
|
-
detect_project: 'Detecting project',
|
|
162
|
-
git_branch: 'Reading branch',
|
|
163
|
-
git_diff: 'Reading diff',
|
|
164
|
-
git_log: 'Reading log',
|
|
165
|
-
git_status: 'Reading git status',
|
|
166
|
-
knowledge_search: 'Searching knowledge',
|
|
167
|
-
list_files: 'Listing files',
|
|
168
|
-
list_tests: 'Listing tests',
|
|
169
|
-
mcp_tool: 'Calling MCP tool',
|
|
170
|
-
memory_recall: 'Recalling memory',
|
|
171
|
-
note_write: 'Writing note',
|
|
172
|
-
read_diagnostics: 'Reading diagnostics',
|
|
173
|
-
read_file: 'Reading file',
|
|
174
|
-
request_user_approval: 'Requesting approval',
|
|
175
|
-
run_plugin: 'Running plugin',
|
|
176
|
-
run_shell: 'Running command',
|
|
177
|
-
search_code: 'Searching code',
|
|
178
|
-
semantic_index: 'Indexing code',
|
|
179
|
-
semantic_search: 'Searching code',
|
|
180
|
-
symbols: 'Inspecting symbols',
|
|
181
|
-
update_plan: 'Updating plan',
|
|
182
|
-
verify: 'Verifying',
|
|
183
|
-
view_image: 'Viewing image',
|
|
184
|
-
web_search: 'Searching web',
|
|
185
|
-
write_file: 'Writing file',
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
label: labels[action] ?? 'Using tool',
|
|
189
|
-
target: formatActivityTarget(target),
|
|
190
|
-
kind: activityKind(action, target),
|
|
191
|
-
state: 'active',
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function activitySummary(items: AgentActivity[]) {
|
|
196
|
-
const files = new Set<string>()
|
|
197
|
-
const folders = new Set<string>()
|
|
198
|
-
for (const item of items) {
|
|
199
|
-
if (item.kind === 'file') files.add(item.target)
|
|
200
|
-
if (item.kind === 'folder') folders.add(item.target)
|
|
201
|
-
}
|
|
202
|
-
const parts = [
|
|
203
|
-
files.size ? `${files.size} ${files.size === 1 ? 'file' : 'files'}` : '',
|
|
204
|
-
folders.size ? `${folders.size} ${folders.size === 1 ? 'folder' : 'folders'}` : '',
|
|
205
|
-
].filter(Boolean)
|
|
206
|
-
return parts.length ? `Exploring ${parts.join(', ')}` : 'Working through task'
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
|
|
210
|
-
const activities: AgentActivity[] = []
|
|
211
|
-
for (const event of progress) {
|
|
212
|
-
if (event.type === 'ToolStart') {
|
|
213
|
-
activities.push(describeTool(event.data.action, event.data.input))
|
|
214
|
-
} else if (event.type === 'ToolEnd') {
|
|
215
|
-
for (let index = activities.length - 1; index >= 0; index -= 1) {
|
|
216
|
-
if (activities[index].state !== 'active') continue
|
|
217
|
-
activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
|
|
218
|
-
if (activities[index].state === 'error') activities[index].label = 'Failed'
|
|
219
|
-
break
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const items = activities.slice(-12)
|
|
224
|
-
return { summary: activitySummary(activities), items }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function renderAgentActivityTable(activityView: AgentActivityView) {
|
|
228
|
-
return (
|
|
229
|
-
<div className="agent-activity-list">
|
|
230
|
-
<div className="agent-activity-table-head" aria-hidden="true">
|
|
231
|
-
<span>Tool</span>
|
|
232
|
-
<span />
|
|
233
|
-
<span>Target</span>
|
|
234
|
-
<span />
|
|
235
|
-
</div>
|
|
236
|
-
{activityView.items.map((activity, index) => (
|
|
237
|
-
<div className="agent-activity-item" data-kind={activity.kind} data-state={activity.state} key={`${index}-${activity.label}-${activity.target}`}>
|
|
238
|
-
<span className="agent-activity-label">{activity.label}</span>
|
|
239
|
-
<span className="agent-activity-icon" aria-hidden="true" />
|
|
240
|
-
<span className="agent-activity-text">{activity.target}</span>
|
|
241
|
-
<span className="agent-activity-chevron" aria-hidden="true">></span>
|
|
242
|
-
</div>
|
|
243
|
-
))}
|
|
244
|
-
</div>
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
249
|
-
if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
|
|
250
|
-
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 }
|
|
251
|
-
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 }
|
|
252
|
-
if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
|
|
253
|
-
if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
|
|
254
|
-
if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
|
|
255
|
-
if (approval.McpTool) {
|
|
256
|
-
const { server, tool, arguments: args } = approval.McpTool
|
|
257
|
-
return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
|
|
258
|
-
}
|
|
259
|
-
if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
|
|
260
|
-
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 }
|
|
261
|
-
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function renderDiff(diffText: string) {
|
|
265
|
-
if (!diffText) return null
|
|
266
|
-
const lines = diffText.split('\n')
|
|
267
|
-
return (
|
|
268
|
-
<div style={{
|
|
269
|
-
background: '#0b0f19',
|
|
270
|
-
borderRadius: '6px',
|
|
271
|
-
padding: '8px',
|
|
272
|
-
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
273
|
-
overflowX: 'auto',
|
|
274
|
-
maxHeight: '400px',
|
|
275
|
-
fontFamily: 'monospace',
|
|
276
|
-
fontSize: '0.74rem',
|
|
277
|
-
lineHeight: '1.4',
|
|
278
|
-
}}>
|
|
279
|
-
{lines.map((line, idx) => {
|
|
280
|
-
let style: any = {
|
|
281
|
-
whiteSpace: 'pre-wrap',
|
|
282
|
-
padding: '2px 6px',
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
|
|
286
|
-
style = {
|
|
287
|
-
...style,
|
|
288
|
-
color: '#64748b',
|
|
289
|
-
fontWeight: 'bold',
|
|
290
|
-
}
|
|
291
|
-
} else if (line.startsWith('+')) {
|
|
292
|
-
style = {
|
|
293
|
-
...style,
|
|
294
|
-
background: 'rgba(16, 185, 129, 0.12)',
|
|
295
|
-
borderLeft: '3px solid #10b981',
|
|
296
|
-
color: '#a7f3d0'
|
|
297
|
-
}
|
|
298
|
-
} else if (line.startsWith('-')) {
|
|
299
|
-
style = {
|
|
300
|
-
...style,
|
|
301
|
-
background: 'rgba(239, 68, 68, 0.12)',
|
|
302
|
-
borderLeft: '3px solid #ef4444',
|
|
303
|
-
color: '#fca5a5'
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
style = {
|
|
307
|
-
...style,
|
|
308
|
-
color: '#e2e8f0',
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return (
|
|
313
|
-
<div key={idx} style={style}>
|
|
314
|
-
{line}
|
|
315
|
-
</div>
|
|
316
|
-
)
|
|
317
|
-
})}
|
|
318
|
-
</div>
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
35
|
|
|
322
36
|
interface ChatPanelProps {
|
|
323
37
|
interactions: any[]
|
|
@@ -343,7 +57,6 @@ interface ChatPanelProps {
|
|
|
343
57
|
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
344
58
|
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
345
59
|
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
346
|
-
onReadClipboardImage: () => Promise<boolean>
|
|
347
60
|
onSetMessage: (message: string) => void
|
|
348
61
|
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
349
62
|
onRemoveImage: (idx: number) => void
|
|
@@ -353,581 +66,13 @@ interface ChatPanelProps {
|
|
|
353
66
|
onSetSmartContext: (enabled: boolean) => void
|
|
354
67
|
onSetAgentMode: (enabled: boolean) => void
|
|
355
68
|
onSetProvider: (provider: string) => void
|
|
356
|
-
onApproval: (approved: boolean, autoApproveSession?: boolean) => void
|
|
69
|
+
onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
|
|
357
70
|
onToggleMobileSidebar: () => void
|
|
358
71
|
settingsConfig: any
|
|
359
72
|
onSetModel: (model: string) => void
|
|
73
|
+
onCancelMessage: () => void
|
|
360
74
|
}
|
|
361
75
|
|
|
362
|
-
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
363
|
-
const [copied, setCopied] = useState(false)
|
|
364
|
-
|
|
365
|
-
const handleCopy = async () => {
|
|
366
|
-
try {
|
|
367
|
-
await navigator.clipboard.writeText(code)
|
|
368
|
-
setCopied(true)
|
|
369
|
-
setTimeout(() => setCopied(false), 2000)
|
|
370
|
-
} catch (err) {
|
|
371
|
-
console.error('Failed to copy code: ', err)
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const handleDownload = () => {
|
|
376
|
-
try {
|
|
377
|
-
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
378
|
-
const url = URL.createObjectURL(blob)
|
|
379
|
-
const link = document.createElement('a')
|
|
380
|
-
link.href = url
|
|
381
|
-
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
382
|
-
link.click()
|
|
383
|
-
URL.revokeObjectURL(url)
|
|
384
|
-
} catch (err) {
|
|
385
|
-
console.error('Failed to download code: ', err)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
390
|
-
|
|
391
|
-
return (
|
|
392
|
-
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
393
|
-
<div className="chat-code-block-header">
|
|
394
|
-
<span className="chat-code-block-lang">{displayLang}</span>
|
|
395
|
-
<div className="chat-code-block-actions">
|
|
396
|
-
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
397
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
398
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
399
|
-
<polyline points="7 10 12 15 17 10" />
|
|
400
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
401
|
-
</svg>
|
|
402
|
-
</button>
|
|
403
|
-
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
404
|
-
{copied ? (
|
|
405
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
406
|
-
<polyline points="20 6 9 17 4 12" />
|
|
407
|
-
</svg>
|
|
408
|
-
) : (
|
|
409
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
410
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
411
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
412
|
-
</svg>
|
|
413
|
-
)}
|
|
414
|
-
</button>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
<pre className="chat-code-block-body">
|
|
418
|
-
<code>{code}</code>
|
|
419
|
-
</pre>
|
|
420
|
-
</div>
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const isTableLine = (line: string): boolean => {
|
|
425
|
-
const trimmed = line.trim()
|
|
426
|
-
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function renderFormattedMessage(text: string) {
|
|
430
|
-
const displayText = readableAssistantText(text)
|
|
431
|
-
if (!displayText) return null
|
|
432
|
-
|
|
433
|
-
const formatInline = (str: string) => {
|
|
434
|
-
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
435
|
-
return codeParts.map((codePart, codeIndex) => {
|
|
436
|
-
if (codeIndex % 2 === 1) {
|
|
437
|
-
return (
|
|
438
|
-
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
439
|
-
{codePart}
|
|
440
|
-
</code>
|
|
441
|
-
)
|
|
442
|
-
}
|
|
443
|
-
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
444
|
-
return boldParts.map((boldPart, boldIndex) => {
|
|
445
|
-
if (boldIndex % 2 === 1) {
|
|
446
|
-
return (
|
|
447
|
-
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
448
|
-
{boldPart}
|
|
449
|
-
</strong>
|
|
450
|
-
)
|
|
451
|
-
}
|
|
452
|
-
return boldPart
|
|
453
|
-
})
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const renderHtmlTable = (tableLines: string[], key: string) => {
|
|
458
|
-
const rows: string[][] = []
|
|
459
|
-
for (const line of tableLines) {
|
|
460
|
-
const trimmed = line.trim()
|
|
461
|
-
if (!trimmed) continue
|
|
462
|
-
const content = trimmed.slice(1, -1)
|
|
463
|
-
const contentEscaped = content.replace(/\\\|/g, '\u0000')
|
|
464
|
-
const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
|
|
465
|
-
rows.push(cols)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (rows.length === 0) return null
|
|
469
|
-
|
|
470
|
-
let hasSeparator = false
|
|
471
|
-
let separatorIdx = -1
|
|
472
|
-
for (let i = 0; i < rows.length; i++) {
|
|
473
|
-
const row = rows[i]
|
|
474
|
-
if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
|
|
475
|
-
hasSeparator = true
|
|
476
|
-
separatorIdx = i
|
|
477
|
-
break
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
let headerRow: string[] | null = null
|
|
482
|
-
const dataRows: string[][] = []
|
|
483
|
-
|
|
484
|
-
if (hasSeparator) {
|
|
485
|
-
if (separatorIdx > 0) {
|
|
486
|
-
headerRow = rows[separatorIdx - 1]
|
|
487
|
-
for (let i = 0; i < rows.length; i++) {
|
|
488
|
-
if (i !== separatorIdx && i !== separatorIdx - 1) {
|
|
489
|
-
dataRows.push(rows[i])
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
for (let i = 0; i < rows.length; i++) {
|
|
494
|
-
if (i !== separatorIdx) {
|
|
495
|
-
dataRows.push(rows[i])
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
headerRow = rows[0]
|
|
501
|
-
for (let i = 1; i < rows.length; i++) {
|
|
502
|
-
dataRows.push(rows[i])
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return (
|
|
507
|
-
<div key={key} className="chat-table-container" style={{
|
|
508
|
-
overflowX: 'auto',
|
|
509
|
-
margin: '14px 0',
|
|
510
|
-
width: '100%',
|
|
511
|
-
borderRadius: '8px',
|
|
512
|
-
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
513
|
-
background: 'rgba(30, 41, 59, 0.35)',
|
|
514
|
-
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
515
|
-
}}>
|
|
516
|
-
<table className="chat-table" style={{
|
|
517
|
-
width: '100%',
|
|
518
|
-
borderCollapse: 'collapse',
|
|
519
|
-
fontSize: '0.86rem',
|
|
520
|
-
textAlign: 'left',
|
|
521
|
-
lineHeight: '1.5',
|
|
522
|
-
}}>
|
|
523
|
-
{headerRow && (
|
|
524
|
-
<thead>
|
|
525
|
-
<tr style={{
|
|
526
|
-
background: 'rgba(255, 255, 255, 0.04)',
|
|
527
|
-
borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
|
|
528
|
-
}}>
|
|
529
|
-
{headerRow.map((col, idx) => (
|
|
530
|
-
<th key={`th-${idx}`} style={{
|
|
531
|
-
padding: '12px 16px',
|
|
532
|
-
fontWeight: 700,
|
|
533
|
-
color: 'var(--accent, #38bdf8)',
|
|
534
|
-
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
535
|
-
}}>
|
|
536
|
-
{formatInline(col)}
|
|
537
|
-
</th>
|
|
538
|
-
))}
|
|
539
|
-
</tr>
|
|
540
|
-
</thead>
|
|
541
|
-
)}
|
|
542
|
-
<tbody>
|
|
543
|
-
{dataRows.map((row, rIdx) => (
|
|
544
|
-
<tr key={`tr-${rIdx}`} style={{
|
|
545
|
-
background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
|
|
546
|
-
borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
|
|
547
|
-
}}>
|
|
548
|
-
{row.map((col, cIdx) => (
|
|
549
|
-
<td key={`td-${cIdx}`} style={{
|
|
550
|
-
padding: '12px 16px',
|
|
551
|
-
color: 'var(--text-main, #e2e8f0)',
|
|
552
|
-
}}>
|
|
553
|
-
{formatInline(col)}
|
|
554
|
-
</td>
|
|
555
|
-
))}
|
|
556
|
-
</tr>
|
|
557
|
-
))}
|
|
558
|
-
</tbody>
|
|
559
|
-
</table>
|
|
560
|
-
</div>
|
|
561
|
-
)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const lines = displayText.split('\n')
|
|
565
|
-
const items: any[] = []
|
|
566
|
-
|
|
567
|
-
let inCodeBlock = false
|
|
568
|
-
let codeBlockLang = ''
|
|
569
|
-
let codeBlockLines: string[] = []
|
|
570
|
-
|
|
571
|
-
let inTable = false
|
|
572
|
-
let tableLines: string[] = []
|
|
573
|
-
|
|
574
|
-
const flushTable = (index: number) => {
|
|
575
|
-
if (tableLines.length > 0) {
|
|
576
|
-
items.push(renderHtmlTable(tableLines, `table-${index}`))
|
|
577
|
-
tableLines = []
|
|
578
|
-
inTable = false
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
for (let i = 0; i < lines.length; i++) {
|
|
583
|
-
const line = lines[i]
|
|
584
|
-
|
|
585
|
-
if (line.trim().startsWith('```')) {
|
|
586
|
-
if (inTable) {
|
|
587
|
-
flushTable(i)
|
|
588
|
-
}
|
|
589
|
-
if (inCodeBlock) {
|
|
590
|
-
const codeText = codeBlockLines.join('\n')
|
|
591
|
-
items.push(
|
|
592
|
-
<ChatCodeBlock
|
|
593
|
-
key={`code-block-${i}`}
|
|
594
|
-
code={codeText}
|
|
595
|
-
language={codeBlockLang}
|
|
596
|
-
/>
|
|
597
|
-
)
|
|
598
|
-
inCodeBlock = false
|
|
599
|
-
codeBlockLines = []
|
|
600
|
-
} else {
|
|
601
|
-
inCodeBlock = true
|
|
602
|
-
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
603
|
-
}
|
|
604
|
-
continue
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (inCodeBlock) {
|
|
608
|
-
codeBlockLines.push(line)
|
|
609
|
-
continue
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (isTableLine(line)) {
|
|
613
|
-
inTable = true
|
|
614
|
-
tableLines.push(line)
|
|
615
|
-
} else {
|
|
616
|
-
if (inTable) {
|
|
617
|
-
flushTable(i)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
621
|
-
if (headerMatch) {
|
|
622
|
-
const level = headerMatch[1].length
|
|
623
|
-
const content = headerMatch[2]
|
|
624
|
-
|
|
625
|
-
const style = {
|
|
626
|
-
fontWeight: 'bold',
|
|
627
|
-
display: 'block',
|
|
628
|
-
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
629
|
-
marginBottom: '6px',
|
|
630
|
-
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
631
|
-
color: 'var(--text-main)',
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
items.push(
|
|
635
|
-
<span key={`line-${i}`} style={style}>
|
|
636
|
-
{formatInline(content)}
|
|
637
|
-
</span>
|
|
638
|
-
)
|
|
639
|
-
} else {
|
|
640
|
-
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
|
|
641
|
-
if (listMatch) {
|
|
642
|
-
const indent = listMatch[1]
|
|
643
|
-
const content = listMatch[3]
|
|
644
|
-
items.push(
|
|
645
|
-
<Fragment key={`line-${i}`}>
|
|
646
|
-
{indent}• {formatInline(content)}
|
|
647
|
-
{i < lines.length - 1 && '\n'}
|
|
648
|
-
</Fragment>
|
|
649
|
-
)
|
|
650
|
-
} else {
|
|
651
|
-
items.push(
|
|
652
|
-
<Fragment key={`line-${i}`}>
|
|
653
|
-
{formatInline(line)}
|
|
654
|
-
{i < lines.length - 1 && '\n'}
|
|
655
|
-
</Fragment>
|
|
656
|
-
)
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (inTable) {
|
|
663
|
-
flushTable(lines.length)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
667
|
-
const codeText = codeBlockLines.join('\n')
|
|
668
|
-
items.push(
|
|
669
|
-
<ChatCodeBlock
|
|
670
|
-
key={`code-block-end`}
|
|
671
|
-
code={codeText}
|
|
672
|
-
language={codeBlockLang}
|
|
673
|
-
/>
|
|
674
|
-
)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return items
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const THINKING_LABELS = {
|
|
681
|
-
live: 'Thinking…',
|
|
682
|
-
completed: (count: number) => `Thinking process (${count} steps)`,
|
|
683
|
-
step: (index: number) => `Step ${index}`,
|
|
684
|
-
emptyHint: 'Model didn\'t send thinking steps this time',
|
|
685
|
-
} as const
|
|
686
|
-
|
|
687
|
-
function ThinkingBlock({
|
|
688
|
-
thoughts,
|
|
689
|
-
isLive = false,
|
|
690
|
-
blockKey,
|
|
691
|
-
expanded,
|
|
692
|
-
onExpandedChange,
|
|
693
|
-
showEmptyHint = false,
|
|
694
|
-
}: {
|
|
695
|
-
thoughts: string[]
|
|
696
|
-
isLive?: boolean
|
|
697
|
-
blockKey: string
|
|
698
|
-
expanded?: boolean
|
|
699
|
-
onExpandedChange?: (key: string, open: boolean) => void
|
|
700
|
-
showEmptyHint?: boolean
|
|
701
|
-
}) {
|
|
702
|
-
const contentRef = useRef<HTMLDivElement>(null)
|
|
703
|
-
const isControlled = expanded !== undefined && Boolean(onExpandedChange)
|
|
704
|
-
const [localOpen, setLocalOpen] = useState(isLive)
|
|
705
|
-
const isOpen = isControlled ? Boolean(expanded) : localOpen
|
|
706
|
-
|
|
707
|
-
const setOpen = (open: boolean) => {
|
|
708
|
-
if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
|
|
709
|
-
else setLocalOpen(open)
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
useEffect(() => {
|
|
713
|
-
if (isLive && !isControlled) setLocalOpen(true)
|
|
714
|
-
}, [isLive, isControlled])
|
|
715
|
-
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
if (!isLive || !isOpen || !contentRef.current) return
|
|
718
|
-
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
719
|
-
}, [thoughts, isLive, isOpen])
|
|
720
|
-
|
|
721
|
-
if (thoughts.length === 0 && !showEmptyHint) return null
|
|
722
|
-
|
|
723
|
-
if (thoughts.length === 0) {
|
|
724
|
-
return (
|
|
725
|
-
<div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
|
|
726
|
-
<span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
|
|
727
|
-
</div>
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return (
|
|
732
|
-
<div className={`thinking-block${isLive ? ' is-live' : ''}`}>
|
|
733
|
-
<button
|
|
734
|
-
type="button"
|
|
735
|
-
className="thinking-block-header"
|
|
736
|
-
onClick={() => setOpen(!isOpen)}
|
|
737
|
-
aria-expanded={isOpen}
|
|
738
|
-
>
|
|
739
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
|
|
740
|
-
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
741
|
-
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
742
|
-
<circle cx="12" cy="12" r="10" />
|
|
743
|
-
</svg>
|
|
744
|
-
<span className="thinking-block-label">
|
|
745
|
-
{isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
|
|
746
|
-
</span>
|
|
747
|
-
{isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
|
|
748
|
-
<span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
|
|
749
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
750
|
-
<polyline points="9 18 15 12 9 6" />
|
|
751
|
-
</svg>
|
|
752
|
-
</span>
|
|
753
|
-
</button>
|
|
754
|
-
{isOpen && (
|
|
755
|
-
<div className="thinking-block-body">
|
|
756
|
-
<div className="thinking-block-content" ref={contentRef}>
|
|
757
|
-
{thoughts.map((thought, index) => (
|
|
758
|
-
<div className="thinking-block-step" key={`${blockKey}-${index}`}>
|
|
759
|
-
{thoughts.length > 1 && (
|
|
760
|
-
<div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
|
|
761
|
-
)}
|
|
762
|
-
<div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
|
|
763
|
-
</div>
|
|
764
|
-
))}
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
)}
|
|
768
|
-
</div>
|
|
769
|
-
)
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function readableAssistantText(text: string) {
|
|
773
|
-
if (typeof text !== 'string') return ''
|
|
774
|
-
const trimmed = text.trim()
|
|
775
|
-
if (!trimmed.startsWith('{')) return text
|
|
776
|
-
try {
|
|
777
|
-
const value = JSON.parse(trimmed)
|
|
778
|
-
if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
|
|
779
|
-
return value.input.summary
|
|
780
|
-
}
|
|
781
|
-
if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
|
|
782
|
-
return value.finish.summary
|
|
783
|
-
}
|
|
784
|
-
} catch {
|
|
785
|
-
return text
|
|
786
|
-
}
|
|
787
|
-
return text
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function renderSpeakerIcon(isSpeaking: boolean) {
|
|
791
|
-
if (isSpeaking) {
|
|
792
|
-
return (
|
|
793
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
794
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
795
|
-
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
796
|
-
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
797
|
-
</svg>
|
|
798
|
-
)
|
|
799
|
-
}
|
|
800
|
-
return (
|
|
801
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
802
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
803
|
-
</svg>
|
|
804
|
-
)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function cleanSpeechText(text: string) {
|
|
808
|
-
return readableAssistantText(text)
|
|
809
|
-
.replace(/\*\*([\s\S]*?)\*\*/g, '$1')
|
|
810
|
-
.replace(/[*_`#]/g, '')
|
|
811
|
-
.trim()
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function numericSetting(value: unknown, fallback: number) {
|
|
815
|
-
const numeric = Number(value)
|
|
816
|
-
return Number.isFinite(numeric) ? numeric : fallback
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
interface DiffHunk {
|
|
820
|
-
oldText: string
|
|
821
|
-
newText: string
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
interface FileChange {
|
|
825
|
-
path: string
|
|
826
|
-
created: boolean
|
|
827
|
-
additions: number
|
|
828
|
-
deletions: number
|
|
829
|
-
hunks: DiffHunk[]
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
|
|
833
|
-
const changes = new Map<string, FileChange>()
|
|
834
|
-
let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
|
|
835
|
-
|
|
836
|
-
for (const event of progress || []) {
|
|
837
|
-
if (event.type === 'ToolStart') {
|
|
838
|
-
if (event.data.action === 'apply_patch') {
|
|
839
|
-
const patch = (event.data.input as any)?.patch
|
|
840
|
-
if (patch && typeof patch.path === 'string') {
|
|
841
|
-
let additions = 0
|
|
842
|
-
let deletions = 0
|
|
843
|
-
const hunksList: DiffHunk[] = []
|
|
844
|
-
const hunks = patch.hunks
|
|
845
|
-
if (Array.isArray(hunks)) {
|
|
846
|
-
for (const hunk of hunks) {
|
|
847
|
-
const oldText = hunk?.oldText || ''
|
|
848
|
-
const newText = hunk?.newText || ''
|
|
849
|
-
const oldLines = oldText ? oldText.split('\n').length : 0
|
|
850
|
-
const newLines = newText ? newText.split('\n').length : 0
|
|
851
|
-
deletions += oldLines
|
|
852
|
-
additions += newLines
|
|
853
|
-
hunksList.push({ oldText, newText })
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
activeEdit = {
|
|
857
|
-
action: 'apply_patch',
|
|
858
|
-
path: patch.path,
|
|
859
|
-
created: false,
|
|
860
|
-
additions,
|
|
861
|
-
deletions,
|
|
862
|
-
hunks: hunksList
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
} else if (event.data.action === 'write_file') {
|
|
866
|
-
const path = (event.data.input as any)?.path
|
|
867
|
-
const fileContent = (event.data.input as any)?.file_content || ''
|
|
868
|
-
if (typeof path === 'string') {
|
|
869
|
-
const additions = fileContent ? fileContent.split('\n').length : 0
|
|
870
|
-
activeEdit = {
|
|
871
|
-
action: 'write_file',
|
|
872
|
-
path,
|
|
873
|
-
created: true,
|
|
874
|
-
additions,
|
|
875
|
-
deletions: 0,
|
|
876
|
-
hunks: [{ oldText: '', newText: fileContent }]
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
} else {
|
|
880
|
-
activeEdit = null
|
|
881
|
-
}
|
|
882
|
-
} else if (event.type === 'ToolEnd') {
|
|
883
|
-
if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
|
|
884
|
-
const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
|
|
885
|
-
if (!isError) {
|
|
886
|
-
try {
|
|
887
|
-
const applied = JSON.parse(event.data.result)
|
|
888
|
-
const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
|
|
889
|
-
|
|
890
|
-
for (const path of appliedPaths) {
|
|
891
|
-
const existing = changes.get(path)
|
|
892
|
-
if (existing) {
|
|
893
|
-
existing.additions += activeEdit.additions
|
|
894
|
-
existing.deletions += activeEdit.deletions
|
|
895
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
896
|
-
} else {
|
|
897
|
-
changes.set(path, {
|
|
898
|
-
path,
|
|
899
|
-
created: activeEdit.created,
|
|
900
|
-
additions: activeEdit.additions,
|
|
901
|
-
deletions: activeEdit.deletions,
|
|
902
|
-
hunks: [...activeEdit.hunks]
|
|
903
|
-
})
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
} catch (e) {
|
|
907
|
-
const path = activeEdit.path
|
|
908
|
-
const existing = changes.get(path)
|
|
909
|
-
if (existing) {
|
|
910
|
-
existing.additions += activeEdit.additions
|
|
911
|
-
existing.deletions += activeEdit.deletions
|
|
912
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
913
|
-
} else {
|
|
914
|
-
changes.set(path, {
|
|
915
|
-
path,
|
|
916
|
-
created: activeEdit.created,
|
|
917
|
-
additions: activeEdit.additions,
|
|
918
|
-
deletions: activeEdit.deletions,
|
|
919
|
-
hunks: [...activeEdit.hunks]
|
|
920
|
-
})
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
activeEdit = null
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
return Array.from(changes.values())
|
|
930
|
-
}
|
|
931
76
|
|
|
932
77
|
export default function ChatPanel({
|
|
933
78
|
interactions,
|
|
@@ -953,7 +98,6 @@ export default function ChatPanel({
|
|
|
953
98
|
onSelectImage,
|
|
954
99
|
onSelectDocument,
|
|
955
100
|
onPasteImage,
|
|
956
|
-
onReadClipboardImage,
|
|
957
101
|
onSetMessage,
|
|
958
102
|
onSendVoiceMessage,
|
|
959
103
|
onRemoveImage,
|
|
@@ -967,14 +111,45 @@ export default function ChatPanel({
|
|
|
967
111
|
onToggleMobileSidebar,
|
|
968
112
|
settingsConfig,
|
|
969
113
|
onSetModel,
|
|
114
|
+
onCancelMessage,
|
|
970
115
|
}: ChatPanelProps) {
|
|
971
116
|
const agentActivities = activitiesFrom(agentProgress)
|
|
972
117
|
const activeFallbackNotice = fallbackNotice(streamedResponse)
|
|
118
|
+
const lastThinkingProgress = [...agentProgress].reverse().find(p => p.type === 'Thinking')
|
|
119
|
+
let activeAgentName: string | null = null
|
|
120
|
+
let activeModelName: string | null = null
|
|
121
|
+
if (lastThinkingProgress && lastThinkingProgress.type === 'Thinking') {
|
|
122
|
+
activeAgentName = (lastThinkingProgress.data as any).agent_name || null
|
|
123
|
+
activeModelName = (lastThinkingProgress.data as any).model_name || null
|
|
124
|
+
}
|
|
973
125
|
const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
|
|
974
126
|
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
975
127
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
976
128
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
977
129
|
const toolMenuRef = useRef<HTMLDivElement | null>(null)
|
|
130
|
+
const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const fetchOllamaModels = async () => {
|
|
134
|
+
if (status?.activeProvider !== 'ollama') return;
|
|
135
|
+
const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
|
|
136
|
+
const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${cleanHost}/api/tags`);
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
if (data && Array.isArray(data.models)) {
|
|
142
|
+
setDynamicOllamaModels(data.models.map((m: any) => m.name));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// fallback to default if fetch fails
|
|
148
|
+
}
|
|
149
|
+
setDynamicOllamaModels(OLLAMA_MODELS);
|
|
150
|
+
}
|
|
151
|
+
fetchOllamaModels();
|
|
152
|
+
}, [status?.activeProvider, settingsConfig?.ollamaHost])
|
|
978
153
|
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
979
154
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
980
155
|
|
|
@@ -995,7 +170,7 @@ export default function ChatPanel({
|
|
|
995
170
|
case 'local_openai':
|
|
996
171
|
return LOCAL_MODELS
|
|
997
172
|
case 'ollama':
|
|
998
|
-
return
|
|
173
|
+
return dynamicOllamaModels
|
|
999
174
|
default:
|
|
1000
175
|
return []
|
|
1001
176
|
}
|
|
@@ -1048,60 +223,40 @@ export default function ChatPanel({
|
|
|
1048
223
|
return () => clearInterval(timer)
|
|
1049
224
|
}, [sending])
|
|
1050
225
|
|
|
1051
|
-
const [isRecording, setIsRecording] = useState(false)
|
|
1052
|
-
const [voiceMode, setVoiceMode] = useState(false)
|
|
1053
|
-
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
1054
|
-
const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
|
|
1055
226
|
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
1056
|
-
const recognitionRef = useRef<SpeechRecognition | null>(null)
|
|
1057
|
-
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
|
1058
|
-
const mediaStreamRef = useRef<MediaStream | null>(null)
|
|
1059
|
-
const audioContextRef = useRef<AudioContext | null>(null)
|
|
1060
|
-
const vadTimerRef = useRef<number | null>(null)
|
|
1061
227
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
1062
228
|
const speechRunRef = useRef(0)
|
|
1063
229
|
const historyReadyRef = useRef(false)
|
|
1064
230
|
const submittedDuringSessionRef = useRef(false)
|
|
1065
231
|
const lastAutoSpokenIdRef = useRef<number | string | null>(null)
|
|
1066
|
-
const voiceModeRef = useRef(false)
|
|
1067
|
-
const sendingRef = useRef(false)
|
|
1068
|
-
const voiceAwaitingResponseRef = useRef(false)
|
|
1069
232
|
const speakingRef = useRef<string | null>(null)
|
|
1070
|
-
|
|
233
|
+
|
|
234
|
+
const {
|
|
235
|
+
isRecording,
|
|
236
|
+
voiceMode,
|
|
237
|
+
setVoiceMode,
|
|
238
|
+
voiceTranscript,
|
|
239
|
+
setVoiceTranscript,
|
|
240
|
+
voiceAwaitingResponse,
|
|
241
|
+
voiceAwaitingResponseRef,
|
|
242
|
+
voiceModeRef,
|
|
243
|
+
startRecognition,
|
|
244
|
+
stopRecognition,
|
|
245
|
+
scheduleVoiceListen,
|
|
246
|
+
clearRestartTimer
|
|
247
|
+
} = useSpeechToText({
|
|
248
|
+
language: settingsConfig?.language,
|
|
249
|
+
message,
|
|
250
|
+
sending,
|
|
251
|
+
isSpeaking: Boolean(speakingText),
|
|
252
|
+
onSendVoiceMessage,
|
|
253
|
+
onSetMessage: (val) => onSetMessage(val)
|
|
254
|
+
})
|
|
255
|
+
|
|
1071
256
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
1072
257
|
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
1073
258
|
|
|
1074
|
-
|
|
1075
|
-
if (restartTimerRef.current === null) return
|
|
1076
|
-
window.clearTimeout(restartTimerRef.current)
|
|
1077
|
-
restartTimerRef.current = null
|
|
1078
|
-
}
|
|
1079
|
-
const clearVadTimer = () => {
|
|
1080
|
-
if (vadTimerRef.current === null) return
|
|
1081
|
-
window.clearInterval(vadTimerRef.current)
|
|
1082
|
-
vadTimerRef.current = null
|
|
1083
|
-
}
|
|
1084
|
-
const stopRecognition = () => {
|
|
1085
|
-
clearRestartTimer()
|
|
1086
|
-
recognitionRef.current?.stop()
|
|
1087
|
-
recognitionRef.current = null
|
|
1088
|
-
clearVadTimer()
|
|
1089
|
-
if (mediaRecorderRef.current?.state === 'recording') mediaRecorderRef.current.stop()
|
|
1090
|
-
mediaRecorderRef.current = null
|
|
1091
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1092
|
-
mediaStreamRef.current = null
|
|
1093
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1094
|
-
audioContextRef.current = null
|
|
1095
|
-
setIsRecording(false)
|
|
1096
|
-
}
|
|
1097
|
-
const scheduleVoiceListen = (delayMs = 350) => {
|
|
1098
|
-
clearRestartTimer()
|
|
1099
|
-
if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1100
|
-
restartTimerRef.current = window.setTimeout(() => {
|
|
1101
|
-
restartTimerRef.current = null
|
|
1102
|
-
startRecognition(true)
|
|
1103
|
-
}, delayMs)
|
|
1104
|
-
}
|
|
259
|
+
|
|
1105
260
|
const cancelSpeech = () => {
|
|
1106
261
|
speechRunRef.current += 1
|
|
1107
262
|
audioRef.current?.pause()
|
|
@@ -1123,7 +278,11 @@ export default function ChatPanel({
|
|
|
1123
278
|
utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
|
|
1124
279
|
utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
|
|
1125
280
|
utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
|
|
1126
|
-
const voice = window.speechSynthesis.getVoices().find((item) =>
|
|
281
|
+
const voice = window.speechSynthesis.getVoices().find((item) => {
|
|
282
|
+
const lang = item.lang.toLowerCase()
|
|
283
|
+
const target = hasThai ? 'th' : 'en'
|
|
284
|
+
return lang.startsWith(target) || lang.includes(target)
|
|
285
|
+
})
|
|
1127
286
|
if (voice) utterance.voice = voice
|
|
1128
287
|
const finishSpeech = () => {
|
|
1129
288
|
setSpeakingText((current) => (current === displayText ? null : current))
|
|
@@ -1179,213 +338,24 @@ export default function ChatPanel({
|
|
|
1179
338
|
}
|
|
1180
339
|
speakNative(speechText, text)
|
|
1181
340
|
}
|
|
1182
|
-
const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
|
|
1183
|
-
const reader = new FileReader()
|
|
1184
|
-
reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
|
|
1185
|
-
reader.onload = () => resolve(String(reader.result))
|
|
1186
|
-
reader.readAsDataURL(blob)
|
|
1187
|
-
})
|
|
1188
|
-
const preferredAudioMimeType = () => {
|
|
1189
|
-
const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
|
|
1190
|
-
return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
|
|
1191
|
-
}
|
|
1192
|
-
const startAudioRecording = async (autoSend: boolean) => {
|
|
1193
|
-
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
1194
|
-
alert("Sorry, this system cannot access the microphone or record audio in this browser")
|
|
1195
|
-
voiceModeRef.current = false
|
|
1196
|
-
setVoiceMode(false)
|
|
1197
|
-
return
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
try {
|
|
1201
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
1202
|
-
const mimeType = preferredAudioMimeType()
|
|
1203
|
-
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
|
|
1204
|
-
const chunks: Blob[] = []
|
|
1205
|
-
let heardVoice = false
|
|
1206
|
-
let quietSince = 0
|
|
1207
|
-
const startedAt = Date.now()
|
|
1208
|
-
const audioContext = new AudioContext()
|
|
1209
|
-
const source = audioContext.createMediaStreamSource(stream)
|
|
1210
|
-
const analyser = audioContext.createAnalyser()
|
|
1211
|
-
const samples = new Uint8Array(analyser.fftSize)
|
|
1212
|
-
source.connect(analyser)
|
|
1213
|
-
|
|
1214
|
-
mediaStreamRef.current = stream
|
|
1215
|
-
mediaRecorderRef.current = recorder
|
|
1216
|
-
audioContextRef.current = audioContext
|
|
1217
|
-
setVoiceTranscript('Listening to microphone...')
|
|
1218
|
-
|
|
1219
|
-
recorder.ondataavailable = (event) => {
|
|
1220
|
-
if (event.data.size > 0) chunks.push(event.data)
|
|
1221
|
-
}
|
|
1222
|
-
recorder.onstop = async () => {
|
|
1223
|
-
clearVadTimer()
|
|
1224
|
-
mediaRecorderRef.current = null
|
|
1225
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1226
|
-
mediaStreamRef.current = null
|
|
1227
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1228
|
-
audioContextRef.current = null
|
|
1229
|
-
setIsRecording(false)
|
|
1230
|
-
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
1231
|
-
if (!heardVoice) {
|
|
1232
|
-
setVoiceTranscript('No speech detected')
|
|
1233
|
-
scheduleVoiceListen(700)
|
|
1234
|
-
return
|
|
1235
|
-
}
|
|
1236
|
-
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
1237
|
-
const audioDataUri = await blobToDataUri(blob)
|
|
1238
|
-
setVoiceTranscript('Sent audio to AI')
|
|
1239
|
-
voiceAwaitingResponseRef.current = true
|
|
1240
|
-
setVoiceAwaitingResponse(true)
|
|
1241
|
-
onSendVoiceMessage('', audioDataUri)
|
|
1242
|
-
.catch((error) => console.error('Voice audio message failed', error))
|
|
1243
|
-
.finally(() => {
|
|
1244
|
-
voiceAwaitingResponseRef.current = false
|
|
1245
|
-
setVoiceAwaitingResponse(false)
|
|
1246
|
-
scheduleVoiceListen()
|
|
1247
|
-
})
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
recorder.start()
|
|
1251
|
-
setIsRecording(true)
|
|
1252
|
-
vadTimerRef.current = window.setInterval(() => {
|
|
1253
|
-
analyser.getByteTimeDomainData(samples)
|
|
1254
|
-
let peak = 0
|
|
1255
|
-
for (const sample of samples) {
|
|
1256
|
-
peak = Math.max(peak, Math.abs(sample - 128))
|
|
1257
|
-
}
|
|
1258
|
-
const now = Date.now()
|
|
1259
|
-
if (peak > 12) {
|
|
1260
|
-
heardVoice = true
|
|
1261
|
-
quietSince = 0
|
|
1262
|
-
setVoiceTranscript('Listening...')
|
|
1263
|
-
} else if (heardVoice) {
|
|
1264
|
-
quietSince = quietSince || now
|
|
1265
|
-
}
|
|
1266
|
-
const silenceElapsed = quietSince ? now - quietSince : 0
|
|
1267
|
-
const totalElapsed = now - startedAt
|
|
1268
|
-
if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
|
|
1269
|
-
recorder.stop()
|
|
1270
|
-
}
|
|
1271
|
-
}, 120)
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
console.error('Failed to record microphone audio', error)
|
|
1274
|
-
setIsRecording(false)
|
|
1275
|
-
voiceModeRef.current = false
|
|
1276
|
-
setVoiceMode(false)
|
|
1277
|
-
alert('Failed to open microphone. Please check microphone permissions in your browser')
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
const startRecognition = (autoSend: boolean) => {
|
|
1281
|
-
if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1282
|
-
|
|
1283
|
-
const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
1284
|
-
if (!SpeechRecognitionApi) {
|
|
1285
|
-
startAudioRecording(autoSend)
|
|
1286
|
-
return
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
let sentTranscript = false
|
|
1290
341
|
|
|
1291
|
-
try {
|
|
1292
|
-
const recognition = new SpeechRecognitionApi()
|
|
1293
|
-
recognition.continuous = false
|
|
1294
|
-
recognition.interimResults = true
|
|
1295
|
-
recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
|
|
1296
|
-
recognition.onstart = () => setIsRecording(true)
|
|
1297
|
-
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
1298
|
-
let interimText = ''
|
|
1299
|
-
let finalText = ''
|
|
1300
|
-
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
1301
|
-
const transcript = event.results[index]?.[0]?.transcript ?? ''
|
|
1302
|
-
if (event.results[index]?.isFinal) finalText += transcript
|
|
1303
|
-
else interimText += transcript
|
|
1304
|
-
}
|
|
1305
|
-
const displayText = (finalText || interimText).trim()
|
|
1306
|
-
if (displayText) setVoiceTranscript(displayText)
|
|
1307
|
-
const resultText = finalText.trim()
|
|
1308
|
-
if (!resultText) return
|
|
1309
|
-
sentTranscript = true
|
|
1310
|
-
recognition.stop()
|
|
1311
|
-
setVoiceTranscript(resultText)
|
|
1312
|
-
if (autoSend) {
|
|
1313
|
-
voiceAwaitingResponseRef.current = true
|
|
1314
|
-
setVoiceAwaitingResponse(true)
|
|
1315
|
-
onSendVoiceMessage(resultText)
|
|
1316
|
-
.catch((error) => console.error('Voice message failed', error))
|
|
1317
|
-
.finally(() => {
|
|
1318
|
-
voiceAwaitingResponseRef.current = false
|
|
1319
|
-
setVoiceAwaitingResponse(false)
|
|
1320
|
-
scheduleVoiceListen()
|
|
1321
|
-
})
|
|
1322
|
-
} else {
|
|
1323
|
-
onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
recognition.onerror = (event: Event) => {
|
|
1327
|
-
console.error('Speech recognition error', event)
|
|
1328
|
-
setIsRecording(false)
|
|
1329
|
-
}
|
|
1330
|
-
recognition.onend = () => {
|
|
1331
|
-
recognitionRef.current = null
|
|
1332
|
-
setIsRecording(false)
|
|
1333
|
-
if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
|
|
1334
|
-
}
|
|
1335
|
-
recognitionRef.current = recognition
|
|
1336
|
-
recognition.start()
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
console.error('Failed to start speech recognition', error)
|
|
1339
|
-
recognitionRef.current = null
|
|
1340
|
-
setIsRecording(false)
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
342
|
const toggleRecording = () => {
|
|
1344
343
|
if (voiceMode) {
|
|
1345
|
-
voiceModeRef.current = false
|
|
1346
|
-
voiceAwaitingResponseRef.current = false
|
|
1347
344
|
setVoiceMode(false)
|
|
1348
|
-
setVoiceAwaitingResponse(false)
|
|
1349
|
-
setVoiceTranscript('')
|
|
1350
345
|
stopRecognition()
|
|
1351
346
|
cancelSpeech()
|
|
1352
347
|
return
|
|
1353
348
|
}
|
|
1354
349
|
|
|
1355
|
-
voiceModeRef.current = true
|
|
1356
350
|
setVoiceMode(true)
|
|
1357
|
-
setVoiceAwaitingResponse(false)
|
|
1358
|
-
setVoiceTranscript('')
|
|
1359
351
|
startRecognition(true)
|
|
1360
352
|
}
|
|
1361
353
|
|
|
1362
354
|
useEffect(() => {
|
|
1363
355
|
return () => {
|
|
1364
|
-
clearRestartTimer()
|
|
1365
356
|
cancelSpeech()
|
|
1366
|
-
stopRecognition()
|
|
1367
357
|
}
|
|
1368
358
|
}, [])
|
|
1369
|
-
useEffect(() => {
|
|
1370
|
-
voiceModeRef.current = voiceMode
|
|
1371
|
-
if (!voiceMode) {
|
|
1372
|
-
clearRestartTimer()
|
|
1373
|
-
setVoiceTranscript('')
|
|
1374
|
-
}
|
|
1375
|
-
}, [voiceMode])
|
|
1376
|
-
useEffect(() => {
|
|
1377
|
-
sendingRef.current = sending
|
|
1378
|
-
}, [sending])
|
|
1379
|
-
useEffect(() => {
|
|
1380
|
-
voiceAwaitingResponseRef.current = voiceAwaitingResponse
|
|
1381
|
-
}, [voiceAwaitingResponse])
|
|
1382
|
-
useEffect(() => {
|
|
1383
|
-
speakingRef.current = speakingText
|
|
1384
|
-
}, [speakingText])
|
|
1385
|
-
useEffect(() => {
|
|
1386
|
-
if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
|
|
1387
|
-
scheduleVoiceListen()
|
|
1388
|
-
}, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
|
|
1389
359
|
useEffect(() => {
|
|
1390
360
|
if (sending) submittedDuringSessionRef.current = true
|
|
1391
361
|
}, [sending])
|
|
@@ -1491,21 +461,20 @@ export default function ChatPanel({
|
|
|
1491
461
|
if (onPasteImage(event.clipboardData)) {
|
|
1492
462
|
event.preventDefault()
|
|
1493
463
|
event.stopPropagation()
|
|
1494
|
-
} else {
|
|
1495
|
-
window.setTimeout(() => void onReadClipboardImage(), 0)
|
|
1496
464
|
}
|
|
1497
465
|
}
|
|
1498
466
|
window.addEventListener('paste', handleWindowPaste, true)
|
|
1499
467
|
return () => window.removeEventListener('paste', handleWindowPaste, true)
|
|
1500
|
-
}, [onPasteImage
|
|
468
|
+
}, [onPasteImage])
|
|
1501
469
|
useEffect(() => {
|
|
1502
|
-
const
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
470
|
+
const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
471
|
+
if (event.key === 'Escape' && sending) {
|
|
472
|
+
onCancelMessage()
|
|
473
|
+
}
|
|
1505
474
|
}
|
|
1506
|
-
window.addEventListener('keydown',
|
|
1507
|
-
return () => window.removeEventListener('keydown',
|
|
1508
|
-
}, [
|
|
475
|
+
window.addEventListener('keydown', handleEscapeKeyDown, true)
|
|
476
|
+
return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
|
|
477
|
+
}, [sending, onCancelMessage])
|
|
1509
478
|
useEffect(() => {
|
|
1510
479
|
if (message) return
|
|
1511
480
|
const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
|
|
@@ -1528,24 +497,72 @@ export default function ChatPanel({
|
|
|
1528
497
|
const renderCompletedActivity = (interaction: any) => {
|
|
1529
498
|
const interactionId = String(interaction.id)
|
|
1530
499
|
const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
|
|
1531
|
-
if (activityView.items.length === 0) return null
|
|
1532
500
|
const isOpen = Boolean(openActivityIds[interactionId])
|
|
1533
501
|
return (
|
|
1534
|
-
<
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
502
|
+
<AgentActivityDrawer
|
|
503
|
+
activityView={activityView}
|
|
504
|
+
isOpen={isOpen}
|
|
505
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
|
|
506
|
+
isHistorical={true}
|
|
507
|
+
/>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const renderWebSearchSources = (interaction: any) => {
|
|
512
|
+
const interactionId = String(interaction.id)
|
|
513
|
+
const progress = agentActivitySnapshots[interactionId] ?? interaction.agentActivity ?? []
|
|
514
|
+
const sources = parseWebSearchSources(progress)
|
|
515
|
+
if (sources.length === 0) return null
|
|
516
|
+
return (
|
|
517
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '8px' }}>
|
|
518
|
+
<span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sources</span>
|
|
519
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
|
520
|
+
{sources.map((src, i) => (
|
|
521
|
+
<a
|
|
522
|
+
key={i}
|
|
523
|
+
href={src.url}
|
|
524
|
+
target="_blank"
|
|
525
|
+
rel="noopener noreferrer"
|
|
526
|
+
title={src.snippet || src.title}
|
|
527
|
+
style={{
|
|
528
|
+
display: 'flex',
|
|
529
|
+
alignItems: 'center',
|
|
530
|
+
gap: '6px',
|
|
531
|
+
padding: '5px 10px',
|
|
532
|
+
background: 'rgba(255,255,255,0.04)',
|
|
533
|
+
border: '1px solid rgba(255,255,255,0.08)',
|
|
534
|
+
borderRadius: '8px',
|
|
535
|
+
textDecoration: 'none',
|
|
536
|
+
color: '#cbd5e1',
|
|
537
|
+
fontSize: '0.78rem',
|
|
538
|
+
maxWidth: '220px',
|
|
539
|
+
overflow: 'hidden',
|
|
540
|
+
cursor: 'pointer',
|
|
541
|
+
transition: 'background 0.15s, border-color 0.15s',
|
|
542
|
+
}}
|
|
543
|
+
onMouseEnter={e => {
|
|
544
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.08)'
|
|
545
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.18)'
|
|
546
|
+
}}
|
|
547
|
+
onMouseLeave={e => {
|
|
548
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.04)'
|
|
549
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.08)'
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
<img
|
|
553
|
+
src={src.faviconUrl}
|
|
554
|
+
alt=""
|
|
555
|
+
width={14}
|
|
556
|
+
height={14}
|
|
557
|
+
style={{ borderRadius: '2px', flexShrink: 0 }}
|
|
558
|
+
onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
|
|
559
|
+
/>
|
|
560
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
561
|
+
{src.domain}
|
|
562
|
+
</span>
|
|
563
|
+
</a>
|
|
564
|
+
))}
|
|
565
|
+
</div>
|
|
1549
566
|
</div>
|
|
1550
567
|
)
|
|
1551
568
|
}
|
|
@@ -1842,6 +859,7 @@ export default function ChatPanel({
|
|
|
1842
859
|
)
|
|
1843
860
|
})()}
|
|
1844
861
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
862
|
+
{renderWebSearchSources(interaction)}
|
|
1845
863
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1846
864
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
1847
865
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
@@ -1876,18 +894,13 @@ export default function ChatPanel({
|
|
|
1876
894
|
{sending && (
|
|
1877
895
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1878
896
|
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
|
|
1879
|
-
{agentMode &&
|
|
1880
|
-
<
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
</span>
|
|
1887
|
-
</div>
|
|
1888
|
-
{renderAgentActivityTable(agentActivities)}
|
|
1889
|
-
</div>
|
|
1890
|
-
</div>
|
|
897
|
+
{agentMode && (
|
|
898
|
+
<AgentActivityDrawer
|
|
899
|
+
activityView={agentActivities}
|
|
900
|
+
isOpen={openActivityIds['live'] ?? true}
|
|
901
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, live: !(current['live'] ?? true) }))}
|
|
902
|
+
pendingApproval={!!pendingApproval}
|
|
903
|
+
/>
|
|
1891
904
|
)}
|
|
1892
905
|
{renderActiveFileChanges()}
|
|
1893
906
|
<div className="message ai-message thinking-message">
|
|
@@ -1924,7 +937,12 @@ export default function ChatPanel({
|
|
|
1924
937
|
/>
|
|
1925
938
|
</path>
|
|
1926
939
|
</svg>
|
|
1927
|
-
|
|
940
|
+
<span>
|
|
941
|
+
{activeAgentName && activeModelName
|
|
942
|
+
? `${activeAgentName} (${activeModelName}) is thinking... (${elapsedSeconds}s)`
|
|
943
|
+
: `Thinking for ${elapsedSeconds}s (Esc to cancel)`
|
|
944
|
+
}
|
|
945
|
+
</span>
|
|
1928
946
|
</div>
|
|
1929
947
|
)}
|
|
1930
948
|
</span>
|
|
@@ -1962,42 +980,17 @@ export default function ChatPanel({
|
|
|
1962
980
|
</div>
|
|
1963
981
|
)}
|
|
1964
982
|
|
|
1965
|
-
{pendingApproval && (
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1973
|
-
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1974
|
-
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1975
|
-
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
1976
|
-
<div className="approval-card-title">{details.title}</div>
|
|
1977
|
-
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1978
|
-
{diffText ? (
|
|
1979
|
-
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
1980
|
-
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
1981
|
-
{renderDiff(diffText)}
|
|
1982
|
-
</div>
|
|
1983
|
-
) : (
|
|
1984
|
-
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1985
|
-
)}
|
|
1986
|
-
</div>
|
|
1987
|
-
<div className="approval-card-actions">
|
|
1988
|
-
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
1989
|
-
<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>
|
|
1990
|
-
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
1991
|
-
</div>
|
|
1992
|
-
</div>
|
|
1993
|
-
</div>
|
|
1994
|
-
</div>
|
|
1995
|
-
)
|
|
1996
|
-
})()}
|
|
983
|
+
{pendingApproval && (
|
|
984
|
+
<ApprovalCard
|
|
985
|
+
key={pendingApproval.id || JSON.stringify(pendingApproval)}
|
|
986
|
+
pendingApproval={pendingApproval}
|
|
987
|
+
onApproval={onApproval}
|
|
988
|
+
/>
|
|
989
|
+
)}
|
|
1997
990
|
<div ref={chatEnd} />
|
|
1998
991
|
</div>
|
|
1999
992
|
|
|
2000
|
-
<div className=
|
|
993
|
+
<div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
|
|
2001
994
|
{isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
|
|
2002
995
|
<div className="smart-context-bar">
|
|
2003
996
|
<div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
@@ -2011,8 +1004,21 @@ export default function ChatPanel({
|
|
|
2011
1004
|
{voiceMode && (
|
|
2012
1005
|
<div className="voice-mode-bar" data-state={voiceStatus}>
|
|
2013
1006
|
<span className="voice-mode-dot" />
|
|
2014
|
-
|
|
2015
|
-
|
|
1007
|
+
{voiceTranscript ? (
|
|
1008
|
+
voiceTranscript === 'Listening to microphone...' ||
|
|
1009
|
+
voiceTranscript === 'Listening...' ||
|
|
1010
|
+
voiceTranscript === 'No speech detected' ||
|
|
1011
|
+
voiceTranscript === 'Sent audio to AI' ? (
|
|
1012
|
+
<span>{voiceTranscript}</span>
|
|
1013
|
+
) : (
|
|
1014
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }}>
|
|
1015
|
+
<span>{voiceStatusLabel}:</span>
|
|
1016
|
+
<span className="voice-mode-transcript">"{voiceTranscript}"</span>
|
|
1017
|
+
</span>
|
|
1018
|
+
)
|
|
1019
|
+
) : (
|
|
1020
|
+
<span>{voiceStatusLabel}</span>
|
|
1021
|
+
)}
|
|
2016
1022
|
</div>
|
|
2017
1023
|
)}
|
|
2018
1024
|
|
|
@@ -2134,7 +1140,7 @@ export default function ChatPanel({
|
|
|
2134
1140
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
2135
1141
|
})}
|
|
2136
1142
|
</select>
|
|
2137
|
-
{availableModels.length > 0 && (
|
|
1143
|
+
{(availableModels.length > 0 || activeModel) && (
|
|
2138
1144
|
<select
|
|
2139
1145
|
value={activeModel}
|
|
2140
1146
|
onChange={(event) => onSetModel(event.target.value)}
|
|
@@ -2184,14 +1190,32 @@ export default function ChatPanel({
|
|
|
2184
1190
|
</svg>
|
|
2185
1191
|
)}
|
|
2186
1192
|
</button>
|
|
2187
|
-
|
|
2188
|
-
<
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
1193
|
+
{sending ? (
|
|
1194
|
+
<button
|
|
1195
|
+
id="send-btn"
|
|
1196
|
+
className="stop-btn"
|
|
1197
|
+
type="button"
|
|
1198
|
+
onClick={onCancelMessage}
|
|
1199
|
+
title="Stop generating"
|
|
1200
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1201
|
+
>
|
|
1202
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
1203
|
+
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
|
|
1204
|
+
</svg>
|
|
1205
|
+
</button>
|
|
1206
|
+
) : (
|
|
1207
|
+
<button id="send-btn" type="submit" disabled={!canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1208
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
1209
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
1210
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
1211
|
+
</svg>
|
|
1212
|
+
</button>
|
|
1213
|
+
)}
|
|
2193
1214
|
</form>
|
|
2194
1215
|
</div>
|
|
1216
|
+
<p className="input-disclaimer">
|
|
1217
|
+
Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
|
|
1218
|
+
</p>
|
|
2195
1219
|
</section>
|
|
2196
1220
|
)
|
|
2197
1221
|
}
|