From dd6baa8dad643c7000125feafcb2ff2f182b6672 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Tue, 7 Apr 2026 20:54:15 +0800 Subject: [PATCH] Improve injection preview readability --- panel.js | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++- style.css | 131 +++++++++++++++++++++++++++ 2 files changed, 387 insertions(+), 3 deletions(-) diff --git a/panel.js b/panel.js index 90c92ad..128bd0a 100644 --- a/panel.js +++ b/panel.js @@ -1207,9 +1207,7 @@ async function _refreshInjectionPreview() { try { const { estimateTokens } = await import("./injector.js"); const totalTokens = estimateTokens(injection); - const preview = document.createElement("div"); - preview.className = "bme-injection-preview"; - preview.textContent = injection; + const preview = _buildInjectionPreviewNode(injection); container.replaceChildren(preview); if (tokenEl) tokenEl.textContent = `≈ ${totalTokens} tokens`; } catch (error) { @@ -1222,6 +1220,261 @@ async function _refreshInjectionPreview() { } } +function _buildInjectionPreviewNode(injectionText = "") { + const parsed = _parseInjectionPreview(String(injectionText || "")); + if (!parsed.sections.length) { + const preview = document.createElement("div"); + preview.className = "bme-injection-preview"; + preview.textContent = injectionText; + return preview; + } + + const root = document.createElement("div"); + root.className = "bme-injection-rich"; + + const hint = document.createElement("div"); + hint.className = "bme-injection-rich__hint"; + hint.textContent = "这里是结构化预览,便于阅读;实际发给模型的仍是原始注入文本。"; + root.appendChild(hint); + + for (const section of parsed.sections) { + const card = document.createElement("section"); + card.className = `bme-injection-card ${_getInjectionSectionFlavor(section.title)}`; + + const title = document.createElement("div"); + title.className = "bme-injection-card__title"; + title.textContent = section.title; + card.appendChild(title); + + if (section.note) { + const note = document.createElement("div"); + note.className = "bme-injection-card__note"; + note.textContent = section.note; + card.appendChild(note); + } + + for (const block of section.blocks) { + if (block.type === "table") { + card.appendChild(_buildInjectionTableNode(block)); + } else if (block.type === "text" && block.text) { + const text = document.createElement("div"); + text.className = "bme-injection-card__text"; + text.textContent = block.text; + card.appendChild(text); + } + } + + root.appendChild(card); + } + + return root; +} + +function _parseInjectionPreview(injectionText = "") { + const lines = String(injectionText || "").replace(/\r/g, "").split("\n"); + const sections = []; + let index = 0; + let currentSection = null; + + function ensureSection(title = "Memory") { + if (!currentSection) { + currentSection = { + title, + note: "", + blocks: [], + }; + sections.push(currentSection); + } + return currentSection; + } + + while (index < lines.length) { + const rawLine = lines[index] ?? ""; + const line = rawLine.trim(); + + if (!line) { + index += 1; + continue; + } + + const sectionMatch = line.match(/^\[(Memory\s*-\s*.+)]$/i); + if (sectionMatch) { + currentSection = { + title: sectionMatch[1], + note: "", + blocks: [], + }; + sections.push(currentSection); + index += 1; + + const noteCandidate = (lines[index] ?? "").trim(); + if ( + noteCandidate && + !noteCandidate.startsWith("[") && + !noteCandidate.endsWith(":") && + !noteCandidate.startsWith("|") && + !noteCandidate.startsWith("## ") + ) { + currentSection.note = noteCandidate; + index += 1; + } + continue; + } + + const section = ensureSection(); + + if (line.endsWith(":") && String(lines[index + 1] || "").trim().startsWith("|")) { + const tableName = line.slice(0, -1).trim(); + const tableLines = []; + index += 1; + while (index < lines.length) { + const tableLine = String(lines[index] || ""); + if (!tableLine.trim().startsWith("|")) { + break; + } + tableLines.push(tableLine.trim()); + index += 1; + } + const parsedTable = _parseInjectionTable(tableName, tableLines); + if (parsedTable) { + section.blocks.push(parsedTable); + } + continue; + } + + const textLines = []; + while (index < lines.length) { + const candidate = String(lines[index] || "").trim(); + if (!candidate) { + index += 1; + if (textLines.length > 0) { + break; + } + continue; + } + if ( + /^\[(Memory\s*-\s*.+)]$/i.test(candidate) || + (candidate.endsWith(":") && String(lines[index + 1] || "").trim().startsWith("|")) + ) { + break; + } + textLines.push(candidate); + index += 1; + } + if (textLines.length > 0) { + section.blocks.push({ + type: "text", + text: textLines.join("\n"), + }); + } + } + + return { sections }; +} + +function _parseInjectionTable(tableName, tableLines = []) { + if (!Array.isArray(tableLines) || tableLines.length < 2) { + return null; + } + + const headerCells = _splitInjectionTableRow(tableLines[0]); + if (!headerCells.length) { + return null; + } + + const rows = tableLines + .slice(2) + .map((row) => _splitInjectionTableRow(row)) + .filter((cells) => cells.length > 0); + + return { + type: "table", + name: tableName, + headers: headerCells, + rows, + }; +} + +function _splitInjectionTableRow(row = "") { + const text = String(row || "").trim(); + if (!text.startsWith("|")) { + return []; + } + + const inner = text.replace(/^\|/, "").replace(/\|$/, ""); + const cells = []; + let current = ""; + let escaped = false; + + for (const ch of inner) { + if (escaped) { + current += ch; + escaped = false; + continue; + } + if (ch === "\\") { + escaped = true; + continue; + } + if (ch === "|") { + cells.push(current.trim()); + current = ""; + continue; + } + current += ch; + } + + cells.push(current.trim()); + return cells.map((cell) => cell.replace(/\\\|/g, "|").trim()); +} + +function _buildInjectionTableNode(table) { + const wrap = document.createElement("div"); + wrap.className = "bme-injection-table-wrap"; + + const name = document.createElement("div"); + name.className = "bme-injection-table-name"; + name.textContent = table.name; + wrap.appendChild(name); + + const tableEl = document.createElement("table"); + tableEl.className = "bme-injection-table"; + + const thead = document.createElement("thead"); + const headRow = document.createElement("tr"); + for (const header of table.headers) { + const th = document.createElement("th"); + th.textContent = header; + headRow.appendChild(th); + } + thead.appendChild(headRow); + tableEl.appendChild(thead); + + const tbody = document.createElement("tbody"); + for (const row of table.rows) { + const tr = document.createElement("tr"); + const normalizedCells = table.headers.map((_, idx) => row[idx] ?? ""); + for (const cell of normalizedCells) { + const td = document.createElement("td"); + td.textContent = cell; + tr.appendChild(td); + } + tbody.appendChild(tr); + } + tableEl.appendChild(tbody); + wrap.appendChild(tableEl); + return wrap; +} + +function _getInjectionSectionFlavor(title = "") { + const normalized = String(title || "").toLowerCase(); + if (normalized.includes("character pov")) return "character-pov"; + if (normalized.includes("user pov")) return "user-pov"; + if (normalized.includes("current region")) return "objective-current"; + if (normalized.includes("global")) return "objective-global"; + return "generic"; +} + // ==================== 图谱 ==================== /** SillyTavern 用户显示名(name1),用于图谱分区:误标为角色的用户 POV 强制归用户区 */ diff --git a/style.css b/style.css index 58b022b..8ad26bd 100644 --- a/style.css +++ b/style.css @@ -909,6 +909,137 @@ overflow-y: auto; } +.bme-injection-rich { + display: flex; + flex-direction: column; + gap: 12px; + max-height: 100%; + overflow-y: auto; +} + +.bme-injection-rich__hint { + font-size: 11px; + line-height: 1.5; + color: var(--bme-on-surface-dim); + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + padding: 10px 12px; +} + +.bme-injection-card { + display: flex; + flex-direction: column; + gap: 10px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.015)); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 12px; + padding: 12px; +} + +.bme-injection-card.character-pov { + border-color: rgba(255, 113, 145, 0.26); + background: linear-gradient(180deg, rgba(255, 113, 145, 0.08), rgba(255, 255, 255, 0.02)); +} + +.bme-injection-card.user-pov { + border-color: rgba(104, 183, 255, 0.24); + background: linear-gradient(180deg, rgba(104, 183, 255, 0.08), rgba(255, 255, 255, 0.02)); +} + +.bme-injection-card.objective-current { + border-color: rgba(111, 230, 179, 0.22); + background: linear-gradient(180deg, rgba(111, 230, 179, 0.07), rgba(255, 255, 255, 0.02)); +} + +.bme-injection-card.objective-global { + border-color: rgba(255, 211, 102, 0.18); + background: linear-gradient(180deg, rgba(255, 211, 102, 0.06), rgba(255, 255, 255, 0.02)); +} + +.bme-injection-card__title { + font-size: 13px; + line-height: 1.35; + font-weight: 800; + color: var(--bme-on-surface); +} + +.bme-injection-card__note { + font-size: 11px; + line-height: 1.55; + color: var(--bme-on-surface-dim); + padding: 8px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.035); +} + +.bme-injection-card__text { + font-size: 12px; + line-height: 1.65; + color: var(--bme-on-surface); + white-space: pre-wrap; + word-break: break-word; +} + +.bme-injection-table-wrap { + display: flex; + flex-direction: column; + gap: 8px; + overflow-x: auto; +} + +.bme-injection-table-name { + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 10px; + line-height: 1.4; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--bme-on-surface-dim); +} + +.bme-injection-table { + width: 100%; + min-width: 560px; + border-collapse: collapse; + table-layout: fixed; + background: rgba(0, 0, 0, 0.18); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + overflow: hidden; +} + +.bme-injection-table th, +.bme-injection-table td { + padding: 8px 9px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-right: 1px solid rgba(255, 255, 255, 0.05); + font-size: 11px; + line-height: 1.55; + color: var(--bme-on-surface); + word-break: break-word; + overflow-wrap: anywhere; +} + +.bme-injection-table th:last-child, +.bme-injection-table td:last-child { + border-right: none; +} + +.bme-injection-table tr:last-child td { + border-bottom: none; +} + +.bme-injection-table th { + font-size: 10px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--bme-on-surface-dim); + background: rgba(255, 255, 255, 0.05); +} + .bme-injection-section-label { font-size: 10px; font-weight: 700;