Add persistent conversations and update checks

This commit is contained in:
2026-05-20 07:48:51 +08:00
parent a4b7ef034a
commit 12bed7b6ba
16 changed files with 854 additions and 31 deletions
+106 -5
View File
@@ -1,6 +1,8 @@
const state = {
running: false,
messages: [],
currentConversationId: "",
conversations: [],
};
async function jsonFetch(url, options) {
@@ -52,6 +54,35 @@ function addMessage(role, content, meta = "", options = {}) {
return article;
}
function clearMessages() {
const messages = document.querySelector("#messages");
if (messages) messages.innerHTML = "";
const events = document.querySelector("#events");
if (events) events.innerHTML = "";
}
function setConversationHeader(conversation) {
const title = document.querySelector("#chat-title");
const subtitle = document.querySelector("#chat-subtitle");
if (title) title.textContent = conversation?.title || "Chat";
if (subtitle) {
subtitle.textContent = conversation?.workspace
? `Workspace: ${conversation.workspace}`
: "Messages are processed by the local Qwen role mapping through Duck Core.";
}
}
function addStoredMessage(message) {
const article = addMessage(
message.role,
message.content,
message.status || "saved",
{reasoning: message.role === "assistant" && Boolean(message.reasoning_content)},
);
if (message.reasoning_content) finishInlineReasoning(article, message.reasoning_content);
return article;
}
function createInlineReasoning() {
const section = document.createElement("section");
section.className = "message-reasoning is-collapsed";
@@ -112,6 +143,9 @@ function formatToolCommand(tool, args) {
if (tool === "shell_exec_safe") return `$ ${args.command || tool}`;
if (tool === "file_read") return `$ file_read ${args.path || ""}`.trim();
if (tool === "file_write") return `$ file_write ${args.path || ""}`.trim();
if (tool === "list_dir") return `$ list_dir ${args.path || "."}`.trim();
if (tool === "search_files") return `$ search_files ${args.query || ""}`.trim();
if (tool === "os_update_check") return "$ os_update_check";
return `$ ${tool || "tool"}`;
}
@@ -363,6 +397,9 @@ async function handleAssistantStreamEvent(pending, name, data, context) {
if (data.task_id) context.taskId = data.task_id;
if (name === "task_created") {
context.taskId = data.task_id;
if (data.payload?.conversation_id) {
state.currentConversationId = data.payload.conversation_id;
}
setStatus("#task-status", data.task_id, "warn");
return;
}
@@ -396,6 +433,7 @@ async function handleAssistantStreamEvent(pending, name, data, context) {
return;
}
if (name === "done") {
if (data.conversation_id) state.currentConversationId = data.conversation_id;
if (!context.contentStarted) {
setMessagePending(pending, data.final_response || "No final content returned.");
}
@@ -403,6 +441,7 @@ async function handleAssistantStreamEvent(pending, name, data, context) {
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
finishInlineReasoning(pending, data.reasoning_content);
await refreshEvents(data.task_id);
await refreshConversations();
return;
}
if (name === "error") {
@@ -434,6 +473,59 @@ async function continueAfterInlineApproval(article, taskId, approvalId) {
}
}
async function refreshConversations() {
const list = document.querySelector("#conversation-list");
if (!list) return;
state.conversations = await jsonFetch("/v1/conversations");
list.innerHTML = "";
if (!state.conversations.length) {
const empty = document.createElement("p");
empty.className = "conversation-empty";
empty.textContent = "No saved chats.";
list.append(empty);
return;
}
for (const conversation of state.conversations) {
const button = document.createElement("button");
button.type = "button";
button.className = "conversation-item";
button.dataset.conversationId = conversation.conversation_id;
button.classList.toggle("active", conversation.conversation_id === state.currentConversationId);
const title = document.createElement("strong");
title.textContent = conversation.title;
const workspace = document.createElement("span");
workspace.textContent = conversation.workspace;
button.append(title, workspace);
list.append(button);
}
}
async function selectConversation(conversationId) {
const conversation = await jsonFetch(`/v1/conversations/${conversationId}`);
state.currentConversationId = conversation.conversation_id;
document.querySelector("#workspace").value = conversation.workspace;
setConversationHeader(conversation);
clearMessages();
if (!conversation.messages.length) {
addMessage("assistant", "Новый чат готов.", "ready");
} else {
for (const message of conversation.messages) addStoredMessage(message);
}
await refreshConversations();
}
async function createNewConversation() {
const workspace = document.querySelector("#workspace").value || "./workspace";
const conversation = await jsonFetch("/v1/conversations", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({title: "New chat", workspace}),
});
await selectConversation(conversation.conversation_id);
setStatus("#task-status", "none");
}
async function sendMessage() {
if (state.running) return;
const input = document.querySelector("#message");
@@ -451,6 +543,7 @@ async function sendMessage() {
try {
await streamChat({
message,
conversation_id: state.currentConversationId || null,
workspace: document.querySelector("#workspace").value,
debug: document.querySelector("#debug").checked,
}, async ({name, data}) => {
@@ -500,11 +593,14 @@ function bindChat() {
}
});
document.querySelector("#new-chat")?.addEventListener("click", () => {
const messages = document.querySelector("#messages");
messages.innerHTML = "";
addMessage("assistant", "Новая сессия готова.", "ready");
document.querySelector("#events").innerHTML = "";
setStatus("#task-status", "none");
createNewConversation().catch(console.error);
});
document.querySelector("#reload-chat")?.addEventListener("click", () => {
if (state.currentConversationId) selectConversation(state.currentConversationId).catch(console.error);
});
document.querySelector("#conversation-list")?.addEventListener("click", (event) => {
const item = event.target.closest("[data-conversation-id]");
if (item) selectConversation(item.dataset.conversationId).catch(console.error);
});
document.querySelector("#messages")?.addEventListener("click", (event) => {
const approvalButton = event.target.closest("[data-inline-approval-action]");
@@ -617,3 +713,8 @@ document.querySelector("#memory-search")?.addEventListener("click", async () =>
bindChat();
checkRuntime();
loadSimplePages().catch(console.error);
refreshConversations().then(() => {
if (state.conversations[0]) {
return selectConversation(state.conversations[0].conversation_id);
}
}).catch(console.error);
+71 -2
View File
@@ -233,7 +233,8 @@ button {
}
.settings-panel,
.status-panel {
.status-panel,
.conversation-panel {
display: grid;
gap: 12px;
padding: 14px;
@@ -243,11 +244,79 @@ button {
}
.settings-panel h2,
.status-panel h2 {
.status-panel h2,
.conversation-panel h2 {
font-size: 13px;
color: #f8fafc;
}
.panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.panel-heading button {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 7px;
background: #f8fafc;
color: #111827;
font-size: 18px;
font-weight: 800;
}
.conversation-list {
display: grid;
gap: 6px;
max-height: 260px;
overflow: auto;
}
.conversation-item {
display: grid;
gap: 3px;
width: 100%;
border: 1px solid transparent;
border-radius: 7px;
padding: 9px 10px;
background: transparent;
color: #e5edf7;
text-align: left;
}
.conversation-item:hover,
.conversation-item.active {
background: var(--sidebar-soft);
border-color: rgba(255,255,255,0.12);
}
.conversation-item strong,
.conversation-item span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-item strong {
font-size: 13px;
}
.conversation-item span,
.conversation-empty {
color: #9ca3af;
font-size: 11px;
}
.conversation-empty {
margin: 0;
}
label {
display: grid;
gap: 7px;
+11 -3
View File
@@ -25,6 +25,14 @@
<a href="/experience">Experience</a>
</nav>
<section class="conversation-panel" aria-labelledby="conversations-title">
<div class="panel-heading">
<h2 id="conversations-title">Chats</h2>
<button id="new-chat" type="button" title="New chat">+</button>
</div>
<div id="conversation-list" class="conversation-list"></div>
</section>
<section class="settings-panel" aria-labelledby="settings-title">
<h2 id="settings-title">Session</h2>
<label>
@@ -59,10 +67,10 @@
<main class="chat-shell">
<header class="chat-header">
<div>
<h2>Chat</h2>
<p>Messages are processed by the local Qwen role mapping through Duck Core.</p>
<h2 id="chat-title">Chat</h2>
<p id="chat-subtitle">Messages are processed by the local Qwen role mapping through Duck Core.</p>
</div>
<button id="new-chat" class="secondary-button" type="button">New Chat</button>
<button id="reload-chat" class="secondary-button" type="button">Reload</button>
</header>
<section id="messages" class="messages" aria-live="polite">