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();

152
public/index.html Normal file
View File

@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>My DeepSeek WebUI</title>
<!-- Local vendor (CSP-friendly) -->
<link rel="stylesheet" href="/vendor/github.min.css">
<link rel="stylesheet" href="/style.css">
<!-- ✅ 新增:只在这里补一点点样式(避免你还要改第三个文件) -->
<style>
/* 顶部栏的图标按钮 */
.icon-btn{
border: none;
background: transparent;
cursor: pointer;
font-size: 18px;
padding: 6px 10px;
border-radius: 8px;
line-height: 1;
}
.icon-btn:hover{
background: rgba(0,0,0,0.06);
}
/* 隐藏侧栏:通过 .app.sidebar-hidden 控制 */
.app.sidebar-hidden .sidebar{
width: 0;
min-width: 0;
overflow: hidden;
border-right: none;
}
/* 左侧 chat item标题+日期+删除按钮 */
#chatList li.chat-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.chat-item-left {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.chat-title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-date {
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
.chat-del {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.75);
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
}
.chat-del:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
/* 消息时间戳 */
.message .msg-meta {
margin-top: 6px;
font-size: 12px;
opacity: 0.65;
}
.message.user .msg-meta {
text-align: right;
}
/* 空状态提示 */
.empty-state{
margin: 24px auto;
max-width: 720px;
padding: 18px 16px;
border-radius: 12px;
background: rgba(0,0,0,0.04);
color: rgba(0,0,0,0.72);
font-size: 14px;
line-height: 1.6;
}
.empty-state h3{
margin: 0 0 8px 0;
font-size: 16px;
color: rgba(0,0,0,0.85);
}
.empty-state code{
background: rgba(0,0,0,0.06);
padding: 2px 6px;
border-radius: 6px;
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="sidebar-header">
<h2>Chats</h2>
<button id="newChatBtn" type="button"></button>
</div>
<ul id="chatList"></ul>
</aside>
<main class="main">
<header class="topbar">
<div class="topbar-left">
<!-- ✅ 新增:隐藏/展开侧栏按钮 -->
<button id="sidebarToggle" type="button" class="icon-btn" title="Toggle sidebar"></button>
<label>Model:</label>
<select id="modelSelect">
<option value="deepseek-chat">deepseek-chat</option>
<option value="deepseek-reasoner">deepseek-reasoner</option>
</select>
<label>Temp:</label>
<input id="tempInput" type="number" step="0.1" value="0.7" />
</div>
<button id="clearBtn" type="button">Clear</button>
</header>
<section id="messages" class="messages"></section>
<footer class="inputbar">
<textarea id="input" placeholder="Type your message... (Enter=send, Shift+Enter=newline)"></textarea>
<button id="sendBtn" type="button">Send</button>
</footer>
</main>
</div>
<!-- Local vendor (CSP-friendly) -->
<script src="/vendor/marked.min.js"></script>
<script src="/vendor/highlight.js"></script>
<script src="/app.js"></script>
</body>
</html>

247
public/style.css Normal file
View File

@@ -0,0 +1,247 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont;
background: #f7f7f8;
}
.app {
display: flex;
height: 100vh;
}
/* Sidebar */
.sidebar {
width: 260px;
background: #202123;
color: #fff;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header button {
background: #444;
color: #fff;
border: none;
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
}
#chatList {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
}
#chatList li {
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid #333;
}
#chatList li.active {
background: #343541;
}
/* Main */
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.topbar {
padding: 10px;
background: #fff;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
}
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
}
.message {
margin-bottom: 16px;
max-width: 800px;
}
.message {
max-width: 78%;
padding: 12px 14px;
border-radius: 16px;
line-height: 1.6;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
}
.message.user {
align-self: flex-end;
background: linear-gradient(135deg, #6fa8ff, #000000);
color: #03173e;
}
.message.assistant {
align-self: flex-start;
background: #fff;
border: 1px solid #eee;
}
.message.user {
align-self: flex-end;
background: #daf1ff;
padding: 12px;
border-radius: 12px;
}
.message.assistant {
background: #fff;
padding: 12px;
border-radius: 12px;
border: 1px solid #eee;
}
.message.thinking {
color: #888;
font-style: italic;
}
.inputbar {
padding: 12px;
display: flex;
gap: 10px;
border-top: 1px solid #ddd;
background: #fff;
}
textarea {
flex: 1;
resize: none;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid #ddd;
min-height: 48px;
max-height: 180px;
outline: none;
}
textarea:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.15);
}
#sendBtn {
min-width: 88px;
border-radius: 14px;
}
.topbar-left {
display: flex;
align-items: center;
gap: 10px;
}
.topbar select,
.topbar input {
height: 34px;
border-radius: 10px;
border: 1px solid #ddd;
padding: 0 10px;
}
.topbar label {
color: #555;
font-size: 14px;
}
textarea {
flex: 1;
resize: none;
padding: 10px;
border-radius: 8px;
border: 1px solid #ccc;
}
button {
padding: 8px 14px;
border-radius: 8px;
border: none;
background: #10a37f;
color: white;
cursor: pointer;
}
/* 左侧 chat item标题+日期+删除按钮 */
#chatList li.chat-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.chat-item-left {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.chat-title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-date {
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
}
.chat-del {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.75);
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
}
.chat-del:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
/* 消息:内容 + 时间戳 */
.message .msg-meta {
margin-top: 6px;
font-size: 12px;
opacity: 0.65;
}
/* 让 user 的时间戳靠右一点更像聊天软件 */
.message.user .msg-meta {
text-align: right;
}

7
public/vendor/github.min.css vendored Normal file
View File

@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-dark
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}

6
public/vendor/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long