feat:增加重置功能

This commit is contained in:
2025-12-29 18:57:44 +08:00
parent 08efdf4cd3
commit 3a3c007e33

View File

@@ -23,6 +23,8 @@ export default function App() {
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isBotTyping, setIsBotTyping] = useState(false); const [isBotTyping, setIsBotTyping] = useState(false);
const [rethinkTargetIdx, setRethinkTargetIdx] = useState(null);
const bottomRef = useRef(null); const bottomRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -67,6 +69,49 @@ export default function App() {
setIsSending(false); setIsSending(false);
} }
async function handleRethink(targetIdx) {
if (isSending || isBotTyping) return;
const target = messages[targetIdx];
if (!target || target.role !== "assistant") return;
setRethinkTargetIdx(targetIdx);
setIsBotTyping(true);
// 把目标消息内容替换成占位文本
setMessages((prev) => {
const next = [...prev];
next[targetIdx] = { ...next[targetIdx], content: "正在重新思考…" };
return next;
});
// 用“该条 assistant 之前的历史” + 重新思考指令
const before = messages.slice(0, targetIdx);
const historyForModel = [
{ role: "system", content: "你是一个简洁的助手,用中文回答。" },
...before.map(({ role, content }) => ({ role, content })),
{
role: "user",
content: "我对你刚才的回答不满意。请重新思考并改进:更准确、更有条理;如有错误请纠正;尽量避免重复原句。",
},
];
let newReply = "";
try {
newReply = await requestBotReply(historyForModel);
} catch (e) {
newReply = "重新思考失败了,请稍后再试。";
}
// 覆盖原来的那条 assistant 内容
setMessages((prev) => {
const next = [...prev];
next[targetIdx] = { ...next[targetIdx], content: newReply };
return next;
});
setIsBotTyping(false);
setRethinkTargetIdx(null);
}
function handleKeyDown(e) { function handleKeyDown(e) {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@@ -78,26 +123,29 @@ export default function App() {
<div className="min-h-screen bg-gradient-to-b from-background to-content2/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"> <div className="mx-auto max-w-3xl">
<Card className="shadow-xl"> <Card className="shadow-xl">
{/* 顶部 */} <CardHeader>
<CardHeader className="flex items-center"> <span className="font-semibold">Chatbot Demo</span>
<div className="flex flex-col">
<span className="font-semibold">Chatbot Demo</span>
<span className="text-xs text-default-500">
</span>
</div>
</CardHeader> </CardHeader>
<Divider /> <Divider />
{/* 聊天区域 */}
<CardBody className="p-0"> <CardBody className="p-0">
<ScrollShadow className="h-[520px] px-4 py-4"> <ScrollShadow className="h-[520px] px-4 py-4">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-4">
{messages.map((m) => { {messages.map((m, idx) => {
const isUser = m.role === "user"; const isUser = m.role === "user";
const isAssistant = m.role === "assistant";
const isInitialGreeting = idx === 0;
const canRethink = isAssistant && !isInitialGreeting;
return ( return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`} > <div
<div className={`flex max-w-[80%] ${isUser ? "flex-row-reverse" : ""}`} > key={idx}
className={`flex ${isUser ? "justify-end" : "justify-start"}`}
>
{/* 以气泡为参照:按钮放在气泡“下面的右侧” */}
<div className="relative max-w-[80%]">
{/* 气泡 */}
<div <div
className={`rounded-2xl px-4 py-3 text-sm leading-relaxed ${isUser className={`rounded-2xl px-4 py-3 text-sm leading-relaxed ${isUser
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
@@ -106,18 +154,51 @@ export default function App() {
> >
{m.content} {m.content}
</div> </div>
{/* 在气泡下方*/}
{canRethink && (
<button
type="button"
title="重新思考"
disabled={isSending || isBotTyping}
onClick={() => handleRethink(idx)}
className="
absolute
right-0
top-full
mt-2
h-7
w-7
rounded-full
border
border-default-200
bg-background
text-default-500
text-sm
shadow-sm
hover:bg-content2
hover:text-foreground
disabled:opacity-40
disabled:cursor-not-allowed
"
>
<span className={rethinkTargetIdx === idx ? "inline-block animate-spin" : ""}>
</span>
</button>
)}
</div> </div>
</div> </div>
); );
})} })}
{isBotTyping && (
{/* 全局“正在回复…” */}
{isBotTyping && rethinkTargetIdx === null && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="flex items-center"> <div className="flex items-center gap-2 rounded-2xl bg-content2 px-4 py-2 text-sm text-default-600">
<div className="flex items-center gap-2 rounded-2xl bg-content2 px-4 py-2 text-sm text-default-600"> <Spinner size="sm" />
<Spinner size="sm" /> 正在回复
正在回复
</div>
</div> </div>
</div> </div>
)} )}
@@ -130,35 +211,32 @@ export default function App() {
{/* 输入区 */} {/* 输入区 */}
<div className="p-4"> <div className="p-4">
{/* 这个容器视觉上就是“一个输入框” */}
<div className="rounded-xl border border-default-200 bg-background shadow-sm"> <div className="rounded-xl border border-default-200 bg-background shadow-sm">
{/* 上半部分:真正可滚动/可拖拉的 textarea */}
<div className="px-3 pt-3"> <div className="px-3 pt-3">
<textarea <textarea
value={inputText} value={inputText}
onChange={(e) => setInputText(e.target.value)} onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="输入消息Enter 发送Shift+Enter 换行" placeholder="输入消息Enter 发送Shift+Enter 换行"
disabled={isSending} disabled={isSending || isBotTyping}
rows={2} rows={2}
className=" className="
w-full bg-transparent outline-none w-full bg-transparent outline-none
text-sm leading-relaxed text-foreground text-sm leading-relaxed text-foreground
placeholder:text-default-400 placeholder:text-default-400
resize-y resize-none
min-h-[72px] max-h-[220px] min-h-[72px] max-h-[220px]
overflow-auto overflow-auto
" "
/> />
</div> </div>
{/* 下半部分:输入框“内部”的工具栏,按钮在右下角 */} <div className="flex justify-end px-3 py-2">
<div className="flex items-center justify-end gap-2 px-3 py-2">
<Button <Button
color="primary" color="primary"
size="sm" size="sm"
onPress={handleSend} onPress={handleSend}
isDisabled={isSending || !inputText.trim()} isDisabled={isSending || isBotTyping || !inputText.trim()}
> >
{isSending ? "发送中…" : "发送"} {isSending ? "发送中…" : "发送"}
</Button> </Button>