feat:优化评分网页,可关闭状态看板

This commit is contained in:
SengokuCola
2026-04-19 01:43:27 +08:00
parent 4924b70184
commit 32fa254c45
25 changed files with 392 additions and 6337 deletions

View File

@@ -748,6 +748,12 @@ INDEX_HTML_V2 = r"""<!doctype html>
flex-wrap: wrap;
}
.header-tools { margin-bottom: 0; justify-content: flex-end; }
.global-evaluator {
width: 150px;
}
.global-evaluator {
width: 150px;
}
input, select, textarea, button {
font: inherit;
border: 1px solid var(--line);
@@ -868,6 +874,8 @@ INDEX_HTML_V2 = r"""<!doctype html>
<header>
<h1>Maisaka 回复效果评分预览</h1>
<div class="toolbar header-tools">
<span class="meta">评价人</span>
<input id="globalEvaluator" class="global-evaluator" placeholder="manual" oninput="saveGlobalEvaluator()" />
<button id="browseTab" class="tab-button active" onclick="setMode('browse')">浏览</button>
<button id="rateTab" class="tab-button" onclick="setMode('rate')">逐条评分</button>
<button class="secondary" onclick="reloadAll()">刷新</button>
@@ -1527,6 +1535,33 @@ INDEX_HTML_V3 = r"""<!doctype html>
}
.message-name { font-weight: 650; color: var(--text); }
.message-text { white-space: pre-wrap; word-break: break-word; line-height: 1.45; }
.quote-card {
border-left: 3px solid var(--accent);
background: var(--accent-soft);
border-radius: 6px;
padding: 6px 8px;
margin: 0 0 6px;
font-size: 12px;
color: var(--muted);
}
.quote-card.missing {
border-left-color: var(--warn);
background: #fff7ed;
}
.quote-title {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 3px;
font-weight: 650;
color: var(--text);
}
.quote-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.35;
}
.message-attachments {
display: flex;
gap: 6px;
@@ -1578,6 +1613,8 @@ INDEX_HTML_V3 = r"""<!doctype html>
<header>
<h1>Maisaka 回复效果评分预览</h1>
<div class="toolbar header-tools">
<span class="meta">评价人</span>
<input id="globalEvaluator" class="global-evaluator" placeholder="manual" oninput="saveGlobalEvaluator()" />
<button id="browseTab" class="tab-button active" onclick="setMode('browse')">浏览</button>
<button id="rateTab" class="tab-button" onclick="setMode('rate')">逐条评分</button>
<button class="secondary" onclick="reloadAll()">刷新</button>
@@ -1632,6 +1669,8 @@ INDEX_HTML_V3 = r"""<!doctype html>
let selectedEffect = "";
let activeMode = "browse";
let selectedFivePointScore = 0;
let currentTargetMessageId = "";
let currentMessageIndex = new Map();
async function api(path, options) {
const res = await fetch(path, options);
@@ -1763,6 +1802,11 @@ INDEX_HTML_V3 = r"""<!doctype html>
const reply = record.reply || {};
const manual = record._manual || {};
const followups = record.followup_messages || [];
currentTargetMessageId = String(reply.target_message_id || "");
const context = normalizeContextMessages(record.context_snapshot || []);
const normalizedFollowups = normalizeFollowupMessages(followups);
const botReply = normalizeBotReply(reply);
buildCurrentMessageIndex(context, botReply, normalizedFollowups);
selectedFivePointScore = Number(manual.manual_score_5 || score100ToFive(manual.manual_score) || 0);
document.getElementById("detailPane").innerHTML = `
<div class="block">
@@ -1784,10 +1828,6 @@ INDEX_HTML_V3 = r"""<!doctype html>
id="scoreButton${score}" onclick="selectFivePointScore(${score})">${score}</button>
`).join("")}
</div>
<label>评价人</label>
<input id="evaluator" value="${escapeAttr(manual.evaluator || "manual")}" />
<label>备注</label>
<textarea id="manualNotes">${escapeHtml(manual.notes || "")}</textarea>
<div class="toolbar">
<button onclick="saveFivePointManual('${escapeAttr(record.session.platform_type_id)}','${escapeAttr(record.effect_id)}', false)">
保存人工评分
@@ -1796,11 +1836,11 @@ INDEX_HTML_V3 = r"""<!doctype html>
</div>
<div class="block">
<h2>回复内容</h2>
${renderBotReplyCard(reply.reply_text || "")}
${renderChatMessageCard(botReply)}
</div>
<div class="block">
<h2>后续消息</h2>
${renderFollowupCards(followups)}
${renderMessageCards(normalizedFollowups, "暂无")}
</div>
<div class="block">
<h2>完整 JSON</h2>
@@ -1877,6 +1917,9 @@ INDEX_HTML_V3 = r"""<!doctype html>
const followups = record.followup_messages || [];
currentTargetMessageId = String(reply.target_message_id || "");
const context = normalizeContextMessages(record.context_snapshot || []);
const normalizedFollowups = normalizeFollowupMessages(followups);
const botReply = normalizeBotReply(reply);
buildCurrentMessageIndex(context, botReply, normalizedFollowups);
document.getElementById("detailPane").innerHTML = `
<div class="rate-top">
<div class="toolbar">
@@ -1904,11 +1947,11 @@ INDEX_HTML_V3 = r"""<!doctype html>
</div>
<div class="block">
<h2>Bot 回复</h2>
${renderBotReplyCard(reply.reply_text || "")}
${renderChatMessageCard(botReply)}
</div>
<div class="block">
<h2>后续消息</h2>
${renderFollowupCards(followups)}
${renderMessageCards(normalizedFollowups, "暂无")}
</div>
<div class="block">
<h2>人工五点评分</h2>
@@ -1918,10 +1961,6 @@ INDEX_HTML_V3 = r"""<!doctype html>
<button class="score-button" id="scoreButton${score}" onclick="selectFivePointScore(${score})">${score}</button>
`).join("")}
</div>
<label>评价人</label>
<input id="ratingEvaluator" value="manual" />
<label>备注</label>
<textarea id="ratingNotes"></textarea>
<div class="toolbar">
<button onclick="saveFivePointManual('${escapeAttr(record.session.platform_type_id)}','${escapeAttr(record.effect_id)}', true)">
保存并下一条
@@ -1950,8 +1989,8 @@ INDEX_HTML_V3 = r"""<!doctype html>
effect_id: effectId,
manual_score_5: selectedFivePointScore,
manual_label: "",
evaluator: valueOf("ratingEvaluator") || valueOf("evaluator") || "manual",
notes: valueOf("ratingNotes") || valueOf("manualNotes"),
evaluator: currentEvaluator(),
notes: "",
};
try {
await api("/api/annotations", {
@@ -1974,16 +2013,20 @@ INDEX_HTML_V3 = r"""<!doctype html>
const items = Array.isArray(context) ? context : [];
return items.filter(item => !isToolContextMessage(item)).map((item, index) => {
const parsed = parseVisibleText(item.text || "");
const rawText = parsed.content || item.text || "";
const messageId = item.message_id || parsed.messageId || "";
const attachments = Array.isArray(item.attachments) ? item.attachments : [];
return {
index,
role: item.role || parsed.role || "message",
source: item.source || "",
timestamp: item.timestamp || parsed.time || "",
name: parsed.name || roleName(item.role, item.source),
messageId: parsed.messageId || "",
text: cleanMessageText(parsed.content || item.text || ""),
attachments: Array.isArray(item.attachments) ? item.attachments : [],
isTarget: parsed.messageId && String(parsed.messageId) === String(selectedTargetMessageId()),
messageId,
text: cleanMessageText(rawText, attachments),
quoteTargetIds: quoteTargetIdsFromMessage(item, rawText),
attachments,
isTarget: messageId && String(messageId) === String(selectedTargetMessageId()),
};
});
}
@@ -2000,9 +2043,23 @@ INDEX_HTML_V3 = r"""<!doctype html>
function parseVisibleText(text) {
const value = String(text || "");
const pattern = /^(?<time>\d{1,2}:\d{2}:\d{2})?(?:\[msg_id:(?<messageId>[^\]]+)\])?(?:\[(?<name>[^\]]+)\])?(?<content>[\s\S]*)$/;
const plannerPattern = /^\s*\[时间\](?<time>[^\n]*)\n\[用户名\](?<name>[^\n]*)\n(?:\[用户群昵称\][^\n]*\n)?(?:\[msg_id\](?<plannerMessageId>[^\n]*)\n)?\[发言内容\](?<plannerContent>[\s\S]*)$/;
const plannerMatch = value.match(plannerPattern);
if (plannerMatch && plannerMatch.groups) {
return {
time: (plannerMatch.groups.time || "").trim(),
messageId: (plannerMatch.groups.plannerMessageId || "").trim(),
name: (plannerMatch.groups.name || "").trim(),
content: (plannerMatch.groups.plannerContent || "").trim(),
};
}
const pattern = /^\s*(?<time>\d{1,2}:\d{2}:\d{2})?(?:\[msg_id(?::|\])(?<messageId>[^\]\n]+)\]?)?(?:\[(?<name>[^\]]+)\])?(?<content>[\s\S]*)$/;
const match = value.match(pattern);
if (!match || !match.groups) return { content: value };
if (!match.groups.time && !match.groups.messageId && match.groups.name && !(match.groups.content || "").trim()) {
return { content: value };
}
return {
time: match.groups.time || "",
messageId: match.groups.messageId || "",
@@ -2015,8 +2072,71 @@ INDEX_HTML_V3 = r"""<!doctype html>
return currentTargetMessageId;
}
function renderMessageCards(messages) {
if (!messages.length) return `<div class="empty">暂无上下文</div>`;
function normalizeBotReply(reply) {
const metadata = reply.reply_metadata || {};
const sentIds = Array.isArray(metadata.sent_message_ids) ? metadata.sent_message_ids : [];
return {
side: "bot",
role: "assistant",
source: "guided_reply",
name: "Bot",
timestamp: "本次回复",
messageId: sentIds[0] || "",
messageIds: sentIds,
text: cleanMessageText(reply.reply_text || "", []),
quoteTargetIds: [],
attachments: [],
isTarget: false,
};
}
function normalizeFollowupMessages(followups) {
if (!followups || !followups.length) return [];
return followups.map(message => {
const rawText = message.visible_text || message.plain_text || "";
const parsed = parseVisibleText(rawText);
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
const messageId = message.message_id || parsed.messageId || "";
return {
side: "user",
role: "user",
source: "followup",
name: `${userName(message) || parsed.name}${message.is_target_user ? " · 目标用户" : ""}`,
timestamp: message.timestamp || "",
messageId,
text: cleanMessageText(parsed.content || rawText, attachments),
quoteTargetIds: quoteTargetIdsFromMessage(message, rawText),
attachments,
isTarget: message.is_target_user,
};
});
}
function buildCurrentMessageIndex(contextMessages, botReply, followupMessages) {
currentMessageIndex = new Map();
[...contextMessages, botReply, ...followupMessages].forEach(message => {
const ids = [message.messageId, ...(Array.isArray(message.messageIds) ? message.messageIds : [])]
.map(id => String(id || "").trim())
.filter(Boolean);
ids.forEach(id => {
if (!currentMessageIndex.has(id)) currentMessageIndex.set(id, message);
});
});
}
function quoteTargetIdsFromMessage(message, text) {
const structuredIds = Array.isArray(message.quote_target_ids) ? message.quote_target_ids : [];
const textIds = [];
String(text || "").replace(/\[引用回复\]\(([^)]+)\)/g, (_, id) => {
const normalizedId = String(id || "").trim();
if (normalizedId) textIds.push(normalizedId);
return "";
});
return [...new Set([...structuredIds, ...textIds].map(id => String(id || "").trim()).filter(Boolean))];
}
function renderMessageCards(messages, emptyText = "暂无上下文") {
if (!messages.length) return `<div class="empty">${escapeHtml(emptyText)}</div>`;
return messages.map(message => {
const side = isBotContextMessage(message) ? "bot" : "user";
return renderChatMessageCard({
@@ -2027,43 +2147,13 @@ INDEX_HTML_V3 = r"""<!doctype html>
timestamp: message.timestamp,
messageId: message.messageId,
text: message.text,
quoteTargetIds: message.quoteTargetIds || [],
attachments: message.attachments,
isTarget: message.isTarget,
});
}).join("");
}
function renderBotReplyCard(text) {
return renderChatMessageCard({
side: "bot",
role: "assistant",
source: "guided_reply",
name: "Bot",
timestamp: "本次回复",
messageId: "",
text,
attachments: [],
isTarget: false,
});
}
function renderFollowupCards(followups) {
if (!followups || !followups.length) return `<div class="empty">暂无</div>`;
return followups.map(message => `
${renderChatMessageCard({
side: "user",
role: "user",
source: "followup",
name: `${userName(message)}${message.is_target_user ? " · 目标用户" : ""}`,
timestamp: message.timestamp || "",
messageId: message.message_id || "",
text: cleanMessageText(message.visible_text || message.plain_text || ""),
attachments: Array.isArray(message.attachments) ? message.attachments : [],
isTarget: message.is_target_user,
})}
`).join("");
}
function renderChatMessageCard(message) {
const messageIdText = message.messageId ? ` · ${escapeHtml(message.messageId)}` : "";
const textHtml = message.text ? `<div class="message-text">${escapeHtml(message.text)}</div>` : "";
@@ -2074,6 +2164,7 @@ INDEX_HTML_V3 = r"""<!doctype html>
<span class="message-name">${escapeHtml(message.name || "消息")}</span>
<span>${escapeHtml(message.timestamp || "")}${messageIdText}</span>
</div>
${renderQuoteCards(message.quoteTargetIds || [])}
${textHtml}
${renderAttachments(message.attachments || [])}
</div>
@@ -2081,6 +2172,35 @@ INDEX_HTML_V3 = r"""<!doctype html>
`;
}
function renderQuoteCards(quoteTargetIds) {
if (!quoteTargetIds || !quoteTargetIds.length) return "";
return quoteTargetIds.map(targetId => {
const quoted = currentMessageIndex.get(String(targetId || ""));
if (!quoted) {
return `
<div class="quote-card missing">
<div class="quote-title">
<span>引用回复</span>
<span>${escapeHtml(targetId)}</span>
</div>
<div class="quote-text">未在本记录的上下文或后续消息中找到这条消息</div>
</div>
`;
}
const quotedName = quoted.name || roleName(quoted.role, quoted.source);
const quotedText = quoted.text || attachmentSummary(quoted.attachments || []) || "无文本内容";
return `
<div class="quote-card">
<div class="quote-title">
<span>引用 ${escapeHtml(quotedName)}</span>
<span>${escapeHtml(targetId)}</span>
</div>
<div class="quote-text">${escapeHtml(quotedText)}</div>
</div>
`;
}).join("");
}
function renderAttachments(attachments) {
const shown = (attachments || []).filter(item => attachmentUrl(item));
if (!shown.length) return "";
@@ -2089,7 +2209,6 @@ INDEX_HTML_V3 = r"""<!doctype html>
${shown.map(item => `
<div>
<img class="message-image" src="${escapeAttr(attachmentUrl(item))}" alt="${escapeAttr(item.content || item.kind || "图片")}" loading="lazy" />
${item.content ? `<div class="message-image-caption">${escapeHtml(item.content)}</div>` : ""}
</div>
`).join("")}
</div>
@@ -2104,11 +2223,32 @@ INDEX_HTML_V3 = r"""<!doctype html>
return "";
}
function cleanMessageText(text) {
return String(text || "")
.replace(/\[图片\]/g, "")
.replace(/\[表情包?\]/g, "")
.trim();
function cleanMessageText(text, attachments = []) {
let normalized = stripVisibleMessagePrefix(String(text || "")).replace(/\[引用回复\]\([^)]+\)/g, "");
const shownAttachments = (attachments || []).filter(item => attachmentUrl(item));
if (shownAttachments.length) {
normalized = normalized
.replace(/\[图片\]/g, "")
.replace(/\[表情包?\]/g, "");
for (const attachment of shownAttachments) {
const content = String(attachment.content || "").trim();
if (!content) continue;
normalized = normalized.split(content).join("");
}
}
return normalized.trim();
}
function stripVisibleMessagePrefix(text) {
const parsed = parseVisibleText(text);
if (parsed.content && parsed.content !== text) return parsed.content;
return String(text || "");
}
function attachmentSummary(attachments) {
const count = Array.isArray(attachments) ? attachments.length : 0;
if (!count) return "";
return count === 1 ? "[图片]" : `[${count} 张图片]`;
}
function isBotContextMessage(message) {
@@ -2152,6 +2292,28 @@ INDEX_HTML_V3 = r"""<!doctype html>
return element ? element.value : "";
}
function currentEvaluator() {
return valueOf("globalEvaluator").trim() || "manual";
}
function saveGlobalEvaluator() {
try {
localStorage.setItem("replyEffectEvaluator", currentEvaluator());
} catch (_err) {
return;
}
}
function restoreGlobalEvaluator() {
const input = document.getElementById("globalEvaluator");
if (!input) return;
try {
input.value = localStorage.getItem("replyEffectEvaluator") || "manual";
} catch (_err) {
input.value = "manual";
}
}
function scoreText(v) {
return v === null || v === undefined || v === "" ? "N/A" : Number(v).toFixed(1);
}
@@ -2174,6 +2336,7 @@ INDEX_HTML_V3 = r"""<!doctype html>
return escapeHtml(value).replace(/`/g, "&#96;");
}
restoreGlobalEvaluator();
reloadAll();
</script>
</body>