init: AI-webUI-V1 first backup
This commit is contained in:
355
public/app.js
Normal file
355
public/app.js
Normal 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();
|
||||
Reference in New Issue
Block a user