Replace repository with DuckLM runtime
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
const state = {
|
||||
running: false,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
async function jsonFetch(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function escapeText(value) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
function setStatus(id, text, tone = "neutral") {
|
||||
const node = document.querySelector(id);
|
||||
if (!node) return;
|
||||
node.textContent = text;
|
||||
node.dataset.tone = tone;
|
||||
}
|
||||
|
||||
function addMessage(role, content, meta = "", options = {}) {
|
||||
const list = document.querySelector("#messages");
|
||||
if (!list) return;
|
||||
|
||||
const article = document.createElement("article");
|
||||
article.className = `message ${role}`;
|
||||
|
||||
const avatar = document.createElement("div");
|
||||
avatar.className = "avatar";
|
||||
avatar.textContent = role === "user" ? "U" : "D";
|
||||
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = "bubble";
|
||||
|
||||
const messageMeta = document.createElement("div");
|
||||
messageMeta.className = "message-meta";
|
||||
messageMeta.innerHTML = `<strong>${role === "user" ? "You" : "DuckLM"}</strong><span>${escapeText(meta)}</span>`;
|
||||
|
||||
const text = document.createElement("p");
|
||||
text.textContent = content;
|
||||
|
||||
bubble.append(messageMeta);
|
||||
if (role === "assistant" && options.reasoning) {
|
||||
bubble.append(createInlineReasoning());
|
||||
}
|
||||
bubble.append(text);
|
||||
article.append(avatar, bubble);
|
||||
list.append(article);
|
||||
list.scrollTop = list.scrollHeight;
|
||||
return article;
|
||||
}
|
||||
|
||||
function createInlineReasoning() {
|
||||
const section = document.createElement("section");
|
||||
section.className = "message-reasoning is-collapsed";
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.className = "message-reasoning-toggle";
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.textContent = "Размышление";
|
||||
const status = document.createElement("span");
|
||||
status.className = "message-reasoning-status";
|
||||
status.textContent = "streaming";
|
||||
button.append(title, status);
|
||||
|
||||
const body = document.createElement("pre");
|
||||
body.hidden = true;
|
||||
body.textContent = "";
|
||||
|
||||
section.append(button, body);
|
||||
return section;
|
||||
}
|
||||
|
||||
function createToolTerminal(eventPayload) {
|
||||
const payload = eventPayload.payload || eventPayload;
|
||||
const args = payload.args || {};
|
||||
const terminal = document.createElement("section");
|
||||
terminal.className = "tool-terminal";
|
||||
terminal.dataset.toolIndex = String(payload.index || "");
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "tool-terminal-header";
|
||||
|
||||
const dots = document.createElement("span");
|
||||
dots.className = "terminal-dots";
|
||||
dots.innerHTML = "<i></i><i></i><i></i>";
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.className = "tool-terminal-title";
|
||||
title.textContent = formatToolCommand(payload.tool, args);
|
||||
|
||||
const status = document.createElement("span");
|
||||
status.className = "tool-terminal-status";
|
||||
status.textContent = "running";
|
||||
|
||||
header.append(dots, title, status);
|
||||
|
||||
const body = document.createElement("pre");
|
||||
body.className = "tool-terminal-body";
|
||||
body.textContent = formatToolStart(payload.tool, args);
|
||||
|
||||
terminal.append(header, body);
|
||||
return terminal;
|
||||
}
|
||||
|
||||
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();
|
||||
return `$ ${tool || "tool"}`;
|
||||
}
|
||||
|
||||
function formatToolStart(tool, args) {
|
||||
const lines = [formatToolCommand(tool, args)];
|
||||
const serializedArgs = JSON.stringify(args || {}, null, 2);
|
||||
if (serializedArgs !== "{}") lines.push(serializedArgs);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function appendToolTerminal(article, eventPayload) {
|
||||
const paragraph = article?.querySelector("p");
|
||||
const terminal = createToolTerminal(eventPayload);
|
||||
paragraph?.before(terminal);
|
||||
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
|
||||
}
|
||||
|
||||
function updateToolTerminal(article, eventPayload) {
|
||||
const payload = eventPayload.payload || eventPayload;
|
||||
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
|
||||
const body = terminal?.querySelector(".tool-terminal-body");
|
||||
const status = terminal?.querySelector(".tool-terminal-status");
|
||||
const result = payload.result || {};
|
||||
if (!body || !status) return;
|
||||
terminal.classList.toggle("is-error", !result.ok);
|
||||
status.textContent = result.ok ? "ok" : "error";
|
||||
|
||||
const parts = [body.textContent.trim()];
|
||||
if (result.output) parts.push("\nstdout\n" + result.output.trimEnd());
|
||||
if (result.error) parts.push("\nstderr\n" + result.error.trimEnd());
|
||||
if (result.metadata && Object.keys(result.metadata).length) {
|
||||
parts.push("\nmetadata\n" + JSON.stringify(result.metadata, null, 2));
|
||||
}
|
||||
body.textContent = parts.join("\n");
|
||||
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
|
||||
}
|
||||
|
||||
function appendApprovalTerminal(article, eventPayload) {
|
||||
const payload = eventPayload.payload || eventPayload;
|
||||
appendToolTerminal(article, {
|
||||
payload: {
|
||||
index: payload.index,
|
||||
tool: payload.tool,
|
||||
args: payload.action?.args || {},
|
||||
},
|
||||
});
|
||||
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
|
||||
const body = terminal?.querySelector(".tool-terminal-body");
|
||||
const status = terminal?.querySelector(".tool-terminal-status");
|
||||
terminal?.classList.add("is-waiting");
|
||||
if (status) status.textContent = "approval";
|
||||
if (body) body.textContent += `\n\napproval required\n${payload.reason || ""}`;
|
||||
}
|
||||
|
||||
function setMessagePending(article, text) {
|
||||
const paragraph = article?.querySelector("p");
|
||||
if (paragraph) paragraph.textContent = text;
|
||||
}
|
||||
|
||||
function appendMessageText(article, delta) {
|
||||
const paragraph = article?.querySelector("p");
|
||||
if (!paragraph) return;
|
||||
paragraph.textContent += delta;
|
||||
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
|
||||
}
|
||||
|
||||
function appendInlineReasoning(article, delta) {
|
||||
const block = article?.querySelector(".message-reasoning");
|
||||
const body = block?.querySelector("pre");
|
||||
const status = block?.querySelector(".message-reasoning-status");
|
||||
if (!body) return;
|
||||
body.textContent += delta;
|
||||
if (status) status.textContent = "streaming";
|
||||
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
|
||||
}
|
||||
|
||||
function finishInlineReasoning(article, reasoning) {
|
||||
const block = article?.querySelector(".message-reasoning");
|
||||
const body = block?.querySelector("pre");
|
||||
const status = block?.querySelector(".message-reasoning-status");
|
||||
if (!body) return;
|
||||
body.textContent = reasoning?.trim() || body.textContent.trim() || "Размышления не были получены.";
|
||||
if (status) status.textContent = "done";
|
||||
}
|
||||
|
||||
async function refreshEvents(taskId) {
|
||||
const events = await jsonFetch(`/v1/tasks/${taskId}/events`);
|
||||
const list = document.querySelector("#events");
|
||||
if (!list) return events;
|
||||
|
||||
list.innerHTML = "";
|
||||
for (const event of events) {
|
||||
const item = document.createElement("li");
|
||||
const title = document.createElement("strong");
|
||||
const detail = document.createElement("span");
|
||||
title.textContent = `${event.sequence}. ${event.event_type}`;
|
||||
detail.textContent = summarizeEvent(event.payload);
|
||||
item.append(title, detail);
|
||||
list.appendChild(item);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
function summarizeEvent(payload) {
|
||||
if (!payload || typeof payload !== "object") return "";
|
||||
if (payload.role && payload.latency_ms) {
|
||||
return `${payload.role} · ${Math.round(payload.latency_ms)} ms`;
|
||||
}
|
||||
if (payload.content) {
|
||||
return payload.content.slice(0, 140);
|
||||
}
|
||||
if (payload.final_response) {
|
||||
return payload.final_response.slice(0, 140);
|
||||
}
|
||||
if (payload.error) {
|
||||
return payload.error;
|
||||
}
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function toggleInlineReasoning(button) {
|
||||
const block = button.closest(".message-reasoning");
|
||||
const body = block?.querySelector("pre");
|
||||
if (!block || !body) return;
|
||||
const expanded = button.getAttribute("aria-expanded") === "true";
|
||||
button.setAttribute("aria-expanded", String(!expanded));
|
||||
body.hidden = expanded;
|
||||
block.classList.toggle("is-collapsed", expanded);
|
||||
}
|
||||
|
||||
function parseSseBlock(block) {
|
||||
const event = {name: "message", data: ""};
|
||||
for (const line of block.split("\n")) {
|
||||
if (line.startsWith("event:")) event.name = line.slice(6).trim();
|
||||
if (line.startsWith("data:")) event.data += line.slice(5).trimStart();
|
||||
}
|
||||
if (!event.data) return null;
|
||||
return {name: event.name, data: JSON.parse(event.data)};
|
||||
}
|
||||
|
||||
async function streamChat(payload, onEvent) {
|
||||
const response = await fetch("/v1/chat/stream", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
if (!response.body) throw new Error("Streaming response is not available in this browser.");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const {value, done} = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const blocks = buffer.split("\n\n");
|
||||
buffer = blocks.pop() || "";
|
||||
for (const block of blocks) {
|
||||
const event = parseSseBlock(block);
|
||||
if (event) await onEvent(event);
|
||||
}
|
||||
}
|
||||
buffer += decoder.decode();
|
||||
if (buffer.trim()) {
|
||||
const event = parseSseBlock(buffer);
|
||||
if (event) await onEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (state.running) return;
|
||||
const input = document.querySelector("#message");
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
state.running = true;
|
||||
document.querySelector("#run").disabled = true;
|
||||
setStatus("#task-status", "running", "warn");
|
||||
addMessage("user", message, "submitted");
|
||||
input.value = "";
|
||||
const pending = addMessage("assistant", "", "thinking", {reasoning: true});
|
||||
let taskId = "";
|
||||
let contentStarted = false;
|
||||
|
||||
try {
|
||||
await streamChat({
|
||||
message,
|
||||
workspace: document.querySelector("#workspace").value,
|
||||
debug: document.querySelector("#debug").checked,
|
||||
}, async ({name, data}) => {
|
||||
if (data.task_id) taskId = data.task_id;
|
||||
if (name === "task_created") {
|
||||
taskId = data.task_id;
|
||||
setStatus("#task-status", taskId, "warn");
|
||||
return;
|
||||
}
|
||||
if (name === "reasoning_delta") {
|
||||
pending.querySelector(".message-meta span").textContent = "reasoning";
|
||||
appendInlineReasoning(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_started") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
appendToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_finished") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
updateToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_approval_requested") {
|
||||
pending.querySelector(".message-meta span").textContent = "approval";
|
||||
appendApprovalTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "content_delta") {
|
||||
if (!contentStarted) {
|
||||
contentStarted = true;
|
||||
setMessagePending(pending, "");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = "answering";
|
||||
appendMessageText(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "done") {
|
||||
if (!contentStarted) {
|
||||
setMessagePending(pending, data.final_response || "No final content returned.");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = data.status;
|
||||
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
|
||||
finishInlineReasoning(pending, data.reasoning_content);
|
||||
await refreshEvents(data.task_id);
|
||||
return;
|
||||
}
|
||||
if (name === "error") {
|
||||
throw new Error(data.error || "Stream failed.");
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (!taskId) input.value = message;
|
||||
setMessagePending(pending, error.message);
|
||||
pending.querySelector(".message-meta span").textContent = "failed";
|
||||
setStatus("#task-status", "failed", "bad");
|
||||
if (taskId) await refreshEvents(taskId);
|
||||
} finally {
|
||||
state.running = false;
|
||||
document.querySelector("#run").disabled = false;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRuntime() {
|
||||
try {
|
||||
await jsonFetch("/health");
|
||||
setStatus("#api-status", "online", "ok");
|
||||
} catch {
|
||||
setStatus("#api-status", "offline", "bad");
|
||||
}
|
||||
|
||||
try {
|
||||
const roles = await jsonFetch("/v1/models/ping");
|
||||
const ok = Object.values(roles).every((item) => item.ok);
|
||||
setStatus("#model-status", ok ? "online" : "degraded", ok ? "ok" : "warn");
|
||||
} catch {
|
||||
setStatus("#model-status", "offline", "bad");
|
||||
}
|
||||
}
|
||||
|
||||
function bindChat() {
|
||||
const composer = document.querySelector("#composer");
|
||||
const input = document.querySelector("#message");
|
||||
composer?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
});
|
||||
input?.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
document.querySelector("#new-chat")?.addEventListener("click", () => {
|
||||
const messages = document.querySelector("#messages");
|
||||
messages.innerHTML = "";
|
||||
addMessage("assistant", "Новая сессия готова.", "ready");
|
||||
document.querySelector("#events").innerHTML = "";
|
||||
setStatus("#task-status", "none");
|
||||
});
|
||||
document.querySelector("#messages")?.addEventListener("click", (event) => {
|
||||
const button = event.target.closest(".message-reasoning-toggle");
|
||||
if (button) toggleInlineReasoning(button);
|
||||
});
|
||||
document.querySelector("#debug")?.addEventListener("change", (event) => {
|
||||
document.querySelector("#debug-panel").hidden = !event.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSimplePages() {
|
||||
const skills = document.querySelector("#skills");
|
||||
if (skills) skills.textContent = JSON.stringify(await jsonFetch("/v1/skills"), null, 2);
|
||||
const experience = document.querySelector("#experience");
|
||||
if (experience) experience.textContent = JSON.stringify(await jsonFetch("/v1/experience"), null, 2);
|
||||
const approvals = document.querySelector("#approvals");
|
||||
if (approvals) await renderApprovals(approvals);
|
||||
}
|
||||
|
||||
async function renderApprovals(container) {
|
||||
const approvals = await jsonFetch("/v1/approvals/pending");
|
||||
container.innerHTML = "";
|
||||
if (!approvals.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "No pending approvals.";
|
||||
container.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const approval of approvals) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "approval-card";
|
||||
card.dataset.approvalId = approval.approval_id;
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "approval-card-header";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = approval.normalized_action?.tool || "Tool action";
|
||||
const status = document.createElement("span");
|
||||
status.textContent = approval.status;
|
||||
header.append(title, status);
|
||||
|
||||
const meta = document.createElement("dl");
|
||||
meta.className = "approval-meta";
|
||||
meta.append(metaRow("Task", approval.task_id));
|
||||
meta.append(metaRow("Approval", approval.approval_id));
|
||||
meta.append(metaRow("Created", approval.created_at));
|
||||
|
||||
const action = document.createElement("pre");
|
||||
action.className = "approval-action";
|
||||
action.textContent = JSON.stringify(approval.normalized_action, null, 2);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "approval-actions";
|
||||
actions.append(
|
||||
approvalButton("Allow once", "allow_once"),
|
||||
approvalButton("Allow forever", "allow_forever"),
|
||||
approvalButton("Deny", "deny", "danger"),
|
||||
);
|
||||
|
||||
card.append(header, meta, action, actions);
|
||||
container.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
function metaRow(label, value) {
|
||||
const row = document.createElement("div");
|
||||
const dt = document.createElement("dt");
|
||||
const dd = document.createElement("dd");
|
||||
dt.textContent = label;
|
||||
dd.textContent = value || "";
|
||||
row.append(dt, dd);
|
||||
return row;
|
||||
}
|
||||
|
||||
function approvalButton(label, action, tone = "") {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
button.dataset.approvalAction = action;
|
||||
if (tone) button.dataset.tone = tone;
|
||||
return button;
|
||||
}
|
||||
|
||||
document.querySelector("#approvals")?.addEventListener("click", async (event) => {
|
||||
const button = event.target.closest("[data-approval-action]");
|
||||
if (!button) return;
|
||||
const card = button.closest(".approval-card");
|
||||
const approvalId = card?.dataset.approvalId;
|
||||
if (!approvalId) return;
|
||||
|
||||
button.disabled = true;
|
||||
const action = button.dataset.approvalAction;
|
||||
await jsonFetch(`/v1/approvals/${approvalId}/${action}`, {method: "POST"});
|
||||
await renderApprovals(document.querySelector("#approvals"));
|
||||
});
|
||||
|
||||
document.querySelector("#memory-search")?.addEventListener("click", async () => {
|
||||
const q = document.querySelector("#memory-query").value;
|
||||
document.querySelector("#memory-results").textContent =
|
||||
JSON.stringify(await jsonFetch(`/v1/memory/search?q=${encodeURIComponent(q)}`), null, 2);
|
||||
});
|
||||
|
||||
bindChat();
|
||||
checkRuntime();
|
||||
loadSimplePages().catch(console.error);
|
||||
@@ -0,0 +1,673 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #eef2f6;
|
||||
--sidebar: #111827;
|
||||
--sidebar-soft: #1f2937;
|
||||
--panel: #ffffff;
|
||||
--panel-strong: #f8fafc;
|
||||
--text: #111827;
|
||||
--muted: #64748b;
|
||||
--border: #d7dee8;
|
||||
--accent: #1f6feb;
|
||||
--accent-strong: #174ea6;
|
||||
--ok: #12805c;
|
||||
--warn: #b7791f;
|
||||
--bad: #b42318;
|
||||
--shadow: 0 18px 50px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.simple-page {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.simple-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.simple-header h1,
|
||||
.simple-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.simple-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.simple-header p {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.approval-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.approval-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.approval-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-card h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.approval-card-header span {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #fef3c7;
|
||||
color: #854d0e;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.approval-meta {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.approval-meta div {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.approval-meta dd {
|
||||
max-width: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.approval-action {
|
||||
margin: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
color: #d1fae5;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.approval-actions button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.approval-actions button[data-tone="danger"] {
|
||||
background: var(--bad);
|
||||
}
|
||||
|
||||
.approval-actions button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 292px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-height: 100vh;
|
||||
padding: 22px;
|
||||
background: var(--sidebar);
|
||||
color: #e5edf7;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.brand-mark, .avatar {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
background: #f8fafc;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.brand h1, .brand p,
|
||||
.chat-header h2, .chat-header p,
|
||||
.settings-panel h2, .status-panel h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin-top: 2px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.side-nav a {
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
padding: 10px 12px;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.side-nav a:hover,
|
||||
.side-nav a.active {
|
||||
background: var(--sidebar-soft);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.settings-panel,
|
||||
.status-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.settings-panel h2,
|
||||
.status-panel h2 {
|
||||
font-size: 13px;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 11px 12px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sidebar input {
|
||||
border-color: rgba(255,255,255,0.16);
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #e5edf7;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
[data-tone="ok"] { color: #86efac; }
|
||||
[data-tone="warn"] { color: #fde68a; }
|
||||
[data-tone="bad"] { color: #fca5a5; }
|
||||
|
||||
.chat-shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
height: 100vh;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 20px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.chat-header h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-header p {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.secondary-button,
|
||||
.composer button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: #edf2f7;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 18px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.message {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
grid-template-columns: minmax(0, 1fr) 36px;
|
||||
}
|
||||
|
||||
.message.user .avatar {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.message.assistant .avatar {
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.message.user .bubble {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 12px 14px;
|
||||
background: var(--panel-strong);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bubble p {
|
||||
margin: 8px 0 0;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-reasoning {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding: 9px 10px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.message-reasoning.is-collapsed {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.message-reasoning-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-reasoning-status {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.message-reasoning pre {
|
||||
margin: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tool-terminal {
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.tool-terminal-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 34px;
|
||||
padding: 8px 10px;
|
||||
background: #111827;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
}
|
||||
|
||||
.terminal-dots {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.terminal-dots i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.terminal-dots i:nth-child(1) { background: #ef4444; }
|
||||
.terminal-dots i:nth-child(2) { background: #f59e0b; }
|
||||
.terminal-dots i:nth-child(3) { background: #22c55e; }
|
||||
|
||||
.tool-terminal-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #d1d5db;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tool-terminal-status {
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: #1d4ed8;
|
||||
color: #dbeafe;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tool-terminal.is-error .tool-terminal-status {
|
||||
background: #7f1d1d;
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.tool-terminal.is-waiting .tool-terminal-status {
|
||||
background: #854d0e;
|
||||
color: #fef3c7;
|
||||
}
|
||||
|
||||
.tool-terminal-body {
|
||||
margin: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
color: #d1fae5;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-meta strong {
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.debug-column {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.debug-column h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
pre,
|
||||
#events {
|
||||
margin: 0;
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#events {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
#events li strong,
|
||||
#events li span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#events li span {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.composer textarea {
|
||||
min-height: 86px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.composer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#composer-hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.composer button {
|
||||
min-width: 96px;
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.composer button:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
.composer button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.chat-shell {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.chat-header,
|
||||
.debug-panel,
|
||||
.composer-actions {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user