Skip to content

Commit 3ff8f97

Browse files
authored
Merge pull request #218 from xianmin/video_confirm2
feat: 添加视频生成确认功能
2 parents 7de67e8 + 5aeff82 commit 3ff8f97

15 files changed

Lines changed: 676 additions & 50 deletions

react/src/components/chat/Chat.tsx

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
8080

8181
const sessionIdRef = useRef<string>(session?.id || nanoid())
8282
const [expandingToolCalls, setExpandingToolCalls] = useState<string[]>([])
83+
const [pendingToolConfirmations, setPendingToolConfirmations] = useState<string[]>([])
8384

8485
const scrollRef = useRef<HTMLDivElement>(null)
8586
const isAtBottomRef = useRef(false)
@@ -141,7 +142,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
141142
last.content.at(-1) &&
142143
last.content.at(-1)!.type === 'text'
143144
) {
144-
;(last.content.at(-1) as { text: string }).text += data.text
145+
; (last.content.at(-1) as { text: string }).text += data.text
145146
}
146147
} else {
147148
prev.push({
@@ -203,6 +204,118 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
203204
[sessionId]
204205
)
205206

207+
const handleToolCallPendingConfirmation = useCallback(
208+
(data: TEvents['Socket::Session::ToolCallPendingConfirmation']) => {
209+
if (data.session_id && data.session_id !== sessionId) {
210+
return
211+
}
212+
213+
const existToolCall = messages.find(
214+
(m) =>
215+
m.role === 'assistant' &&
216+
m.tool_calls &&
217+
m.tool_calls.find((t) => t.id == data.id)
218+
)
219+
220+
if (existToolCall) {
221+
return
222+
}
223+
224+
setMessages(
225+
produce((prev) => {
226+
console.log('👇tool_call_pending_confirmation event get', data)
227+
setPending('tool')
228+
prev.push({
229+
role: 'assistant',
230+
content: '',
231+
tool_calls: [
232+
{
233+
type: 'function',
234+
function: {
235+
name: data.name,
236+
arguments: data.arguments,
237+
},
238+
id: data.id,
239+
},
240+
],
241+
})
242+
})
243+
)
244+
245+
setPendingToolConfirmations(
246+
produce((prev) => {
247+
prev.push(data.id)
248+
})
249+
)
250+
251+
// 自动展开需要确认的工具调用
252+
setExpandingToolCalls(
253+
produce((prev) => {
254+
if (!prev.includes(data.id)) {
255+
prev.push(data.id)
256+
}
257+
})
258+
)
259+
},
260+
[sessionId]
261+
)
262+
263+
const handleToolCallConfirmed = useCallback(
264+
(data: TEvents['Socket::Session::ToolCallConfirmed']) => {
265+
if (data.session_id && data.session_id !== sessionId) {
266+
return
267+
}
268+
269+
setPendingToolConfirmations(
270+
produce((prev) => {
271+
return prev.filter((id) => id !== data.id)
272+
})
273+
)
274+
275+
setExpandingToolCalls(
276+
produce((prev) => {
277+
if (!prev.includes(data.id)) {
278+
prev.push(data.id)
279+
}
280+
})
281+
)
282+
},
283+
[sessionId]
284+
)
285+
286+
const handleToolCallCancelled = useCallback(
287+
(data: TEvents['Socket::Session::ToolCallCancelled']) => {
288+
if (data.session_id && data.session_id !== sessionId) {
289+
return
290+
}
291+
292+
setPendingToolConfirmations(
293+
produce((prev) => {
294+
return prev.filter((id) => id !== data.id)
295+
})
296+
)
297+
298+
// 更新工具调用的状态
299+
setMessages(
300+
produce((prev) => {
301+
prev.forEach((msg) => {
302+
if (msg.role === 'assistant' && msg.tool_calls) {
303+
msg.tool_calls.forEach((tc) => {
304+
if (tc.id === data.id) {
305+
// 添加取消状态标记
306+
tc.result = "工具调用已取消"
307+
}
308+
})
309+
}
310+
})
311+
})
312+
)
313+
},
314+
[sessionId]
315+
)
316+
317+
318+
206319
const handleToolCallArguments = useCallback(
207320
(data: TEvents['Socket::Session::ToolCallArguments']) => {
208321
if (data.session_id && data.session_id !== sessionId) {
@@ -224,14 +337,18 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
224337
(t) => t.id == data.id
225338
)
226339
if (toolCall) {
340+
// 检查是否是待确认的工具调用,如果是则跳过参数追加
341+
if (pendingToolConfirmations.includes(data.id)) {
342+
return
343+
}
227344
toolCall.function.arguments += data.text
228345
}
229346
}
230347
})
231348
)
232349
scrollToBottom()
233350
},
234-
[sessionId, scrollToBottom]
351+
[sessionId, scrollToBottom, pendingToolConfirmations]
235352
)
236353

