feat: 优化运行时判定
This commit is contained in:
@@ -147,6 +147,33 @@
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.switch input { accent-color: var(--blue); width: 16px; height: 16px; }
|
||||
.stage-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.segmented {
|
||||
display: inline-flex;
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.segmented button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
color: var(--secondary);
|
||||
transition: background 0.18s, color 0.18s;
|
||||
}
|
||||
.segmented button.active {
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
.empty {
|
||||
max-width: 36rem;
|
||||
color: var(--secondary);
|
||||
@@ -168,6 +195,15 @@
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.when { margin: 0; color: var(--tertiary); font-size: 12px; }
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--fill);
|
||||
color: var(--secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.prompt {
|
||||
margin: 10px 0 16px;
|
||||
font-size: 17px;
|
||||
@@ -214,7 +250,7 @@
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card.arrive { animation: none; }
|
||||
.split span, .track i { transition: none; }
|
||||
.split span, .track i, .segmented button { transition: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -244,7 +280,28 @@ const METERS = [
|
||||
["needs_web", "先查文档"],
|
||||
["needs_deep_reasoning", "先想清楚"],
|
||||
];
|
||||
const state = { records: [], follow: true, selected: null, fresh: new Set() };
|
||||
const EVENTS = {
|
||||
UserPromptSubmit: "用户提问",
|
||||
PreToolUse: "工具前",
|
||||
PostToolUse: "工具后",
|
||||
Stop: "结束检查",
|
||||
judge: "主动判定",
|
||||
};
|
||||
const ACTIONS = {
|
||||
inject: "注入",
|
||||
deny: "拦截",
|
||||
ask: "询问",
|
||||
block: "拦截",
|
||||
silent: "沉默",
|
||||
};
|
||||
const SORT_KEY = "laya-router:turn-sort";
|
||||
const state = {
|
||||
records: [],
|
||||
follow: true,
|
||||
selected: null,
|
||||
fresh: new Set(),
|
||||
sort: localStorage.getItem(SORT_KEY) === "asc" ? "asc" : "desc",
|
||||
};
|
||||
|
||||
function keyOf(record) {
|
||||
if (record.session_id) return record.session_id;
|
||||
@@ -295,7 +352,7 @@ function render() {
|
||||
aside.innerHTML = groups.map(([key, items]) => {
|
||||
const last = items[items.length - 1];
|
||||
const snippet = (last.prompt || "没有记下原文").replace(/\s+/g, " ");
|
||||
const levelName = LEVELS.find(([id]) => id === last.level)?.[1] || last.level;
|
||||
const levelName = LEVELS.find(([id]) => id === last.level)?.[1] || ACTIONS[last.action] || EVENTS[last.event] || last.level || "";
|
||||
return `<button class="session ${key === state.selected ? "active" : ""}" data-key="${encodeURIComponent(key)}" type="button">
|
||||
<div class="rowline"><span>${escapeHtml(titleOf(last))}</span><span class="level quiet">${escapeHtml(levelName)}</span></div>
|
||||
<p class="snippet">${escapeHtml(snippet)}</p>
|
||||
@@ -312,24 +369,32 @@ function render() {
|
||||
const [key, items] = group;
|
||||
const head = items[items.length - 1];
|
||||
const followChecked = state.follow ? "checked" : "";
|
||||
const ordered = state.sort === "desc"
|
||||
? items.map((r, i) => [i, r]).reverse()
|
||||
: items.map((r, i) => [i, r]);
|
||||
stage.innerHTML = `<div class="stage-head">
|
||||
<div>
|
||||
<h2>${escapeHtml(titleOf(head))}</h2>
|
||||
<p class="path">${escapeHtml(head.cwd || "没有记录工作目录")}</p>
|
||||
</div>
|
||||
<label class="switch"><input id="follow" type="checkbox" ${followChecked} /> 跟随最新</label>
|
||||
<div class="stage-controls">
|
||||
<div class="segmented" role="group" aria-label="排序方式">
|
||||
<button type="button" data-sort="desc" class="${state.sort === "desc" ? "active" : ""}" aria-pressed="${state.sort === "desc"}">最新在前</button>
|
||||
<button type="button" data-sort="asc" class="${state.sort === "asc" ? "active" : ""}" aria-pressed="${state.sort === "asc"}">最早在前</button>
|
||||
</div>
|
||||
<label class="switch"><input id="follow" type="checkbox" ${followChecked} /> 跟随最新</label>
|
||||
</div>
|
||||
</div>
|
||||
${items.map((record, index) => {
|
||||
${ordered.map(([index, record]) => {
|
||||
const id = recordId(record);
|
||||
const arrive = state.fresh.has(id) ? "arrive" : "";
|
||||
state.fresh.delete(id);
|
||||
const probs = record.level_probabilities || {};
|
||||
const levelName = LEVELS.find(([name]) => name === record.level)?.[1] || record.level;
|
||||
return `<article class="turn">
|
||||
<div class="card ${arrive}">
|
||||
<p class="when">第 ${index + 1} 次 · ${when(record.ts)} · ${record.elapsed_ms ?? "?"} 毫秒</p>
|
||||
<p class="prompt">${escapeHtml(record.prompt || "原文没有写入日志")}</p>
|
||||
<div class="split" role="img" aria-label="复杂度分布">
|
||||
const eventLabel = EVENTS[record.event] || (record.event ? record.event : "用户提问");
|
||||
const actionLabel = ACTIONS[record.action] || "";
|
||||
const hasLevel = Boolean(record.level);
|
||||
const distribution = hasLevel ? `<div class="split" role="img" aria-label="复杂度分布">
|
||||
${LEVELS.map(([name]) => `<span class="${name}" style="width:${Math.max(0, Math.min(100, Number(probs[name] || 0) * 100))}%"></span>`).join("")}
|
||||
</div>
|
||||
<div class="legend">
|
||||
@@ -339,7 +404,14 @@ function render() {
|
||||
const value = Number(record[field] || 0);
|
||||
return `<div class="meter"><div class="rowline"><span>${label}</span><span class="quiet">${pct(value)}</span></div><div class="track ${value >= 0.7 ? "hot" : ""}"><i style="width:${Math.max(0, Math.min(100, value * 100))}%"></i></div></div>`;
|
||||
}).join("")}
|
||||
<p class="note">${escapeHtml(levelName)}。${escapeHtml(advice(record))}</p>
|
||||
<p class="note">${escapeHtml(levelName)}。${escapeHtml(advice(record))}</p>` : "";
|
||||
const hint = !hasLevel && record.hint ? `<p class="note">${escapeHtml(record.hint)}</p>` : "";
|
||||
return `<article class="turn">
|
||||
<div class="card ${arrive}">
|
||||
<p class="when"><span class="tag">${escapeHtml(eventLabel)}</span>${actionLabel ? `<span class="tag">${escapeHtml(actionLabel)}</span>` : ""}第 ${index + 1} 次 · ${when(record.ts)} · ${record.elapsed_ms ?? "?"} 毫秒</p>
|
||||
<p class="prompt">${escapeHtml(record.prompt || "原文没有写入日志")}</p>
|
||||
${distribution}
|
||||
${hint}
|
||||
</div>
|
||||
</article>`;
|
||||
}).join("")}`;
|
||||
@@ -350,10 +422,22 @@ function render() {
|
||||
render();
|
||||
});
|
||||
}
|
||||
if (state.follow) stage.scrollTop = stage.scrollHeight;
|
||||
const segmented = stage.querySelector(".segmented");
|
||||
if (segmented) {
|
||||
segmented.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("button[data-sort]");
|
||||
if (!button || button.classList.contains("active")) return;
|
||||
state.sort = button.dataset.sort === "asc" ? "asc" : "desc";
|
||||
localStorage.setItem(SORT_KEY, state.sort);
|
||||
render();
|
||||
});
|
||||
}
|
||||
if (state.follow) {
|
||||
stage.scrollTop = state.sort === "desc" ? 0 : stage.scrollHeight;
|
||||
}
|
||||
}
|
||||
function recordId(record) {
|
||||
return `${record.ts}|${record.prompt_sha256}|${record.session_id || ""}`;
|
||||
return `${record.seq || ""}|${record.ts}|${record.prompt_sha256}|${record.session_id || ""}|${record.event || ""}|${record.tool_name || ""}`;
|
||||
}
|
||||
function remember(record) {
|
||||
const id = recordId(record);
|
||||
|
||||
Reference in New Issue
Block a user