phase 5 part 1: typescript, vue, react, ssr(Next.js 16) topics with lessons, scenarios, interviews, verified examples
This commit is contained in:
1 parent
7a32d2c9a6
commit
7b5cae7d24
39 files changed
+2482
-48
No files matched your search
@@ -0,0 +1,45 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user