237354
const handleToolCallResult = useCallback(
@@ -333,6 +450,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
333450

334451
eventBus.on('Socket::Session::Delta', handleDelta)
335452
eventBus.on('Socket::Session::ToolCall', handleToolCall)
453+
eventBus.on('Socket::Session::ToolCallPendingConfirmation', handleToolCallPendingConfirmation)
454+
eventBus.on('Socket::Session::ToolCallConfirmed', handleToolCallConfirmed)
455+
eventBus.on('Socket::Session::ToolCallCancelled', handleToolCallCancelled)
336456
eventBus.on('Socket::Session::ToolCallArguments', handleToolCallArguments)
337457
eventBus.on('Socket::Session::ToolCallResult', handleToolCallResult)
338458
eventBus.on('Socket::Session::ImageGenerated', handleImageGenerated)
@@ -345,6 +465,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
345465

346466
eventBus.off('Socket::Session::Delta', handleDelta)
347467
eventBus.off('Socket::Session::ToolCall', handleToolCall)
468+
eventBus.off('Socket::Session::ToolCallPendingConfirmation', handleToolCallPendingConfirmation)
469+
eventBus.off('Socket::Session::ToolCallConfirmed', handleToolCallConfirmed)
470+
eventBus.off('Socket::Session::ToolCallCancelled', handleToolCallCancelled)
348471
eventBus.off(
349472
'Socket::Session::ToolCallArguments',
350473
handleToolCallArguments
@@ -506,6 +629,35 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
506629
])
507630
}
508631
}}
632+
requiresConfirmation={pendingToolConfirmations.includes(toolCall.id)}
633+
onConfirm={() => {
634+
// 发送确认事件到后端
635+
fetch('/api/tool_confirmation', {
636+
method: 'POST',
637+
headers: {
638+
'Content-Type': 'application/json',
639+
},
640+
body: JSON.stringify({
641+
session_id: sessionId,
642+
tool_call_id: toolCall.id,
643+
confirmed: true,
644+
}),
645+
})
646+
}}
647+
onCancel={() => {
648+
// 发送取消事件到后端
649+
fetch('/api/tool_confirmation', {
650+
method: 'POST',
651+
headers: {
652+
'Content-Type': 'application/json',
653+
},
654+
body: JSON.stringify({
655+
session_id: sessionId,
656+
tool_call_id: toolCall.id,
657+
confirmed: false,
658+
}),
659+
})
660+
}}
509661
/>
510662
)
511663
})}

react/src/components/chat/Message/ToolCallTag.tsx

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
22
import { TOOL_CALL_NAME_MAPPING } from '@/constants'
33
import { cn } from '@/lib/utils'
44
import { ToolCall } from '@/types/types'
5-
import { ChevronDown, ChevronRight } from 'lucide-react'
5+
import { ChevronDown, ChevronRight, AlertTriangle, Check, X } from 'lucide-react'
66
import { AnimatePresence, motion } from 'motion/react'
77
import Markdown from 'react-markdown'
88
import MultiChoicePrompt from '../MultiChoicePrompt'
@@ -17,12 +17,18 @@ type ToolCallTagProps = {
1717
toolCall: ToolCall
1818
isExpanded: boolean
1919
onToggleExpand: () => void
20+
requiresConfirmation?: boolean
21+
onConfirm?: () => void
22+
onCancel?: () => void
2023
}
2124

