admin-projects/src/pages/TaskAssistant.tsx

203 lines
5.9 KiB
TypeScript
Raw Normal View History

import { useState, useRef, useEffect } from 'react'
import { Typography, Input, Button, Space, Avatar, Spin, theme } from 'antd'
import { SendOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons'
import { sendMessage } from '@/services/ai-chat'
const { Title, Text } = Typography
const { TextArea } = Input
interface Message {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}
export default function TaskAssistant() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const { token } = theme.useToken()
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const handleSend = async () => {
const text = input.trim()
if (!text || loading) return
const userMsg: Message = {
id: Date.now().toString(),
role: 'user',
content: text,
timestamp: Date.now(),
}
setMessages(prev => [...prev, userMsg])
setInput('')
setLoading(true)
try {
const reply = await sendMessage([...messages, userMsg].map(m => ({
role: m.role,
content: m.content,
})))
const assistantMsg: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: reply,
timestamp: Date.now(),
}
setMessages(prev => [...prev, assistantMsg])
} catch (err) {
const errorMsg: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: '抱歉,请求失败:' + (err instanceof Error ? err.message : '未知错误'),
timestamp: Date.now(),
}
setMessages(prev => [...prev, errorMsg])
} finally {
setLoading(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 112px)' }}>
<div style={{ marginBottom: 16 }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary">AI </Text>
</div>
<div
style={{
flex: 1,
overflowY: 'auto',
background: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
padding: '20px 24px',
marginBottom: 16,
border: `1px solid ${token.colorBorderSecondary}`,
}}
>
{messages.length === 0 && (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: token.colorTextQuaternary,
}}
>
<RobotOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<Text type="secondary" style={{ fontSize: 16 }}></Text>
<Text type="secondary" style={{ fontSize: 13, marginTop: 8 }}>
AI
</Text>
</div>
)}
{messages.map(msg => (
<div
key={msg.id}
style={{
display: 'flex',
gap: 12,
marginBottom: 20,
flexDirection: msg.role === 'user' ? 'row-reverse' : 'row',
}}
>
<Avatar
size={36}
icon={msg.role === 'user' ? <UserOutlined /> : <RobotOutlined />}
style={{
backgroundColor: msg.role === 'user' ? token.colorPrimary : token.colorSuccess,
flexShrink: 0,
}}
/>
<div
style={{
maxWidth: '72%',
padding: '10px 16px',
borderRadius: 12,
background: msg.role === 'user'
? token.colorPrimary
: token.colorFillAlter,
color: msg.role === 'user' ? '#fff' : token.colorText,
lineHeight: 1.7,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{msg.content}
</div>
</div>
))}
{loading && (
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
<Avatar
size={36}
icon={<RobotOutlined />}
style={{ backgroundColor: token.colorSuccess, flexShrink: 0 }}
/>
<div
style={{
maxWidth: '72%',
padding: '10px 16px',
borderRadius: 12,
background: token.colorFillAlter,
}}
>
<Spin size="small" /> <Text type="secondary">...</Text>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div
style={{
padding: '12px 16px',
background: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
border: `1px solid ${token.colorBorderSecondary}`,
}}
>
<Space.Compact style={{ width: '100%' }}>
<TextArea
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入你的问题Enter 发送Shift+Enter 换行"
autoSize={{ minRows: 1, maxRows: 4 }}
disabled={loading}
style={{ resize: 'none' }}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
disabled={!input.trim()}
style={{ height: 'auto' }}
>
</Button>
</Space.Compact>
</div>
</div>
)
}