@vibe-forge/client 0.2.0-alpha.9 → 0.4.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/cli.cjs +1 -1
- package/dist/assets/{arc-CybT1Fs2.js → arc-DgIxeTMg.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-BY5Aoa-D.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-F42hTbzS.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
- package/dist/assets/channel-DhtnrNJ6.js +1 -0
- package/dist/assets/{classDiagram-beda092f-D-tIPp-3.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-J57aCe6u.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
- package/dist/assets/clone-7bHB6YkC.js +1 -0
- package/dist/assets/{createText-1719965b-ByfEqOF-.js → createText-1719965b-C1_HJcCc.js} +1 -1
- package/dist/assets/devicon-BWlTeAUU.woff +0 -0
- package/dist/assets/devicon-CirD-cQx.ttf +0 -0
- package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
- package/dist/assets/devicon-TqfHp33-.eot +0 -0
- package/dist/assets/{edges-96097737-CMEArkOa.js → edges-96097737-BU8qStzd.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-Cf8mX2aj.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-DG6WKyo7.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-CstUxz-w.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1--4CRoQ-H.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-DYgHcKd-.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-DDSVpfsd.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
- package/dist/assets/{graph-CRWF39gX.js → graph-CIkpD4Kx.js} +1 -1
- package/dist/assets/{index-5325376f-W1hft795.js → index-5325376f-aVVRRTIu.js} +1 -1
- package/dist/assets/index-D1giUI7r.css +1 -0
- package/dist/assets/index-DRSI_ZIL.js +514 -0
- package/dist/assets/{infoDiagram-8eee0895-D4SHcix6.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-MWgCkVoE.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
- package/dist/assets/{layout-C88ObkCf.js → layout-Tnmha8Nh.js} +1 -1
- package/dist/assets/{line-C7WAYMt5.js → line-BQR2SOyl.js} +1 -1
- package/dist/assets/{linear-C4msxfcU.js → linear-DlG0eemV.js} +1 -1
- package/dist/assets/{mermaid.core-Cabag9SZ.js → mermaid.core-BnwYO0He.js} +6 -6
- package/dist/assets/{mindmap-definition-8da855dc-CeS8ETXx.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-BvjyKnq5.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-DzYvpbNM.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-DHIoDbyo.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-BFSGnQGs.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-_LM3BJ5-.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-DwORjOzl.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-B4cAWWz1.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
- package/dist/assets/{styles-b4e223ce-D_rmV3B_.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
- package/dist/assets/{styles-ca3715f6-BFx4VuFc.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
- package/dist/assets/{styles-d45a18b0-BE3106vL.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-DwDTO1op.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-C4b8qUQZ.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-BRJ9Z4u-.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
- package/dist/index.html +2 -7
- package/index.html +0 -5
- package/package.json +11 -6
- package/src/App.tsx +2 -0
- package/src/api/README.md +26 -0
- package/src/api/automation.ts +88 -0
- package/src/api/base.ts +54 -0
- package/src/api/benchmark.ts +45 -0
- package/src/api/config.ts +24 -0
- package/src/api/knowledge.ts +72 -0
- package/src/api/projects.ts +15 -0
- package/src/api/sessions.ts +82 -0
- package/src/api/types.ts +20 -0
- package/src/api.ts +44 -241
- package/src/components/AutomationView/AutomationView.scss +5 -1
- package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
- package/src/components/AutomationView/TaskList.scss +4 -6
- package/src/components/AutomationView/TaskList.tsx +2 -1
- package/src/components/AutomationView/TriggerList.scss +4 -1
- package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
- package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
- package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
- package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
- package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
- package/src/components/BenchmarkView/index.tsx +197 -0
- package/src/components/BenchmarkView/types.ts +10 -0
- package/src/components/BenchmarkView/utils.ts +21 -0
- package/src/components/Chat.tsx +37 -29
- package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
- package/src/components/ConfigView.tsx +13 -1
- package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
- package/src/components/NavRail.tsx +7 -0
- package/src/components/chat/ChatHeader.scss +37 -19
- package/src/components/chat/ChatHeader.tsx +6 -9
- package/src/components/chat/ChatHistoryView.tsx +89 -45
- package/src/components/chat/CurrentTodoList.tsx +10 -9
- package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
- package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
- package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
- package/src/components/chat/NewSessionGuide.scss +35 -13
- package/src/components/chat/NewSessionGuide.tsx +20 -10
- package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
- package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
- package/src/components/chat/tools/DefaultTool.tsx +184 -21
- package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
- package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
- package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
- package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
- package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
- package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
- package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
- package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
- package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
- package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
- package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
- package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
- package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
- package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
- package/src/components/chat/tools/adapter-claude/index.ts +4 -10
- package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
- package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
- package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
- package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
- package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
- package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
- package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
- package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
- package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
- package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
- package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
- package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
- package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
- package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
- package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
- package/src/components/config/AppSettingsPanel.tsx +33 -0
- package/src/components/config/ConfigSectionForm.tsx +12 -1
- package/src/components/config/channelDefinitions.ts +6 -0
- package/src/components/config/configSchema.ts +10 -1
- package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
- package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
- package/src/components/config/recordEditors/index.tsx +1 -0
- package/src/components/knowledge-base/KnowledgeBaseView.tsx +51 -3
- package/src/components/knowledge-base/components/RuleItem.tsx +79 -0
- package/src/components/knowledge-base/components/RuleList.scss +5 -0
- package/src/components/knowledge-base/components/RuleList.tsx +70 -0
- package/src/components/knowledge-base/components/RulesTab.tsx +32 -7
- package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
- package/src/hooks/chat/use-chat-interaction.ts +26 -0
- package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +65 -16
- package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
- package/src/hooks/chat/use-chat-scroll.ts +51 -0
- package/src/hooks/chat/use-chat-session-actions.ts +147 -0
- package/src/hooks/chat/use-chat-session-messages.ts +250 -0
- package/src/hooks/chat/use-chat-session.ts +57 -0
- package/src/hooks/chat/use-chat-view.ts +39 -0
- package/src/main.tsx +10 -13
- package/src/resources/locales/en.json +73 -0
- package/src/resources/locales/zh.json +73 -0
- package/src/runtime-config.ts +52 -0
- package/src/store/index.ts +2 -0
- package/src/vite-env.d.ts +11 -0
- package/src/ws.ts +5 -3
- package/vite.config.ts +12 -4
- package/dist/assets/channel-DrWdSpqV.js +0 -1
- package/dist/assets/clone-D0cC8LLB.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-Bf_DH7dp.js +0 -1
- package/dist/assets/index-CNMzWvKV.js +0 -497
- package/dist/assets/index-PEmISxiy.css +0 -1
- package/src/components/chat/ToolCallBox.scss +0 -137
- package/src/components/chat/useChatSession.ts +0 -370
- /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
- /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
- /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
- /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
- /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
- /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
- /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
- /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
- /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
|
@@ -6,11 +6,12 @@ import React, { useEffect, useRef, useState } from 'react'
|
|
|
6
6
|
import { useTranslation } from 'react-i18next'
|
|
7
7
|
import useSWR from 'swr'
|
|
8
8
|
|
|
9
|
-
import type { AskUserQuestionParams, SessionInfo, SessionStatus } from '@vibe-forge/core'
|
|
9
|
+
import type { AskUserQuestionParams, ChatMessageContent, SessionInfo, SessionStatus } from '@vibe-forge/core'
|
|
10
10
|
import type { CompletionItem } from './CompletionMenu'
|
|
11
|
+
import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
|
|
11
12
|
import { CompletionMenu } from './CompletionMenu'
|
|
12
13
|
import { ThinkingStatus } from './ThinkingStatus'
|
|
13
|
-
import { isShortcutMatch } from '
|
|
14
|
+
import { isShortcutMatch } from '../../../utils/shortcutUtils'
|
|
14
15
|
|
|
15
16
|
const { TextArea } = Input
|
|
16
17
|
|
|
@@ -25,8 +26,17 @@ interface ModelSelectGroup {
|
|
|
25
26
|
options: ModelSelectOption[]
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface PendingImage {
|
|
30
|
+
id: string
|
|
31
|
+
url: string
|
|
32
|
+
name?: string
|
|
33
|
+
size?: number
|
|
34
|
+
mimeType?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export function Sender({
|
|
29
38
|
onSend,
|
|
39
|
+
onSendContent,
|
|
30
40
|
sessionStatus,
|
|
31
41
|
onInterrupt,
|
|
32
42
|
onClear,
|
|
@@ -37,9 +47,13 @@ export function Sender({
|
|
|
37
47
|
modelOptions,
|
|
38
48
|
selectedModel,
|
|
39
49
|
onModelChange,
|
|
50
|
+
permissionMode,
|
|
51
|
+
permissionModeOptions,
|
|
52
|
+
onPermissionModeChange,
|
|
40
53
|
modelUnavailable
|
|
41
54
|
}: {
|
|
42
55
|
onSend: (text: string) => void
|
|
56
|
+
onSendContent: (content: ChatMessageContent[]) => void
|
|
43
57
|
sessionStatus?: SessionStatus
|
|
44
58
|
onInterrupt: () => void
|
|
45
59
|
onClear?: () => void
|
|
@@ -50,6 +64,9 @@ export function Sender({
|
|
|
50
64
|
modelOptions?: ModelSelectGroup[]
|
|
51
65
|
selectedModel?: string
|
|
52
66
|
onModelChange?: (model: string) => void
|
|
67
|
+
permissionMode: PermissionMode
|
|
68
|
+
permissionModeOptions: Array<{ value: PermissionMode; label: React.ReactNode }>
|
|
69
|
+
onPermissionModeChange: (mode: PermissionMode) => void
|
|
53
70
|
modelUnavailable?: boolean
|
|
54
71
|
}) {
|
|
55
72
|
const { t } = useTranslation()
|
|
@@ -63,7 +80,9 @@ export function Sender({
|
|
|
63
80
|
const [showToolsList, setShowToolsList] = useState(false)
|
|
64
81
|
const textareaRef = useRef<TextAreaRef>(null)
|
|
65
82
|
const toolsRef = useRef<HTMLDivElement>(null)
|
|
83
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
66
84
|
const isMac = navigator.platform.includes('Mac')
|
|
85
|
+
const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
|
|
67
86
|
|
|
68
87
|
const { data: configRes } = useSWR<{
|
|
69
88
|
sources?: {
|
|
@@ -96,8 +115,54 @@ export function Sender({
|
|
|
96
115
|
const [historyIndex, setHistoryIndex] = useState(-1)
|
|
97
116
|
const [draft, setDraft] = useState('')
|
|
98
117
|
|
|
118
|
+
const readFileAsDataUrl = (file: File) => {
|
|
119
|
+
return new Promise<string>((resolve, reject) => {
|
|
120
|
+
const reader = new FileReader()
|
|
121
|
+
reader.onload = () => {
|
|
122
|
+
resolve(typeof reader.result === 'string' ? reader.result : '')
|
|
123
|
+
}
|
|
124
|
+
reader.onerror = () => {
|
|
125
|
+
reject(new Error('read_failed'))
|
|
126
|
+
}
|
|
127
|
+
reader.readAsDataURL(file)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const addImageFiles = async (files: File[]) => {
|
|
132
|
+
const maxSize = 5 * 1024 * 1024
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
if (!file.type.startsWith('image/')) continue
|
|
135
|
+
if (file.size > maxSize) {
|
|
136
|
+
void message.error(t('chat.imageTooLarge'))
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const url = await readFileAsDataUrl(file)
|
|
141
|
+
if (url === '') {
|
|
142
|
+
void message.error(t('chat.imageReadFailed'))
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
setPendingImages(prev => [
|
|
146
|
+
...prev,
|
|
147
|
+
{
|
|
148
|
+
id: globalThis.crypto?.randomUUID
|
|
149
|
+
? globalThis.crypto.randomUUID()
|
|
150
|
+
: `img-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
151
|
+
url,
|
|
152
|
+
name: file.name,
|
|
153
|
+
size: file.size,
|
|
154
|
+
mimeType: file.type
|
|
155
|
+
}
|
|
156
|
+
])
|
|
157
|
+
} catch (err) {
|
|
158
|
+
void message.error(t('chat.imageReadFailed'))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
99
163
|
const handleSend = () => {
|
|
100
|
-
if (
|
|
164
|
+
if (isThinking) return
|
|
165
|
+
if (input.trim() === '' && pendingImages.length === 0) return
|
|
101
166
|
|
|
102
167
|
if (modelUnavailable) {
|
|
103
168
|
void message.warning(t('chat.modelConfigRequired'))
|
|
@@ -105,12 +170,31 @@ export function Sender({
|
|
|
105
170
|
}
|
|
106
171
|
|
|
107
172
|
if (interactionRequest != null && onInteractionResponse != null) {
|
|
173
|
+
if (pendingImages.length > 0) {
|
|
174
|
+
void message.warning(t('chat.imageNotSupportedInInteraction'))
|
|
175
|
+
return
|
|
176
|
+
}
|
|
108
177
|
onInteractionResponse(interactionRequest.id, input.trim())
|
|
109
178
|
setInput('')
|
|
110
179
|
return
|
|
111
180
|
}
|
|
112
181
|
|
|
113
|
-
|
|
182
|
+
if (pendingImages.length > 0) {
|
|
183
|
+
const content: ChatMessageContent[] = []
|
|
184
|
+
if (input.trim() !== '') {
|
|
185
|
+
content.push({ type: 'text', text: input.trim() })
|
|
186
|
+
}
|
|
187
|
+
content.push(...pendingImages.map(img => ({
|
|
188
|
+
type: 'image',
|
|
189
|
+
url: img.url,
|
|
190
|
+
name: img.name,
|
|
191
|
+
size: img.size,
|
|
192
|
+
mimeType: img.mimeType
|
|
193
|
+
})))
|
|
194
|
+
onSendContent(content)
|
|
195
|
+
} else {
|
|
196
|
+
onSend(input)
|
|
197
|
+
}
|
|
114
198
|
|
|
115
199
|
// Save to local storage history
|
|
116
200
|
try {
|
|
@@ -122,11 +206,48 @@ export function Sender({
|
|
|
122
206
|
}
|
|
123
207
|
|
|
124
208
|
setInput('')
|
|
209
|
+
setPendingImages([])
|
|
125
210
|
setDraft('')
|
|
126
211
|
setShowCompletion(false)
|
|
127
212
|
setHistoryIndex(-1)
|
|
128
213
|
}
|
|
129
214
|
|
|
215
|
+
const handleImageUpload = () => {
|
|
216
|
+
if (isThinking) return
|
|
217
|
+
if (modelUnavailable) {
|
|
218
|
+
void message.warning(t('chat.modelConfigRequired'))
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (interactionRequest != null) {
|
|
222
|
+
void message.warning(t('chat.imageNotSupportedInInteraction'))
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
fileInputRef.current?.click()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const handleImageFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
229
|
+
const fileList = Array.from(event.target.files ?? [])
|
|
230
|
+
if (fileList.length === 0) return
|
|
231
|
+
await addImageFiles(fileList)
|
|
232
|
+
event.target.value = ''
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const handleRemovePendingImage = (id: string) => {
|
|
236
|
+
setPendingImages(prev => prev.filter(img => img.id !== id))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handlePaste = async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
|
240
|
+
const items = Array.from(event.clipboardData?.items ?? [])
|
|
241
|
+
const files = items
|
|
242
|
+
.filter(item => item.kind === 'file' && item.type.startsWith('image/'))
|
|
243
|
+
.map(item => item.getAsFile())
|
|
244
|
+
.filter((file): file is File => file != null)
|
|
245
|
+
if (files.length > 0) {
|
|
246
|
+
event.preventDefault()
|
|
247
|
+
await addImageFiles(files)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
130
251
|
const clearInputValue = () => {
|
|
131
252
|
if (input === '') return
|
|
132
253
|
setInput('')
|
|
@@ -422,6 +543,18 @@ export function Sender({
|
|
|
422
543
|
{t('chat.modelConfigRequired')}
|
|
423
544
|
</div>
|
|
424
545
|
)}
|
|
546
|
+
{pendingImages.length > 0 && (
|
|
547
|
+
<div className='pending-images'>
|
|
548
|
+
{pendingImages.map(img => (
|
|
549
|
+
<div key={img.id} className='pending-image'>
|
|
550
|
+
<img src={img.url} alt={img.name ?? 'image'} />
|
|
551
|
+
<div className='pending-image-remove' onClick={() => handleRemovePendingImage(img.id)}>
|
|
552
|
+
<span className='material-symbols-rounded'>close</span>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
))}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
425
558
|
{showCompletion && (
|
|
426
559
|
<CompletionMenu
|
|
427
560
|
items={completionItems}
|
|
@@ -437,12 +570,21 @@ export function Sender({
|
|
|
437
570
|
value={input}
|
|
438
571
|
onChange={handleInputChange}
|
|
439
572
|
onKeyDown={handleKeyDown}
|
|
573
|
+
onPaste={handlePaste}
|
|
440
574
|
autoSize={{ minRows: 1, maxRows: 10 }}
|
|
441
575
|
variant='borderless'
|
|
442
576
|
disabled={modelUnavailable}
|
|
443
577
|
/>
|
|
444
578
|
|
|
445
579
|
<div className='chat-input-toolbar'>
|
|
580
|
+
<input
|
|
581
|
+
ref={fileInputRef}
|
|
582
|
+
type='file'
|
|
583
|
+
accept='image/*'
|
|
584
|
+
multiple
|
|
585
|
+
onChange={handleImageFileChange}
|
|
586
|
+
className='file-input-hidden'
|
|
587
|
+
/>
|
|
446
588
|
<div className='toolbar-left'>
|
|
447
589
|
<Tooltip title='快捷指令'>
|
|
448
590
|
<span>
|
|
@@ -467,7 +609,7 @@ export function Sender({
|
|
|
467
609
|
</Tooltip>
|
|
468
610
|
<Tooltip title='上传图片'>
|
|
469
611
|
<span>
|
|
470
|
-
<div className='toolbar-btn' onClick={
|
|
612
|
+
<div className='toolbar-btn' onClick={handleImageUpload}>
|
|
471
613
|
<span className='material-symbols-rounded'>image</span>
|
|
472
614
|
</div>
|
|
473
615
|
</span>
|
|
@@ -522,6 +664,20 @@ export function Sender({
|
|
|
522
664
|
popupMatchSelectWidth={false}
|
|
523
665
|
/>
|
|
524
666
|
|
|
667
|
+
<Select
|
|
668
|
+
className='permission-mode-select'
|
|
669
|
+
classNames={{ popup: { root: 'permission-mode-select-popup' } }}
|
|
670
|
+
value={permissionMode}
|
|
671
|
+
options={permissionModeOptions}
|
|
672
|
+
showSearch={false}
|
|
673
|
+
allowClear={false}
|
|
674
|
+
disabled={modelUnavailable || isThinking}
|
|
675
|
+
onChange={(value) => onPermissionModeChange(value)}
|
|
676
|
+
placeholder='权限模式'
|
|
677
|
+
optionLabelProp='label'
|
|
678
|
+
popupMatchSelectWidth={false}
|
|
679
|
+
/>
|
|
680
|
+
|
|
525
681
|
<div
|
|
526
682
|
className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${isThinking ? 'thinking' : ''} ${modelUnavailable ? 'disabled' : ''}`}
|
|
527
683
|
onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
|
|
@@ -1,10 +1,172 @@
|
|
|
1
1
|
import type { ChatMessageContent } from '@vibe-forge/core'
|
|
2
|
-
import React from 'react'
|
|
3
2
|
import { useTranslation } from 'react-i18next'
|
|
4
|
-
import { CodeBlock } from '
|
|
5
|
-
import { MarkdownContent } from '
|
|
6
|
-
import { ToolCallBox } from '
|
|
7
|
-
import { safeJsonStringify } from '
|
|
3
|
+
import { CodeBlock } from '#~/components/CodeBlock'
|
|
4
|
+
import { MarkdownContent } from '#~/components/MarkdownContent'
|
|
5
|
+
import { ToolCallBox } from './core/ToolCallBox'
|
|
6
|
+
import { safeJsonStringify, toSerializable } from '#~/utils/safe-serialize'
|
|
7
|
+
|
|
8
|
+
interface StructuredTextBlock {
|
|
9
|
+
type: 'text'
|
|
10
|
+
text: string
|
|
11
|
+
format: 'text' | 'markdown'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface StructuredImageBlock {
|
|
15
|
+
type: 'image'
|
|
16
|
+
src: string
|
|
17
|
+
alt?: string
|
|
18
|
+
title?: string
|
|
19
|
+
width?: number
|
|
20
|
+
height?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type StructuredBlock = StructuredTextBlock | StructuredImageBlock
|
|
24
|
+
|
|
25
|
+
function parseStructuredInput(value: unknown) {
|
|
26
|
+
if (typeof value !== 'string') {
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
const trimmed = value.trim()
|
|
30
|
+
if (
|
|
31
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
32
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
33
|
+
) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(trimmed)
|
|
36
|
+
} catch {
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveImageSource(value: Record<string, unknown>) {
|
|
44
|
+
const directUrl = typeof value.url === 'string'
|
|
45
|
+
? value.url
|
|
46
|
+
: typeof value.src === 'string'
|
|
47
|
+
? value.src
|
|
48
|
+
: typeof value.image_url === 'string'
|
|
49
|
+
? value.image_url
|
|
50
|
+
: typeof value.imageUrl === 'string'
|
|
51
|
+
? value.imageUrl
|
|
52
|
+
: typeof value.dataUrl === 'string'
|
|
53
|
+
? value.dataUrl
|
|
54
|
+
: null
|
|
55
|
+
if (directUrl) {
|
|
56
|
+
return directUrl
|
|
57
|
+
}
|
|
58
|
+
const source = value.source != null && typeof value.source === 'object'
|
|
59
|
+
? (value.source as Record<string, unknown>)
|
|
60
|
+
: null
|
|
61
|
+
const data = typeof value.data === 'string'
|
|
62
|
+
? value.data
|
|
63
|
+
: typeof value.base64 === 'string'
|
|
64
|
+
? value.base64
|
|
65
|
+
: source != null && typeof source.data === 'string'
|
|
66
|
+
? source.data
|
|
67
|
+
: null
|
|
68
|
+
if (!data) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
const mimeType = typeof value.mimeType === 'string'
|
|
72
|
+
? value.mimeType
|
|
73
|
+
: typeof value.mime_type === 'string'
|
|
74
|
+
? value.mime_type
|
|
75
|
+
: source != null && typeof source.media_type === 'string'
|
|
76
|
+
? source.media_type
|
|
77
|
+
: source != null && typeof source.mimeType === 'string'
|
|
78
|
+
? source.mimeType
|
|
79
|
+
: source != null && typeof source.mime_type === 'string'
|
|
80
|
+
? source.mime_type
|
|
81
|
+
: 'image/png'
|
|
82
|
+
return `data:${mimeType};base64,${data}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseBlock(value: unknown): StructuredBlock | null {
|
|
86
|
+
if (value == null || typeof value !== 'object') {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
const obj = value as Record<string, unknown>
|
|
90
|
+
const rawType = typeof obj.type === 'string' ? obj.type.toLowerCase() : ''
|
|
91
|
+
if (rawType === 'text' || rawType === 'markdown' || rawType === 'md') {
|
|
92
|
+
const text = typeof obj.text === 'string'
|
|
93
|
+
? obj.text
|
|
94
|
+
: typeof obj.content === 'string'
|
|
95
|
+
? obj.content
|
|
96
|
+
: null
|
|
97
|
+
if (text == null) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
const rawFormat = typeof obj.format === 'string' ? obj.format.toLowerCase() : 'markdown'
|
|
101
|
+
const format = rawType === 'text'
|
|
102
|
+
? (rawFormat === 'text' || rawFormat === 'plain' ? 'text' : 'markdown')
|
|
103
|
+
: 'markdown'
|
|
104
|
+
return { type: 'text', text, format }
|
|
105
|
+
}
|
|
106
|
+
if (rawType === 'image') {
|
|
107
|
+
const src = resolveImageSource(obj)
|
|
108
|
+
if (!src) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
const alt = typeof obj.alt === 'string' ? obj.alt : undefined
|
|
112
|
+
const title = typeof obj.title === 'string' ? obj.title : undefined
|
|
113
|
+
const width = typeof obj.width === 'number' ? obj.width : undefined
|
|
114
|
+
const height = typeof obj.height === 'number' ? obj.height : undefined
|
|
115
|
+
return { type: 'image', src, alt, title, width, height }
|
|
116
|
+
}
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getStructuredBlocks(value: unknown): StructuredBlock[] | null {
|
|
121
|
+
const serializable = toSerializable(value)
|
|
122
|
+
const parsed = parseStructuredInput(serializable)
|
|
123
|
+
if (Array.isArray(parsed)) {
|
|
124
|
+
const blocks = parsed.map(parseBlock)
|
|
125
|
+
return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
|
|
126
|
+
}
|
|
127
|
+
if (parsed != null && typeof parsed === 'object') {
|
|
128
|
+
const container = parsed as Record<string, unknown>
|
|
129
|
+
const content = container.content ?? container.items ?? container.blocks
|
|
130
|
+
if (Array.isArray(content)) {
|
|
131
|
+
const blocks = content.map(parseBlock)
|
|
132
|
+
return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const single = parseBlock(parsed)
|
|
136
|
+
return single ? [single] : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function StructuredToolResult({ blocks }: { blocks: StructuredBlock[] }) {
|
|
140
|
+
return (
|
|
141
|
+
<div className='tool-result-structured'>
|
|
142
|
+
{blocks.map((block, index) => {
|
|
143
|
+
if (block.type === 'text') {
|
|
144
|
+
return (
|
|
145
|
+
<div className='tool-result-text' key={`text-${index}`}>
|
|
146
|
+
{block.format === 'markdown'
|
|
147
|
+
? <MarkdownContent content={block.text} />
|
|
148
|
+
: <div className='tool-result-text-content'>{block.text}</div>}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<div className='tool-result-image-wrapper' key={`image-${index}`}>
|
|
154
|
+
<img
|
|
155
|
+
className='tool-result-image'
|
|
156
|
+
src={block.src}
|
|
157
|
+
alt={block.alt ?? ''}
|
|
158
|
+
width={block.width}
|
|
159
|
+
height={block.height}
|
|
160
|
+
/>
|
|
161
|
+
{block.title != null && block.title.length > 0 && (
|
|
162
|
+
<div className='tool-result-image-caption'>{block.title}</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
8
170
|
|
|
9
171
|
export function DefaultTool({
|
|
10
172
|
item,
|
|
@@ -14,17 +176,16 @@ export function DefaultTool({
|
|
|
14
176
|
resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
|
|
15
177
|
}) {
|
|
16
178
|
const { t } = useTranslation()
|
|
179
|
+
const structuredBlocks = resultItem != null ? getStructuredBlocks(resultItem.content) : null
|
|
17
180
|
return (
|
|
18
181
|
<div className='tool-group'>
|
|
19
182
|
<ToolCallBox
|
|
20
183
|
header={
|
|
21
|
-
|
|
22
|
-
<span className='material-symbols-rounded'
|
|
23
|
-
<span>{item.name}</span>
|
|
24
|
-
<span
|
|
25
|
-
|
|
26
|
-
</span>
|
|
27
|
-
</>
|
|
184
|
+
<div className='tool-header-content'>
|
|
185
|
+
<span className='material-symbols-rounded tool-header-icon'>build</span>
|
|
186
|
+
<span className='tool-header-title'>{item.name}</span>
|
|
187
|
+
<span className='tool-header-hint'>{t('chat.tools.call')}</span>
|
|
188
|
+
</div>
|
|
28
189
|
}
|
|
29
190
|
content={
|
|
30
191
|
<div className='tool-content'>
|
|
@@ -40,20 +201,22 @@ export function DefaultTool({
|
|
|
40
201
|
type='result'
|
|
41
202
|
isError={resultItem.is_error}
|
|
42
203
|
header={
|
|
43
|
-
|
|
44
|
-
<span className='material-symbols-rounded'
|
|
204
|
+
<div className='tool-header-content'>
|
|
205
|
+
<span className='material-symbols-rounded tool-header-icon'>
|
|
45
206
|
{resultItem.is_error === true ? 'error' : 'check_circle'}
|
|
46
207
|
</span>
|
|
47
|
-
<span>{t('chat.result')}</span>
|
|
48
|
-
|
|
208
|
+
<span className='tool-header-title'>{t('chat.result')}</span>
|
|
209
|
+
</div>
|
|
49
210
|
}
|
|
50
211
|
content={
|
|
51
212
|
<div className='tool-content'>
|
|
52
|
-
{
|
|
53
|
-
?
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
213
|
+
{structuredBlocks != null
|
|
214
|
+
? <StructuredToolResult blocks={structuredBlocks} />
|
|
215
|
+
: (typeof resultItem.content === 'string'
|
|
216
|
+
? (resultItem.content.startsWith('```')
|
|
217
|
+
? <MarkdownContent content={resultItem.content} />
|
|
218
|
+
: <CodeBlock code={resultItem.content} lang='text' />)
|
|
219
|
+
: <CodeBlock code={safeJsonStringify(resultItem.content, 2)} lang='json' />)}
|
|
57
220
|
</div>
|
|
58
221
|
}
|
|
59
222
|
/>
|
|
@@ -1,71 +1,87 @@
|
|
|
1
1
|
.tool-group.bash-tool {
|
|
2
|
-
.
|
|
2
|
+
.tool-call-header {
|
|
3
|
+
height: auto;
|
|
4
|
+
min-height: 32px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.bash-tool__header {
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-width: 0;
|
|
3
10
|
display: flex;
|
|
4
11
|
align-items: center;
|
|
5
|
-
gap:
|
|
12
|
+
gap: 8px;
|
|
13
|
+
}
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
.bash-tool__icon {
|
|
16
|
+
font-size: 16px;
|
|
17
|
+
color: var(--sub-text-color);
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
width: 16px;
|
|
22
|
+
height: 16px;
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
align-self: center;
|
|
25
|
+
}
|
|
10
26
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
.bash-tool__header-main {
|
|
28
|
+
flex: 1;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: 2px;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
.bash-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
.bash-tool__command-row {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 6px;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.bash-tool__command-text {
|
|
43
|
+
flex: 1;
|
|
44
|
+
min-width: 0;
|
|
21
45
|
overflow: hidden;
|
|
22
46
|
text-overflow: ellipsis;
|
|
23
47
|
white-space: nowrap;
|
|
24
|
-
flex: 1;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.tool-reason {
|
|
28
|
-
margin: 0 0 4px 0;
|
|
29
48
|
font-size: 12px;
|
|
30
|
-
color: var(--
|
|
31
|
-
|
|
49
|
+
color: var(--text-color);
|
|
50
|
+
}
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
background-color: transparent;
|
|
37
|
-
padding: 0;
|
|
52
|
+
.bash-tool__command-text--clickable {
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
color: var(--text-color);
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
padding: 0 !important;
|
|
42
|
-
}
|
|
56
|
+
&:hover {
|
|
57
|
+
text-shadow: 0 0 .5px currentColor;
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
.bash-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
.bash-tool__reason-row {
|
|
62
|
+
display: flex;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
.bash-tool__reason-text {
|
|
67
|
+
min-width: 0;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
}
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
.bash-tool__header-tags {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 6px;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.bash-tool__command-detail {
|
|
81
|
+
margin-bottom: 6px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.tool-scroll {
|
|
85
|
+
max-height: 270px;
|
|
70
86
|
}
|
|
71
87
|
}
|