init: AI-webUI-V1 first backup

This commit is contained in:
2026-02-15 23:08:47 +08:00
commit f372004545
9 changed files with 1882 additions and 0 deletions

355
public/app.js Normal file
View File

@@ -0,0 +1,355 @@
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();