@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 DragEvent, type FormEvent, type KeyboardEvent, type RefObject } 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[]
|
|
@@ -391,7 +58,6 @@ interface ChatPanelProps {
|
|
|
391
58
|
onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
|
|
392
59
|
onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
|
|
393
60
|
onPasteImage: (clipboardData: DataTransfer) => boolean
|
|
394
|
-
onReadClipboardImage: () => Promise<boolean>
|
|
395
61
|
onSetMessage: (message: string) => void
|
|
396
62
|
onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
|
|
397
63
|
onRemoveImage: (idx: number) => void
|
|
@@ -405,587 +71,9 @@ interface ChatPanelProps {
|
|
|
405
71
|
onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
|
|
406
72
|
settingsConfig: any
|
|
407
73
|
onSetModel: (model: string) => void
|
|
74
|
+
onCancelMessage: () => void
|
|
408
75
|
}
|
|
409
76
|
|
|
410
|
-
function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
|
|
411
|
-
const [copied, setCopied] = useState(false)
|
|
412
|
-
|
|
413
|
-
const handleCopy = async () => {
|
|
414
|
-
try {
|
|
415
|
-
await navigator.clipboard.writeText(code)
|
|
416
|
-
setCopied(true)
|
|
417
|
-
setTimeout(() => setCopied(false), 2000)
|
|
418
|
-
} catch (err) {
|
|
419
|
-
console.error('Failed to copy code: ', err)
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const handleDownload = () => {
|
|
424
|
-
try {
|
|
425
|
-
const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
|
|
426
|
-
const url = URL.createObjectURL(blob)
|
|
427
|
-
const link = document.createElement('a')
|
|
428
|
-
link.href = url
|
|
429
|
-
link.download = `code.${language === 'plaintext' ? 'txt' : language}`
|
|
430
|
-
link.click()
|
|
431
|
-
URL.revokeObjectURL(url)
|
|
432
|
-
} catch (err) {
|
|
433
|
-
console.error('Failed to download code: ', err)
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
|
|
438
|
-
|
|
439
|
-
return (
|
|
440
|
-
<div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
|
|
441
|
-
<div className="chat-code-block-header">
|
|
442
|
-
<span className="chat-code-block-lang">{displayLang}</span>
|
|
443
|
-
<div className="chat-code-block-actions">
|
|
444
|
-
<button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
|
|
445
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
446
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
447
|
-
<polyline points="7 10 12 15 17 10" />
|
|
448
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
449
|
-
</svg>
|
|
450
|
-
</button>
|
|
451
|
-
<button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
|
|
452
|
-
{copied ? (
|
|
453
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
|
454
|
-
<polyline points="20 6 9 17 4 12" />
|
|
455
|
-
</svg>
|
|
456
|
-
) : (
|
|
457
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
458
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
459
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
460
|
-
</svg>
|
|
461
|
-
)}
|
|
462
|
-
</button>
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
<pre className="chat-code-block-body">
|
|
466
|
-
<code>{code}</code>
|
|
467
|
-
</pre>
|
|
468
|
-
</div>
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const isTableLine = (line: string): boolean => {
|
|
473
|
-
const trimmed = line.trim()
|
|
474
|
-
return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function renderFormattedMessage(text: string) {
|
|
478
|
-
const displayText = readableAssistantText(text)
|
|
479
|
-
if (!displayText) return null
|
|
480
|
-
|
|
481
|
-
const formatInline = (str: string) => {
|
|
482
|
-
const codeParts = str.split(/`([\s\S]*?)`/g)
|
|
483
|
-
return codeParts.map((codePart, codeIndex) => {
|
|
484
|
-
if (codeIndex % 2 === 1) {
|
|
485
|
-
return (
|
|
486
|
-
<code key={`code-${codeIndex}`} className="chat-inline-code">
|
|
487
|
-
{codePart}
|
|
488
|
-
</code>
|
|
489
|
-
)
|
|
490
|
-
}
|
|
491
|
-
const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
|
|
492
|
-
return boldParts.map((boldPart, boldIndex) => {
|
|
493
|
-
if (boldIndex % 2 === 1) {
|
|
494
|
-
return (
|
|
495
|
-
<strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
|
|
496
|
-
{boldPart}
|
|
497
|
-
</strong>
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
|
|
501
|
-
return mentionParts.map((mentionPart, mentionIndex) => {
|
|
502
|
-
if (mentionIndex % 2 === 1) {
|
|
503
|
-
return (
|
|
504
|
-
<span key={`mention-${mentionIndex}`} className="chat-mention">
|
|
505
|
-
{mentionPart}
|
|
506
|
-
</span>
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
return mentionPart
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
})
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const renderHtmlTable = (tableLines: string[], key: string) => {
|
|
516
|
-
const rows: string[][] = []
|
|
517
|
-
for (const line of tableLines) {
|
|
518
|
-
const trimmed = line.trim()
|
|
519
|
-
if (!trimmed) continue
|
|
520
|
-
const content = trimmed.slice(1, -1)
|
|
521
|
-
const contentEscaped = content.replace(/\\\|/g, '\u0000')
|
|
522
|
-
const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
|
|
523
|
-
rows.push(cols)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (rows.length === 0) return null
|
|
527
|
-
|
|
528
|
-
let hasSeparator = false
|
|
529
|
-
let separatorIdx = -1
|
|
530
|
-
for (let i = 0; i < rows.length; i++) {
|
|
531
|
-
const row = rows[i]
|
|
532
|
-
if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
|
|
533
|
-
hasSeparator = true
|
|
534
|
-
separatorIdx = i
|
|
535
|
-
break
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
let headerRow: string[] | null = null
|
|
540
|
-
const dataRows: string[][] = []
|
|
541
|
-
|
|
542
|
-
if (hasSeparator) {
|
|
543
|
-
if (separatorIdx > 0) {
|
|
544
|
-
headerRow = rows[separatorIdx - 1]
|
|
545
|
-
for (let i = 0; i < rows.length; i++) {
|
|
546
|
-
if (i !== separatorIdx && i !== separatorIdx - 1) {
|
|
547
|
-
dataRows.push(rows[i])
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
} else {
|
|
551
|
-
for (let i = 0; i < rows.length; i++) {
|
|
552
|
-
if (i !== separatorIdx) {
|
|
553
|
-
dataRows.push(rows[i])
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
} else {
|
|
558
|
-
headerRow = rows[0]
|
|
559
|
-
for (let i = 1; i < rows.length; i++) {
|
|
560
|
-
dataRows.push(rows[i])
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return (
|
|
565
|
-
<div key={key} className="chat-table-container" style={{
|
|
566
|
-
overflowX: 'auto',
|
|
567
|
-
margin: '14px 0',
|
|
568
|
-
width: '100%',
|
|
569
|
-
borderRadius: '8px',
|
|
570
|
-
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
571
|
-
background: 'rgba(30, 41, 59, 0.35)',
|
|
572
|
-
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
573
|
-
}}>
|
|
574
|
-
<table className="chat-table" style={{
|
|
575
|
-
width: '100%',
|
|
576
|
-
borderCollapse: 'collapse',
|
|
577
|
-
fontSize: '0.86rem',
|
|
578
|
-
textAlign: 'left',
|
|
579
|
-
lineHeight: '1.5',
|
|
580
|
-
}}>
|
|
581
|
-
{headerRow && (
|
|
582
|
-
<thead>
|
|
583
|
-
<tr style={{
|
|
584
|
-
background: 'rgba(255, 255, 255, 0.04)',
|
|
585
|
-
borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
|
|
586
|
-
}}>
|
|
587
|
-
{headerRow.map((col, idx) => (
|
|
588
|
-
<th key={`th-${idx}`} style={{
|
|
589
|
-
padding: '12px 16px',
|
|
590
|
-
fontWeight: 700,
|
|
591
|
-
color: 'var(--accent, #38bdf8)',
|
|
592
|
-
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
593
|
-
}}>
|
|
594
|
-
{formatInline(col)}
|
|
595
|
-
</th>
|
|
596
|
-
))}
|
|
597
|
-
</tr>
|
|
598
|
-
</thead>
|
|
599
|
-
)}
|
|
600
|
-
<tbody>
|
|
601
|
-
{dataRows.map((row, rIdx) => (
|
|
602
|
-
<tr key={`tr-${rIdx}`} style={{
|
|
603
|
-
background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
|
|
604
|
-
borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
|
|
605
|
-
}}>
|
|
606
|
-
{row.map((col, cIdx) => (
|
|
607
|
-
<td key={`td-${cIdx}`} style={{
|
|
608
|
-
padding: '12px 16px',
|
|
609
|
-
color: 'var(--text-main, #e2e8f0)',
|
|
610
|
-
}}>
|
|
611
|
-
{formatInline(col)}
|
|
612
|
-
</td>
|
|
613
|
-
))}
|
|
614
|
-
</tr>
|
|
615
|
-
))}
|
|
616
|
-
</tbody>
|
|
617
|
-
</table>
|
|
618
|
-
</div>
|
|
619
|
-
)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const lines = displayText.split('\n')
|
|
623
|
-
const items: any[] = []
|
|
624
|
-
|
|
625
|
-
let inCodeBlock = false
|
|
626
|
-
let codeBlockLang = ''
|
|
627
|
-
let codeBlockLines: string[] = []
|
|
628
|
-
|
|
629
|
-
let inTable = false
|
|
630
|
-
let tableLines: string[] = []
|
|
631
|
-
|
|
632
|
-
const flushTable = (index: number) => {
|
|
633
|
-
if (tableLines.length > 0) {
|
|
634
|
-
items.push(renderHtmlTable(tableLines, `table-${index}`))
|
|
635
|
-
tableLines = []
|
|
636
|
-
inTable = false
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
for (let i = 0; i < lines.length; i++) {
|
|
641
|
-
const line = lines[i]
|
|
642
|
-
|
|
643
|
-
if (line.trim().startsWith('```')) {
|
|
644
|
-
if (inTable) {
|
|
645
|
-
flushTable(i)
|
|
646
|
-
}
|
|
647
|
-
if (inCodeBlock) {
|
|
648
|
-
const codeText = codeBlockLines.join('\n')
|
|
649
|
-
items.push(
|
|
650
|
-
<ChatCodeBlock
|
|
651
|
-
key={`code-block-${i}`}
|
|
652
|
-
code={codeText}
|
|
653
|
-
language={codeBlockLang}
|
|
654
|
-
/>
|
|
655
|
-
)
|
|
656
|
-
inCodeBlock = false
|
|
657
|
-
codeBlockLines = []
|
|
658
|
-
} else {
|
|
659
|
-
inCodeBlock = true
|
|
660
|
-
codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
|
|
661
|
-
}
|
|
662
|
-
continue
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (inCodeBlock) {
|
|
666
|
-
codeBlockLines.push(line)
|
|
667
|
-
continue
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (isTableLine(line)) {
|
|
671
|
-
inTable = true
|
|
672
|
-
tableLines.push(line)
|
|
673
|
-
} else {
|
|
674
|
-
if (inTable) {
|
|
675
|
-
flushTable(i)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
|
679
|
-
if (headerMatch) {
|
|
680
|
-
const level = headerMatch[1].length
|
|
681
|
-
const content = headerMatch[2]
|
|
682
|
-
|
|
683
|
-
const style = {
|
|
684
|
-
fontWeight: 'bold',
|
|
685
|
-
display: 'block',
|
|
686
|
-
marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
|
|
687
|
-
marginBottom: '6px',
|
|
688
|
-
fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
|
|
689
|
-
color: 'var(--text-main)',
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
items.push(
|
|
693
|
-
<span key={`line-${i}`} style={style}>
|
|
694
|
-
{formatInline(content)}
|
|
695
|
-
</span>
|
|
696
|
-
)
|
|
697
|
-
} else {
|
|
698
|
-
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
|
|
699
|
-
if (listMatch) {
|
|
700
|
-
const indent = listMatch[1]
|
|
701
|
-
const content = listMatch[3]
|
|
702
|
-
items.push(
|
|
703
|
-
<Fragment key={`line-${i}`}>
|
|
704
|
-
{indent}• {formatInline(content)}
|
|
705
|
-
{i < lines.length - 1 && '\n'}
|
|
706
|
-
</Fragment>
|
|
707
|
-
)
|
|
708
|
-
} else {
|
|
709
|
-
items.push(
|
|
710
|
-
<Fragment key={`line-${i}`}>
|
|
711
|
-
{formatInline(line)}
|
|
712
|
-
{i < lines.length - 1 && '\n'}
|
|
713
|
-
</Fragment>
|
|
714
|
-
)
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (inTable) {
|
|
721
|
-
flushTable(lines.length)
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
725
|
-
const codeText = codeBlockLines.join('\n')
|
|
726
|
-
items.push(
|
|
727
|
-
<ChatCodeBlock
|
|
728
|
-
key={`code-block-end`}
|
|
729
|
-
code={codeText}
|
|
730
|
-
language={codeBlockLang}
|
|
731
|
-
/>
|
|
732
|
-
)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return items
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const THINKING_LABELS = {
|
|
739
|
-
live: 'Thinking…',
|
|
740
|
-
completed: (count: number) => `Thinking process (${count} steps)`,
|
|
741
|
-
step: (index: number) => `Step ${index}`,
|
|
742
|
-
emptyHint: 'Model didn\'t send thinking steps this time',
|
|
743
|
-
} as const
|
|
744
|
-
|
|
745
|
-
function ThinkingBlock({
|
|
746
|
-
thoughts,
|
|
747
|
-
isLive = false,
|
|
748
|
-
blockKey,
|
|
749
|
-
expanded,
|
|
750
|
-
onExpandedChange,
|
|
751
|
-
showEmptyHint = false,
|
|
752
|
-
}: {
|
|
753
|
-
thoughts: string[]
|
|
754
|
-
isLive?: boolean
|
|
755
|
-
blockKey: string
|
|
756
|
-
expanded?: boolean
|
|
757
|
-
onExpandedChange?: (key: string, open: boolean) => void
|
|
758
|
-
showEmptyHint?: boolean
|
|
759
|
-
}) {
|
|
760
|
-
const contentRef = useRef<HTMLDivElement>(null)
|
|
761
|
-
const isControlled = expanded !== undefined && Boolean(onExpandedChange)
|
|
762
|
-
const [localOpen, setLocalOpen] = useState(isLive)
|
|
763
|
-
const isOpen = isControlled ? Boolean(expanded) : localOpen
|
|
764
|
-
|
|
765
|
-
const setOpen = (open: boolean) => {
|
|
766
|
-
if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
|
|
767
|
-
else setLocalOpen(open)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
useEffect(() => {
|
|
771
|
-
if (isLive && !isControlled) setLocalOpen(true)
|
|
772
|
-
}, [isLive, isControlled])
|
|
773
|
-
|
|
774
|
-
useEffect(() => {
|
|
775
|
-
if (!isLive || !isOpen || !contentRef.current) return
|
|
776
|
-
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
777
|
-
}, [thoughts, isLive, isOpen])
|
|
778
|
-
|
|
779
|
-
if (thoughts.length === 0 && !showEmptyHint) return null
|
|
780
|
-
|
|
781
|
-
if (thoughts.length === 0) {
|
|
782
|
-
return (
|
|
783
|
-
<div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
|
|
784
|
-
<span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
|
|
785
|
-
</div>
|
|
786
|
-
)
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
return (
|
|
790
|
-
<div className={`thinking-block${isLive ? ' is-live' : ''}`}>
|
|
791
|
-
<button
|
|
792
|
-
type="button"
|
|
793
|
-
className="thinking-block-header"
|
|
794
|
-
onClick={() => setOpen(!isOpen)}
|
|
795
|
-
aria-expanded={isOpen}
|
|
796
|
-
>
|
|
797
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
|
|
798
|
-
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
799
|
-
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
800
|
-
<circle cx="12" cy="12" r="10" />
|
|
801
|
-
</svg>
|
|
802
|
-
<span className="thinking-block-label">
|
|
803
|
-
{isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
|
|
804
|
-
</span>
|
|
805
|
-
{isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
|
|
806
|
-
<span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
|
|
807
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
808
|
-
<polyline points="9 18 15 12 9 6" />
|
|
809
|
-
</svg>
|
|
810
|
-
</span>
|
|
811
|
-
</button>
|
|
812
|
-
{isOpen && (
|
|
813
|
-
<div className="thinking-block-body">
|
|
814
|
-
<div className="thinking-block-content" ref={contentRef}>
|
|
815
|
-
{thoughts.map((thought, index) => (
|
|
816
|
-
<div className="thinking-block-step" key={`${blockKey}-${index}`}>
|
|
817
|
-
{thoughts.length > 1 && (
|
|
818
|
-
<div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
|
|
819
|
-
)}
|
|
820
|
-
<div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
|
|
821
|
-
</div>
|
|
822
|
-
))}
|
|
823
|
-
</div>
|
|
824
|
-
</div>
|
|
825
|
-
)}
|
|
826
|
-
</div>
|
|
827
|
-
)
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function readableAssistantText(text: string) {
|
|
831
|
-
if (typeof text !== 'string') return ''
|
|
832
|
-
const trimmed = text.trim()
|
|
833
|
-
if (!trimmed.startsWith('{')) return text
|
|
834
|
-
try {
|
|
835
|
-
const value = JSON.parse(trimmed)
|
|
836
|
-
if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
|
|
837
|
-
return value.input.summary
|
|
838
|
-
}
|
|
839
|
-
if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
|
|
840
|
-
return value.finish.summary
|
|
841
|
-
}
|
|
842
|
-
} catch {
|
|
843
|
-
return text
|
|
844
|
-
}
|
|
845
|
-
return text
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function renderSpeakerIcon(isSpeaking: boolean) {
|
|
849
|
-
if (isSpeaking) {
|
|
850
|
-
return (
|
|
851
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
852
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
853
|
-
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
854
|
-
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
855
|
-
</svg>
|
|
856
|
-
)
|
|
857
|
-
}
|
|
858
|
-
return (
|
|
859
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
|
|
860
|
-
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
861
|
-
</svg>
|
|
862
|
-
)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function cleanSpeechText(text: string) {
|
|
866
|
-
return readableAssistantText(text)
|
|
867
|
-
.replace(/\*\*([\s\S]*?)\*\*/g, '$1')
|
|
868
|
-
.replace(/[*_`#]/g, '')
|
|
869
|
-
.trim()
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function numericSetting(value: unknown, fallback: number) {
|
|
873
|
-
const numeric = Number(value)
|
|
874
|
-
return Number.isFinite(numeric) ? numeric : fallback
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
interface DiffHunk {
|
|
878
|
-
oldText: string
|
|
879
|
-
newText: string
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
interface FileChange {
|
|
883
|
-
path: string
|
|
884
|
-
created: boolean
|
|
885
|
-
additions: number
|
|
886
|
-
deletions: number
|
|
887
|
-
hunks: DiffHunk[]
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
|
|
891
|
-
const changes = new Map<string, FileChange>()
|
|
892
|
-
let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
|
|
893
|
-
|
|
894
|
-
for (const event of progress || []) {
|
|
895
|
-
if (event.type === 'ToolStart') {
|
|
896
|
-
if (event.data.action === 'apply_patch') {
|
|
897
|
-
const patch = (event.data.input as any)?.patch
|
|
898
|
-
if (patch && typeof patch.path === 'string') {
|
|
899
|
-
let additions = 0
|
|
900
|
-
let deletions = 0
|
|
901
|
-
const hunksList: DiffHunk[] = []
|
|
902
|
-
const hunks = patch.hunks
|
|
903
|
-
if (Array.isArray(hunks)) {
|
|
904
|
-
for (const hunk of hunks) {
|
|
905
|
-
const oldText = hunk?.oldText || ''
|
|
906
|
-
const newText = hunk?.newText || ''
|
|
907
|
-
const oldLines = oldText ? oldText.split('\n').length : 0
|
|
908
|
-
const newLines = newText ? newText.split('\n').length : 0
|
|
909
|
-
deletions += oldLines
|
|
910
|
-
additions += newLines
|
|
911
|
-
hunksList.push({ oldText, newText })
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
activeEdit = {
|
|
915
|
-
action: 'apply_patch',
|
|
916
|
-
path: patch.path,
|
|
917
|
-
created: false,
|
|
918
|
-
additions,
|
|
919
|
-
deletions,
|
|
920
|
-
hunks: hunksList
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
} else if (event.data.action === 'write_file') {
|
|
924
|
-
const path = (event.data.input as any)?.path
|
|
925
|
-
const fileContent = (event.data.input as any)?.file_content || ''
|
|
926
|
-
if (typeof path === 'string') {
|
|
927
|
-
const additions = fileContent ? fileContent.split('\n').length : 0
|
|
928
|
-
activeEdit = {
|
|
929
|
-
action: 'write_file',
|
|
930
|
-
path,
|
|
931
|
-
created: true,
|
|
932
|
-
additions,
|
|
933
|
-
deletions: 0,
|
|
934
|
-
hunks: [{ oldText: '', newText: fileContent }]
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
} else {
|
|
938
|
-
activeEdit = null
|
|
939
|
-
}
|
|
940
|
-
} else if (event.type === 'ToolEnd') {
|
|
941
|
-
if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
|
|
942
|
-
const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
|
|
943
|
-
if (!isError) {
|
|
944
|
-
try {
|
|
945
|
-
const applied = JSON.parse(event.data.result)
|
|
946
|
-
const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
|
|
947
|
-
|
|
948
|
-
for (const path of appliedPaths) {
|
|
949
|
-
const existing = changes.get(path)
|
|
950
|
-
if (existing) {
|
|
951
|
-
existing.additions += activeEdit.additions
|
|
952
|
-
existing.deletions += activeEdit.deletions
|
|
953
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
954
|
-
} else {
|
|
955
|
-
changes.set(path, {
|
|
956
|
-
path,
|
|
957
|
-
created: activeEdit.created,
|
|
958
|
-
additions: activeEdit.additions,
|
|
959
|
-
deletions: activeEdit.deletions,
|
|
960
|
-
hunks: [...activeEdit.hunks]
|
|
961
|
-
})
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
} catch (e) {
|
|
965
|
-
const path = activeEdit.path
|
|
966
|
-
const existing = changes.get(path)
|
|
967
|
-
if (existing) {
|
|
968
|
-
existing.additions += activeEdit.additions
|
|
969
|
-
existing.deletions += activeEdit.deletions
|
|
970
|
-
existing.hunks.push(...activeEdit.hunks)
|
|
971
|
-
} else {
|
|
972
|
-
changes.set(path, {
|
|
973
|
-
path,
|
|
974
|
-
created: activeEdit.created,
|
|
975
|
-
additions: activeEdit.additions,
|
|
976
|
-
deletions: activeEdit.deletions,
|
|
977
|
-
hunks: [...activeEdit.hunks]
|
|
978
|
-
})
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
activeEdit = null
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
return Array.from(changes.values())
|
|
988
|
-
}
|
|
989
77
|
|
|
990
78
|
export default function ChatPanel({
|
|
991
79
|
interactions,
|
|
@@ -1012,7 +100,6 @@ export default function ChatPanel({
|
|
|
1012
100
|
onSelectImage,
|
|
1013
101
|
onSelectDocument,
|
|
1014
102
|
onPasteImage,
|
|
1015
|
-
onReadClipboardImage,
|
|
1016
103
|
onSetMessage,
|
|
1017
104
|
onSendVoiceMessage,
|
|
1018
105
|
onRemoveImage,
|
|
@@ -1026,20 +113,23 @@ export default function ChatPanel({
|
|
|
1026
113
|
onApproval,
|
|
1027
114
|
settingsConfig,
|
|
1028
115
|
onSetModel,
|
|
116
|
+
onCancelMessage,
|
|
1029
117
|
}: ChatPanelProps) {
|
|
1030
118
|
const agentActivities = activitiesFrom(agentProgress)
|
|
1031
119
|
const activeFallbackNotice = fallbackNotice(streamedResponse)
|
|
120
|
+
const lastThinkingProgress = [...agentProgress].reverse().find(p => p.type === 'Thinking')
|
|
121
|
+
let activeAgentName: string | null = null
|
|
122
|
+
let activeModelName: string | null = null
|
|
123
|
+
if (lastThinkingProgress && lastThinkingProgress.type === 'Thinking') {
|
|
124
|
+
activeAgentName = (lastThinkingProgress.data as any).agent_name || null
|
|
125
|
+
activeModelName = (lastThinkingProgress.data as any).model_name || null
|
|
126
|
+
}
|
|
1032
127
|
const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
|
|
1033
128
|
const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
|
|
1034
129
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
1035
130
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
1036
131
|
const [elapsedSeconds, setElapsedSeconds] = useState(0)
|
|
1037
132
|
const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
|
|
1038
|
-
const [askAnswer, setAskAnswer] = useState('')
|
|
1039
|
-
|
|
1040
|
-
useEffect(() => {
|
|
1041
|
-
setAskAnswer('')
|
|
1042
|
-
}, [pendingApproval])
|
|
1043
133
|
|
|
1044
134
|
useEffect(() => {
|
|
1045
135
|
const fetchOllamaModels = async () => {
|
|
@@ -1079,27 +169,37 @@ export default function ChatPanel({
|
|
|
1079
169
|
|
|
1080
170
|
return () => clearInterval(timer)
|
|
1081
171
|
}, [sending])
|
|
1082
|
-
const [isRecording, setIsRecording] = useState(false)
|
|
1083
|
-
const [voiceMode, setVoiceMode] = useState(false)
|
|
1084
|
-
const [voiceTranscript, setVoiceTranscript] = useState('')
|
|
1085
|
-
const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
|
|
1086
172
|
const [speakingText, setSpeakingText] = useState<string | null>(null)
|
|
1087
173
|
const toolMenuRef = useRef<HTMLDivElement | null>(null)
|
|
1088
|
-
const recognitionRef = useRef<SpeechRecognition | null>(null)
|
|
1089
|
-
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
|
1090
|
-
const mediaStreamRef = useRef<MediaStream | null>(null)
|
|
1091
|
-
const audioContextRef = useRef<AudioContext | null>(null)
|
|
1092
|
-
const vadTimerRef = useRef<number | null>(null)
|
|
1093
174
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
1094
175
|
const speechRunRef = useRef(0)
|
|
1095
176
|
const historyReadyRef = useRef(false)
|
|
1096
177
|
const submittedDuringSessionRef = useRef(false)
|
|
1097
178
|
const lastAutoSpokenIdRef = useRef<number | string | null>(null)
|
|
1098
|
-
const voiceModeRef = useRef(false)
|
|
1099
|
-
const sendingRef = useRef(false)
|
|
1100
|
-
const voiceAwaitingResponseRef = useRef(false)
|
|
1101
179
|
const speakingRef = useRef<string | null>(null)
|
|
1102
|
-
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
isRecording,
|
|
183
|
+
voiceMode,
|
|
184
|
+
setVoiceMode,
|
|
185
|
+
voiceTranscript,
|
|
186
|
+
setVoiceTranscript,
|
|
187
|
+
voiceAwaitingResponse,
|
|
188
|
+
voiceAwaitingResponseRef,
|
|
189
|
+
voiceModeRef,
|
|
190
|
+
startRecognition,
|
|
191
|
+
stopRecognition,
|
|
192
|
+
scheduleVoiceListen,
|
|
193
|
+
clearRestartTimer
|
|
194
|
+
} = useSpeechToText({
|
|
195
|
+
language: settingsConfig?.language,
|
|
196
|
+
message,
|
|
197
|
+
sending,
|
|
198
|
+
isSpeaking: Boolean(speakingText),
|
|
199
|
+
onSendVoiceMessage,
|
|
200
|
+
onSetMessage: (val) => onSetMessage(val)
|
|
201
|
+
})
|
|
202
|
+
|
|
1103
203
|
const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
|
|
1104
204
|
const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
|
|
1105
205
|
const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
|
|
@@ -1124,8 +224,13 @@ export default function ChatPanel({
|
|
|
1124
224
|
case 'ollama':
|
|
1125
225
|
return dynamicOllamaModels
|
|
1126
226
|
default:
|
|
227
|
+
if (provider.startsWith('custom:')) {
|
|
228
|
+
const id = provider.replace(/^custom:/, '')
|
|
229
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
230
|
+
return cp?.models.map(m => m.modelId) ?? []
|
|
231
|
+
}
|
|
1127
232
|
return []
|
|
1128
|
-
|
|
233
|
+
}
|
|
1129
234
|
}
|
|
1130
235
|
|
|
1131
236
|
const activeProvider = status?.activeProvider ?? ''
|
|
@@ -1151,6 +256,11 @@ export default function ChatPanel({
|
|
|
1151
256
|
case 'ollama':
|
|
1152
257
|
return settingsConfig.ollamaModel
|
|
1153
258
|
default:
|
|
259
|
+
if (provider.startsWith('custom:')) {
|
|
260
|
+
const id = provider.replace(/^custom:/, '')
|
|
261
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
262
|
+
return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
|
|
263
|
+
}
|
|
1154
264
|
return ''
|
|
1155
265
|
}
|
|
1156
266
|
}
|
|
@@ -1166,39 +276,7 @@ export default function ChatPanel({
|
|
|
1166
276
|
const handleInputKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
1167
277
|
submitOnEnter(event)
|
|
1168
278
|
}
|
|
1169
|
-
|
|
1170
|
-
if (restartTimerRef.current === null) return
|
|
1171
|
-
window.clearTimeout(restartTimerRef.current)
|
|
1172
|
-
restartTimerRef.current = null
|
|
1173
|
-
}
|
|
1174
|
-
const clearVadTimer = () => {
|
|
1175
|
-
if (vadTimerRef.current === null) return
|
|
1176
|
-
window.clearInterval(vadTimerRef.current)
|
|
1177
|
-
vadTimerRef.current = null
|
|
1178
|
-
}
|
|
1179
|
-
const stopRecognition = () => {
|
|
1180
|
-
clearRestartTimer()
|
|
1181
|
-
recognitionRef.current?.stop()
|
|
1182
|
-
recognitionRef.current = null
|
|
1183
|
-
clearVadTimer()
|
|
1184
|
-
if (mediaRecorderRef.current?.state === 'recording') {
|
|
1185
|
-
mediaRecorderRef.current.stop()
|
|
1186
|
-
}
|
|
1187
|
-
mediaRecorderRef.current = null
|
|
1188
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1189
|
-
mediaStreamRef.current = null
|
|
1190
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1191
|
-
audioContextRef.current = null
|
|
1192
|
-
setIsRecording(false)
|
|
1193
|
-
}
|
|
1194
|
-
const scheduleVoiceListen = (delayMs = 350) => {
|
|
1195
|
-
clearRestartTimer()
|
|
1196
|
-
if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1197
|
-
restartTimerRef.current = window.setTimeout(() => {
|
|
1198
|
-
restartTimerRef.current = null
|
|
1199
|
-
startRecognition(true)
|
|
1200
|
-
}, delayMs)
|
|
1201
|
-
}
|
|
279
|
+
|
|
1202
280
|
const cancelSpeech = () => {
|
|
1203
281
|
speechRunRef.current += 1
|
|
1204
282
|
audioRef.current?.pause()
|
|
@@ -1220,7 +298,11 @@ export default function ChatPanel({
|
|
|
1220
298
|
utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
|
|
1221
299
|
utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
|
|
1222
300
|
utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
|
|
1223
|
-
const voice = window.speechSynthesis.getVoices().find((item) =>
|
|
301
|
+
const voice = window.speechSynthesis.getVoices().find((item) => {
|
|
302
|
+
const lang = item.lang.toLowerCase()
|
|
303
|
+
const target = hasThai ? 'th' : 'en'
|
|
304
|
+
return lang.startsWith(target) || lang.includes(target)
|
|
305
|
+
})
|
|
1224
306
|
if (voice) utterance.voice = voice
|
|
1225
307
|
const finishSpeech = () => {
|
|
1226
308
|
setSpeakingText((current) => (current === displayText ? null : current))
|
|
@@ -1275,183 +357,16 @@ export default function ChatPanel({
|
|
|
1275
357
|
}
|
|
1276
358
|
speakNative(speechText, text)
|
|
1277
359
|
}
|
|
1278
|
-
const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
|
|
1279
|
-
const reader = new FileReader()
|
|
1280
|
-
reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
|
|
1281
|
-
reader.onload = () => resolve(String(reader.result))
|
|
1282
|
-
reader.readAsDataURL(blob)
|
|
1283
|
-
})
|
|
1284
|
-
const preferredAudioMimeType = () => {
|
|
1285
|
-
const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
|
|
1286
|
-
return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
|
|
1287
|
-
}
|
|
1288
|
-
const startAudioRecording = async (autoSend: boolean) => {
|
|
1289
|
-
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
1290
|
-
alert('Sorry, this system cannot access the microphone or record audio in this WebView')
|
|
1291
|
-
voiceModeRef.current = false
|
|
1292
|
-
setVoiceMode(false)
|
|
1293
|
-
return
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
try {
|
|
1297
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
1298
|
-
const mimeType = preferredAudioMimeType()
|
|
1299
|
-
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
|
|
1300
|
-
const chunks: Blob[] = []
|
|
1301
|
-
let heardVoice = false
|
|
1302
|
-
let quietSince = 0
|
|
1303
|
-
const startedAt = Date.now()
|
|
1304
|
-
const audioContext = new AudioContext()
|
|
1305
|
-
const source = audioContext.createMediaStreamSource(stream)
|
|
1306
|
-
const analyser = audioContext.createAnalyser()
|
|
1307
|
-
const samples = new Uint8Array(analyser.fftSize)
|
|
1308
|
-
source.connect(analyser)
|
|
1309
|
-
|
|
1310
|
-
mediaStreamRef.current = stream
|
|
1311
|
-
mediaRecorderRef.current = recorder
|
|
1312
|
-
audioContextRef.current = audioContext
|
|
1313
|
-
setVoiceTranscript('Listening to microphone...')
|
|
1314
|
-
|
|
1315
|
-
recorder.ondataavailable = (event) => {
|
|
1316
|
-
if (event.data.size > 0) chunks.push(event.data)
|
|
1317
|
-
}
|
|
1318
|
-
recorder.onstop = async () => {
|
|
1319
|
-
clearVadTimer()
|
|
1320
|
-
mediaRecorderRef.current = null
|
|
1321
|
-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
|
|
1322
|
-
mediaStreamRef.current = null
|
|
1323
|
-
audioContextRef.current?.close().catch(() => {})
|
|
1324
|
-
audioContextRef.current = null
|
|
1325
|
-
setIsRecording(false)
|
|
1326
|
-
if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
|
|
1327
|
-
if (!heardVoice) {
|
|
1328
|
-
setVoiceTranscript('No speech detected')
|
|
1329
|
-
scheduleVoiceListen(700)
|
|
1330
|
-
return
|
|
1331
|
-
}
|
|
1332
|
-
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
|
|
1333
|
-
const audioDataUri = await blobToDataUri(blob)
|
|
1334
|
-
setVoiceTranscript('Sent audio to AI')
|
|
1335
|
-
voiceAwaitingResponseRef.current = true
|
|
1336
|
-
setVoiceAwaitingResponse(true)
|
|
1337
|
-
onSendVoiceMessage('', audioDataUri)
|
|
1338
|
-
.catch((error) => console.error('Voice audio message failed', error))
|
|
1339
|
-
.finally(() => {
|
|
1340
|
-
voiceAwaitingResponseRef.current = false
|
|
1341
|
-
setVoiceAwaitingResponse(false)
|
|
1342
|
-
scheduleVoiceListen()
|
|
1343
|
-
})
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
recorder.start()
|
|
1347
|
-
setIsRecording(true)
|
|
1348
|
-
vadTimerRef.current = window.setInterval(() => {
|
|
1349
|
-
analyser.getByteTimeDomainData(samples)
|
|
1350
|
-
let peak = 0
|
|
1351
|
-
for (const sample of samples) {
|
|
1352
|
-
peak = Math.max(peak, Math.abs(sample - 128))
|
|
1353
|
-
}
|
|
1354
|
-
const now = Date.now()
|
|
1355
|
-
if (peak > 12) {
|
|
1356
|
-
heardVoice = true
|
|
1357
|
-
quietSince = 0
|
|
1358
|
-
setVoiceTranscript('Listening...')
|
|
1359
|
-
} else if (heardVoice) {
|
|
1360
|
-
quietSince = quietSince || now
|
|
1361
|
-
}
|
|
1362
|
-
const silenceElapsed = quietSince ? now - quietSince : 0
|
|
1363
|
-
const totalElapsed = now - startedAt
|
|
1364
|
-
if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
|
|
1365
|
-
recorder.stop()
|
|
1366
|
-
}
|
|
1367
|
-
}, 120)
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
console.error('Failed to record microphone audio', error)
|
|
1370
|
-
setIsRecording(false)
|
|
1371
|
-
voiceModeRef.current = false
|
|
1372
|
-
setVoiceMode(false)
|
|
1373
|
-
alert('Failed to open microphone. Please check microphone permissions for this app')
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
const startRecognition = (autoSend: boolean) => {
|
|
1377
|
-
if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
|
|
1378
|
-
|
|
1379
|
-
const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
1380
|
-
if (!SpeechRecognitionApi) {
|
|
1381
|
-
startAudioRecording(autoSend)
|
|
1382
|
-
return
|
|
1383
|
-
}
|
|
1384
360
|
|
|
1385
|
-
let sentTranscript = false
|
|
1386
|
-
|
|
1387
|
-
try {
|
|
1388
|
-
const recognition = new SpeechRecognitionApi()
|
|
1389
|
-
recognition.continuous = false
|
|
1390
|
-
recognition.interimResults = true
|
|
1391
|
-
recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
|
|
1392
|
-
recognition.onstart = () => setIsRecording(true)
|
|
1393
|
-
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
1394
|
-
let interimText = ''
|
|
1395
|
-
let finalText = ''
|
|
1396
|
-
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
1397
|
-
const transcript = event.results[index]?.[0]?.transcript ?? ''
|
|
1398
|
-
if (event.results[index]?.isFinal) finalText += transcript
|
|
1399
|
-
else interimText += transcript
|
|
1400
|
-
}
|
|
1401
|
-
const displayText = (finalText || interimText).trim()
|
|
1402
|
-
if (displayText) setVoiceTranscript(displayText)
|
|
1403
|
-
const resultText = finalText.trim()
|
|
1404
|
-
if (!resultText) return
|
|
1405
|
-
sentTranscript = true
|
|
1406
|
-
recognition.stop()
|
|
1407
|
-
setVoiceTranscript(resultText)
|
|
1408
|
-
if (autoSend) {
|
|
1409
|
-
voiceAwaitingResponseRef.current = true
|
|
1410
|
-
setVoiceAwaitingResponse(true)
|
|
1411
|
-
onSendVoiceMessage(resultText)
|
|
1412
|
-
.catch((error) => console.error('Voice message failed', error))
|
|
1413
|
-
.finally(() => {
|
|
1414
|
-
voiceAwaitingResponseRef.current = false
|
|
1415
|
-
setVoiceAwaitingResponse(false)
|
|
1416
|
-
scheduleVoiceListen()
|
|
1417
|
-
})
|
|
1418
|
-
} else {
|
|
1419
|
-
onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
recognition.onerror = (event: Event) => {
|
|
1423
|
-
console.error('Speech recognition error', event)
|
|
1424
|
-
setIsRecording(false)
|
|
1425
|
-
}
|
|
1426
|
-
recognition.onend = () => {
|
|
1427
|
-
recognitionRef.current = null
|
|
1428
|
-
setIsRecording(false)
|
|
1429
|
-
if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
|
|
1430
|
-
}
|
|
1431
|
-
recognitionRef.current = recognition
|
|
1432
|
-
recognition.start()
|
|
1433
|
-
} catch (error) {
|
|
1434
|
-
console.error('Failed to start speech recognition', error)
|
|
1435
|
-
recognitionRef.current = null
|
|
1436
|
-
setIsRecording(false)
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
361
|
const toggleRecording = () => {
|
|
1440
362
|
if (voiceMode) {
|
|
1441
|
-
voiceModeRef.current = false
|
|
1442
|
-
voiceAwaitingResponseRef.current = false
|
|
1443
363
|
setVoiceMode(false)
|
|
1444
|
-
setVoiceAwaitingResponse(false)
|
|
1445
|
-
setVoiceTranscript('')
|
|
1446
364
|
stopRecognition()
|
|
1447
365
|
cancelSpeech()
|
|
1448
366
|
return
|
|
1449
367
|
}
|
|
1450
368
|
|
|
1451
|
-
voiceModeRef.current = true
|
|
1452
369
|
setVoiceMode(true)
|
|
1453
|
-
setVoiceAwaitingResponse(false)
|
|
1454
|
-
setVoiceTranscript('')
|
|
1455
370
|
startRecognition(true)
|
|
1456
371
|
}
|
|
1457
372
|
const resizeInput = (element: HTMLTextAreaElement) => {
|
|
@@ -1460,31 +375,9 @@ export default function ChatPanel({
|
|
|
1460
375
|
}
|
|
1461
376
|
useEffect(() => {
|
|
1462
377
|
return () => {
|
|
1463
|
-
clearRestartTimer()
|
|
1464
378
|
cancelSpeech()
|
|
1465
|
-
stopRecognition()
|
|
1466
379
|
}
|
|
1467
380
|
}, [])
|
|
1468
|
-
useEffect(() => {
|
|
1469
|
-
voiceModeRef.current = voiceMode
|
|
1470
|
-
if (!voiceMode) {
|
|
1471
|
-
clearRestartTimer()
|
|
1472
|
-
setVoiceTranscript('')
|
|
1473
|
-
}
|
|
1474
|
-
}, [voiceMode])
|
|
1475
|
-
useEffect(() => {
|
|
1476
|
-
sendingRef.current = sending
|
|
1477
|
-
}, [sending])
|
|
1478
|
-
useEffect(() => {
|
|
1479
|
-
voiceAwaitingResponseRef.current = voiceAwaitingResponse
|
|
1480
|
-
}, [voiceAwaitingResponse])
|
|
1481
|
-
useEffect(() => {
|
|
1482
|
-
speakingRef.current = speakingText
|
|
1483
|
-
}, [speakingText])
|
|
1484
|
-
useEffect(() => {
|
|
1485
|
-
if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
|
|
1486
|
-
scheduleVoiceListen()
|
|
1487
|
-
}, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
|
|
1488
381
|
useEffect(() => {
|
|
1489
382
|
if (sending) submittedDuringSessionRef.current = true
|
|
1490
383
|
}, [sending])
|
|
@@ -1499,14 +392,19 @@ export default function ChatPanel({
|
|
|
1499
392
|
}
|
|
1500
393
|
}
|
|
1501
394
|
if (sending) return
|
|
1502
|
-
if (!
|
|
1503
|
-
|
|
395
|
+
if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
|
|
396
|
+
|
|
397
|
+
if (!settingsConfig?.enableVoiceReply) {
|
|
398
|
+
lastAutoSpokenIdRef.current = latest.id
|
|
399
|
+
if (voiceMode) {
|
|
400
|
+
scheduleVoiceListen(350)
|
|
401
|
+
}
|
|
1504
402
|
return
|
|
1505
403
|
}
|
|
1506
|
-
|
|
404
|
+
|
|
1507
405
|
lastAutoSpokenIdRef.current = latest.id
|
|
1508
406
|
speak(latest.aiText)
|
|
1509
|
-
}, [interactions, sending, settingsConfig?.enableVoiceReply])
|
|
407
|
+
}, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
|
|
1510
408
|
useEffect(() => {
|
|
1511
409
|
if (!toolMenuOpen) return
|
|
1512
410
|
const closeMenu = (event: MouseEvent) => {
|
|
@@ -1522,21 +420,20 @@ export default function ChatPanel({
|
|
|
1522
420
|
if (onPasteImage(event.clipboardData)) {
|
|
1523
421
|
event.preventDefault()
|
|
1524
422
|
event.stopPropagation()
|
|
1525
|
-
} else {
|
|
1526
|
-
window.setTimeout(() => void onReadClipboardImage(), 0)
|
|
1527
423
|
}
|
|
1528
424
|
}
|
|
1529
425
|
window.addEventListener('paste', handleWindowPaste, true)
|
|
1530
426
|
return () => window.removeEventListener('paste', handleWindowPaste, true)
|
|
1531
|
-
}, [onPasteImage
|
|
427
|
+
}, [onPasteImage])
|
|
1532
428
|
useEffect(() => {
|
|
1533
|
-
const
|
|
1534
|
-
if (
|
|
1535
|
-
|
|
429
|
+
const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
430
|
+
if (event.key === 'Escape' && sending) {
|
|
431
|
+
onCancelMessage()
|
|
432
|
+
}
|
|
1536
433
|
}
|
|
1537
|
-
window.addEventListener('keydown',
|
|
1538
|
-
return () => window.removeEventListener('keydown',
|
|
1539
|
-
}, [
|
|
434
|
+
window.addEventListener('keydown', handleEscapeKeyDown, true)
|
|
435
|
+
return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
|
|
436
|
+
}, [sending, onCancelMessage])
|
|
1540
437
|
useEffect(() => {
|
|
1541
438
|
if (message) return
|
|
1542
439
|
const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
|
|
@@ -1572,24 +469,72 @@ export default function ChatPanel({
|
|
|
1572
469
|
const renderCompletedActivity = (interaction: any) => {
|
|
1573
470
|
const interactionId = String(interaction.id)
|
|
1574
471
|
const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
|
|
1575
|
-
if (activityView.items.length === 0) return null
|
|
1576
472
|
const isOpen = Boolean(openActivityIds[interactionId])
|
|
1577
473
|
return (
|
|
1578
|
-
<
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
474
|
+
<AgentActivityDrawer
|
|
475
|
+
activityView={activityView}
|
|
476
|
+
isOpen={isOpen}
|
|
477
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
|
|
478
|
+
isHistorical={true}
|
|
479
|
+
/>
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const renderWebSearchSources = (interaction: any) => {
|
|
484
|
+
const interactionId = String(interaction.id)
|
|
485
|
+
const progress = agentActivitySnapshots[interactionId] ?? interaction.agentActivity ?? []
|
|
486
|
+
const sources = parseWebSearchSources(progress)
|
|
487
|
+
if (sources.length === 0) return null
|
|
488
|
+
return (
|
|
489
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '8px' }}>
|
|
490
|
+
<span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sources</span>
|
|
491
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
|
492
|
+
{sources.map((src, i) => (
|
|
493
|
+
<a
|
|
494
|
+
key={i}
|
|
495
|
+
href={src.url}
|
|
496
|
+
target="_blank"
|
|
497
|
+
rel="noopener noreferrer"
|
|
498
|
+
title={src.snippet || src.title}
|
|
499
|
+
style={{
|
|
500
|
+
display: 'flex',
|
|
501
|
+
alignItems: 'center',
|
|
502
|
+
gap: '6px',
|
|
503
|
+
padding: '5px 10px',
|
|
504
|
+
background: 'rgba(255,255,255,0.04)',
|
|
505
|
+
border: '1px solid rgba(255,255,255,0.08)',
|
|
506
|
+
borderRadius: '8px',
|
|
507
|
+
textDecoration: 'none',
|
|
508
|
+
color: '#cbd5e1',
|
|
509
|
+
fontSize: '0.78rem',
|
|
510
|
+
maxWidth: '220px',
|
|
511
|
+
overflow: 'hidden',
|
|
512
|
+
cursor: 'pointer',
|
|
513
|
+
transition: 'background 0.15s, border-color 0.15s',
|
|
514
|
+
}}
|
|
515
|
+
onMouseEnter={e => {
|
|
516
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.08)'
|
|
517
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.18)'
|
|
518
|
+
}}
|
|
519
|
+
onMouseLeave={e => {
|
|
520
|
+
(e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.04)'
|
|
521
|
+
;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.08)'
|
|
522
|
+
}}
|
|
523
|
+
>
|
|
524
|
+
<img
|
|
525
|
+
src={src.faviconUrl}
|
|
526
|
+
alt=""
|
|
527
|
+
width={14}
|
|
528
|
+
height={14}
|
|
529
|
+
style={{ borderRadius: '2px', flexShrink: 0 }}
|
|
530
|
+
onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
|
|
531
|
+
/>
|
|
532
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
533
|
+
{src.domain}
|
|
534
|
+
</span>
|
|
535
|
+
</a>
|
|
536
|
+
))}
|
|
537
|
+
</div>
|
|
1593
538
|
</div>
|
|
1594
539
|
)
|
|
1595
540
|
}
|
|
@@ -1837,6 +782,7 @@ export default function ChatPanel({
|
|
|
1837
782
|
)
|
|
1838
783
|
})()}
|
|
1839
784
|
<div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
|
|
785
|
+
{renderWebSearchSources(interaction)}
|
|
1840
786
|
<div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1841
787
|
<button className="provider-badge">{interaction.provider} • {interaction.model}</button>
|
|
1842
788
|
{fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
|
|
@@ -1859,18 +805,13 @@ export default function ChatPanel({
|
|
|
1859
805
|
{sending && (
|
|
1860
806
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1861
807
|
<div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
|
|
1862
|
-
{agentMode &&
|
|
1863
|
-
<
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
</span>
|
|
1870
|
-
</div>
|
|
1871
|
-
<AgentActivityTable activityView={agentActivities} />
|
|
1872
|
-
</div>
|
|
1873
|
-
</div>
|
|
808
|
+
{agentMode && (
|
|
809
|
+
<AgentActivityDrawer
|
|
810
|
+
activityView={agentActivities}
|
|
811
|
+
isOpen={openActivityIds['live'] ?? true}
|
|
812
|
+
onToggle={() => setOpenActivityIds((current) => ({ ...current, live: !(current['live'] ?? true) }))}
|
|
813
|
+
pendingApproval={!!pendingApproval}
|
|
814
|
+
/>
|
|
1874
815
|
)}
|
|
1875
816
|
{renderActiveFileChanges()}
|
|
1876
817
|
<div className="message ai-message thinking-message">
|
|
@@ -1907,7 +848,12 @@ export default function ChatPanel({
|
|
|
1907
848
|
/>
|
|
1908
849
|
</path>
|
|
1909
850
|
</svg>
|
|
1910
|
-
|
|
851
|
+
<span>
|
|
852
|
+
{activeAgentName && activeModelName
|
|
853
|
+
? `${activeAgentName} (${activeModelName}) is thinking... (${elapsedSeconds}s)`
|
|
854
|
+
: `Thinking for ${elapsedSeconds}s (Esc to cancel)`
|
|
855
|
+
}
|
|
856
|
+
</span>
|
|
1911
857
|
</div>
|
|
1912
858
|
)}
|
|
1913
859
|
</span>
|
|
@@ -1933,85 +879,17 @@ export default function ChatPanel({
|
|
|
1933
879
|
</div>
|
|
1934
880
|
)}
|
|
1935
881
|
|
|
1936
|
-
{pendingApproval && (
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
return (
|
|
1944
|
-
<div className="message ai-message" style={{ width: '100%' }}>
|
|
1945
|
-
<div className="bubble-wrapper" style={{ width: '100%' }}>
|
|
1946
|
-
<div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
|
|
1947
|
-
<div className="approval-card-content" style={{ width: '100%' }}>
|
|
1948
|
-
<div className="approval-card-title">{details.title}</div>
|
|
1949
|
-
<div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
|
|
1950
|
-
{diffText ? (
|
|
1951
|
-
<div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
|
|
1952
|
-
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
|
|
1953
|
-
{renderDiff(diffText)}
|
|
1954
|
-
</div>
|
|
1955
|
-
) : (
|
|
1956
|
-
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1957
|
-
)}
|
|
1958
|
-
{isAskUser && (
|
|
1959
|
-
<div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
|
|
1960
|
-
<textarea
|
|
1961
|
-
style={{
|
|
1962
|
-
width: '100%',
|
|
1963
|
-
minHeight: '70px',
|
|
1964
|
-
padding: '10px',
|
|
1965
|
-
background: 'rgba(0, 0, 0, 0.2)',
|
|
1966
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
1967
|
-
borderRadius: '6px',
|
|
1968
|
-
color: 'var(--text-main, #f8fafc)',
|
|
1969
|
-
fontSize: '0.9rem',
|
|
1970
|
-
fontFamily: 'inherit',
|
|
1971
|
-
resize: 'vertical',
|
|
1972
|
-
outline: 'none',
|
|
1973
|
-
}}
|
|
1974
|
-
placeholder="Type your answer here..."
|
|
1975
|
-
value={askAnswer}
|
|
1976
|
-
onChange={(e) => setAskAnswer(e.target.value)}
|
|
1977
|
-
/>
|
|
1978
|
-
</div>
|
|
1979
|
-
)}
|
|
1980
|
-
</div>
|
|
1981
|
-
{isAskUser ? (
|
|
1982
|
-
<div className="approval-card-actions">
|
|
1983
|
-
<button
|
|
1984
|
-
type="button"
|
|
1985
|
-
className="approval-btn approval-btn-approve"
|
|
1986
|
-
disabled={!askAnswer.trim()}
|
|
1987
|
-
onClick={() => onApproval(true, false, askAnswer)}
|
|
1988
|
-
>
|
|
1989
|
-
Submit Answer
|
|
1990
|
-
</button>
|
|
1991
|
-
<button
|
|
1992
|
-
type="button"
|
|
1993
|
-
className="approval-btn approval-btn-cancel"
|
|
1994
|
-
onClick={() => onApproval(false)}
|
|
1995
|
-
>
|
|
1996
|
-
Decline
|
|
1997
|
-
</button>
|
|
1998
|
-
</div>
|
|
1999
|
-
) : (
|
|
2000
|
-
<div className="approval-card-actions">
|
|
2001
|
-
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
2002
|
-
<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>
|
|
2003
|
-
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
2004
|
-
</div>
|
|
2005
|
-
)}
|
|
2006
|
-
</div>
|
|
2007
|
-
</div>
|
|
2008
|
-
</div>
|
|
2009
|
-
)
|
|
2010
|
-
})()}
|
|
882
|
+
{pendingApproval && (
|
|
883
|
+
<ApprovalCard
|
|
884
|
+
key={pendingApproval.id || JSON.stringify(pendingApproval)}
|
|
885
|
+
pendingApproval={pendingApproval}
|
|
886
|
+
onApproval={onApproval}
|
|
887
|
+
/>
|
|
888
|
+
)}
|
|
2011
889
|
<div ref={chatEnd} />
|
|
2012
890
|
</div>
|
|
2013
891
|
|
|
2014
|
-
<div className=
|
|
892
|
+
<div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
|
|
2015
893
|
{isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
|
|
2016
894
|
<button type="button" className="workspace-select-btn" onClick={onSelectWorkspace}>
|
|
2017
895
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
@@ -2041,8 +919,21 @@ export default function ChatPanel({
|
|
|
2041
919
|
{voiceMode && (
|
|
2042
920
|
<div className="voice-mode-bar" data-state={voiceStatus}>
|
|
2043
921
|
<span className="voice-mode-dot" />
|
|
2044
|
-
|
|
2045
|
-
|
|
922
|
+
{voiceTranscript ? (
|
|
923
|
+
voiceTranscript === 'Listening to microphone...' ||
|
|
924
|
+
voiceTranscript === 'Listening...' ||
|
|
925
|
+
voiceTranscript === 'No speech detected' ||
|
|
926
|
+
voiceTranscript === 'Sent audio to AI' ? (
|
|
927
|
+
<span>{voiceTranscript}</span>
|
|
928
|
+
) : (
|
|
929
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }}>
|
|
930
|
+
<span>{voiceStatusLabel}:</span>
|
|
931
|
+
<span className="voice-mode-transcript">"{voiceTranscript}"</span>
|
|
932
|
+
</span>
|
|
933
|
+
)
|
|
934
|
+
) : (
|
|
935
|
+
<span>{voiceStatusLabel}</span>
|
|
936
|
+
)}
|
|
2046
937
|
</div>
|
|
2047
938
|
)}
|
|
2048
939
|
|
|
@@ -2179,6 +1070,11 @@ export default function ChatPanel({
|
|
|
2179
1070
|
else if (provider === 'huggingface') displayName = 'HF'
|
|
2180
1071
|
else if (provider === 'local_openai') displayName = 'Local'
|
|
2181
1072
|
else if (provider === 'ollama') displayName = 'Ollama'
|
|
1073
|
+
else if (provider.startsWith('custom:')) {
|
|
1074
|
+
const id = provider.replace(/^custom:/, '')
|
|
1075
|
+
const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
|
|
1076
|
+
displayName = cp?.displayName || id
|
|
1077
|
+
}
|
|
2182
1078
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
2183
1079
|
})}
|
|
2184
1080
|
</select>
|
|
@@ -2232,12 +1128,27 @@ export default function ChatPanel({
|
|
|
2232
1128
|
</svg>
|
|
2233
1129
|
)}
|
|
2234
1130
|
</button>
|
|
2235
|
-
|
|
2236
|
-
<
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
1131
|
+
{sending ? (
|
|
1132
|
+
<button
|
|
1133
|
+
id="send-btn"
|
|
1134
|
+
className="stop-btn"
|
|
1135
|
+
type="button"
|
|
1136
|
+
onClick={onCancelMessage}
|
|
1137
|
+
title="Stop generating"
|
|
1138
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
1139
|
+
>
|
|
1140
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
1141
|
+
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
|
|
1142
|
+
</svg>
|
|
1143
|
+
</button>
|
|
1144
|
+
) : (
|
|
1145
|
+
<button id="send-btn" type="submit" disabled={!canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1146
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
1147
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
1148
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
1149
|
+
</svg>
|
|
1150
|
+
</button>
|
|
1151
|
+
)}
|
|
2241
1152
|
</form>
|
|
2242
1153
|
</div>
|
|
2243
1154
|
<p className="input-disclaimer">
|