gui+webui: 聊天界面升级(PiDeck风格思考/工具卡片+滴入动画) & 流式卡顿修复(防抖局部增量渲染) & 主题系统修复(启动恢复色板/弹窗溢出wrap)

This commit is contained in:
JianFeeeee
2026-08-16 09:57:29 +08:00
parent 3f32006341
commit c52331cd5e
3 changed files with 566 additions and 208 deletions

View File

@ -289,6 +289,8 @@ function toggleAppearance() {
(() => {
var saved = localStorage.getItem("ha-theme");
setTheme(saved || "light");
var savedColor = localStorage.getItem("ha-color");
if (savedColor) setColor(savedColor);
var finalSrc = localStorage.getItem("ha-bg-final");
var img = localStorage.getItem("ha-bg-img");
var blur = localStorage.getItem("ha-bg-blur");
@ -963,6 +965,27 @@ function chanLetter(src) {
return /[A-Za-z0-9]/.test(ch) ? ch : "C";
}
// PiDeck 风格思考卡片Brain 图标 + 折叠时单行预览(流式中扫光)+ 展开/收起
// PiDeck 风格思考卡片Brain 图标 + 折叠单行预览(流式中扫光)+ 展开懒加载全文
function renderReasoningCard(text, isStreaming, idx) {
var preview =
typeof marked === "undefined"
? escHtml(text).replace(/<[^>]+>/g, " ").slice(0, 60)
: text.replace(/[\s\n]+/g, " ").slice(0, 60);
return (
'<div class="reasoning-card' + (isStreaming ? " rc-streaming" : "") + '" data-idx="' + (idx | 0) + '">' +
'<div class="reasoning-head" onclick="toggleReasoning(this)">' +
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rc-ico"><path d="M9 3a2 2 0 0 0-2 2v2a2 2 0 0 1-2 2H3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a2 2 0 0 1-2-2V3a2 2 0 0 0-2-2H9zM12 8v4m0 4h.01"/></svg>' +
'<span class="rc-title">' + (isStreaming ? __("思考中...","Thinking...") : __("思考","Thinking")) + '</span>' +
'<span class="rc-chev">▾</span></div>' +
'<div class="reasoning-body" style="display:' + (isStreaming ? "block" : "none") + '">' +
(isStreaming
? '<div class="reasoning-preview">' + escHtml(preview) + '</div><div class="reasoning-sweep"></div>'
: '<div class="reasoning-content">' + (typeof marked !== "undefined" ? marked.parse(text) : escHtml(text)) + '</div>') +
'</div></div>'
);
}
function renderChat() {
if (!_chatLayoutBuilt) {
buildChatLayout();
@ -1068,24 +1091,7 @@ function renderChat() {
var isChan = !!(m.source && m.source !== "webui");
var rc = "";
if (m.reasoning_content) {
var rcBody =
typeof marked === "undefined"
? escHtml(m.reasoning_content)
: marked.parse(m.reasoning_content);
rc =
'<div class="msg-bubble"><div class="reasoning">' +
"<div class=\"reasoning-title\" onclick=\"var n=this.nextElementSibling;n.style.display=n.style.display==='none'?'block':'none';this.textContent=this.textContent==='" +
__("展开思考", "Expand") +
"'?'" +
__("收起思考", "Collapse") +
"':'" +
__("展开思考", "Expand") +
"'\">" +
__("展开思考", "Expand") +
"</div>" +
'<div class="reasoning-body" style="display:none">' +
rcBody +
"</div></div></div>";
rc = renderReasoningCard(m.reasoning_content, isStreamingLast, i);
}
var tcs = "";
if (m.tool_calls && m.tool_calls.length > 0) {
@ -1099,41 +1105,47 @@ function renderChat() {
? JSON.stringify(tc.result, null, 1)
: String(tc.result)
: "";
var statusIcon =
var running = !resultStr && tc.status !== "denied";
var error = tc.status === "error" || tc.status === "denied" || !!tc.error;
var drip = newlyDone.indexOf(tc.tool || tc.name || "") !== -1 ? " tool-drip-in" : "";
var iconSvg =
error
? '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
: running
? '<span class="tc-spinner"></span>'
: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
var statusHtml =
tc.status === "denied"
? '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--error);vertical-align:-1px"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--accent);vertical-align:-1px"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
? '<span class="tc-state tc-deny">' + __("已拒绝","Denied") + "</span>"
: running
? '<span class="tc-state tc-run">' + __("调用中","Running") + "</span>"
: '<span class="tc-state tc-done">' + __("完成","Done") + "</span>";
var pluginHtml = tc.plugin
? '<span class="tc-plugin">' + escHtml(tc.plugin) + "</span>"
: "";
tcs +=
'<div class="msg-bubble"><div class="tool-call' +
(newlyDone.indexOf(tc.tool || tc.name || "") === -1
? ""
: " tool-drip-in") +
'<div class="tool-card' +
(error ? " tc-error" : running ? " tc-running" : " tc-done") +
drip +
'" data-tool="' +
escHtml(tc.tool || tc.name || "") +
'" onclick="toggleToolCall(this)">' +
'<div class="tc-line"><span class="tc-ico">' +
statusIcon +
iconSvg +
'</span><span class="tc-name">' +
escHtml(tc.tool || tc.name || "") +
"</span>" +
(tc.status === "denied"
? '<span class="tc-state tc-deny">' +
__("已拒绝", "Denied") +
"</span>"
: resultStr
? '<span class="tc-state tc-done">' +
__("完成", "Done") +
"</span>"
: '<span class="tc-state tc-run">' +
__("调用中", "Running") +
"</span>") +
pluginHtml +
statusHtml +
'<span class="tc-caret">▾</span></div>' +
'<div class="tc-detail" style="display:none">' +
(argsStr && argsStr !== "{}"
? '<div class="tc-args">' + escHtml(argsStr) + "</div>"
? '<div class="tc-args"><div class="tc-detail-label">' + __("参数","Args") + "</div>" + escHtml(argsStr) + "</div>"
: "") +
(resultStr
? '<div class="tc-result">' + escHtml(resultStr) + "</div>"
? '<div class="tc-result"><div class="tc-detail-label">' + __("结果","Result") + "</div>" + escHtml(resultStr) + "</div>"
: "") +
"</div></div></div>";
"</div></div>";
});
}
var body = rc + tcs;
@ -1302,6 +1314,29 @@ function toggleToolCall(el) {
}
}
function toggleReasoning(el) {
var card = el.closest(".reasoning-card");
if (!card) return;
var body = card.querySelector(".reasoning-body");
if (!body) return;
var open = body.style.display !== "none";
if (open) {
body.style.display = "none";
card.classList.remove("open");
return;
}
// 展开时懒加载全文(流式中只有 preview未 parse 全文)
var content = card.querySelector(".reasoning-content");
if (content && !content.childElementCount) {
var idx = parseInt(card.getAttribute("data-idx"), 10) || 0;
var text = (state.messages[idx] && state.messages[idx].reasoning_content) || "";
content.innerHTML =
typeof marked !== "undefined" ? marked.parse(text) : escHtml(text);
}
body.style.display = "block";
card.classList.add("open");
}
function renderChatStarmap() {
var cont = document.getElementById("sm-container-chat");
if (!cont) return;
@ -4487,10 +4522,78 @@ async function connectFetchSSE(url) {
function rerenderChatIfActive() {
var tab = document.getElementById("view-chat");
if (tab && tab.classList.contains("active")) {
renderChat();
renderChatStarmap();
renderTerminals();
renderCmdHistory();
if (!tab || !tab.classList.contains("active")) return;
// 流式增量路径:防抖合并 + 只更新最后一条消息的正文/思考节点,避免全量重建
var msgs = state.messages;
var last = msgs.length ? msgs[msgs.length - 1] : null;
var streamingLast =
!!last && last.role === "assistant" && !last._final && state.chatLoading;
if (streamingLast) {
if (state._streamTimer) clearTimeout(state._streamTimer);
state._streamTimer = setTimeout(function () {
state._streamTimer = null;
renderChatStreamChunk();
}, 90);
return;
}
// 非流式(完成/工具/历史变化):全量渲染
if (state._streamTimer) {
clearTimeout(state._streamTimer);
state._streamTimer = null;
}
renderChat();
renderChatStarmap();
renderTerminals();
renderCmdHistory();
}
// 流式增量渲染:仅更新最后一条 assistant 消息的正文(渐进,节流 parse与思考预览
function renderChatStreamChunk() {
var msgsEl = document.getElementById("chat-msgs");
var msgs = state.messages;
var last = msgs.length ? msgs[msgs.length - 1] : null;
if (!msgsEl || !last) return;
var el = msgsEl.lastElementChild;
if (!el) {
renderChat();
return;
}
// 更新正文文本(节流 parse内容变化 >200 字符或时间 >300ms 才 parse
var textEl = el.querySelector(".msg-bubble .text");
var c = last.content || "";
if (textEl) {
var now = Date.now();
var lastParse = el.__lastParse || 0;
var lastLen = el.__lastLen || 0;
if (c.length - lastLen > 200 || now - lastParse > 300) {
textEl.innerHTML =
typeof marked !== "undefined" ? marked.parse(c) : escHtml(c);
el.__lastParse = now;
el.__lastLen = c.length;
} else {
// 小增量:纯文本渐进,避免反复 parse
var tail = c.slice(lastLen);
if (tail) {
var tn = document.createTextNode(tail);
textEl.appendChild(tn);
}
el.__lastLen = c.length;
}
if (state.chatStick !== false) {
try {
msgsEl.scrollTop = msgsEl.scrollHeight;
} catch (e) {}
}
return;
}
// 思考预览更新(流式中折叠,只刷 preview + sweep
var rc = el.querySelector(".reasoning-card.rc-streaming .reasoning-preview");
if (rc && last.reasoning_content) {
var prev = last.reasoning_content.replace(/[\s\n]+/g, " ").slice(0, 60);
rc.textContent = prev;
return;
}
// 兜底:结构变化则全量
renderChat();
}

View File

@ -553,11 +553,15 @@ body.maximized .tb-max svg {
filter: blur(calc(var(--bg-blur, 0) * 1px));
transition: filter 0.25s var(--ease-out);
}
.palette-pop {
position: fixed;
bottom: 56px;
left: 6px;
width: 210px;
width: 216px;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 120px);
overflow-y: auto;
padding: 12px;
background: var(--glass-bg-strong);
backdrop-filter: blur(12px);
@ -586,7 +590,7 @@ body.maximized .tb-max svg {
margin: 3px;
padding: 0;
vertical-align: middle;
transition: transform 0.15s;
transition: transform 0.15s, box-shadow 0.15s;
}
.palette-pop button.cdot:hover {
transform: scale(1.25);
@ -607,27 +611,33 @@ body.maximized .tb-max svg {
margin-bottom: 6px;
outline: none;
}
.palette-pop input[type="range"] {
width: 80px;
margin: 0;
accent-color: var(--accent);
vertical-align: middle;
}
.palette-pop .pp-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
gap: 5px;
margin-top: 4px;
}
.palette-pop .pp-row .btn {
padding: 3px 10px;
padding: 3px 9px;
font-size: 11px;
white-space: nowrap;
}
.palette-pop .pp-val {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
min-width: 96px;
min-width: 0;
text-align: right;
margin-left: auto;
}
.palette-pop input[type="range"] {
width: 72px;
margin: 0;
accent-color: var(--accent);
vertical-align: middle;
}
.lang-btn {
font-size: 13px;
@ -1179,26 +1189,73 @@ code {
max-width: 100%;
border-radius: var(--radius-sm);
}
.msg-bubble .reasoning {
border-left: 2px solid #dddddd;
padding-left: 8px;
/* ===== 思考卡片PiDeck 风格)===== */
.reasoning-card {
margin: 6px 0;
font-size: 11px;
opacity: 0.8;
border: 1px dashed var(--kv-border);
border-radius: var(--radius-sm);
background: var(--bg-panel, var(--bg-hover));
overflow: hidden;
}
.msg-bubble .reasoning-title {
.reasoning-card .reasoning-head {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
font-size: 10px;
font-weight: 600;
user-select: none;
margin-bottom: 2px;
color: var(--text-secondary);
}
.reasoning-card .reasoning-head:hover {
background: var(--bg-hover);
}
.reasoning-card .rc-ico {
color: var(--accent);
flex-shrink: 0;
}
.reasoning-card .rc-title {
font-weight: 600;
color: var(--text-primary);
}
.reasoning-card .rc-chev {
margin-left: auto;
font-size: 10px;
color: var(--text-muted);
transition: transform 0.2s var(--ease-out);
}
.reasoning-card.open .rc-chev {
transform: rotate(180deg);
}
.reasoning-card .reasoning-body {
padding: 4px 10px 8px;
}
.reasoning-card .reasoning-content {
line-height: 1.5;
color: var(--text-tertiary);
font-size: 11.5px;
}
.reasoning-card .reasoning-content p {
margin: 4px 0;
}
.reasoning-card .reasoning-sweep {
height: 2px;
border-radius: 2px;
margin-top: 6px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
background-size: 200% 100%;
animation: reasoningSweep 1.2s var(--ease-out) infinite;
opacity: 0.6;
}
.msg-bubble .reasoning-body {
line-height: 1.4;
@keyframes reasoningSweep {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
.msg-bubble .tool-call {
background: var(--bg-hover);
/* ===== 工具卡片PiDeck 风格,扁平不套气泡)===== */
.tool-card {
background: var(--bg-panel, var(--bg-hover));
color: var(--text-secondary);
border-radius: var(--radius-sm);
padding: 7px 10px;
@ -1207,55 +1264,101 @@ code {
border: 1px solid var(--kv-border);
border-left: 3px solid var(--accent);
cursor: pointer;
transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.msg-bubble .tool-call .tc-line {
.tool-card.tc-running {
border-left-color: var(--accent);
}
.tool-card.tc-done {
border-left-color: rgba(23, 169, 100, 0.8);
}
.tool-card.tc-error {
border-left-color: var(--error, #db3694);
}
.tool-card .tc-line {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
user-select: none;
}
.msg-bubble .tool-call .tc-ico {
.tool-card .tc-ico {
display: inline-flex;
flex-shrink: 0;
color: var(--accent);
}
.msg-bubble .tool-call .tc-name {
.tool-card.tc-error .tc-ico {
color: var(--error, #db3694);
}
.tool-card.tc-done .tc-ico {
color: rgba(23, 169, 100, 0.9);
}
.tool-card .tc-name {
font-weight: 600;
color: var(--text-primary);
}
.msg-bubble .tool-call .tc-state {
margin-left: 6px;
.tool-card .tc-plugin {
font-size: 9.5px;
color: var(--text-muted);
background: var(--pre-bg);
border-radius: 4px;
padding: 0 4px;
line-height: 1.6;
flex-shrink: 0;
}
.tool-card .tc-state {
margin-left: auto;
font-size: 9.5px;
font-weight: 500;
padding: 1px 6px;
border-radius: 999px;
flex-shrink: 0;
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 4px;
}
.msg-bubble .tool-call .tc-run {
.tool-card .tc-run {
background: rgba(63, 110, 245, 0.18);
color: #a3b8ff;
}
.msg-bubble .tool-call .tc-done {
.tool-card .tc-done {
background: rgba(23, 169, 100, 0.18);
color: #6ee7a8;
}
.msg-bubble .tool-call .tc-deny {
.tool-card .tc-deny {
background: rgba(219, 54, 148, 0.18);
color: #ff9ec6;
}
.msg-bubble .tool-call .tc-caret {
.tool-card .tc-spinner {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid rgba(63, 110, 245, 0.25);
border-top-color: #3f6ef5;
animation: spin 0.7s linear infinite;
display: inline-block;
}
.tool-card .tc-caret {
font-size: 10px;
color: var(--text-muted);
transition: transform 0.15s var(--ease-out);
}
.msg-bubble .tool-call.open .tc-caret {
.tool-card.open .tc-caret {
transform: rotate(180deg);
}
.msg-bubble .tool-call .tc-detail {
.tool-card .tc-detail {
margin-top: 6px;
}
.msg-bubble .tool-call .tc-args {
.tool-card .tc-detail-label {
font-size: 9.5px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-bottom: 2px;
}
.tool-card .tc-args,
.tool-card .tc-result {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.85;
@ -1267,29 +1370,37 @@ code {
border-radius: 4px;
padding: 5px 7px;
}
.msg-bubble .tool-call .tc-result {
font-family: var(--font-mono);
font-size: 11px;
.tool-card .tc-result {
opacity: 0.72;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 4px;
padding: 5px 7px;
max-height: 80px;
overflow-y: auto;
}
[data-theme="light"] .msg-bubble .tool-call .tc-run {
[data-theme="light"] .tool-card .tc-run {
color: #3f6ef5;
}
[data-theme="light"] .msg-bubble .tool-call .tc-done {
[data-theme="light"] .tool-card .tc-done {
color: #128a52;
}
[data-theme="light"] .msg-bubble .tool-call .tc-deny {
[data-theme="light"] .tool-card .tc-deny {
color: #c2185b;
}
.tool-card.tool-drip-in {
animation: toolDripIn 0.32s var(--ease-out) both;
}
@keyframes toolDripIn {
0% {
opacity: 0;
transform: translateY(-18px) scale(0.96);
}
70% {
opacity: 1;
transform: translateY(2px) scale(1.005);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.msg-bubble .thinking-tools {
display: flex;
gap: 6px;

View File

@ -518,7 +518,10 @@ background:
position: fixed;
bottom: 66px;
left: 16px;
width: 200px;
width: 216px;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 130px);
overflow-y: auto;
padding: 12px;
background: var(--glass-bg-strong);
backdrop-filter: blur(12px);
@ -526,7 +529,7 @@ background:
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: none;
z-index: 90;
z-index: 300;
}
.palette-pop.on {
display: block;
@ -547,7 +550,7 @@ background:
margin: 3px;
padding: 0;
vertical-align: middle;
transition: transform var(--dur-micro) var(--ease-out);
transition: transform 0.15s, box-shadow 0.15s;
}
.palette-pop button.cdot:hover {
transform: scale(1.25);
@ -568,26 +571,33 @@ background:
margin-bottom: 6px;
outline: none;
}
.palette-pop input[type="range"] {
width: 100%;
margin: 2px 0 4px;
accent-color: var(--accent);
}
.palette-pop .pp-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
gap: 5px;
margin-top: 4px;
}
.palette-pop .pp-row .btn {
padding: 3px 10px;
padding: 3px 9px;
font-size: 11px;
white-space: nowrap;
}
.palette-pop .pp-val {
font-size: 11px;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
min-width: 30px;
min-width: 0;
text-align: right;
margin-left: auto;
}
.palette-pop input[type="range"] {
width: 72px;
margin: 0;
accent-color: var(--accent);
vertical-align: middle;
}
.container {
padding: 24px;
@ -1373,26 +1383,73 @@ background:
max-width: 100%;
border-radius: 6px;
}
.msg-bubble .reasoning {
border-left: 2px solid #dddddd;
padding-left: 8px;
/* ===== 思考卡片PiDeck 风格)===== */
.reasoning-card {
margin: 6px 0;
font-size: 11px;
opacity: 0.8;
border: 1px dashed var(--kv-border);
border-radius: var(--radius-sm);
background: var(--bg-panel, var(--bg-hover));
overflow: hidden;
}
.msg-bubble .reasoning-title {
.reasoning-card .reasoning-head {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
font-size: 10px;
font-weight: 600;
user-select: none;
margin-bottom: 2px;
color: var(--text-secondary);
}
.reasoning-card .reasoning-head:hover {
background: var(--bg-hover);
}
.reasoning-card .rc-ico {
color: var(--accent);
flex-shrink: 0;
}
.reasoning-card .rc-title {
font-weight: 600;
color: var(--text-primary);
}
.reasoning-card .rc-chev {
margin-left: auto;
font-size: 10px;
color: var(--text-muted);
transition: transform 0.2s var(--ease-out);
}
.reasoning-card.open .rc-chev {
transform: rotate(180deg);
}
.reasoning-card .reasoning-body {
padding: 4px 10px 8px;
}
.reasoning-card .reasoning-content {
line-height: 1.5;
color: var(--text-tertiary);
font-size: 11.5px;
}
.reasoning-card .reasoning-content p {
margin: 4px 0;
}
.reasoning-card .reasoning-sweep {
height: 2px;
border-radius: 2px;
margin-top: 6px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
background-size: 200% 100%;
animation: reasoningSweep 1.2s var(--ease-out) infinite;
opacity: 0.6;
}
.msg-bubble .reasoning-body {
line-height: 1.4;
@keyframes reasoningSweep {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
.msg-bubble .tool-call {
background: var(--bg-hover);
/* ===== 工具卡片PiDeck 风格,扁平不套气泡)===== */
.tool-card {
background: var(--bg-panel, var(--bg-hover));
color: var(--text-secondary);
border-radius: var(--radius-sm);
padding: 7px 10px;
@ -1401,54 +1458,101 @@ background:
border: 1px solid var(--kv-border);
border-left: 3px solid var(--accent);
cursor: pointer;
transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.msg-bubble .tool-call .tc-line {
.tool-card.tc-running {
border-left-color: var(--accent);
}
.tool-card.tc-done {
border-left-color: rgba(23, 169, 100, 0.8);
}
.tool-card.tc-error {
border-left-color: var(--error, #db3694);
}
.tool-card .tc-line {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
user-select: none;
}
.msg-bubble .tool-call .tc-ico {
.tool-card .tc-ico {
display: inline-flex;
flex-shrink: 0;
color: var(--accent);
}
.msg-bubble .tool-call .tc-name {
.tool-card.tc-error .tc-ico {
color: var(--error, #db3694);
}
.tool-card.tc-done .tc-ico {
color: rgba(23, 169, 100, 0.9);
}
.tool-card .tc-name {
font-weight: 600;
color: var(--text-primary);
}
.msg-bubble .tool-call .tc-state {
.tool-card .tc-plugin {
font-size: 9.5px;
color: var(--text-muted);
background: var(--pre-bg);
border-radius: 4px;
padding: 0 4px;
line-height: 1.6;
flex-shrink: 0;
}
.tool-card .tc-state {
margin-left: auto;
font-size: 9.5px;
font-weight: 500;
padding: 1px 6px;
border-radius: 999px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
}
.msg-bubble .tool-call .tc-run {
.tool-card .tc-run {
background: rgba(63, 110, 245, 0.18);
color: #a3b8ff;
}
.msg-bubble .tool-call .tc-done {
.tool-card .tc-done {
background: rgba(23, 169, 100, 0.18);
color: #6ee7a8;
}
.msg-bubble .tool-call .tc-deny {
.tool-card .tc-deny {
background: rgba(219, 54, 148, 0.18);
color: #ff9ec6;
}
.msg-bubble .tool-call .tc-caret {
.tool-card .tc-spinner {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid rgba(63, 110, 245, 0.25);
border-top-color: #3f6ef5;
animation: spin 0.7s linear infinite;
display: inline-block;
}
.tool-card .tc-caret {
font-size: 10px;
color: var(--text-muted);
transition: transform 0.15s var(--ease-out);
}
.msg-bubble .tool-call.open .tc-caret {
.tool-card.open .tc-caret {
transform: rotate(180deg);
}
.msg-bubble .tool-call .tc-detail {
.tool-card .tc-detail {
margin-top: 6px;
}
.msg-bubble .tool-call .tc-args {
.tool-card .tc-detail-label {
font-size: 9.5px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-bottom: 2px;
}
.tool-card .tc-args,
.tool-card .tc-result {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.85;
@ -1460,29 +1564,37 @@ background:
border-radius: 4px;
padding: 5px 7px;
}
.msg-bubble .tool-call .tc-result {
font-family: var(--font-mono);
font-size: 11px;
.tool-card .tc-result {
opacity: 0.72;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 4px;
padding: 5px 7px;
max-height: 80px;
overflow-y: auto;
}
[data-theme="light"] .msg-bubble .tool-call .tc-run {
[data-theme="light"] .tool-card .tc-run {
color: #3f6ef5;
}
[data-theme="light"] .msg-bubble .tool-call .tc-done {
[data-theme="light"] .tool-card .tc-done {
color: #128a52;
}
[data-theme="light"] .msg-bubble .tool-call .tc-deny {
[data-theme="light"] .tool-card .tc-deny {
color: #c2185b;
}
.tool-card.tool-drip-in {
animation: toolDripIn 0.32s var(--ease-out) both;
}
@keyframes toolDripIn {
0% {
opacity: 0;
transform: translateY(-18px) scale(0.96);
}
70% {
opacity: 1;
transform: translateY(2px) scale(1.005);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.msg-bubble .thinking-tools {
display: flex;
gap: 6px;
@ -2387,6 +2499,8 @@ background:
(function () {
var saved = localStorage.getItem("ha-theme");
setTheme(saved || "light");
var savedColor = localStorage.getItem("ha-color");
if (savedColor) setColor(savedColor);
var sb = localStorage.getItem("ha-sidebar");
if (sb === "1" || (!sb && window.innerWidth <= 768)) {
document.body.classList.add("sidebar-collapsed");
@ -3097,70 +3211,65 @@ background:
c = escHtml(c);
}
var isChan = !!(m.source && m.source !== "webui");
var rc = "";
if (m.reasoning_content) {
var rcBody =
typeof marked !== "undefined"
? marked.parse(m.reasoning_content)
: escHtml(m.reasoning_content);
rc =
'<div class="msg-bubble"><div class="reasoning">' +
"<div class=\"reasoning-title\" onclick=\"var n=this.nextElementSibling;n.style.display=n.style.display==='none'?'block':'none';this.textContent=this.textContent==='" +
__("展开思考", "Expand") +
"'?'" +
__("收起思考", "Collapse") +
"':'" +
__("展开思考", "Expand") +
"'\">" +
__("展开思考", "Expand") +
"</div>" +
'<div class="reasoning-body" style="display:none">' +
rcBody +
"</div></div></div>";
}
var tcs = "";
if (m.tool_calls && m.tool_calls.length > 0) {
m.tool_calls.forEach(function (tc) {
var argsStr =
typeof tc.args === "object"
? JSON.stringify(tc.args, null, 1)
: tc.args || "";
var resultStr = tc.result
? typeof tc.result === "object"
? JSON.stringify(tc.result, null, 1)
: String(tc.result)
: "";
var statusIcon =
tc.status === "denied"
? '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--error);vertical-align:-1px"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--accent);vertical-align:-1px"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
tcs +=
'<div class="msg-bubble"><div class="tool-call' +
(newlyDone.indexOf(tc.tool || tc.name || "") !== -1
? " tool-drip-in"
: "") +
'" onclick="toggleToolCall(this)">' +
'<div class="tc-line"><span class="tc-ico">' +
statusIcon +
'</span><span class="tc-name">' +
escHtml(tc.tool || tc.name || "") +
"</span>" +
(tc.status === "denied"
? '<span class="tc-state tc-deny">' + __("已拒绝", "Denied") + "</span>"
: resultStr
? '<span class="tc-state tc-done">' + __("完成", "Done") + "</span>"
: '<span class="tc-state tc-run">' + __("调用中", "Running") + "</span>") +
'<span class="tc-caret">▾</span></div>' +
'<div class="tc-detail" style="display:none">' +
(argsStr && argsStr !== "{}"
? '<div class="tc-args">' + escHtml(argsStr) + "</div>"
: "") +
(resultStr
? '<div class="tc-result">' + escHtml(resultStr) + "</div>"
: "") +
"</div></div></div>";
});
}
var rc = "";
if (m.reasoning_content) {
rc = renderReasoningCard(m.reasoning_content, isStreamingLast);
}
var tcs = "";
if (m.tool_calls && m.tool_calls.length > 0) {
m.tool_calls.forEach(function (tc) {
var argsStr =
typeof tc.args === "object"
? JSON.stringify(tc.args, null, 1)
: tc.args || "";
var resultStr = tc.result
? typeof tc.result === "object"
? JSON.stringify(tc.result, null, 1)
: String(tc.result)
: "";
var running = !resultStr && tc.status !== "denied";
var error = tc.status === "error" || tc.status === "denied" || !!tc.error;
var drip = newlyDone.indexOf(tc.tool || tc.name || "") !== -1 ? " tool-drip-in" : "";
var iconSvg =
error
? '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
: running
? '<span class="tc-spinner"></span>'
: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
var statusHtml =
tc.status === "denied"
? '<span class="tc-state tc-deny">' + __("已拒绝", "Denied") + "</span>"
: running
? '<span class="tc-state tc-run">' + __("调用中", "Running") + "</span>"
: '<span class="tc-state tc-done">' + __("完成", "Done") + "</span>";
var pluginHtml = tc.plugin
? '<span class="tc-plugin">' + escHtml(tc.plugin) + "</span>"
: "";
tcs +=
'<div class="tool-card' +
(error ? " tc-error" : running ? " tc-running" : " tc-done") +
drip +
'" data-tool="' +
escHtml(tc.tool || tc.name || "") +
'" onclick="toggleToolCall(this)">' +
'<div class="tc-line"><span class="tc-ico">' +
iconSvg +
'</span><span class="tc-name">' +
escHtml(tc.tool || tc.name || "") +
"</span>" +
pluginHtml +
statusHtml +
'<span class="tc-caret">▾</span></div>' +
'<div class="tc-detail" style="display:none">' +
(argsStr && argsStr !== "{}"
? '<div class="tc-args"><div class="tc-detail-label">' + __("参数", "Args") + "</div>" + escHtml(argsStr) + "</div>"
: "") +
(resultStr
? '<div class="tc-result"><div class="tc-detail-label">' + __("结果", "Result") + "</div>" + escHtml(resultStr) + "</div>"
: "") +
"</div></div>";
});
}
var body = rc + tcs;
var growCls = m._grow ? " grow-in" : "";
if (m._grow) m._grow = false;
@ -3274,6 +3383,41 @@ background:
}
}
function renderReasoningCard(text, isStreaming) {
var body =
typeof marked !== "undefined"
? marked.parse(text)
: escHtml(text);
var preview =
typeof marked !== "undefined"
? text.replace(/[\s\n]+/g, " ").slice(0, 60)
: escHtml(text).replace(/<[^>]+>/g, " ").slice(0, 60);
return (
'<div class="reasoning-card">' +
'<div class="reasoning-head" onclick="toggleReasoning(this)">' +
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rc-ico"><path d="M9 3a2 2 0 0 0-2 2v2a2 2 0 0 1-2 2H3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a2 2 0 0 1-2-2V3a2 2 0 0 0-2-2H9zM12 8v4m0 4h.01"/></svg>' +
'<span class="rc-title">' + (isStreaming ? __("思考中...", "Thinking...") : __("思考", "Thinking")) + '</span>' +
'<span class="rc-chev">▾</span></div>' +
'<div class="reasoning-body" style="display:' + (isStreaming ? "block" : "none") + '">' +
'<div class="reasoning-content">' + body + '</div>' +
(isStreaming ? '<div class="reasoning-sweep"></div>' : "") +
'</div></div>'
);
}
function toggleReasoning(el) {
var card = el.closest(".reasoning-card");
if (!card) return;
var body = card.querySelector(".reasoning-body");
if (!body) return;
var open = body.style.display !== "none";
body.style.display = open ? "none" : "block";
if (open) {
card.classList.remove("open");
} else {
card.classList.add("open");
}
}
function renderChatStarmap() {
var cont = document.getElementById("sm-container-chat");
if (!cont) return;