Add persistent conversations and update checks
This commit is contained in:
+106
-5
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user