fix: use App.useApp modal + Button delete for reliable click handling
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s

This commit is contained in:
WangDL 2026-05-22 11:16:33 +08:00
parent 7438323e96
commit cf2dfc1351

View File

@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useCallback } from 'react' import { useState, useRef, useEffect, useCallback } from 'react'
import { Input, Button, Avatar, Spin, theme, Modal, Tooltip, Typography } from 'antd' import { Input, Button, Avatar, Spin, theme, Typography, App } from 'antd'
import { import {
SendOutlined, RobotOutlined, UserOutlined, PlusOutlined, SendOutlined, RobotOutlined, UserOutlined, PlusOutlined,
DeleteOutlined, StopOutlined, MessageOutlined, DeleteOutlined, StopOutlined, MessageOutlined,
@ -20,7 +20,8 @@ interface Message {
timestamp: number timestamp: number
} }
export default function TaskAssistant() { function ChatPage() {
const { modal } = App.useApp()
const [conversations, setConversations] = useState<Conversation[]>([]) const [conversations, setConversations] = useState<Conversation[]>([])
const [activeId, setActiveId] = useState<string | null>(null) const [activeId, setActiveId] = useState<string | null>(null)
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
@ -28,23 +29,21 @@ export default function TaskAssistant() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [editTitle, setEditTitle] = useState('') const [editTitle, setEditTitle] = useState('')
const [deleting, setDeleting] = useState(false)
const abortRef = useRef<AbortController | null>(null) const abortRef = useRef<AbortController | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const editInputRef = useRef<any>(null) const editInputRef = useRef<any>(null)
const { token } = theme.useToken() const { token } = theme.useToken()
// Load conversations
const loadConversations = useCallback(async () => { const loadConversations = useCallback(async () => {
try { setConversations(await listConversations()) } catch { /* */ } try { setConversations(await listConversations()) } catch { /* */ }
}, []) }, [])
useEffect(() => { loadConversations() }, [loadConversations]) useEffect(() => { loadConversations() }, [loadConversations])
// Scroll to bottom
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages]) }, [messages])
// Load messages when switching conversation
const switchConversation = useCallback(async (id: string) => { const switchConversation = useCallback(async (id: string) => {
if (loading) { abortRef.current?.abort(); setLoading(false) } if (loading) { abortRef.current?.abort(); setLoading(false) }
setActiveId(id) setActiveId(id)
@ -58,7 +57,6 @@ export default function TaskAssistant() {
} catch { /* */ } } catch { /* */ }
}, [loading]) }, [loading])
// New conversation
const handleNew = async () => { const handleNew = async () => {
if (loading) { abortRef.current?.abort(); setLoading(false) } if (loading) { abortRef.current?.abort(); setLoading(false) }
try { try {
@ -70,27 +68,31 @@ export default function TaskAssistant() {
} catch { /* */ } } catch { /* */ }
} }
// Delete conversation const handleDelete = (id: string) => {
const handleDelete = async (id: string) => { modal.confirm({
Modal.confirm({ title: '删除对话',
title: '删除对话', content: '确定要删除这个对话吗?', content: '确定要删除这个对话吗?',
okText: '删除', okType: 'danger', cancelText: '取消', okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => { onOk: async () => {
await deleteConversation(id).catch(() => {}) setDeleting(true)
setConversations(prev => prev.filter(c => c.id !== id)) try {
if (activeId === id) { setActiveId(null); setMessages([]) } await deleteConversation(id)
setConversations(prev => prev.filter(c => c.id !== id))
if (activeId === id) { setActiveId(null); setMessages([]) }
} catch { /* */ }
setDeleting(false)
}, },
}) })
} }
// Start editing title
const startEdit = (conv: Conversation) => { const startEdit = (conv: Conversation) => {
setEditingId(conv.id) setEditingId(conv.id)
setEditTitle(conv.title) setEditTitle(conv.title)
setTimeout(() => editInputRef.current?.focus(), 50) setTimeout(() => editInputRef.current?.focus(), 50)
} }
// Save title
const saveTitle = async (id: string) => { const saveTitle = async (id: string) => {
const title = editTitle.trim() const title = editTitle.trim()
if (title && title !== conversations.find(c => c.id === id)?.title) { if (title && title !== conversations.find(c => c.id === id)?.title) {
@ -100,7 +102,6 @@ export default function TaskAssistant() {
setEditingId(null) setEditingId(null)
} }
// Send message
const handleSend = async () => { const handleSend = async () => {
const text = input.trim() const text = input.trim()
if (!text || loading) return if (!text || loading) return
@ -128,11 +129,10 @@ export default function TaskAssistant() {
loadConversations() loadConversations()
} }
const assistantMsg: Message = { setMessages(prev => [...prev, {
id: (Date.now() + 1).toString(), role: 'assistant', id: (Date.now() + 1).toString(), role: 'assistant',
content: result.content, timestamp: Date.now(), content: result.content, timestamp: Date.now(),
} }])
setMessages(prev => [...prev, assistantMsg])
loadConversations() loadConversations()
} catch (err: any) { } catch (err: any) {
if (err.name === 'AbortError') return if (err.name === 'AbortError') return
@ -155,7 +155,6 @@ export default function TaskAssistant() {
return ( return (
<div style={{ display: 'flex', height: 'calc(100vh - 112px)', gap: 12 }}> <div style={{ display: 'flex', height: 'calc(100vh - 112px)', gap: 12 }}>
{/* Sidebar */}
<div style={{ <div style={{
width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column',
background: token.colorBgContainer, borderRadius: token.borderRadiusLG, background: token.colorBgContainer, borderRadius: token.borderRadiusLG,
@ -184,18 +183,18 @@ export default function TaskAssistant() {
style={{ flex: 1, fontSize: 13 }} style={{ flex: 1, fontSize: 13 }}
/> />
) : ( ) : (
<Text <Text ellipsis style={{ flex: 1, fontSize: 13, lineHeight: '28px' }}
ellipsis onDoubleClick={e => { e.stopPropagation(); startEdit(conv) }}>
style={{ flex: 1, fontSize: 13, lineHeight: '28px' }} {conv.title}
onDoubleClick={e => { e.stopPropagation(); startEdit(conv) }} </Text>
>{conv.title}</Text>
)} )}
<Tooltip title="删除"> <Button
<DeleteOutlined type="text" size="small" danger
onClick={e => { e.stopPropagation(); handleDelete(conv.id) }} icon={<DeleteOutlined />}
style={{ fontSize: 12, color: token.colorTextQuaternary, marginLeft: 4, cursor: 'pointer' }} disabled={deleting}
/> onClick={e => { e.stopPropagation(); handleDelete(conv.id) }}
</Tooltip> style={{ marginLeft: 4, flexShrink: 0 }}
/>
</div> </div>
))} ))}
{conversations.length === 0 && ( {conversations.length === 0 && (
@ -208,9 +207,7 @@ export default function TaskAssistant() {
</div> </div>
</div> </div>
{/* Chat area */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{/* Messages */}
<div style={{ <div style={{
flex: 1, overflowY: 'auto', background: token.colorBgContainer, flex: 1, overflowY: 'auto', background: token.colorBgContainer,
borderRadius: `${token.borderRadiusLG}px ${token.borderRadiusLG}px 0 0`, borderRadius: `${token.borderRadiusLG}px ${token.borderRadiusLG}px 0 0`,
@ -270,9 +267,8 @@ export default function TaskAssistant() {
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Input area — redesigned */}
<div style={{ <div style={{
background: token.colorBgContainer, borderTop: 'none', background: token.colorBgContainer,
border: `1px solid ${token.colorBorderSecondary}`, border: `1px solid ${token.colorBorderSecondary}`,
borderRadius: `0 0 ${token.borderRadiusLG}px ${token.borderRadiusLG}px`, borderRadius: `0 0 ${token.borderRadiusLG}px ${token.borderRadiusLG}px`,
padding: '16px 20px', padding: '16px 20px',
@ -293,18 +289,12 @@ export default function TaskAssistant() {
style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 14 }} style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 14 }}
/> />
{loading ? ( {loading ? (
<Button <Button danger type="primary" icon={<StopOutlined />} onClick={handleStop}
danger type="primary" icon={<StopOutlined />} onClick={handleStop} style={{ borderRadius: 8, height: 34, minWidth: 72 }}></Button>
style={{ borderRadius: 8, height: 34, minWidth: 72 }}>
</Button>
) : ( ) : (
<Button <Button type="primary" icon={<SendOutlined />} onClick={handleSend}
type="primary" icon={<SendOutlined />} onClick={handleSend}
disabled={!input.trim() || !activeId} disabled={!input.trim() || !activeId}
style={{ borderRadius: 8, height: 34, minWidth: 72 }}> style={{ borderRadius: 8, height: 34, minWidth: 72 }}></Button>
</Button>
)} )}
</div> </div>
</div> </div>
@ -312,3 +302,11 @@ export default function TaskAssistant() {
</div> </div>
) )
} }
export default function TaskAssistant() {
return (
<App>
<ChatPage />
</App>
)
}