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
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s
This commit is contained in:
parent
7438323e96
commit
cf2dfc1351
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user