203 lines
5.9 KiB
TypeScript
203 lines
5.9 KiB
TypeScript
|
|
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>
|
|||
|
|
)
|
|||
|
|
}
|