first commit
This commit is contained in:
181
src/App.js
Normal file
181
src/App.js
Normal file
@@ -0,0 +1,181 @@
|
||||
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([
|
||||
{
|
||||
id: 0,
|
||||
role: "assistant",
|
||||
content: "你好,有什么可以帮你?",
|
||||
},
|
||||
]);
|
||||
|
||||
const [inputText, setInputText] = useState("");
|
||||
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [isBotTyping, setIsBotTyping] = useState(false);
|
||||
|
||||
const msgIdRef = useRef(1);
|
||||
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 = {
|
||||
id: msgIdRef.current++,
|
||||
role: "user",
|
||||
content: text,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputText("");
|
||||
setIsSending(true);
|
||||
setIsBotTyping(true);
|
||||
|
||||
// 给模型的上下文
|
||||
const historyForModel = [
|
||||
{ role: "system", content: "你是一个简洁的助手,用中文回答。" },
|
||||
...messages.map(({ role, content }) => ({ role, content })),
|
||||
{ role: "user", content: text },
|
||||
];
|
||||
|
||||
let reply = "";
|
||||
try {
|
||||
reply = await requestBotReply(historyForModel);
|
||||
} catch (e) {
|
||||
reply = "出错了,请稍后再试。";
|
||||
}
|
||||
|
||||
const botMessage = {
|
||||
id: msgIdRef.current++,
|
||||
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 (
|
||||
// min-h-screen 最小高度 = 整个屏幕高度
|
||||
// bg-gradient-to-b 背景是 从上到下的渐变
|
||||
// from-background 渐变起点颜色
|
||||
// to-content2/50 渐变终点颜色 /50 = 50% 透明度
|
||||
// p-4 控制四周内距
|
||||
<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
|
||||
key={m.id}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user