Files
AI-webUI-V1/public/app.js
2026-02-15 23:08:47 +08:00

356 lines
9.6 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.
const messagesEl = document.getElementById("messages");
const inputEl = document.getElementById("input");
const sendBtn = document.getElementById("sendBtn");
const newChatBtn = document.getElementById("newChatBtn");
const chatListEl = document.getElementById("chatList");
const clearBtn = document.getElementById("clearBtn");
const sidebarToggleBtn = document.getElementById("sidebarToggle");
const appRootEl = document.querySelector(".app");
let chats = JSON.parse(localStorage.getItem("ds_chats") || "{}");
let currentChatId = localStorage.getItem("ds_current_chat");
let sidebarHidden = localStorage.getItem("ds_sidebar_hidden") === "1";
/** ---------- 时间格式化 ---------- */
function formatChatDate(ts) {
try {
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date(ts));
} catch {
const d = new Date(ts);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
}
function formatMsgDateTime(ts) {
try {
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(new Date(ts));
} catch {
const d = new Date(ts);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
const ss = String(d.getSeconds()).padStart(2, "0");
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`;
}
}
/** ---------- 兼容旧数据:补齐 createdAt/updatedAt/ts ---------- */
function normalizeData() {
const now = Date.now();
for (const id of Object.keys(chats)) {
const chat = chats[id] || {};
if (!chat.title) chat.title = "New Chat";
if (!Array.isArray(chat.messages)) chat.messages = [];
if (!chat.createdAt) {
const first = chat.messages[0];
chat.createdAt = first?.ts || now;
}
if (!chat.updatedAt) {
const last = chat.messages[chat.messages.length - 1];
chat.updatedAt = last?.ts || chat.createdAt;
}
for (const m of chat.messages) {
if (!m.ts) m.ts = chat.createdAt;
}
chats[id] = chat;
}
}
normalizeData();
/** ---------- 保存 ---------- */
function save() {
localStorage.setItem("ds_chats", JSON.stringify(chats));
localStorage.setItem("ds_current_chat", currentChatId || "");
localStorage.setItem("ds_sidebar_hidden", sidebarHidden ? "1" : "0");
}
/** ---------- 侧栏显示/隐藏 ---------- */
function applySidebarState() {
if (!appRootEl) return;
appRootEl.classList.toggle("sidebar-hidden", sidebarHidden);
}
applySidebarState();
if (sidebarToggleBtn) {
sidebarToggleBtn.onclick = () => {
sidebarHidden = !sidebarHidden;
applySidebarState();
save();
};
}
/** ---------- Chat 操作 ---------- */
function newChat() {
const now = Date.now();
currentChatId = crypto.randomUUID();
chats[currentChatId] = {
title: "New Chat",
createdAt: now,
updatedAt: now,
messages: [],
};
save();
renderChatList();
renderMessages();
}
function deleteChat(id) {
if (!id || !chats[id]) return;
const title = chats[id].title || "This chat";
const ok = confirm(`Delete "${title}" ?`);
if (!ok) return;
delete chats[id];
// ✅ 关键:允许删除最后一个。删完后不自动 newChat()
if (currentChatId === id) {
const remainingIds = Object.keys(chats);
currentChatId = remainingIds[0] || null;
}
save();
renderChatList();
renderMessages();
}
/** ---------- 渲染左侧列表(显示创建日期 + 删除按钮 + 按更新时间排序)---------- */
function renderChatList() {
chatListEl.innerHTML = "";
const entries = Object.entries(chats);
entries.sort((a, b) => (b[1].updatedAt || 0) - (a[1].updatedAt || 0));
// 如果没有任何 chat列表保持空不用提示提示放在右侧 messages
if (entries.length === 0) return;
for (const [id, chat] of entries) {
const li = document.createElement("li");
li.className = "chat-item";
if (id === currentChatId) li.classList.add("active");
const left = document.createElement("div");
left.className = "chat-item-left";
const title = document.createElement("div");
title.className = "chat-title";
title.textContent = chat.title || "New Chat";
const date = document.createElement("div");
date.className = "chat-date";
date.textContent = formatChatDate(chat.createdAt || Date.now());
left.appendChild(title);
left.appendChild(date);
const del = document.createElement("button");
del.className = "chat-del";
del.type = "button";
del.title = "Delete";
del.textContent = "🗑";
del.onclick = (e) => {
e.stopPropagation();
deleteChat(id);
};
li.appendChild(left);
li.appendChild(del);
li.onclick = () => {
currentChatId = id;
save();
renderChatList();
renderMessages();
};
chatListEl.appendChild(li);
}
}
/** ---------- 空状态 UI ---------- */
function renderEmptyState(kind) {
// kind: "no-chat" | "no-messages"
const div = document.createElement("div");
div.className = "empty-state";
if (kind === "no-chat") {
div.innerHTML = `
<h3>还没有会话</h3>
<div>你可以:</div>
<ul>
<li>点击左上角 <code></code> 创建新会话</li>
<li>或者直接在下方输入内容并发送(会自动创建新会话)</li>
<li>需要隐藏左侧列表:点顶部 <code>☰</code></li>
</ul>
`;
} else {
div.innerHTML = `
<h3>这个会话还没有消息</h3>
<div>在下方输入内容,按 <code>Enter</code> 发送;<code>Shift+Enter</code> 换行。</div>
`;
}
messagesEl.appendChild(div);
}
/** ---------- 渲染消息(每条消息显示日期时间)---------- */
function renderMessages() {
messagesEl.innerHTML = "";
// 没有任何 chat
if (!currentChatId || !chats[currentChatId]) {
renderEmptyState("no-chat");
return;
}
const chat = chats[currentChatId];
const msgs = chat.messages || [];
// 有 chat 但没消息
if (msgs.length === 0) {
renderEmptyState("no-messages");
return;
}
for (const m of msgs) {
const wrap = document.createElement("div");
wrap.className = `message ${m.role} ${m.thinking ? "thinking" : ""}`;
const content = document.createElement("div");
content.className = "msg-content";
if (m.role === "assistant" && !m.thinking) {
content.innerHTML = marked.parse(m.content || "");
content.querySelectorAll("pre code").forEach((block) => {
if (window.hljs) window.hljs.highlightElement(block);
});
} else {
content.textContent = m.content || "";
}
const meta = document.createElement("div");
meta.className = "msg-meta";
meta.textContent = formatMsgDateTime(m.ts || Date.now());
wrap.appendChild(content);
wrap.appendChild(meta);
messagesEl.appendChild(wrap);
}
messagesEl.scrollTop = messagesEl.scrollHeight;
}
/** ---------- 发送消息 ---------- */
sendBtn.onclick = async () => {
const text = inputEl.value.trim();
if (!text) return;
sendBtn.disabled = true;
sendBtn.textContent = "Sending...";
// ✅ 如果你把最后一个 chat 删光了:这里会自动新建
if (!currentChatId || !chats[currentChatId]) {
newChat();
}
const chat = chats[currentChatId];
const now = Date.now();
// user 消息带时间戳
chat.messages.push({ role: "user", content: text, ts: now });
chat.updatedAt = now;
inputEl.value = "";
// thinking 消息也带时间戳(后续只改内容/ts
const thinkingMsg = { role: "assistant", content: "🤔 思考中…", thinking: true, ts: Date.now() };
chat.messages.push(thinkingMsg);
chat.updatedAt = thinkingMsg.ts;
renderMessages();
save();
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: document.getElementById("modelSelect").value || "deepseek-chat",
messages: chat.messages.filter((m) => !m.thinking),
temperature: parseFloat(document.getElementById("tempInput").value || 0.7),
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
thinkingMsg.thinking = false;
thinkingMsg.content = data.reply || "No response";
thinkingMsg.ts = Date.now(); // ✅ 用“收到回复的时间”
chat.updatedAt = thinkingMsg.ts;
if (chat.title === "New Chat") {
chat.title = text.slice(0, 20);
}
} catch (e) {
thinkingMsg.thinking = false;
thinkingMsg.content = "❌ 请求失败";
thinkingMsg.ts = Date.now();
chat.updatedAt = thinkingMsg.ts;
console.error(e);
} finally {
sendBtn.disabled = false;
sendBtn.textContent = "Send";
}
save();
renderChatList();
renderMessages();
};
/** Enter=send, Shift+Enter=newline */
inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendBtn.click();
}
});
newChatBtn.onclick = newChat;
clearBtn.onclick = () => {
if (!currentChatId || !chats[currentChatId]) return;
const chat = chats[currentChatId];
chat.messages = [];
chat.title = "New Chat";
chat.updatedAt = Date.now();
save();
renderChatList();
renderMessages();
};
/** ---------- 初始化 ---------- */
renderChatList();
renderMessages();