2225
const ToolCallTag: React.FC<ToolCallTagProps> = ({
2326
toolCall,
2427
isExpanded,
2528
onToggleExpand,
29+
requiresConfirmation = false,
30+
onConfirm,
31+
onCancel,
2632
}) => {
2733
const { name, arguments: inputs } = toolCall.function
2834

@@ -38,15 +44,31 @@ const ToolCallTag: React.FC<ToolCallTagProps> = ({
3844
if (name.startsWith('transfer_to')) {
3945
return null
4046
}
47+
48+
const needsConfirmation = requiresConfirmation
49+
4150
let parsedArgs = null
42-
if (inputs.endsWith('}')) {
51+
try {
52+
parsedArgs = JSON.parse(inputs)
53+
} catch (error) {
54+
console.error('Error parsing args:', error, 'Raw input:', inputs)
55+
// 尝试清理输入字符串,移除可能的额外内容
4356
try {
44-
parsedArgs = JSON.parse(inputs)
45-
} catch (error) {
46-
console.error('Error parsing args:', error)
57+
const cleanedInput = inputs.trim()
58+
const jsonEndIndex = cleanedInput.lastIndexOf('}')
59+
if (jsonEndIndex > 0) {
60+
const jsonPart = cleanedInput.substring(0, jsonEndIndex + 1)
61+
parsedArgs = JSON.parse(jsonPart)
62+
console.log('Successfully parsed cleaned JSON:', jsonPart)
63+
}
64+
} catch (cleanError) {
65+
console.error('Failed to parse even after cleaning:', cleanError)
4766
}
4867
}
4968

69+
70+
71+
// 普通模式的样式
5072
return (
5173
<div className="bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 rounded-md shadow-sm overflow-hidden">
5274
{/* Header */}
@@ -76,6 +98,18 @@ const ToolCallTag: React.FC<ToolCallTagProps> = ({
7698
</p>
7799
</div>
78100
<div className="flex items-center gap-2">
101+
{needsConfirmation && (
102+
<div className="bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 text-xs px-2 py-0.5 rounded-full flex items-center gap-1">
103+
<AlertTriangle className="h-3 w-3" />
104+
需确认
105+
</div>
106+
)}
107+
{!needsConfirmation && toolCall.result === "工具调用已取消" && (
108+
<div className="bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-xs px-2 py-0.5 rounded-full flex items-center gap-1">
109+
<X className="h-3 w-3" />
110+
已取消
111+
</div>
112+
)}
79113
{parsedArgs && Object.keys(parsedArgs).length > 0 && (
80114
<div className="bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200 text-xs px-2 py-0.5 rounded-full">
81115
{Object.keys(parsedArgs).length}
@@ -121,6 +155,39 @@ const ToolCallTag: React.FC<ToolCallTagProps> = ({
121155
</div>
122156
)}
123157
{toolCall.result && <ToolCallContentV2 content={toolCall.result} />}
158+
159+
{/* 确认按钮 - 仅在需要确认时显示 */}
160+
{needsConfirmation && (
161+
<div className="mt-4 pt-4 border-t border-green-200 dark:border-green-800">
162+
<div className="flex gap-2">
163+
<Button
164+
onClick={onConfirm}
165+
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
166+
>
167+
<Check className="h-4 w-4 mr-2" />
168+
确认执行
169+
</Button>
170+
<Button
171+
onClick={onCancel}
172+
variant="outline"
173+
className="flex-1 border-green-300 text-green-700 hover:bg-green-100"
174+
>
175+
<X className="h-4 w-4 mr-2" />
176+
取消
177+
</Button>
178+
</div>
179+
</div>
180+
)}
181+
182+
{/* 取消状态显示 */}
183+
{!needsConfirmation && toolCall.result === "工具调用已取消" && (
184+
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
185+
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
186+
<X className="h-4 w-4" />
187+
<span className="text-sm">工具调用已取消</span>
188+
</div>
189+
</div>
190+
)}
124191
</div>
125192
</div>
126193
)}

react/src/lib/event.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export type TEvents = {
3737
'Socket::Session::ToolCallResult': ISocket.SessionToolCallResultEvent
3838
'Socket::Session::AllMessages': ISocket.SessionAllMessagesEvent
3939
'Socket::Session::ToolCallProgress': ISocket.SessionToolCallProgressEvent
40+
'Socket::Session::ToolCallPendingConfirmation': ISocket.SessionToolCallPendingConfirmationEvent
41+
'Socket::Session::ToolCallConfirmed': ISocket.SessionToolCallConfirmedEvent
42+
'Socket::Session::ToolCallCancelled': ISocket.SessionToolCallCancelledEvent
4043
// ********** Socket events - End **********
4144

4245
// ********** Canvas events - Start **********

react/src/lib/socket.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ export class SocketIOManager {
101101
case ISocket.SessionEventType.ToolCall:
102102
eventBus.emit('Socket::Session::ToolCall', data)
103103
break
104+
case ISocket.SessionEventType.ToolCallPendingConfirmation:
105+
eventBus.emit('Socket::Session::ToolCallPendingConfirmation', data)
106+
break
107+
case ISocket.SessionEventType.ToolCallConfirmed:
108+
eventBus.emit('Socket::Session::ToolCallConfirmed', data)
109+
break
110+
case ISocket.SessionEventType.ToolCallCancelled:
111+
eventBus.emit('Socket::Session::ToolCallCancelled', data)
112+
break
104113
case ISocket.SessionEventType.ToolCallArguments:
105114
eventBus.emit('Socket::Session::ToolCallArguments', data)
106115
break

0 commit comments

Comments
 (0)