first commit

This commit is contained in:
2025-12-29 16:55:59 +08:00
commit 12e699ccb8
13 changed files with 19551 additions and 0 deletions

181
src/App.js Normal file
View 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>
);
}