1. 向上滚动节流获取对话历史。
import { throttle } from "lodash";
const scrollHandle = throttle(function (event) {
const element = event.target;
// 处理切换对话的问题
if (element.scrollHeight <= element.offsetHeight) {
return;
}
// 当触顶时请求
if (element.scrollTop <= 100) {
// 请求逻辑
const preScrollHeight = element.scrollHeight;
// 下次渲染 await $nextTick(),拉取数据后保持原位
element.scrollTo(0, element.scrollHeight - preScrollHeight);
}
}, 1000);
2. 使用 Clipborad 复制文本。
import ClipboardJS from "clipboard";
// 这里是创建一个按钮,每次通过 buttonElement.click() 方式触发
const clipboard = new ClipboardJS("#copy-btn", {
text: () => `你要复制的文本`,
});
clipboard.on("success", () => {
console.log("复制成功");
});
clipboard.on("error", () => {
console.log("复制失败");
});
3. AbortController 终止 fetch 请求
let abortController = new AbortController();
// 比如在销毁之前、切换对话时使用。
const cancelFetchHandle = () => {
if (abortController) {
abortController.abort();
}
};
const fetchRequest = async () => {
cancelFetchHandle();
abortController = new AbortController();
try {
const response = await fetch(`url`, {
method: "get",
headers: {
Authorization: `token`,
},
signal: abortController.signal,
});
} catch (error) {
console.error(error);
} finally {
abortController = null;
}
};
终止 axios 请求
import axios from 'axios';
const cancelSource = axios.CancelToken.source();
const axiosRequest = async () => {
try {
// request 即 axios.create() 实例
const response = await request.get(`url`, { cancelToken: cancelSource.token });
} catch (error) {
console.error(error);
}
}
// 保持统一
let abortController = { abort: (reason = 'abort') => cancelSource.cancel(reason) };
4. 对话流
yarn add @fortaine/fetch-event-source
本段代码来自 ChatGPT-Next-Web
import {
fetchEventSource,
EventStreamContentType,
} from "@fortaine/fetch-event-source";
const prettyObject = (msg) => {
const obj = msg;
if (typeof msg !== "string") {
msg = JSON.stringify(msg, null, " ");
}
if (msg === "{}") {
return obj.toString();
}
if (msg.startsWith("```json")) {
return msg;
}
return ["```json", msg, "```"].join("\n");
};
const fetchRequest = (request) => {
// 超时时间
const REQUEST_TIMEOUT_MS = 60000;
const abortAnswerController = new AbortController();
const requestTimeoutId = setTimeout(
() => abortAnswerController.abort(),
REQUEST_TIMEOUT_MS
);
// status 表示是否正在获取回答
let status = 0;
let responseText = "";
let remainText = "";
let finished = false;
let started = false;
// animate response to make it looks smooth
const animateResponseText = () => {
if (
(finished && remainText.length <= 0) ||
abortAnswerController.signal.aborted
) {
return;
}
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
// replace you answer
// 注意宏任务、微任务问题
responseText;
requestAnimationFrame(animateResponseText);
};
const chatPath = `chat-request-url`;
const chatPayload = {
method: "POST",
headers: {
Authorization: localStorage.getItem("token"),
},
signal: abortAnswerController.signal,
body: JSON.stringify({ request }),
};
const finish = () => {
status = 0;
finished = true;
};
const error = (e) => {
status = 0;
finished = true;
console.error(e);
};
status = 1;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push("登录已过期。");
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices;
const delta = choices[0]?.delta?.content;
if (delta) {
remainText += delta;
if (!started) {
// start animaion
animateResponseText();
started = true;
}
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
error(e);
},
openWhenHidden: true,
});
};
5. markdown
import MarkdownIt from "markdown-it";
const md = new MarkdownIt();
const renderAsMarkdown = (text = "") => {
return md.render(text);
};
样式 github-markdown-css 直接下载引入进来,或者直接找个 gihub 项目把 Markdown.scss、HighLight.scss 拷贝过来
<div class="markdown-body"></div>
6. 获取回答时光标输入效果
.answer-animation {
& > :last-child::after {
display: inline-block;
content: "";
width: 3px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
background-color: #666;
animation: blink 0.6s infinite;
}
@keyframes blink {
from,
to {
opacity: 0;
}
50% {
opacity: 1;
}
}
}