172 lines
4.9 KiB
JavaScript
172 lines
4.9 KiB
JavaScript
import { useEffect, useRef, useState } from "react";
|
||
import {
|
||
Avatar,
|
||
Button,
|
||
Card,
|
||
CardBody,
|
||
CardHeader,
|
||
Divider,
|
||
ScrollShadow,
|
||
Spinner,
|
||
Textarea,
|
||
} from "@heroui/react";
|
||
import { requestBotReply } from "./dsAPI";
|
||
|
||
export default function App() {
|
||
const [messages, setMessages] = useState([
|
||
{
|
||
role: "assistant",
|
||
content: "你好,有什么可以帮你?",
|
||
},
|
||
]);
|
||
|
||
const [inputText, setInputText] = useState("");
|
||
|
||
const [isSending, setIsSending] = useState(false);
|
||
const [isBotTyping, setIsBotTyping] = useState(false);
|
||
|
||
const bottomRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||
}, [messages, isBotTyping]);
|
||
|
||
async function handleSend() {
|
||
const text = inputText.trim();
|
||
if (!text || isSending) return;
|
||
|
||
const userMessage = {
|
||
role: "user",
|
||
content: text,
|
||
};
|
||
|
||
const nextMessages = [...messages, userMessage];
|
||
setMessages(nextMessages);
|
||
setInputText("");
|
||
setIsSending(true);
|
||
setIsBotTyping(true);
|
||
|
||
// 给模型的上下文
|
||
const historyForModel = [
|
||
{ role: "system", content: "你是一个简洁的助手,用中文回答。" },
|
||
...nextMessages.map(({ role, content }) => ({ role, content })),
|
||
];
|
||
|
||
let reply = "";
|
||
try {
|
||
reply = await requestBotReply(historyForModel);
|
||
} catch (e) {
|
||
reply = "出错了,请稍后再试。";
|
||
}
|
||
|
||
const botMessage = {
|
||
role: "assistant",
|
||
content: reply,
|
||
};
|
||
|
||
setMessages((prev) => [...prev, botMessage]);
|
||
setIsBotTyping(false);
|
||
setIsSending(false);
|
||
}
|
||
|
||
function handleKeyDown(e) {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
handleSend();
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-background to-content2/50 p-4">
|
||
<div className="mx-auto max-w-3xl">
|
||
<Card className="shadow-xl">
|
||
{/* 顶部 */}
|
||
<CardHeader className="flex items-center gap-3">
|
||
<Avatar name="Bot" />
|
||
<div className="flex flex-col">
|
||
<span className="font-semibold">Chatbot Demo</span>
|
||
<span className="text-xs text-default-500">
|
||
</span>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<Divider />
|
||
|
||
{/* 聊天区域 */}
|
||
<CardBody className="p-0">
|
||
<ScrollShadow className="h-[520px] px-4 py-4">
|
||
<div className="flex flex-col gap-3">
|
||
{messages.map((m) => {
|
||
const isUser = m.role === "user";
|
||
return (
|
||
<div
|
||
className={`flex ${isUser ? "justify-end" : "justify-start"}`}
|
||
>
|
||
<div
|
||
className={`flex max-w-[80%] items-end gap-2 ${isUser ? "flex-row-reverse" : ""}`}
|
||
>
|
||
<Avatar
|
||
size="sm"
|
||
name={isUser ? "You" : "Bot"}
|
||
/>
|
||
<div
|
||
className={`rounded-2xl px-4 py-3 text-sm leading-relaxed ${isUser
|
||
? "bg-primary text-primary-foreground"
|
||
: "bg-content2 text-foreground"
|
||
}`}
|
||
>
|
||
{m.content}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
{isBotTyping && (
|
||
<div className="flex justify-start">
|
||
<div className="flex items-center gap-2">
|
||
<Avatar size="sm" name="Bot" />
|
||
<div className="flex items-center gap-2 rounded-2xl bg-content2 px-4 py-2 text-sm text-default-600">
|
||
<Spinner size="sm" />
|
||
正在回复…
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div ref={bottomRef} />
|
||
</div>
|
||
</ScrollShadow>
|
||
|
||
<Divider />
|
||
|
||
{/* 输入区 */}
|
||
<div className="p-4">
|
||
<div className="flex items-end gap-3">
|
||
<Textarea
|
||
value={inputText}
|
||
onValueChange={setInputText}
|
||
onKeyDown={handleKeyDown}
|
||
placeholder="输入消息,Enter 发送,Shift+Enter 换行"
|
||
minRows={2}
|
||
maxRows={6}
|
||
isDisabled={isSending}
|
||
variant="bordered"
|
||
className="flex-1"
|
||
/>
|
||
<Button
|
||
color="primary"
|
||
onPress={handleSend}
|
||
isDisabled={isSending || !inputText.trim()}
|
||
>
|
||
{isSending ? "发送中…" : "发送"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardBody>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|