Add DuckLM service scripts and utility model benchmark

This commit is contained in:
2026-05-22 07:27:59 +08:00
parent e6b82f0376
commit ff98224eb6
24 changed files with 2492 additions and 74 deletions
+118 -9
View File
@@ -16,6 +16,100 @@ function escapeText(value) {
return String(value ?? "");
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function renderInlineMarkdown(text) {
let html = escapeHtml(text);
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
html = html.replace(
/(https?:\/\/[^\s<]+)/g,
'<a href="$1" target="_blank" rel="noreferrer">$1</a>',
);
return html;
}
function renderMarkdown(markdown) {
const blocks = [];
const lines = String(markdown ?? "").replace(/\r\n/g, "\n").split("\n");
let paragraph = [];
let listItems = [];
let inFence = false;
let fenceLines = [];
const flushParagraph = () => {
if (!paragraph.length) return;
blocks.push(`<p>${renderInlineMarkdown(paragraph.join(" "))}</p>`);
paragraph = [];
};
const flushList = () => {
if (!listItems.length) return;
blocks.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
listItems = [];
};
for (const line of lines) {
if (line.startsWith("```")) {
if (inFence) {
blocks.push(`<pre><code>${escapeHtml(fenceLines.join("\n"))}</code></pre>`);
fenceLines = [];
inFence = false;
} else {
flushParagraph();
flushList();
inFence = true;
}
continue;
}
if (inFence) {
fenceLines.push(line);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) {
flushParagraph();
flushList();
const level = heading[1].length + 2;
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
continue;
}
const listItem = /^\s*[-*]\s+(.+)$/.exec(line);
if (listItem) {
flushParagraph();
listItems.push(listItem[1]);
continue;
}
if (!line.trim()) {
flushParagraph();
flushList();
continue;
}
flushList();
paragraph.push(line.trim());
}
if (inFence) blocks.push(`<pre><code>${escapeHtml(fenceLines.join("\n"))}</code></pre>`);
flushParagraph();
flushList();
return blocks.join("");
}
function setMarkdownContent(node, content) {
if (!node) return;
node.dataset.markdown = String(content ?? "");
node.innerHTML = renderMarkdown(node.dataset.markdown);
}
function setStatus(id, text, tone = "neutral") {
const node = document.querySelector(id);
if (!node) return;
@@ -67,7 +161,12 @@ function addMessage(role, content, meta = "", options = {}) {
messageMeta.innerHTML = `<strong>${role === "user" ? "You" : "DuckLM"}</strong><span>${escapeText(meta)}</span>`;
const text = document.createElement("p");
text.textContent = content;
text.className = role === "assistant" ? "message-body markdown-body" : "message-body";
if (role === "assistant") {
setMarkdownContent(text, content);
} else {
text.textContent = content;
}
bubble.append(messageMeta);
if (role === "assistant" && options.reasoning) {
@@ -204,11 +303,10 @@ function updateToolTerminal(article, eventPayload) {
terminal.classList.remove("is-waiting");
status.textContent = result.ok ? "ok" : "error";
const title = terminal.querySelector(".tool-terminal-title")?.textContent || body.textContent.trim();
const parts = [title];
if (result.output) parts.push("\nstdout\n" + result.output.trimEnd());
if (result.error) parts.push("\nstderr\n" + result.error.trimEnd());
body.textContent = parts.join("\n");
const parts = [];
if (result.output) parts.push("stdout\n" + result.output.trimEnd());
if (result.error) parts.push("stderr\n" + result.error.trimEnd());
body.textContent = parts.join("\n\n") || "completed with no output";
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
@@ -359,13 +457,22 @@ function humanApprovalDecision(action) {
function setMessagePending(article, text) {
const paragraph = article?.querySelector("p");
if (paragraph) paragraph.textContent = text;
if (!paragraph) return;
if (paragraph.classList.contains("markdown-body")) {
setMarkdownContent(paragraph, text);
} else {
paragraph.textContent = text;
}
}
function appendMessageText(article, delta) {
const paragraph = article?.querySelector("p");
if (!paragraph) return;
paragraph.textContent += delta;
if (paragraph.classList.contains("markdown-body")) {
setMarkdownContent(paragraph, `${paragraph.dataset.markdown || ""}${delta}`);
} else {
paragraph.textContent += delta;
}
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
@@ -758,7 +865,8 @@ async function sendMessage() {
setStatus("#task-status", "running", "warn");
addMessage("user", message, "submitted");
input.value = "";
const pending = addMessage("assistant", "", "thinking", {reasoning: true});
const reasoningEnabled = document.querySelector("#reasoning")?.checked || false;
const pending = addMessage("assistant", "", "thinking", {reasoning: reasoningEnabled});
const context = {taskId: "", contentStarted: false};
try {
@@ -767,6 +875,7 @@ async function sendMessage() {
conversation_id: state.currentConversationId || null,
workspace: document.querySelector("#workspace").value,
debug: document.querySelector("#debug").checked,
reasoning: reasoningEnabled ? "on" : "off",
}, async ({name, data}) => {
await handleAssistantStreamEvent(pending, name, data, context);
});
+54
View File
@@ -500,6 +500,60 @@ dd {
line-height: 1.5;
}
.bubble .markdown-body {
white-space: normal;
}
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body p,
.markdown-body ul,
.markdown-body pre {
margin: 8px 0 0;
}
.markdown-body h3 {
font-size: 17px;
}
.markdown-body h4,
.markdown-body h5 {
font-size: 15px;
}
.markdown-body ul {
padding-left: 22px;
}
.markdown-body code {
padding: 1px 4px;
border-radius: 5px;
background: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.92em;
}
.markdown-body pre {
max-width: 100%;
overflow: auto;
padding: 10px 12px;
border-radius: 8px;
background: #0f172a;
color: #d1fae5;
white-space: pre;
}
.markdown-body pre code {
padding: 0;
background: transparent;
color: inherit;
}
.markdown-body a {
color: var(--accent);
}
.message-reasoning {
display: grid;
gap: 8px;
+4
View File
@@ -46,6 +46,10 @@
<input id="debug" type="checkbox" checked>
<span>Debug mode</span>
</label>
<label class="toggle-row">
<input id="reasoning" type="checkbox">
<span>Reasoning</span>
</label>
</section>
<section class="status-panel" aria-labelledby="status-title">