Files

120 lines
3.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// AI 免费对话 - 前端逻辑
(function () {
const chatEl = document.getElementById("chat");
const inputEl = document.getElementById("input");
const sendBtn = document.getElementById("send");
// 保存对话历史(用于多轮上下文)
const history = [];
let sending = false;
/** 获取当前选择的 provider */
function getProvider() {
const el = document.querySelector('input[name="provider"]:checked');
return el ? el.value : "deepseek";
}
/** 追加一条消息到界面 */
function appendMessage(role, content) {
const wrapper = document.createElement("div");
wrapper.className = `message ${role}`;
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = content;
wrapper.appendChild(bubble);
chatEl.appendChild(wrapper);
chatEl.scrollTop = chatEl.scrollHeight;
return bubble;
}
/** 发送消息并接收 SSE 流 */
async function send() {
if (sending) return;
const text = inputEl.value.trim();
if (!text) return;
sending = true;
sendBtn.disabled = true;
inputEl.value = "";
// 展示用户消息
appendMessage("user", text);
history.push({ role: "user", content: text });
// 展示助手占位
const bubble = appendMessage("assistant", "");
bubble.classList.add("cursor");
let assistantText = "";
try {
const resp = await fetch("api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: getProvider(),
messages: history,
}),
});
if (!resp.ok || !resp.body) {
throw new Error("网络请求失败");
}
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按 SSE 事件分隔(\n\n)
const parts = buffer.split("\n\n");
buffer = parts.pop(); // 保留最后不完整片段
for (const part of parts) {
const line = part.trim();
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
try {
const obj = JSON.parse(data);
if (obj.error) {
assistantText += `\n[错误] ${obj.error}`;
bubble.textContent = assistantText;
} else if (obj.content) {
assistantText += obj.content;
bubble.textContent = assistantText;
chatEl.scrollTop = chatEl.scrollHeight;
}
} catch (e) {
// 忽略解析错误
}
}
}
} catch (err) {
assistantText += `\n[错误] ${err.message}`;
bubble.textContent = assistantText;
} finally {
bubble.classList.remove("cursor");
if (assistantText) {
history.push({ role: "assistant", content: assistantText });
}
sending = false;
sendBtn.disabled = false;
inputEl.focus();
}
}
// 事件绑定
sendBtn.addEventListener("click", send);
inputEl.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
});
})();