46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
"use client";
|
|
// 不自动运行
|
|
// 客户端组件:有状态和事件处理。读取路由处理器返回的流,逐段显示。
|
|
import { useRef, useState } from "react";
|
|
|
|
export function Chat() {
|
|
const [answer, setAnswer] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const ctrl = useRef<AbortController | null>(null);
|
|
|
|
async function ask(formData: FormData) {
|
|
ctrl.current?.abort();
|
|
ctrl.current = new AbortController();
|
|
setAnswer("");
|
|
setBusy(true);
|
|
try {
|
|
const resp = await fetch("/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ question: formData.get("q") }),
|
|
signal: ctrl.current.signal,
|
|
});
|
|
if (!resp.body) throw new Error("没有响应体");
|
|
const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
for (;;) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
setAnswer((a) => a + value);
|
|
}
|
|
} catch (e) {
|
|
if ((e as Error).name !== "AbortError") setAnswer("出错了,请重试");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form action={ask}>
|
|
<input name="q" placeholder="问点什么" required />
|
|
<button disabled={busy}>发送</button>
|
|
{busy && <button type="button" onClick={() => ctrl.current?.abort()}>停止</button>}
|
|
<p aria-live="polite">{answer}</p>
|
|
</form>
|
|
);
|
|
}
|