@pheem49/mint 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +0 -17
- package/Release_Note.md +137 -58
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +150 -45
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/main.rs +929 -412
- package/crates/mint-cli/src/onboard.rs +108 -108
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +108 -52
- package/crates/mint-core/src/browser.rs +56 -1
- package/crates/mint-core/src/channels.rs +1 -0
- package/crates/mint-core/src/chat.rs +126 -8
- package/crates/mint-core/src/code_tools.rs +18 -20
- package/crates/mint-core/src/config.rs +245 -17
- package/crates/mint-core/src/image_gen.rs +5 -5
- package/crates/mint-core/src/lib.rs +16 -2
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +260 -72
- package/crates/mint-core/src/pictures.rs +3 -4
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +46 -19
- package/crates/mint-core/src/tts.rs +3 -1
- 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 +36 -0
- package/shared-modules/linux-audio/fluidsynth2.json +25 -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 +89 -0
- package/shared-modules/linux-audio/lrdf.json +57 -0
- package/shared-modules/linux-audio/lv2.json +20 -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 +85 -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/fileIcons.ts +135 -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 +219 -1308
- package/src/renderer/src/components/MintDashboard.tsx +67 -170
- package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
- package/src/renderer/src/components/Settings/AgentsTab.tsx +381 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
- package/src/renderer/src/components/SettingsWindow.tsx +120 -3
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/styles.css +100 -0
- package/src/renderer/src/tauri.ts +128 -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 +219 -1306
- package/src/renderer/src-web/components/MintDashboard.tsx +42 -170
- package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +381 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
- package/src/renderer/src-web/components/SettingsWindow.tsx +127 -3
- package/src/renderer/src-web/css/styles.css +100 -0
- package/src/renderer/src-web/tauri.ts +180 -102
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +168 -40
- package/src-tauri/src/webhooks.rs +1 -0
- package/src-tauri/src/workflows.rs +1 -0
- 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,364 +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: string[] = []
|
|
72
|
-
|
|
73
|
-
interface ApprovalDetails {
|
|
74
|
-
title: string
|
|
75
|
-
body: string
|
|
76
|
-
reason?: string
|
|
77
|
-
isDangerous: boolean
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function badge(provider: string, model: string) {
|
|
81
|
-
return [provider, model].filter(Boolean).join(' / ')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function providerLabel(provider: string) {
|
|
85
|
-
switch (provider) {
|
|
86
|
-
case 'gemini':
|
|
87
|
-
return 'Gemini'
|
|
88
|
-
case 'openai':
|
|
89
|
-
return 'OpenAI'
|
|
90
|
-
case 'openrouter':
|
|
91
|
-
return 'OpenRouter'
|
|
92
|
-
case 'deepseek':
|
|
93
|
-
return 'DeepSeek'
|
|
94
|
-
case 'anthropic':
|
|
95
|
-
return 'Claude'
|
|
96
|
-
case 'huggingface':
|
|
97
|
-
return 'Hugging Face'
|
|
98
|
-
case 'local_openai':
|
|
99
|
-
return 'Local OpenAI'
|
|
100
|
-
case 'ollama':
|
|
101
|
-
return 'Ollama'
|
|
102
|
-
default:
|
|
103
|
-
return provider || 'Primary provider'
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function fallbackNotice(response: Pick<ChatResponse, 'provider' | 'fallbackProvider'> | null | undefined) {
|
|
108
|
-
if (!response?.fallbackProvider) return ''
|
|
109
|
-
return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
interface AgentActivity {
|
|
113
|
-
label: string
|
|
114
|
-
target: string
|
|
115
|
-
kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
|
|
116
|
-
state: 'active' | 'done' | 'error'
|
|
117
|
-
action?: string
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface AgentActivityView {
|
|
121
|
-
summary: string
|
|
122
|
-
items: AgentActivity[]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function activityDetail(input: Record<string, unknown>, key: string) {
|
|
126
|
-
const value = input[key]
|
|
127
|
-
return typeof value === 'string' && value.trim() ? value : ''
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function formatActivityTarget(value: string) {
|
|
131
|
-
const compact = value.replace(/^\/home\/([^/]+)/, '~')
|
|
132
|
-
return compact || 'workspace'
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function activityKind(action: string, target: string): AgentActivity['kind'] {
|
|
136
|
-
if (['search_code', 'semantic_search', 'knowledge_search', 'web_search', 'memory_recall'].includes(action)) return 'search'
|
|
137
|
-
if (['run_shell', 'verify'].includes(action)) return 'terminal'
|
|
138
|
-
if (['list_files', 'detect_project'].includes(action)) return 'folder'
|
|
139
|
-
if (['read_file', 'symbols', 'read_diagnostics', 'git_diff', 'apply_patch', 'write_file', 'note_write', 'view_image'].includes(action)) return 'file'
|
|
140
|
-
return target.includes('/') && !/\.[^/]+$/.test(target) ? 'folder' : 'tool'
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function describeTool(action: string, input: Record<string, unknown>): AgentActivity {
|
|
144
|
-
const path = activityDetail(input, 'path')
|
|
145
|
-
const query = activityDetail(input, 'query')
|
|
146
|
-
const command = activityDetail(input, 'command')
|
|
147
|
-
const name = activityDetail(input, 'name')
|
|
148
|
-
const tool = activityDetail(input, 'tool')
|
|
149
|
-
const rawTarget = path || query || command || name || tool || action.replaceAll('_', ' ')
|
|
150
|
-
|
|
151
|
-
// Append line range for read_file when startLine / endLine are available
|
|
152
|
-
let target = rawTarget
|
|
153
|
-
if (action === 'read_file' && path) {
|
|
154
|
-
const startLine = typeof input.startLine === 'number' ? input.startLine : undefined
|
|
155
|
-
const endLine = typeof input.endLine === 'number' ? input.endLine : undefined
|
|
156
|
-
if (startLine !== undefined && endLine !== undefined) {
|
|
157
|
-
target = `${rawTarget} #L${startLine}-${endLine}`
|
|
158
|
-
} else if (startLine !== undefined) {
|
|
159
|
-
target = `${rawTarget} #L${startLine}`
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const labels: Record<string, string> = {
|
|
164
|
-
apply_patch: 'Applying patch',
|
|
165
|
-
ask_user: 'Asking user',
|
|
166
|
-
create_plan: 'Creating plan',
|
|
167
|
-
detect_project: 'Detecting project',
|
|
168
|
-
git_branch: 'Reading branch',
|
|
169
|
-
git_diff: 'Reading diff',
|
|
170
|
-
git_log: 'Reading log',
|
|
171
|
-
git_status: 'Reading git status',
|
|
172
|
-
knowledge_search: 'Searching knowledge',
|
|
173
|
-
list_files: 'Listing files',
|
|
174
|
-
list_tests: 'Listing tests',
|
|
175
|
-
mcp_tool: 'Calling MCP tool',
|
|
176
|
-
memory_recall: 'Recalling memory',
|
|
177
|
-
note_write: 'Writing note',
|
|
178
|
-
read_diagnostics: 'Reading diagnostics',
|
|
179
|
-
read_file: 'Reading file',
|
|
180
|
-
request_user_approval: 'Requesting approval',
|
|
181
|
-
run_plugin: 'Running plugin',
|
|
182
|
-
run_shell: 'Running command',
|
|
183
|
-
search_code: 'Searching code',
|
|
184
|
-
semantic_index: 'Indexing code',
|
|
185
|
-
semantic_search: 'Searching code',
|
|
186
|
-
symbols: 'Inspecting symbols',
|
|
187
|
-
update_plan: 'Updating plan',
|
|
188
|
-
verify: 'Verifying',
|
|
189
|
-
view_image: 'Viewing image',
|
|
190
|
-
web_search: 'Searching web',
|
|
191
|
-
write_file: 'Writing file',
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
label: labels[action] ?? 'Using tool',
|
|
195
|
-
target: formatActivityTarget(target),
|
|
196
|
-
kind: activityKind(action, target),
|
|
197
|
-
state: 'active',
|
|
198
|
-
action,
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function activitySummary(items: AgentActivity[]) {
|
|
203
|
-
const files = new Set<string>()
|
|
204
|
-
const folders = new Set<string>()
|
|
205
|
-
for (const item of items) {
|
|
206
|
-
if (item.kind === 'file') files.add(item.target)
|
|
207
|
-
if (item.kind === 'folder') folders.add(item.target)
|
|
208
|
-
}
|
|
209
|
-
const parts = [
|
|
210
|
-
files.size ? `${files.size} ${files.size === 1 ? 'file' : 'files'}` : '',
|
|
211
|
-
folders.size ? `${folders.size} ${folders.size === 1 ? 'folder' : 'folders'}` : '',
|
|
212
|
-
].filter(Boolean)
|
|
213
|
-
return parts.length ? `Exploring ${parts.join(', ')}` : 'Working through task'
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
|
|
217
|
-
const activities: AgentActivity[] = []
|
|
218
|
-
for (const event of progress) {
|
|
219
|
-
if (event.type === 'ToolStart') {
|
|
220
|
-
activities.push(describeTool(event.data.action, event.data.input))
|
|
221
|
-
} else if (event.type === 'ToolEnd') {
|
|
222
|
-
for (let index = activities.length - 1; index >= 0; index -= 1) {
|
|
223
|
-
if (activities[index].state !== 'active') continue
|
|
224
|
-
activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
|
|
225
|
-
if (activities[index].state === 'error') {
|
|
226
|
-
activities[index].label = 'Failed'
|
|
227
|
-
}
|
|
228
|
-
if (activities[index].action === 'ask_user' && event.data.result.startsWith('User answered:')) {
|
|
229
|
-
const answer = event.data.result.replace('User answered:', '').trim()
|
|
230
|
-
activities[index].target = `(Answered: "${answer}") ${activities[index].target}`
|
|
231
|
-
}
|
|
232
|
-
break
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const items = activities.slice(-12)
|
|
237
|
-
return { summary: activitySummary(activities), items }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function AgentActivityTable({ activityView }: { activityView: AgentActivityView }) {
|
|
241
|
-
const [expandedIndices, setExpandedIndices] = useState<Record<number, boolean>>({})
|
|
242
|
-
|
|
243
|
-
const toggleExpand = (index: number) => {
|
|
244
|
-
setExpandedIndices(prev => ({
|
|
245
|
-
...prev,
|
|
246
|
-
[index]: !prev[index]
|
|
247
|
-
}))
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<div className="agent-activity-list">
|
|
252
|
-
<div className="agent-activity-table-head" aria-hidden="true">
|
|
253
|
-
<span>Tool</span>
|
|
254
|
-
<span />
|
|
255
|
-
<span>Target</span>
|
|
256
|
-
<span />
|
|
257
|
-
</div>
|
|
258
|
-
{activityView.items.map((activity, index) => {
|
|
259
|
-
const isExpanded = !!expandedIndices[index]
|
|
260
|
-
return (
|
|
261
|
-
<div
|
|
262
|
-
className="agent-activity-item"
|
|
263
|
-
data-kind={activity.kind}
|
|
264
|
-
data-state={activity.state}
|
|
265
|
-
key={`${index}-${activity.label}-${activity.target}`}
|
|
266
|
-
style={{ cursor: 'pointer' }}
|
|
267
|
-
onClick={() => toggleExpand(index)}
|
|
268
|
-
>
|
|
269
|
-
<span className="agent-activity-label">{activity.label}</span>
|
|
270
|
-
<span className="agent-activity-icon" aria-hidden="true" />
|
|
271
|
-
<span
|
|
272
|
-
className="agent-activity-text"
|
|
273
|
-
style={isExpanded ? { whiteSpace: 'normal', wordBreak: 'break-all', overflow: 'visible', textOverflow: 'clip' } : undefined}
|
|
274
|
-
>
|
|
275
|
-
{activity.target}
|
|
276
|
-
</span>
|
|
277
|
-
<span
|
|
278
|
-
className="agent-activity-chevron"
|
|
279
|
-
style={{
|
|
280
|
-
transform: isExpanded ? 'rotate(90deg)' : 'none',
|
|
281
|
-
transition: 'transform 0.2s ease',
|
|
282
|
-
display: 'inline-block'
|
|
283
|
-
}}
|
|
284
|
-
aria-hidden="true"
|
|
285
|
-
>
|
|
286
|
-
>
|
|
287
|
-
</span>
|
|
288
|
-
</div>
|
|
289
|
-
)
|
|
290
|
-
})}
|
|
291
|
-
</div>
|
|
292
|
-
)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
296
|
-
if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
|
|
297
|
-
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 }
|
|
298
|
-
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 }
|
|
299
|
-
if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
|
|
300
|
-
if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
|
|
301
|
-
if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
|
|
302
|
-
if (approval.McpTool) {
|
|
303
|
-
const { server, tool, arguments: args } = approval.McpTool
|
|
304
|
-
return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
|
|
305
|
-
}
|
|
306
|
-
if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
|
|
307
|
-
if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Type your answer below and submit to respond to the agent.', isDangerous: false }
|
|
308
|
-
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function renderDiff(diffText: string) {
|
|
312
|
-
if (!diffText) return null
|
|
313
|
-
const lines = diffText.split('\n')
|
|
314
|
-
return (
|
|
315
|
-
<div style={{
|
|
316
|
-
background: '#0b0f19',
|
|
317
|
-
borderRadius: '6px',
|
|
318
|
-
padding: '8px',
|
|
319
|
-
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
320
|
-
overflowX: 'auto',
|
|
321
|
-
maxHeight: '400px',
|
|
322
|
-
fontFamily: 'monospace',
|
|
323
|
-
fontSize: '0.74rem',
|
|
324
|
-
lineHeight: '1.4',
|
|
325
|
-
}}>
|
|
326
|
-
{lines.map((line, idx) => {
|
|
327
|
-
let style: any = {
|
|
328
|
-
whiteSpace: 'pre-wrap',
|
|
329
|
-
padding: '2px 6px',
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
|
|
333
|
-
style = {
|
|
334
|
-
...style,
|
|
335
|
-
color: '#64748b',
|
|
336
|
-
fontWeight: 'bold',
|
|
337
|
-
}
|
|
338
|
-
} else if (line.startsWith('+')) {
|
|
339
|
-
style = {
|
|
340
|
-
...style,
|
|
341
|
-
background: 'rgba(16, 185, 129, 0.12)',
|
|
342
|
-
borderLeft: '3px solid #10b981',
|
|
343
|
-
color: '#a7f3d0'
|
|
344
|
-
}
|
|
345
|
-
} else if (line.startsWith('-')) {
|
|
346
|
-
style = {
|
|
347
|
-
...style,
|
|
348
|
-
background: 'rgba(239, 68, 68, 0.12)',
|
|
349
|
-
borderLeft: '3px solid #ef4444',
|
|
350
|
-
color: '#fca5a5'
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
style = {
|
|
354
|
-
...style,
|
|
355
|
-
color: '#e2e8f0',
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return (
|
|
360
|
-
<div key={idx} style={style}>
|
|
361
|
-
{line}
|
|
362
|
-
</div>
|
|
363
|
-
)
|
|
364
|
-
})}
|
|
365
|
-
</div>
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
35
|
|
|
369
36
|
interface ChatPanelProps {
|
|
370
37
|
interactions: any[]
|
|
@@ -390,7 +57,6 @@ interface ChatPanelProps {
|
|
|
390
57
|
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
391
58
|
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
392
59
|
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
393
|
-
onReadClipboardImage: () => Promise<boolean>
|
|
394
60
|
onSetMessage: (message: string) => void
|
|
395
61
|
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
396
62
|
onRemoveImage: (idx: number) => void
|
|
@@ -404,587 +70,9 @@ interface ChatPanelProps {
|
|
|
404
70
|
onToggleMobileSidebar: () => void
|
|
405
71
|
settingsConfig: any
|
|
406
72
|
onSetModel: (model: string) => void
|
|
73
|
+
onCancelMessage: () => void
|
|
407
74
|
}
|
|
408
75
|
|
|
409
|
-
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
410
|
-
const [copied, setCopied] = useState(false)
|
|
411
|
-
|
|
412
|
-
const handleCopy = async () => {
|
|
413
|
-
try {
|
|
414
|
-
await navigator.clipboard.writeText(code)
|
|
415
|
-
setCopied(true)
|
|
416
|
-
setTimeout(() => setCopied(false), 2000)
|
|
417
|
-
} catch (err) {
|
|
418
|
-
console.error('Failed to copy code: ', err)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const handleDownload = () => {
|
|
423
|
-
try {
|
|
424
|
-
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
425
|
-
const url = URL.createObjectURL(blob)
|
|
426
|
-
const link = document.createElement('a')
|
|
427
|
-
link.href = url
|
|
428
|
-
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
429
|
-
link.click()
|
|
430
|
-
URL.revokeObjectURL(url)
|
|
431
|
-
} catch (err) {
|
|
432
|
-
console.error('Failed to download code: ', err)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
440
|
-
<div className="chat-code-block-header">
|
|
441
|
-
<span className="chat-code-block-lang">{displayLang}</span>
|
|
442
|
-
<div className="chat-code-block-actions">
|
|
443
|
-
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
444
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
445
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
446
|
-
<polyline points="7 10 12 15 17 10" />
|
|
447
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
448
|
-
</svg>
|
|
449
|
-
</button>
|
|
450
|
-
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
451
|
-
{copied ? (
|
|
452
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
453
|
-
<polyline points="20 6 9 17 4 12" />
|
|
454
|
-
</svg>
|
|
455
|
-
) : (
|
|
456
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
457
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
458
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
459
|
-
</svg>
|
|
460
|
-
)}
|
|
461
|
-
</button>
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
<pre className="chat-code-block-body">
|
|
465
|
-
<code>{code}</code>
|
|
466
|
-
</pre>
|
|
467
|
-
</div>
|
|
468
|
-
)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const isTableLine = (line: string): boolean => {
|
|
472
|
-
const trimmed = line.trim()
|
|
473
|
-
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function renderFormattedMessage(text: string) {
|
|
477
|
-
const displayText = readableAssistantText(text)
|
|
478
|
-
if (!displayText) return null
|
|
479
|
-
|
|
480
|
-
const formatInline = (str: string) => {
|
|
481
|
-
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
482
|
-
return codeParts.map((codePart, codeIndex) => {
|
|
483
|
-
if (codeIndex % 2 === 1) {
|
|
484
|
-
return (
|
|
485
|
-
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
486
|
-
{codePart}
|
|
487
|
-
</code>
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
491
|
-
return boldParts.map((boldPart, boldIndex) => {
|
|
492
|
-
if (boldIndex % 2 === 1) {
|
|
493
|
-
return (
|
|
494
|
-
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
495
|
-
{boldPart}
|
|
496
|
-
</strong>
|
|
497
|
-
)
|
|
498
|
-
}
|
|
499
|
-
const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
|
|
500
|
-
return mentionParts.map((mentionPart, mentionIndex) => {
|
|
501
|
-
if (mentionIndex % 2 === 1) {
|
|
502
|
-
return (
|
|
503
|
-
<span key={`mention-${mentionIndex}`} className="chat-mention">
|
|
504
|
-
{mentionPart}
|
|
505
|
-
</span>
|
|
506
|
-
)
|
|
507
|
-
}
|
|
508
|
-
return mentionPart
|
|
509
|
-
})
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const renderHtmlTable = (tableLines: string[], key: string) => {
|
|
515
|
-
const rows: string[][] = []
|
|
516
|
-
for (const line of tableLines) {
|
|
517
|
-
const trimmed = line.trim()
|
|
518
|
-
if (!trimmed) continue
|
|
519
|
-
const content = trimmed.slice(1, -1)
|
|
520
|
-
const contentEscaped = content.replace(/\\\|/g, '\u0000')
|
|
521
|
-
const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
|
|
522
|
-
rows.push(cols)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (rows.length === 0) return null
|
|
526
|
-
|
|
527
|
-
let hasSeparator = false
|
|
528
|
-
let separatorIdx = -1
|
|
529
|
-
for (let i = 0; i < rows.length; i++) {
|
|
530
|
-
const row = rows[i]
|
|
531
|
-
if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
|
|
532
|
-
hasSeparator = true
|
|
533
|
-
separatorIdx = i
|
|
534
|
-
break
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
let headerRow: string[] | null = null
|
|
539
|
-
const dataRows: string[][] = []
|
|
540
|
-
|
|
541
|
-
if (hasSeparator) {
|
|
542
|
-
if (separatorIdx > 0) {
|
|
543
|
-
headerRow = rows[separatorIdx - 1]
|
|
544
|
-
for (let i = 0; i < rows.length; i++) {
|
|
545
|
-
if (i !== separatorIdx && i !== separatorIdx - 1) {
|
|
546
|
-
dataRows.push(rows[i])
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
} else {
|
|
550
|
-
for (let i = 0; i < rows.length; i++) {
|
|
551
|
-
if (i !== separatorIdx) {
|
|
552
|
-
dataRows.push(rows[i])
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
} else {
|
|
557
|
-
headerRow = rows[0]
|
|
558
|
-
for (let i = 1; i < rows.length; i++) {
|
|
559
|
-
dataRows.push(rows[i])
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return (
|
|
564
|
-
<div key={key} className="chat-table-container" style={{
|
|
565
|
-
overflowX: 'auto',
|
|
566
|
-
margin: '14px 0',
|
|
567
|
-
width: '100%',
|
|
568
|
-
borderRadius: '8px',
|
|
569
|
-
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
570
|
-
background: 'rgba(30, 41, 59, 0.35)',
|
|
571
|
-
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
572
|
-
}}>
|
|
573
|
-
<table className="chat-table" style={{
|
|
574
|
-
width: '100%',
|
|
575
|
-
borderCollapse: 'collapse',
|
|
576
|
-
fontSize: '0.86rem',
|
|
577
|
-
textAlign: 'left',
|
|
578
|
-
lineHeight: '1.5',
|
|
579
|
-
}}>
|
|
580
|
-
{headerRow && (
|
|
581
|
-
<thead>
|
|
582
|
-
<tr style={{
|
|
583
|
-
background: 'rgba(255, 255, 255, 0.04)',
|
|
584
|
-
borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
|
|
585
|
-
}}>
|
|
586
|
-
{headerRow.map((col, idx) => (
|
|
587
|
-
<th key={`th-${idx}`} style={{
|
|
588
|
-
padding: '12px 16px',
|
|
589
|
-
fontWeight: 700,
|
|
590
|
-
color: 'var(--accent, #38bdf8)',
|
|
591
|
-
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
592
|
-
}}>
|
|
593
|
-
{formatInline(col)}
|
|
594
|
-
</th>
|
|
595
|
-
))}
|
|
596
|
-
</tr>
|
|
597
|
-
</thead>
|
|
598
|
-
)}
|
|
599
|
-
<tbody>
|
|
600
|
-
{dataRows.map((row, rIdx) => (
|
|
601
|
-
<tr key={`tr-${rIdx}`} style={{
|
|
602
|
-
background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
|
|
603
|
-
borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
|
|
604
|
-
}}>
|
|
605
|
-
{row.map((col, cIdx) => (
|
|
606
|
-
<td key={`td-${cIdx}`} style={{
|
|
607
|
-
padding: '12px 16px',
|
|
608
|
-
color: 'var(--text-main, #e2e8f0)',
|
|
609
|
-
}}>
|
|
610
|
-
{formatInline(col)}
|
|
611
|
-
</td>
|
|
612
|
-
))}
|
|
613
|
-
</tr>
|
|
614
|
-
))}
|
|
615
|
-
</tbody>
|
|
616
|
-
</table>
|
|
617
|
-
</div>
|
|
618
|
-
)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const lines = displayText.split('\n')
|
|
622
|
-
const items: any[] = []
|
|
623
|
-
|
|
624
|
-
let inCodeBlock = false
|
|
625
|
-
let codeBlockLang = ''
|
|
626
|
-
let codeBlockLines: string[] = []
|
|
627
|
-
|
|
628
|
-
let inTable = false
|
|
629
|
-
let tableLines: string[] = []
|
|
630
|
-
|
|
631
|
-
const flushTable = (index: number) => {
|
|
632
|
-
if (tableLines.length > 0) {
|
|
633
|
-
items.push(renderHtmlTable(tableLines, `table-${index}`))
|
|
634
|
-
tableLines = []
|
|
635
|
-
inTable = false
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
for (let i = 0; i < lines.length; i++) {
|
|
640
|
-
const line = lines[i]
|
|
641
|
-
|
|
642
|
-
if (line.trim().startsWith('```')) {
|
|
643
|
-
if (inTable) {
|
|
644
|
-
flushTable(i)
|
|
645
|
-
}
|
|
646
|
-
if (inCodeBlock) {
|
|
647
|
-
const codeText = codeBlockLines.join('\n')
|
|
648
|
-
items.push(
|
|
649
|
-
<ChatCodeBlock
|
|
650
|
-
key={`code-block-${i}`}
|
|
651
|
-
code={codeText}
|
|
652
|
-
language={codeBlockLang}
|
|
653
|
-
/>
|
|
654
|
-
)
|
|
655
|
-
inCodeBlock = false
|
|
656
|
-
codeBlockLines = []
|
|
657
|
-
} else {
|
|
658
|
-
inCodeBlock = true
|
|
659
|
-
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
660
|
-
}
|
|
661
|
-
continue
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (inCodeBlock) {
|
|
665
|
-
codeBlockLines.push(line)
|
|
666
|
-
continue
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (isTableLine(line)) {
|
|
670
|
-
inTable = true
|
|
671
|
-
tableLines.push(line)
|
|
672
|
-
} else {
|
|
673
|
-
if (inTable) {
|
|
674
|
-
flushTable(i)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
678
|
-
if (headerMatch) {
|
|
679
|
-
const level = headerMatch[1].length
|
|
680
|
-
const content = headerMatch[2]
|
|
681
|
-
|
|
682
|
-
const style = {
|
|
683
|
-
fontWeight: 'bold',
|
|
684
|
-
display: 'block',
|
|
685
|
-
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
686
|
-
marginBottom: '6px',
|
|
687
|
-
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
688
|
-
color: 'var(--text-main)',
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
items.push(
|
|
692
|
-
<span key={`line-${i}`} style={style}>
|
|
693
|
-
{formatInline(content)}
|
|
694
|
-
</span>
|
|
695
|
-
)
|
|
696
|
-
} else {
|
|
697
|
-
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
|
|
698
|
-
if (listMatch) {
|
|
699
|
-
const indent = listMatch[1]
|
|
700
|
-
const content = listMatch[3]
|
|
701
|
-
items.push(
|
|
702
|
-
<Fragment key={`line-${i}`}>
|
|
703
|
-
{indent}• {formatInline(content)}
|
|
704
|
-
{i < lines.length - 1 && '\n'}
|
|
705
|
-
</Fragment>
|
|
706
|
-
)
|
|
707
|
-
} else {
|
|
708
|
-
items.push(
|
|
709
|
-
<Fragment key={`line-${i}`}>
|
|
710
|
-
{formatInline(line)}
|
|
711
|
-
{i < lines.length - 1 && '\n'}
|
|
712
|
-
</Fragment>
|
|
713
|
-
)
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (inTable) {
|
|
720
|
-
flushTable(lines.length)
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
724
|
-
const codeText = codeBlockLines.join('\n')
|
|
725
|
-
items.push(
|
|
726
|
-
<ChatCodeBlock
|
|
727
|
-
key={`code-block-end`}
|
|
728
|
-
code={codeText}
|
|
729
|
-
language={codeBlockLang}
|
|
730
|
-
/>
|
|
731
|
-
)
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
return items
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const THINKING_LABELS = {
|
|
738
|
-
live: 'Thinking…',
|
|
739
|
-
completed: (count: number) => `Thinking process (${count} steps)`,
|
|
740
|
-
step: (index: number) => `Step ${index}`,
|
|
741
|
-
emptyHint: 'Model didn\'t send thinking steps this time',
|
|
742
|
-
} as const
|
|
743
|
-
|
|
744
|
-
function ThinkingBlock({
|
|
745
|
-
thoughts,
|
|
746
|
-
isLive = false,
|
|
747
|
-
blockKey,
|
|
748
|
-
expanded,
|
|
749
|
-
onExpandedChange,
|
|
750
|
-
showEmptyHint = false,
|
|
751
|
-
}: {
|
|
752
|
-
thoughts: string[]
|
|
753
|
-
isLive?: boolean
|
|
754
|
-
blockKey: string
|
|
755
|
-
expanded?: boolean
|
|
756
|
-
onExpandedChange?: (key: string, open: boolean) => void
|
|
757
|
-
showEmptyHint?: boolean
|
|
758
|
-
}) {
|
|
759
|
-
const contentRef = useRef<HTMLDivElement>(null)
|
|
760
|
-
const isControlled = expanded !== undefined && Boolean(onExpandedChange)
|
|
761
|
-
const [localOpen, setLocalOpen] = useState(isLive)
|
|
762
|
-
const isOpen = isControlled ? Boolean(expanded) : localOpen
|
|
763
|
-
|
|
764
|
-
const setOpen = (open: boolean) => {
|
|
765
|
-
if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
|
|
766
|
-
else setLocalOpen(open)
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
useEffect(() => {
|
|
770
|
-
if (isLive && !isControlled) setLocalOpen(true)
|
|
771
|
-
}, [isLive, isControlled])
|
|
772
|
-
|
|
773
|
-
useEffect(() => {
|
|
774
|
-
if (!isLive || !isOpen || !contentRef.current) return
|
|
775
|
-
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
776
|
-
}, [thoughts, isLive, isOpen])
|
|
777
|
-
|
|
778
|
-
if (thoughts.length === 0 && !showEmptyHint) return null
|
|
779
|
-
|
|
780
|
-
if (thoughts.length === 0) {
|
|
781
|
-
return (
|
|
782
|
-
<div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
|
|
783
|
-
<span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
|
|
784
|
-
</div>
|
|
785
|
-
)
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
return (
|
|
789
|
-
<div className={`thinking-block${isLive ? ' is-live' : ''}`}>
|
|
790
|
-
<button
|
|
791
|
-
type="button"
|
|
792
|
-
className="thinking-block-header"
|
|
793
|
-
onClick={() => setOpen(!isOpen)}
|
|
794
|
-
aria-expanded={isOpen}
|
|
795
|
-
>
|
|
796
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
|
|
797
|
-
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
798
|
-
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
799
|
-
<circle cx="12" cy="12" r="10" />
|
|
800
|
-
</svg>
|
|
801
|
-
<span className="thinking-block-label">
|
|
802
|
-
{isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
|
|
803
|
-
</span>
|
|
804
|
-
{isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
|
|
805
|
-
<span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
|
|
806
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
807
|
-
<polyline points="9 18 15 12 9 6" />
|
|
808
|
-
</svg>
|
|
809
|
-
</span>
|
|
810
|
-
</button>
|
|
811
|
-
{isOpen && (
|
|
812
|
-
<div className="thinking-block-body">
|
|
813
|
-
<div className="thinking-block-content" ref={contentRef}>
|
|
814
|
-
{thoughts.map((thought, index) => (
|
|
815
|
-
<div className="thinking-block-step" key={`${blockKey}-${index}`}>
|
|
816
|
-
{thoughts.length > 1 && (
|
|
817
|
-
<div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
|
|
818
|
-
)}
|
|
819
|
-
<div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
|
|
820
|
-
</div>
|
|
821
|
-
))}
|
|
822
|
-
</div>
|
|
823
|
-
</div>
|
|
824
|
-
)}
|
|
825
|
-
</div>
|
|
826
|
-
)
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function readableAssistantText(text: string) {
|
|
830
|
-
if (typeof text !== 'string') return ''
|
|
831
|
-
const trimmed = text.trim()
|
|
832
|
-
if (!trimmed.startsWith('{')) return text
|
|
833
|
-
try {
|
|
834
|
-
const value = JSON.parse(trimmed)
|
|
835
|
-
if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
|
|
836
|
-
return value.input.summary
|
|
837
|
-
}
|
|
838
|
-
if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
|
|
839
|
-
return value.finish.summary
|
|
840
|
-
}
|
|
841
|
-
} catch {
|
|
842
|
-
return text
|
|
843
|
-
}
|
|
844
|
-
return text
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function renderSpeakerIcon(isSpeaking: boolean) {
|
|
848
|
-
if (isSpeaking) {
|
|
849
|
-
return (
|
|
850
|
-
<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' }}>
|
|
851
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
852
|
-
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
853
|
-
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
854
|
-
</svg>
|
|
855
|
-
)
|
|
856
|
-
}
|
|
857
|
-
return (
|
|
858
|
-
<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' }}>
|
|
859
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
860
|
-
</svg>
|
|
861
|
-
)
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function cleanSpeechText(text: string) {
|
|
865
|
-
return readableAssistantText(text)
|
|
866
|
-
.replace(/\*\*([\s\S]*?)\*\*/g, '$1')
|
|
867
|
-
.replace(/[*_`#]/g, '')
|
|
868
|
-
.trim()
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function numericSetting(value: unknown, fallback: number) {
|
|
872
|
-
const numeric = Number(value)
|
|
873
|
-
return Number.isFinite(numeric) ? numeric : fallback
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
interface DiffHunk {
|
|
877
|
-
oldText: string
|
|
878
|
-
newText: string
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
interface FileChange {
|
|
882
|
-
path: string
|
|
883
|
-
created: boolean
|
|
884
|
-
additions: number
|
|
885
|
-
deletions: number
|
|
886
|
-
hunks: DiffHunk[]
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
|
|
890
|
-
const changes = new Map<string, FileChange>()
|
|
891
|
-
let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
|
|
892
|
-
|
|
893
|
-
for (const event of progress || []) {
|
|
894
|
-
if (event.type === 'ToolStart') {
|
|
895
|
-
if (event.data.action === 'apply_patch') {
|
|
896
|
-
const patch = (event.data.input as any)?.patch
|
|
897
|
-
if (patch && typeof patch.path === 'string') {
|
|
898
|
-
let additions = 0
|
|
899
|
-
let deletions = 0
|
|
900
|
-
const hunksList: DiffHunk[] = []
|
|
901
|
-
const hunks = patch.hunks
|
|
902
|
-
if (Array.isArray(hunks)) {
|
|
903
|
-
for (const hunk of hunks) {
|
|
904
|
-
const oldText = hunk?.oldText || ''
|
|
905
|
-
const newText = hunk?.newText || ''
|
|
906
|
-
const oldLines = oldText ? oldText.split('\n').length : 0
|
|
907
|
-
const newLines = newText ? newText.split('\n').length : 0
|
|
908
|
-
deletions += oldLines
|
|
909
|
-
additions += newLines
|
|
910
|
-
hunksList.push({ oldText, newText })
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
activeEdit = {
|
|
914
|
-
action: 'apply_patch',
|
|
915
|
-
path: patch.path,
|
|
916
|
-
created: false,
|
|
917
|
-
additions,
|
|
918
|
-
deletions,
|
|
919
|
-
hunks: hunksList
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
} else if (event.data.action === 'write_file') {
|
|
923
|
-
const path = (event.data.input as any)?.path
|
|
924
|
-
const fileContent = (event.data.input as any)?.file_content || ''
|
|
925
|
-
if (typeof path === 'string') {
|
|
926
|
-
const additions = fileContent ? fileContent.split('\n').length : 0
|
|
927
|
-
activeEdit = {
|
|
928
|
-
action: 'write_file',
|
|
929
|
-
path,
|
|
930
|
-
created: true,
|
|
931
|
-
additions,
|
|
932
|
-
deletions: 0,
|
|
933
|
-
hunks: [{ oldText: '', newText: fileContent }]
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
} else {
|
|
937
|
-
activeEdit = null
|
|
938
|
-
}
|
|
939
|
-
} else if (event.type === 'ToolEnd') {
|
|
940
|
-
if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
|
|
941
|
-
const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
|
|
942
|
-
if (!isError) {
|
|
943
|
-
try {
|
|
944
|
-
const applied = JSON.parse(event.data.result)
|
|
945
|
-
const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
|
|
946
|
-
|
|
947
|
-
for (const path of appliedPaths) {
|
|
948
|
-
const existing = changes.get(path)
|
|
949
|
-
if (existing) {
|
|
950
|
-
existing.additions += activeEdit.additions
|
|
951
|
-
existing.deletions += activeEdit.deletions
|
|
952
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
953
|
-
} else {
|
|
954
|
-
changes.set(path, {
|
|
955
|
-
path,
|
|
956
|
-
created: activeEdit.created,
|
|
957
|
-
additions: activeEdit.additions,
|
|
958
|
-
deletions: activeEdit.deletions,
|
|
959
|
-
hunks: [...activeEdit.hunks]
|
|
960
|
-
})
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
} catch (e) {
|
|
964
|
-
const path = activeEdit.path
|
|
965
|
-
const existing = changes.get(path)
|
|
966
|
-
if (existing) {
|
|
967
|
-
existing.additions += activeEdit.additions
|
|
968
|
-
existing.deletions += activeEdit.deletions
|
|
969
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
970
|
-
} else {
|
|
971
|
-
changes.set(path, {
|
|
972
|
-
path,
|
|
973
|
-
created: activeEdit.created,
|
|
974
|
-
additions: activeEdit.additions,
|
|
975
|
-
deletions: activeEdit.deletions,
|
|
976
|
-
hunks: [...activeEdit.hunks]
|
|
977
|
-
})
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
activeEdit = null
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
return Array.from(changes.values())
|
|
987
|
-
}
|
|
988
76
|
|
|
989
77
|
export default function ChatPanel({
|
|
990
78
|
interactions,
|
|
@@ -1010,7 +98,6 @@ export default function ChatPanel({
|
|
|
1010
98
|
onSelectImage,
|
|
1011
99
|
onSelectDocument,
|
|
1012
100
|
onPasteImage,
|
|
1013
|
-
onReadClipboardImage,
|
|
1014
101
|
onSetMessage,
|
|
1015
102
|
onSendVoiceMessage,
|
|
1016
103
|
onRemoveImage,
|
|
@@ -1024,20 +111,23 @@ export default function ChatPanel({
|
|
|
1024
111
|
onToggleMobileSidebar,
|
|
1025
112
|
settingsConfig,
|
|
1026
113
|
onSetModel,
|
|
114
|
+
onCancelMessage,
|
|
1027
115
|
}: ChatPanelProps) {
|
|
1028
116
|
const agentActivities = activitiesFrom(agentProgress)
|
|
1029
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
|
+
}
|
|
1030
125
|
const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
|
|
1031
126
|
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
1032
127
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
1033
128
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
1034
129
|
const toolMenuRef = useRef<HTMLDivElement | null>(null)
|
|
1035
130
|
const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
|
|
1036
|
-
const [askAnswer, setAskAnswer] = useState('')
|
|
1037
|
-
|
|
1038
|
-
useEffect(() => {
|
|
1039
|
-
setAskAnswer('')
|
|
1040
|
-
}, [pendingApproval])
|
|
1041
131
|
|
|
1042
132
|
useEffect(() => {
|
|
1043
133
|
const fetchOllamaModels = async () => {
|
|
@@ -1082,8 +172,13 @@ export default function ChatPanel({
|
|
|
1082
172
|
case 'ollama':
|
|
1083
173
|
return dynamicOllamaModels
|
|
1084
174
|
default:
|
|
175
|
+
if (provider.startsWith('custom:')) {
|
|
176
|
+
const id = provider.replace(/^custom:/, '')
|
|
177
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
178
|
+
return cp?.models.map(m => m.modelId) ?? []
|
|
179
|
+
}
|
|
1085
180
|
return []
|
|
1086
|
-
|
|
181
|
+
}
|
|
1087
182
|
}
|
|
1088
183
|
|
|
1089
184
|
const activeProvider = status?.activeProvider ?? ''
|
|
@@ -1109,6 +204,11 @@ export default function ChatPanel({
|
|
|
1109
204
|
case 'ollama':
|
|
1110
205
|
return settingsConfig.ollamaModel
|
|
1111
206
|
default:
|
|
207
|
+
if (provider.startsWith('custom:')) {
|
|
208
|
+
const id = provider.replace(/^custom:/, '')
|
|
209
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
210
|
+
return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
|
|
211
|
+
}
|
|
1112
212
|
return ''
|
|
1113
213
|
}
|
|
1114
214
|
}
|
|
@@ -1133,60 +233,40 @@ export default function ChatPanel({
|
|
|
1133
233
|
return () => clearInterval(timer)
|
|
1134
234
|
}, [sending])
|
|
1135
235
|
|
|
1136
|
-
const [isRecording, setIsRecording] = useState(false)
|
|
1137
|
-
const [voiceMode, setVoiceMode] = useState(false)
|
|
1138
|
-
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
1139
|
-
const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
|
|
1140
236
|
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
1141
|
-
const recognitionRef = useRef<SpeechRecognition | null>(null)
|
|
1142
|
-
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
|
1143
|
-
const mediaStreamRef = useRef<MediaStream | null>(null)
|
|
1144
|
-
const audioContextRef = useRef<AudioContext | null>(null)
|
|
1145
|
-
const vadTimerRef = useRef<number | null>(null)
|
|
1146
237
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
1147
238
|
const speechRunRef = useRef(0)
|
|
1148
239
|
const historyReadyRef = useRef(false)
|
|
1149
240
|
const submittedDuringSessionRef = useRef(false)
|
|
1150
241
|
const lastAutoSpokenIdRef = useRef<number | string | null>(null)
|
|
1151
|
-
const voiceModeRef = useRef(false)
|
|
1152
|
-
const sendingRef = useRef(false)
|
|
1153
|
-
const voiceAwaitingResponseRef = useRef(false)
|
|
1154
242
|
const speakingRef = useRef<string | null>(null)
|
|
1155
|
-
|
|
243
|
+
|
|
244
|
+
const {
|
|
245
|
+
isRecording,
|
|
246
|
+
voiceMode,
|
|
247
|
+
setVoiceMode,
|
|
248
|
+
voiceTranscript,
|
|
249
|
+
setVoiceTranscript,
|
|
250
|
+
voiceAwaitingResponse,
|
|
251
|
+
voiceAwaitingResponseRef,
|
|
252
|
+
voiceModeRef,
|
|
253
|
+
startRecognition,
|
|
254
|
+
stopRecognition,
|
|
255
|
+
scheduleVoiceListen,
|
|
256
|
+
clearRestartTimer
|
|
257
|
+
} = useSpeechToText({
|
|
258
|
+
language: settingsConfig?.language,
|
|
259
|
+
message,
|
|
260
|
+
sending,
|
|
261
|
+
isSpeaking: Boolean(speakingText),
|
|
262
|
+
onSendVoiceMessage,
|
|
263
|
+
onSetMessage: (val) => onSetMessage(val)
|
|
264
|
+
})
|
|
265
|
+
|
|
1156
266
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
1157
267
|
const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
|
|
1158
268
|
|
|
1159
|
-
|
|
1160
|
-
if (restartTimerRef.current === null) return
|
|
1161
|
-
window.clearTimeout(restartTimerRef.current)
|
|
1162
|
-
restartTimerRef.current = null
|
|
1163
|
-
}
|
|
1164
|
-
const clearVadTimer = () => {
|
|
1165
|
-
if (vadTimerRef.current === null) return
|
|
1166
|
-
window.clearInterval(vadTimerRef.current)
|
|
1167
|
-
vadTimerRef.current = null
|
|
1168
|
-
}
|
|
1169
|
-
const stopRecognition = () => {
|
|
1170
|
-
clearRestartTimer()
|
|
1171
|
-
recognitionRef.current?.stop()
|
|
1172
|
-
recognitionRef.current = null
|
|
1173
|
-
clearVadTimer()
|
|
1174
|
-
if (mediaRecorderRef.current?.state === 'recording') mediaRecorderRef.current.stop()
|
|
1175
|
-
mediaRecorderRef.current = null
|
|
1176
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1177
|
-
mediaStreamRef.current = null
|
|
1178
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1179
|
-
audioContextRef.current = null
|
|
1180
|
-
setIsRecording(false)
|
|
1181
|
-
}
|
|
1182
|
-
const scheduleVoiceListen = (delayMs = 350) => {
|
|
1183
|
-
clearRestartTimer()
|
|
1184
|
-
if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1185
|
-
restartTimerRef.current = window.setTimeout(() => {
|
|
1186
|
-
restartTimerRef.current = null
|
|
1187
|
-
startRecognition(true)
|
|
1188
|
-
}, delayMs)
|
|
1189
|
-
}
|
|
269
|
+
|
|
1190
270
|
const cancelSpeech = () => {
|
|
1191
271
|
speechRunRef.current += 1
|
|
1192
272
|
audioRef.current?.pause()
|
|
@@ -1208,7 +288,11 @@ export default function ChatPanel({
|
|
|
1208
288
|
utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
|
|
1209
289
|
utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
|
|
1210
290
|
utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
|
|
1211
|
-
const voice = window.speechSynthesis.getVoices().find((item) =>
|
|
291
|
+
const voice = window.speechSynthesis.getVoices().find((item) => {
|
|
292
|
+
const lang = item.lang.toLowerCase()
|
|
293
|
+
const target = hasThai ? 'th' : 'en'
|
|
294
|
+
return lang.startsWith(target) || lang.includes(target)
|
|
295
|
+
})
|
|
1212
296
|
if (voice) utterance.voice = voice
|
|
1213
297
|
const finishSpeech = () => {
|
|
1214
298
|
setSpeakingText((current) => (current === displayText ? null : current))
|
|
@@ -1264,213 +348,24 @@ export default function ChatPanel({
|
|
|
1264
348
|
}
|
|
1265
349
|
speakNative(speechText, text)
|
|
1266
350
|
}
|
|
1267
|
-
const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
|
|
1268
|
-
const reader = new FileReader()
|
|
1269
|
-
reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
|
|
1270
|
-
reader.onload = () => resolve(String(reader.result))
|
|
1271
|
-
reader.readAsDataURL(blob)
|
|
1272
|
-
})
|
|
1273
|
-
const preferredAudioMimeType = () => {
|
|
1274
|
-
const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
|
|
1275
|
-
return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
|
|
1276
|
-
}
|
|
1277
|
-
const startAudioRecording = async (autoSend: boolean) => {
|
|
1278
|
-
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
1279
|
-
alert("Sorry, this system cannot access the microphone or record audio in this browser")
|
|
1280
|
-
voiceModeRef.current = false
|
|
1281
|
-
setVoiceMode(false)
|
|
1282
|
-
return
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
try {
|
|
1286
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
1287
|
-
const mimeType = preferredAudioMimeType()
|
|
1288
|
-
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
|
|
1289
|
-
const chunks: Blob[] = []
|
|
1290
|
-
let heardVoice = false
|
|
1291
|
-
let quietSince = 0
|
|
1292
|
-
const startedAt = Date.now()
|
|
1293
|
-
const audioContext = new AudioContext()
|
|
1294
|
-
const source = audioContext.createMediaStreamSource(stream)
|
|
1295
|
-
const analyser = audioContext.createAnalyser()
|
|
1296
|
-
const samples = new Uint8Array(analyser.fftSize)
|
|
1297
|
-
source.connect(analyser)
|
|
1298
|
-
|
|
1299
|
-
mediaStreamRef.current = stream
|
|
1300
|
-
mediaRecorderRef.current = recorder
|
|
1301
|
-
audioContextRef.current = audioContext
|
|
1302
|
-
setVoiceTranscript('Listening to microphone...')
|
|
1303
|
-
|
|
1304
|
-
recorder.ondataavailable = (event) => {
|
|
1305
|
-
if (event.data.size > 0) chunks.push(event.data)
|
|
1306
|
-
}
|
|
1307
|
-
recorder.onstop = async () => {
|
|
1308
|
-
clearVadTimer()
|
|
1309
|
-
mediaRecorderRef.current = null
|
|
1310
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1311
|
-
mediaStreamRef.current = null
|
|
1312
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1313
|
-
audioContextRef.current = null
|
|
1314
|
-
setIsRecording(false)
|
|
1315
|
-
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
1316
|
-
if (!heardVoice) {
|
|
1317
|
-
setVoiceTranscript('No speech detected')
|
|
1318
|
-
scheduleVoiceListen(700)
|
|
1319
|
-
return
|
|
1320
|
-
}
|
|
1321
|
-
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
1322
|
-
const audioDataUri = await blobToDataUri(blob)
|
|
1323
|
-
setVoiceTranscript('Sent audio to AI')
|
|
1324
|
-
voiceAwaitingResponseRef.current = true
|
|
1325
|
-
setVoiceAwaitingResponse(true)
|
|
1326
|
-
onSendVoiceMessage('', audioDataUri)
|
|
1327
|
-
.catch((error) => console.error('Voice audio message failed', error))
|
|
1328
|
-
.finally(() => {
|
|
1329
|
-
voiceAwaitingResponseRef.current = false
|
|
1330
|
-
setVoiceAwaitingResponse(false)
|
|
1331
|
-
scheduleVoiceListen()
|
|
1332
|
-
})
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
recorder.start()
|
|
1336
|
-
setIsRecording(true)
|
|
1337
|
-
vadTimerRef.current = window.setInterval(() => {
|
|
1338
|
-
analyser.getByteTimeDomainData(samples)
|
|
1339
|
-
let peak = 0
|
|
1340
|
-
for (const sample of samples) {
|
|
1341
|
-
peak = Math.max(peak, Math.abs(sample - 128))
|
|
1342
|
-
}
|
|
1343
|
-
const now = Date.now()
|
|
1344
|
-
if (peak > 12) {
|
|
1345
|
-
heardVoice = true
|
|
1346
|
-
quietSince = 0
|
|
1347
|
-
setVoiceTranscript('Listening...')
|
|
1348
|
-
} else if (heardVoice) {
|
|
1349
|
-
quietSince = quietSince || now
|
|
1350
|
-
}
|
|
1351
|
-
const silenceElapsed = quietSince ? now - quietSince : 0
|
|
1352
|
-
const totalElapsed = now - startedAt
|
|
1353
|
-
if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
|
|
1354
|
-
recorder.stop()
|
|
1355
|
-
}
|
|
1356
|
-
}, 120)
|
|
1357
|
-
} catch (error) {
|
|
1358
|
-
console.error('Failed to record microphone audio', error)
|
|
1359
|
-
setIsRecording(false)
|
|
1360
|
-
voiceModeRef.current = false
|
|
1361
|
-
setVoiceMode(false)
|
|
1362
|
-
alert('Failed to open microphone. Please check microphone permissions in your browser')
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
const startRecognition = (autoSend: boolean) => {
|
|
1366
|
-
if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1367
|
-
|
|
1368
|
-
const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
1369
|
-
if (!SpeechRecognitionApi) {
|
|
1370
|
-
startAudioRecording(autoSend)
|
|
1371
|
-
return
|
|
1372
|
-
}
|
|
1373
351
|
|
|
1374
|
-
let sentTranscript = false
|
|
1375
|
-
|
|
1376
|
-
try {
|
|
1377
|
-
const recognition = new SpeechRecognitionApi()
|
|
1378
|
-
recognition.continuous = false
|
|
1379
|
-
recognition.interimResults = true
|
|
1380
|
-
recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
|
|
1381
|
-
recognition.onstart = () => setIsRecording(true)
|
|
1382
|
-
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
1383
|
-
let interimText = ''
|
|
1384
|
-
let finalText = ''
|
|
1385
|
-
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
1386
|
-
const transcript = event.results[index]?.[0]?.transcript ?? ''
|
|
1387
|
-
if (event.results[index]?.isFinal) finalText += transcript
|
|
1388
|
-
else interimText += transcript
|
|
1389
|
-
}
|
|
1390
|
-
const displayText = (finalText || interimText).trim()
|
|
1391
|
-
if (displayText) setVoiceTranscript(displayText)
|
|
1392
|
-
const resultText = finalText.trim()
|
|
1393
|
-
if (!resultText) return
|
|
1394
|
-
sentTranscript = true
|
|
1395
|
-
recognition.stop()
|
|
1396
|
-
setVoiceTranscript(resultText)
|
|
1397
|
-
if (autoSend) {
|
|
1398
|
-
voiceAwaitingResponseRef.current = true
|
|
1399
|
-
setVoiceAwaitingResponse(true)
|
|
1400
|
-
onSendVoiceMessage(resultText)
|
|
1401
|
-
.catch((error) => console.error('Voice message failed', error))
|
|
1402
|
-
.finally(() => {
|
|
1403
|
-
voiceAwaitingResponseRef.current = false
|
|
1404
|
-
setVoiceAwaitingResponse(false)
|
|
1405
|
-
scheduleVoiceListen()
|
|
1406
|
-
})
|
|
1407
|
-
} else {
|
|
1408
|
-
onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
recognition.onerror = (event: Event) => {
|
|
1412
|
-
console.error('Speech recognition error', event)
|
|
1413
|
-
setIsRecording(false)
|
|
1414
|
-
}
|
|
1415
|
-
recognition.onend = () => {
|
|
1416
|
-
recognitionRef.current = null
|
|
1417
|
-
setIsRecording(false)
|
|
1418
|
-
if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
|
|
1419
|
-
}
|
|
1420
|
-
recognitionRef.current = recognition
|
|
1421
|
-
recognition.start()
|
|
1422
|
-
} catch (error) {
|
|
1423
|
-
console.error('Failed to start speech recognition', error)
|
|
1424
|
-
recognitionRef.current = null
|
|
1425
|
-
setIsRecording(false)
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
352
|
const toggleRecording = () => {
|
|
1429
353
|
if (voiceMode) {
|
|
1430
|
-
voiceModeRef.current = false
|
|
1431
|
-
voiceAwaitingResponseRef.current = false
|
|
1432
354
|
setVoiceMode(false)
|
|
1433
|
-
setVoiceAwaitingResponse(false)
|
|
1434
|
-
setVoiceTranscript('')
|
|
1435
355
|
stopRecognition()
|
|
1436
356
|
cancelSpeech()
|
|
1437
357
|
return
|
|
1438
358
|
}
|
|
1439
359
|
|
|
1440
|
-
voiceModeRef.current = true
|
|
1441
360
|
setVoiceMode(true)
|
|
1442
|
-
setVoiceAwaitingResponse(false)
|
|
1443
|
-
setVoiceTranscript('')
|
|
1444
361
|
startRecognition(true)
|
|
1445
362
|
}
|
|
1446
363
|
|
|
1447
364
|
useEffect(() => {
|
|
1448
365
|
return () => {
|
|
1449
|
-
clearRestartTimer()
|
|
1450
366
|
cancelSpeech()
|
|
1451
|
-
stopRecognition()
|
|
1452
367
|
}
|
|
1453
368
|
}, [])
|
|
1454
|
-
useEffect(() => {
|
|
1455
|
-
voiceModeRef.current = voiceMode
|
|
1456
|
-
if (!voiceMode) {
|
|
1457
|
-
clearRestartTimer()
|
|
1458
|
-
setVoiceTranscript('')
|
|
1459
|
-
}
|
|
1460
|
-
}, [voiceMode])
|
|
1461
|
-
useEffect(() => {
|
|
1462
|
-
sendingRef.current = sending
|
|
1463
|
-
}, [sending])
|
|
1464
|
-
useEffect(() => {
|
|
1465
|
-
voiceAwaitingResponseRef.current = voiceAwaitingResponse
|
|
1466
|
-
}, [voiceAwaitingResponse])
|
|
1467
|
-
useEffect(() => {
|
|
1468
|
-
speakingRef.current = speakingText
|
|
1469
|
-
}, [speakingText])
|
|
1470
|
-
useEffect(() => {
|
|
1471
|
-
if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
|
|
1472
|
-
scheduleVoiceListen()
|
|
1473
|
-
}, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
|
|
1474
369
|
useEffect(() => {
|
|
1475
370
|
if (sending) submittedDuringSessionRef.current = true
|
|
1476
371
|
}, [sending])
|
|
@@ -1485,14 +380,19 @@ export default function ChatPanel({
|
|
|
1485
380
|
}
|
|
1486
381
|
}
|
|
1487
382
|
if (sending) return
|
|
1488
|
-
if (!
|
|
1489
|
-
|
|
383
|
+
if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
|
|
384
|
+
|
|
385
|
+
if (!settingsConfig?.enableVoiceReply) {
|
|
386
|
+
lastAutoSpokenIdRef.current = latest.id
|
|
387
|
+
if (voiceMode) {
|
|
388
|
+
scheduleVoiceListen(350)
|
|
389
|
+
}
|
|
1490
390
|
return
|
|
1491
391
|
}
|
|
1492
|
-
|
|
392
|
+
|
|
1493
393
|
lastAutoSpokenIdRef.current = latest.id
|
|
1494
394
|
speak(latest.aiText)
|
|
1495
|
-
}, [interactions, sending, settingsConfig?.enableVoiceReply])
|
|
395
|
+
}, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
|
|
1496
396
|
|
|
1497
397
|
// Drag and Drop Zone Overlay
|
|
1498
398
|
const [isDragging, setIsDragging] = useState(false)
|
|
@@ -1576,21 +476,20 @@ export default function ChatPanel({
|
|
|
1576
476
|
if (onPasteImage(event.clipboardData)) {
|
|
1577
477
|
event.preventDefault()
|
|
1578
478
|
event.stopPropagation()
|
|
1579
|
-
} else {
|
|
1580
|
-
window.setTimeout(() => void onReadClipboardImage(), 0)
|
|
1581
479
|
}
|
|
1582
480
|
}
|
|
1583
481
|
window.addEventListener('paste', handleWindowPaste, true)
|
|
1584
482
|
return () => window.removeEventListener('paste', handleWindowPaste, true)
|
|
1585
|
-
}, [onPasteImage
|
|
483
|
+
}, [onPasteImage])
|
|
1586
484
|
useEffect(() => {
|
|
1587
|
-
const
|
|
1588
|
-
if (
|
|
1589
|
-
|
|
485
|
+
const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
486
|
+
if (event.key === 'Escape' && sending) {
|
|
487
|
+
onCancelMessage()
|
|
488
|
+
}
|
|
1590
489
|
}
|
|
1591
|
-
window.addEventListener('keydown',
|
|
1592
|
-
return () => window.removeEventListener('keydown',
|
|
1593
|
-
}, [
|
|
490
|
+
window.addEventListener('keydown', handleEscapeKeyDown, true)
|
|
491
|
+
return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
|
|
492
|
+
}, [sending, onCancelMessage])
|
|
1594
493
|
useEffect(() => {
|
|
1595
494
|
if (message) return
|
|
1596
495
|
const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
|
|
@@ -1613,24 +512,72 @@ export default function ChatPanel({
|
|
|
1613
512
|
const renderCompletedActivity = (interaction: any) => {
|
|
1614
513
|
const interactionId = String(interaction.id)
|
|
1615
514
|
const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
|
|
1616
|
-
if (activityView.items.length === 0) return null
|
|
1617
515
|
const isOpen = Boolean(openActivityIds[interactionId])
|
|
1618
516
|
return (
|
|
1619
|
-
<
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
517
|
+
<AgentActivityDrawer
|
|
518
|
+
activityView={activityView}
|
|
519
|
+
isOpen={isOpen}
|
|
520
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
|
|
521
|
+
isHistorical={true}
|
|
522
|
+
/>
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const renderWebSearchSources = (interaction: any) => {
|
|
527
|
+
const interactionId = String(interaction.id)
|
|
528
|
+
const progress = agentActivitySnapshots[interactionId] ?? interaction.agentActivity ?? []
|
|
529
|
+
const sources = parseWebSearchSources(progress)
|
|
530
|
+
if (sources.length === 0) return null
|
|
531
|
+
return (
|
|
532
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '8px' }}>
|
|
533
|
+
<span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sources</span>
|
|
534
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
|
535
|
+
{sources.map((src, i) => (
|
|
536
|
+
<a
|
|
537
|
+
key={i}
|
|
538
|
+
href={src.url}
|
|
539
|
+
target="_blank"
|
|
540
|
+
rel="noopener noreferrer"
|
|
541
|
+
title={src.snippet || src.title}
|
|
542
|
+
style={{
|
|
543
|
+
display: 'flex',
|
|
544
|
+
alignItems: 'center',
|
|
545
|
+
gap: '6px',
|
|
546
|
+
padding: '5px 10px',
|
|
547
|
+
background: 'rgba(255,255,255,0.04)',
|
|
548
|
+
border: '1px solid rgba(255,255,255,0.08)',
|
|
549
|
+
borderRadius: '8px',
|
|
550
|
+
textDecoration: 'none',
|
|
551
|
+
color: '#cbd5e1',
|
|
552
|
+
fontSize: '0.78rem',
|
|
553
|
+
maxWidth: '220px',
|
|
554
|
+
overflow: 'hidden',
|
|
555
|
+
cursor: 'pointer',
|
|
556
|
+
transition: 'background 0.15s, border-color 0.15s',
|
|
557
|
+
}}
|
|
558
|
+
onMouseEnter={e => {
|
|
559
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.08)'
|
|
560
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.18)'
|
|
561
|
+
}}
|
|
562
|
+
onMouseLeave={e => {
|
|
563
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.04)'
|
|
564
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.08)'
|
|
565
|
+
}}
|
|
566
|
+
>
|
|
567
|
+
<img
|
|
568
|
+
src={src.faviconUrl}
|
|
569
|
+
alt=""
|
|
570
|
+
width={14}
|
|
571
|
+
height={14}
|
|
572
|
+
style={{ borderRadius: '2px', flexShrink: 0 }}
|
|
573
|
+
onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
|
|
574
|
+
/>
|
|
575
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
576
|
+
{src.domain}
|
|
577
|
+
</span>
|
|
578
|
+
</a>
|
|
579
|
+
))}
|
|
580
|
+
</div>
|
|
1634
581
|
</div>
|
|
1635
582
|
)
|
|
1636
583
|
}
|
|
@@ -1927,6 +874,7 @@ export default function ChatPanel({
|
|
|
1927
874
|
)
|
|
1928
875
|
})()}
|
|
1929
876
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
877
|
+
{renderWebSearchSources(interaction)}
|
|
1930
878
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1931
879
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
1932
880
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
@@ -1961,18 +909,13 @@ export default function ChatPanel({
|
|
|
1961
909
|
{sending && (
|
|
1962
910
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1963
911
|
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
|
|
1964
|
-
{agentMode &&
|
|
1965
|
-
<
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
</span>
|
|
1972
|
-
</div>
|
|
1973
|
-
<AgentActivityTable activityView={agentActivities} />
|
|
1974
|
-
</div>
|
|
1975
|
-
</div>
|
|
912
|
+
{agentMode && (
|
|
913
|
+
<AgentActivityDrawer
|
|
914
|
+
activityView={agentActivities}
|
|
915
|
+
isOpen={openActivityIds['live'] ?? true}
|
|
916
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, live: !(current['live'] ?? true) }))}
|
|
917
|
+
pendingApproval={!!pendingApproval}
|
|
918
|
+
/>
|
|
1976
919
|
)}
|
|
1977
920
|
{renderActiveFileChanges()}
|
|
1978
921
|
<div className="message ai-message thinking-message">
|
|
@@ -2009,7 +952,12 @@ export default function ChatPanel({
|
|
|
2009
952
|
/>
|
|
2010
953
|
</path>
|
|
2011
954
|
</svg>
|
|
2012
|
-
|
|
955
|
+
<span>
|
|
956
|
+
{activeAgentName && activeModelName
|
|
957
|
+
? `${activeAgentName} (${activeModelName}) is thinking... (${elapsedSeconds}s)`
|
|
958
|
+
: `Thinking for ${elapsedSeconds}s (Esc to cancel)`
|
|
959
|
+
}
|
|
960
|
+
</span>
|
|
2013
961
|
</div>
|
|
2014
962
|
)}
|
|
2015
963
|
</span>
|
|
@@ -2047,85 +995,17 @@ export default function ChatPanel({
|
|
|
2047
995
|
</div>
|
|
2048
996
|
)}
|
|
2049
997
|
|
|
2050
|
-
{pendingApproval && (
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
return (
|
|
2058
|
-
<div className="message ai-message" style={{ width: '100%' }}>
|
|
2059
|
-
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
2060
|
-
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
2061
|
-
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
2062
|
-
<div className="approval-card-title">{details.title}</div>
|
|
2063
|
-
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
2064
|
-
{diffText ? (
|
|
2065
|
-
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
2066
|
-
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
2067
|
-
{renderDiff(diffText)}
|
|
2068
|
-
</div>
|
|
2069
|
-
) : (
|
|
2070
|
-
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
2071
|
-
)}
|
|
2072
|
-
{isAskUser && (
|
|
2073
|
-
<div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
|
|
2074
|
-
<textarea
|
|
2075
|
-
style={{
|
|
2076
|
-
width: '100%',
|
|
2077
|
-
minHeight: '70px',
|
|
2078
|
-
padding: '10px',
|
|
2079
|
-
background: 'rgba(0, 0, 0, 0.2)',
|
|
2080
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
2081
|
-
borderRadius: '6px',
|
|
2082
|
-
color: 'var(--text-main, #f8fafc)',
|
|
2083
|
-
fontSize: '0.9rem',
|
|
2084
|
-
fontFamily: 'inherit',
|
|
2085
|
-
resize: 'vertical',
|
|
2086
|
-
outline: 'none',
|
|
2087
|
-
}}
|
|
2088
|
-
placeholder="Type your answer here..."
|
|
2089
|
-
value={askAnswer}
|
|
2090
|
-
onChange={(e) => setAskAnswer(e.target.value)}
|
|
2091
|
-
/>
|
|
2092
|
-
</div>
|
|
2093
|
-
)}
|
|
2094
|
-
</div>
|
|
2095
|
-
{isAskUser ? (
|
|
2096
|
-
<div className="approval-card-actions">
|
|
2097
|
-
<button
|
|
2098
|
-
type="button"
|
|
2099
|
-
className="approval-btn approval-btn-approve"
|
|
2100
|
-
disabled={!askAnswer.trim()}
|
|
2101
|
-
onClick={() => onApproval(true, false, askAnswer)}
|
|
2102
|
-
>
|
|
2103
|
-
Submit Answer
|
|
2104
|
-
</button>
|
|
2105
|
-
<button
|
|
2106
|
-
type="button"
|
|
2107
|
-
className="approval-btn approval-btn-cancel"
|
|
2108
|
-
onClick={() => onApproval(false)}
|
|
2109
|
-
>
|
|
2110
|
-
Decline
|
|
2111
|
-
</button>
|
|
2112
|
-
</div>
|
|
2113
|
-
) : (
|
|
2114
|
-
<div className="approval-card-actions">
|
|
2115
|
-
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
2116
|
-
<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>
|
|
2117
|
-
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
2118
|
-
</div>
|
|
2119
|
-
)}
|
|
2120
|
-
</div>
|
|
2121
|
-
</div>
|
|
2122
|
-
</div>
|
|
2123
|
-
)
|
|
2124
|
-
})()}
|
|
998
|
+
{pendingApproval && (
|
|
999
|
+
<ApprovalCard
|
|
1000
|
+
key={pendingApproval.id || JSON.stringify(pendingApproval)}
|
|
1001
|
+
pendingApproval={pendingApproval}
|
|
1002
|
+
onApproval={onApproval}
|
|
1003
|
+
/>
|
|
1004
|
+
)}
|
|
2125
1005
|
<div ref={chatEnd} />
|
|
2126
1006
|
</div>
|
|
2127
1007
|
|
|
2128
|
-
<div className=
|
|
1008
|
+
<div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
|
|
2129
1009
|
{isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
|
|
2130
1010
|
<div className="smart-context-bar">
|
|
2131
1011
|
<div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
@@ -2139,8 +1019,21 @@ export default function ChatPanel({
|
|
|
2139
1019
|
{voiceMode && (
|
|
2140
1020
|
<div className="voice-mode-bar" data-state={voiceStatus}>
|
|
2141
1021
|
<span className="voice-mode-dot" />
|
|
2142
|
-
|
|
2143
|
-
|
|
1022
|
+
{voiceTranscript ? (
|
|
1023
|
+
voiceTranscript === 'Listening to microphone...' ||
|
|
1024
|
+
voiceTranscript === 'Listening...' ||
|
|
1025
|
+
voiceTranscript === 'No speech detected' ||
|
|
1026
|
+
voiceTranscript === 'Sent audio to AI' ? (
|
|
1027
|
+
<span>{voiceTranscript}</span>
|
|
1028
|
+
) : (
|
|
1029
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }}>
|
|
1030
|
+
<span>{voiceStatusLabel}:</span>
|
|
1031
|
+
<span className="voice-mode-transcript">"{voiceTranscript}"</span>
|
|
1032
|
+
</span>
|
|
1033
|
+
)
|
|
1034
|
+
) : (
|
|
1035
|
+
<span>{voiceStatusLabel}</span>
|
|
1036
|
+
)}
|
|
2144
1037
|
</div>
|
|
2145
1038
|
)}
|
|
2146
1039
|
|
|
@@ -2259,6 +1152,11 @@ export default function ChatPanel({
|
|
|
2259
1152
|
else if (provider === 'huggingface') displayName = 'HF'
|
|
2260
1153
|
else if (provider === 'local_openai') displayName = 'Local'
|
|
2261
1154
|
else if (provider === 'ollama') displayName = 'Ollama'
|
|
1155
|
+
else if (provider.startsWith('custom:')) {
|
|
1156
|
+
const id = provider.replace(/^custom:/, '')
|
|
1157
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
1158
|
+
displayName = cp?.displayName || id
|
|
1159
|
+
}
|
|
2262
1160
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
2263
1161
|
})}
|
|
2264
1162
|
</select>
|
|
@@ -2312,12 +1210,27 @@ export default function ChatPanel({
|
|
|
2312
1210
|
</svg>
|
|
2313
1211
|
)}
|
|
2314
1212
|
</button>
|
|
2315
|
-
|
|
2316
|
-
<
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
1213
|
+
{sending ? (
|
|
1214
|
+
<button
|
|
1215
|
+
id="send-btn"
|
|
1216
|
+
className="stop-btn"
|
|
1217
|
+
type="button"
|
|
1218
|
+
onClick={onCancelMessage}
|
|
1219
|
+
title="Stop generating"
|
|
1220
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1221
|
+
>
|
|
1222
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
1223
|
+
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
|
|
1224
|
+
</svg>
|
|
1225
|
+
</button>
|
|
1226
|
+
) : (
|
|
1227
|
+
<button id="send-btn" type="submit" disabled={!canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1228
|
+
<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">
|
|
1229
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
1230
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
1231
|
+
</svg>
|
|
1232
|
+
</button>
|
|
1233
|
+
)}
|
|
2321
1234
|
</form>
|
|
2322
1235
|
</div>
|
|
2323
1236
|
<p className="input-disclaimer">
|