feat: redesign regex editor as single column

This commit is contained in:
Youzini-afk
2026-04-09 21:18:27 +08:00
parent 5d682c5f55
commit 622390d056
2 changed files with 454 additions and 82 deletions

133
style.css
View File

@@ -2694,12 +2694,6 @@
top: auto;
}
.bme-task-regex-top {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.bme-task-block-count {
font-size: 11px;
color: var(--bme-on-surface-dim);
@@ -3063,6 +3057,126 @@
margin-top: 8px;
}
/* ═══════ Single-column Regex Editor ═══════ */
.bme-regex-settings-stack {
display: flex;
flex-direction: column;
gap: 14px;
}
.bme-regex-settings-card .bme-task-section-label {
margin-top: 0;
margin-bottom: 10px;
}
.bme-regex-rule-card .bme-task-empty,
.bme-regex-rule-card .bme-task-note {
margin: 0;
}
.bme-regex-rule-rows {
display: flex;
flex-direction: column;
gap: 4px;
}
.bme-regex-rule-row {
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
background: rgba(255, 255, 255, 0.025);
transition: border-color 0.15s, box-shadow 0.15s;
}
.bme-regex-rule-row:hover {
border-color: rgba(255, 255, 255, 0.12);
}
.bme-regex-rule-row.is-expanded {
border-color: var(--bme-primary);
box-shadow: 0 0 0 1px var(--bme-primary);
}
.bme-regex-rule-row.is-disabled {
opacity: 0.56;
}
.bme-regex-rule-row.dragging {
opacity: 0.35;
}
.bme-regex-rule-row.drag-over-top {
border-top: 2px solid var(--bme-primary);
margin-top: -1px;
}
.bme-regex-rule-row.drag-over-bottom {
border-bottom: 2px solid var(--bme-primary);
margin-bottom: -1px;
}
.bme-regex-rule-row-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
min-height: 44px;
cursor: pointer;
user-select: none;
flex-wrap: wrap;
}
.bme-regex-rule-name {
flex: 0 1 220px;
min-width: 120px;
font-size: 13px;
font-weight: 600;
color: var(--bme-on-surface);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bme-regex-rule-status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 42px;
padding: 2px 7px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
flex-shrink: 0;
}
.bme-regex-rule-status.is-enabled {
background: rgba(80, 180, 120, 0.18);
color: #80d0a0;
}
.bme-regex-rule-status.is-disabled {
background: rgba(160, 160, 180, 0.14);
color: var(--bme-on-surface-dim);
}
.bme-regex-rule-preview {
flex: 1 1 260px;
min-width: 0;
font-size: 11px;
line-height: 1.45;
color: var(--bme-on-surface-dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bme-regex-rule-expand {
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding: 12px;
animation: bme-block-expand-in 0.2s ease;
}
@keyframes bme-block-expand-in {
from {
opacity: 0;
@@ -3934,7 +4048,6 @@
.bme-theme-card-grid,
.bme-task-field-grid,
.bme-task-expand-row2,
.bme-task-regex-top,
.bme-task-debug-grid {
grid-template-columns: 1fr;
}
@@ -4131,6 +4244,12 @@
max-width: 100%;
}
.bme-regex-rule-preview {
flex: 1 1 100%;
order: 10;
margin-left: 28px;
}
/* Dashboard 统计卡片横向滚动 */
.bme-stats-grid {
display: flex;

View File

@@ -282,6 +282,8 @@ let currentTaskProfileTabId = "generation";
let currentTaskProfileBlockId = "";
let currentTaskProfileDragBlockId = "";
let currentTaskProfileRuleId = "";
let currentTaskProfileDragRuleId = "";
let currentTaskProfileDragRuleIsGlobal = false;
let showGlobalRegexPanel = false;
let currentGlobalRegexRuleId = "";
let currentCognitionOwnerKey = "";
@@ -5060,6 +5062,74 @@ function _bindTaskProfileWorkspace() {
currentTaskProfileDragBlockId = "";
_clearTaskBlockDragIndicators(workspace);
});
workspace.addEventListener("dragstart", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const handle = target.closest(".bme-regex-drag-handle");
const row = target.closest(".bme-regex-rule-row");
if (!handle || !(row instanceof HTMLElement)) return;
const ruleId = String(row.dataset.ruleId || "").trim();
if (!ruleId) return;
currentTaskProfileDragRuleId = ruleId;
currentTaskProfileDragRuleIsGlobal = _isGlobalRegexPanelTarget(row);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.dropEffect = "move";
event.dataTransfer.setData("text/plain", ruleId);
}
window.requestAnimationFrame(() => {
row.classList.add("dragging");
});
});
workspace.addEventListener("dragover", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement) || !currentTaskProfileDragRuleId) return;
const row = target.closest(".bme-regex-rule-row");
if (!(row instanceof HTMLElement)) return;
const isGlobalRow = _isGlobalRegexPanelTarget(row);
if (isGlobalRow !== currentTaskProfileDragRuleIsGlobal) return;
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
const position = _getRegexRuleDropPosition(row, event.clientY);
_setRegexRuleDragIndicator(workspace, row, position);
});
workspace.addEventListener("dragleave", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest(".bme-regex-rule-row");
if (!(row instanceof HTMLElement)) return;
const relatedTarget = event.relatedTarget;
if (relatedTarget instanceof Node && row.contains(relatedTarget)) {
return;
}
row.classList.remove("drag-over-top", "drag-over-bottom");
});
workspace.addEventListener("drop", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest(".bme-regex-rule-row");
if (!(row instanceof HTMLElement)) return;
const isGlobalRow = _isGlobalRegexPanelTarget(row);
if (isGlobalRow !== currentTaskProfileDragRuleIsGlobal) return;
event.preventDefault();
const sourceId =
currentTaskProfileDragRuleId ||
String(event.dataTransfer?.getData("text/plain") || "").trim();
const targetId = String(row.dataset.ruleId || "").trim();
const position = _getRegexRuleDropPosition(row, event.clientY);
_clearRegexRuleDragIndicators(workspace);
currentTaskProfileDragRuleId = "";
currentTaskProfileDragRuleIsGlobal = false;
if (!sourceId || !targetId || sourceId === targetId) return;
_reorderRegexRules(sourceId, targetId, position, isGlobalRow);
});
workspace.addEventListener("dragend", () => {
currentTaskProfileDragRuleId = "";
currentTaskProfileDragRuleIsGlobal = false;
_clearRegexRuleDragIndicators(workspace);
});
workspace.dataset.bmeBound = "true";
}
@@ -5280,6 +5350,13 @@ function _handleTaskProfileWorkspaceInput(event) {
} else {
_persistSelectedRegexRuleField(target, false);
}
return;
}
if (target.matches("[data-regex-rule-row-enabled]")) {
const ruleId = String(target.dataset.ruleId || "").trim();
if (!ruleId) return;
_persistRegexRuleEnabledById(ruleId, Boolean(target.checked), isGlobalRegexPanel, false);
}
}
@@ -5353,6 +5430,13 @@ function _handleTaskProfileWorkspaceChange(event) {
} else {
_persistSelectedRegexRuleField(target, true);
}
return;
}
if (target.matches("[data-regex-rule-row-enabled]")) {
const ruleId = String(target.dataset.ruleId || "").trim();
if (!ruleId) return;
_persistRegexRuleEnabledById(ruleId, Boolean(target.checked), isGlobalRegexPanel, true);
}
}
@@ -5392,10 +5476,10 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
if (currentTaskProfileBlockId && !blocks.some((block) => block.id === currentTaskProfileBlockId)) {
currentTaskProfileBlockId = blocks[0]?.id || "";
}
if (!regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) {
if (currentTaskProfileRuleId && !regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) {
currentTaskProfileRuleId = regexRules[0]?.id || "";
}
if (!globalRegexRules.some((rule) => rule.id === currentGlobalRegexRuleId)) {
if (currentGlobalRegexRuleId && !globalRegexRules.some((rule) => rule.id === currentGlobalRegexRuleId)) {
currentGlobalRegexRuleId = globalRegexRules[0]?.id || "";
}
@@ -6112,6 +6196,26 @@ async function _handleTaskProfileWorkspaceClick(event) {
_refreshTaskProfileWorkspace();
return;
}
case "toggle-regex-rule-expand": {
const originEl = event.target;
if (
originEl.closest(".bme-task-row-toggle") ||
originEl.closest(".bme-task-row-btn-danger") ||
originEl.closest(".bme-regex-drag-handle")
) {
return;
}
const ruleId = actionEl.dataset.ruleId || "";
if (_isGlobalRegexPanelTarget(actionEl)) {
currentGlobalRegexRuleId =
currentGlobalRegexRuleId === ruleId ? "" : ruleId;
} else {
currentTaskProfileRuleId =
currentTaskProfileRuleId === ruleId ? "" : ruleId;
}
_refreshTaskProfileWorkspace();
return;
}
case "select-regex-rule":
if (_isGlobalRegexPanelTarget(actionEl)) {
currentGlobalRegexRuleId = actionEl.dataset.ruleId || "";
@@ -6560,7 +6664,6 @@ function _renderTaskRegexTab(state, options = {}) {
const selectedRule =
options.selectedRule === undefined ? state.selectedRule : options.selectedRule;
const normalizedStages = normalizeTaskRegexStages(regex.stages || {});
const selectAction = options.selectAction || "select-regex-rule";
const deleteAction = options.deleteAction || "delete-regex-rule";
const addAction = options.addAction || "add-regex-rule";
const addButtonLabel = options.addButtonLabel || "+ 新增规则";
@@ -6578,6 +6681,9 @@ function _renderTaskRegexTab(state, options = {}) {
const emptyText = options.emptyText || "当前预设还没有本地正则规则。";
const defaultNamePrefix = options.defaultNamePrefix || "本地规则";
const headerExtraActions = options.extraHeaderActions || "";
const enableToggleTitle = options.enableToggleTitle || "启用任务正则";
const enableToggleDesc =
options.enableToggleDesc || "关闭后当前配置不执行任何任务级正则。";
const editorState = {
...state,
selectedRule,
@@ -6585,8 +6691,8 @@ function _renderTaskRegexTab(state, options = {}) {
return `
<div class="bme-task-tab-body${wrapperClassName}">
<div class="bme-task-regex-top">
<div class="bme-config-card">
<div class="bme-regex-settings-stack">
<div class="bme-config-card bme-regex-settings-card">
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">${_escHtml(sectionTitle)}</div>
@@ -6605,8 +6711,8 @@ function _renderTaskRegexTab(state, options = {}) {
<div class="bme-task-toggle-list">
<label class="bme-toggle-item">
<span class="bme-toggle-copy">
<span class="bme-toggle-title">启用任务正则</span>
<span class="bme-toggle-desc">关闭后当前配置不执行任何任务级正则。</span>
<span class="bme-toggle-title">${_escHtml(enableToggleTitle)}</span>
<span class="bme-toggle-desc">${_escHtml(enableToggleDesc)}</span>
</span>
<input
type="checkbox"
@@ -6627,7 +6733,9 @@ function _renderTaskRegexTab(state, options = {}) {
/>
</label>
</div>
</div>
<div class="bme-config-card bme-regex-settings-card">
<div class="bme-task-section-label">复用来源</div>
<div class="bme-task-toggle-list">
${[
@@ -6652,7 +6760,9 @@ function _renderTaskRegexTab(state, options = {}) {
)
.join("")}
</div>
</div>
<div class="bme-config-card bme-regex-settings-card">
<div class="bme-task-section-label">执行阶段</div>
<div class="bme-task-toggle-list">
${TASK_PROFILE_REGEX_STAGES.map(
@@ -6672,42 +6782,37 @@ function _renderTaskRegexTab(state, options = {}) {
).join("")}
</div>
</div>
<div class="bme-config-card">
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">${_escHtml(rulesTitle)}</div>
<div class="bme-config-card-subtitle">
${_escHtml(rulesSubtitle)}
</div>
</div>
<button class="bme-config-secondary-btn" data-task-action="${_escAttr(addAction)}" type="button">
${_escHtml(addButtonLabel)}
</button>
</div>
<div class="bme-task-list">
${regexRules.length
? regexRules
.map((rule, index) =>
_renderRegexRuleListItem(rule, index, editorState, {
selectAction,
deleteAction,
defaultNamePrefix,
})
)
.join("")
: `
<div class="bme-task-empty">
${_escHtml(emptyText)}
</div>
`}
</div>
</div>
</div>
<div class="bme-config-card">
${_renderRegexRuleEditor(editorState)}
<div class="bme-config-card bme-regex-rule-card">
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">${_escHtml(rulesTitle)}</div>
<div class="bme-config-card-subtitle">
${_escHtml(rulesSubtitle)}
</div>
</div>
<button class="bme-config-secondary-btn" data-task-action="${_escAttr(addAction)}" type="button">
${_escHtml(addButtonLabel)}
</button>
</div>
<div class="bme-regex-rule-rows">
${regexRules.length
? regexRules
.map((rule, index) =>
_renderRegexRuleRow(rule, index, editorState, {
deleteAction,
defaultNamePrefix,
})
)
.join("")
: `
<div class="bme-task-empty">
${_escHtml(emptyText)}
</div>
`}
</div>
</div>
</div>
`;
@@ -6730,6 +6835,8 @@ function _renderGlobalRegexPanel(state) {
wrapperClassName: "bme-global-regex-panel",
sectionTitle: "通用正则设置",
sectionSubtitle: "所有任务共享同一套任务正则开关、复用来源、执行阶段与附加规则。",
enableToggleTitle: "启用通用正则",
enableToggleDesc: "关闭后所有任务都不执行任何共享正则配置。",
rulesTitle: "通用附加规则",
rulesSubtitle: "这里维护所有任务共享的附加规则。",
emptyText: "当前还没有通用正则规则。",
@@ -7825,63 +7932,94 @@ function _renderGenerationField(field, value, state = {}) {
`;
}
function _renderRegexRuleListItem(rule, index, state, options = {}) {
const isSelected = rule.id === state.selectedRule?.id;
const selectAction = options.selectAction || "select-regex-rule";
function _formatRegexRulePreview(findRegex = "") {
const collapsed = String(findRegex || "")
.replace(/\s+/g, " ")
.trim();
return collapsed || "(未填写 find_regex)";
}
function _renderRegexRuleRow(rule, index, state, options = {}) {
const isExpanded = rule.id === state.selectedRule?.id;
const deleteAction = options.deleteAction || "delete-regex-rule";
const defaultNamePrefix = options.defaultNamePrefix || "本地规则";
const statusLabel = rule.enabled ? "启用" : "停用";
const previewText = _formatRegexRulePreview(rule.find_regex);
return `
<div class="bme-task-list-entry">
<button
class="bme-task-list-item ${isSelected ? "active" : ""}"
data-task-action="${_escAttr(selectAction)}"
<div
class="bme-regex-rule-row ${isExpanded ? "is-expanded" : ""} ${rule.enabled ? "" : "is-disabled"}"
data-rule-id="${_escAttr(rule.id)}"
>
<div
class="bme-regex-rule-row-header"
data-task-action="toggle-regex-rule-expand"
data-rule-id="${_escAttr(rule.id)}"
type="button"
>
<span class="bme-task-list-index">#${index + 1}</span>
<span class="bme-task-list-copy">
<span class="bme-task-list-title">${_escHtml(rule.script_name || `${defaultNamePrefix} ${index + 1}`)}</span>
<span class="bme-task-list-meta">
${rule.enabled ? "启用" : "停用"} · ${_escHtml(rule.find_regex || "(未填写 find_regex)")}
</span>
<span
class="bme-task-drag-handle bme-regex-drag-handle"
title="拖拽排序"
aria-label="拖拽排序"
draggable="true"
>
<i class="fa-solid fa-grip-vertical"></i>
</span>
<span class="bme-regex-rule-name">
${_escHtml(rule.script_name || `${defaultNamePrefix} ${index + 1}`)}
</span>
<span class="bme-regex-rule-status ${rule.enabled ? "is-enabled" : "is-disabled"}">
${_escHtml(statusLabel)}
</span>
<span class="bme-regex-rule-preview" title="${_escAttr(previewText)}">
${_escHtml(previewText)}
</span>
</button>
<div class="bme-task-inline-actions">
<button
class="bme-config-secondary-btn bme-task-mini-btn"
class="bme-task-row-btn"
data-task-action="toggle-regex-rule-expand"
data-rule-id="${_escAttr(rule.id)}"
type="button"
title="编辑"
>
<i class="fa-solid fa-pen"></i>
</button>
<button
class="bme-task-row-btn bme-task-row-btn-danger"
data-task-action="${_escAttr(deleteAction)}"
data-rule-id="${_escAttr(rule.id)}"
type="button"
title="删除"
>
删除
<i class="fa-solid fa-xmark"></i>
</button>
<label class="bme-task-row-toggle" title="${rule.enabled ? "已启用" : "已停用"}">
<input
type="checkbox"
data-regex-rule-row-enabled="true"
data-rule-id="${_escAttr(rule.id)}"
${rule.enabled ? "checked" : ""}
/>
<span class="bme-task-row-toggle-slider"></span>
</label>
</div>
${isExpanded
? `
<div class="bme-regex-rule-expand">
${_renderRegexRuleInlineEditor(rule)}
</div>
`
: ""}
</div>
`;
}
function _renderRegexRuleEditor(state) {
const rule = state.selectedRule;
if (!rule) {
return `
<div class="bme-config-card-title">规则详情</div>
<div class="bme-config-help">从左侧规则列表选择一条规则进行编辑。</div>
`;
}
function _renderRegexRuleInlineEditor(rule) {
const trimStrings = Array.isArray(rule.trim_strings)
? rule.trim_strings.join("\n")
: String(rule.trim_strings || "");
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">规则详情</div>
<div class="bme-config-card-subtitle">
字段尽量与 Tavern 正则结构保持对齐,方便后续导入导出与对照。
</div>
</div>
<span class="bme-task-pill">${rule.enabled ? "启用中" : "已停用"}</span>
<div class="bme-task-note">
字段尽量与 Tavern 正则结构保持对齐,方便后续导入导出与对照。
</div>
<div class="bme-config-row">
@@ -8005,6 +8143,17 @@ function _renderRegexRuleEditor(state) {
/>
</label>
</div>
<div class="bme-task-expand-footer">
<button
class="bme-config-secondary-btn"
data-task-action="toggle-regex-rule-expand"
data-rule-id="${_escAttr(rule.id)}"
type="button"
>
<i class="fa-solid fa-chevron-up"></i> 收起
</button>
</div>
`;
}
@@ -8111,6 +8260,110 @@ function _deleteRegexRule(ruleId) {
});
}
function _getRegexRuleDropPosition(row, clientY) {
const rect = row.getBoundingClientRect();
return clientY < rect.top + rect.height / 2 ? "before" : "after";
}
function _clearRegexRuleDragIndicators(workspace = document) {
workspace
.querySelectorAll(".bme-regex-rule-row.dragging, .bme-regex-rule-row.drag-over-top, .bme-regex-rule-row.drag-over-bottom")
.forEach((row) => {
row.classList.remove("dragging", "drag-over-top", "drag-over-bottom");
});
}
function _setRegexRuleDragIndicator(workspace, activeRow, position) {
workspace.querySelectorAll(".bme-regex-rule-row").forEach((row) => {
if (row !== activeRow) {
row.classList.remove("drag-over-top", "drag-over-bottom");
return;
}
row.classList.toggle("drag-over-top", position === "before");
row.classList.toggle("drag-over-bottom", position === "after");
});
}
function _reorderRegexRules(sourceRuleId, targetRuleId, position = "before", isGlobal = false) {
if (!sourceRuleId || !targetRuleId || sourceRuleId === targetRuleId) return;
const applyReorder = (rules = []) => {
const nextRules = Array.isArray(rules) ? [...rules] : [];
const sourceIndex = nextRules.findIndex((item) => item.id === sourceRuleId);
const targetIndex = nextRules.findIndex((item) => item.id === targetRuleId);
if (sourceIndex < 0 || targetIndex < 0) {
return null;
}
const [sourceRule] = nextRules.splice(sourceIndex, 1);
let insertIndex = targetIndex;
if (sourceIndex < targetIndex) {
insertIndex -= 1;
}
if (position === "after") {
insertIndex += 1;
}
insertIndex = Math.max(0, Math.min(nextRules.length, insertIndex));
nextRules.splice(insertIndex, 0, sourceRule);
return nextRules;
};
if (isGlobal) {
_updateGlobalTaskRegex((draft) => {
const localRules = applyReorder(draft.localRules);
if (!localRules) return null;
draft.localRules = localRules;
return { selectRuleId: sourceRuleId };
});
return;
}
_updateCurrentTaskProfile((draft) => {
const localRules = applyReorder(draft.regex?.localRules);
if (!localRules) return null;
draft.regex = {
...(draft.regex || {}),
localRules,
};
return { selectRuleId: sourceRuleId };
});
}
function _persistRegexRuleEnabledById(ruleId, enabled, isGlobal = false, refresh = true) {
if (!ruleId) return;
if (isGlobal) {
_updateGlobalTaskRegex(
(draft) => {
const localRules = Array.isArray(draft.localRules) ? [...draft.localRules] : [];
const rule = localRules.find((item) => item.id === ruleId);
if (!rule) return null;
rule.enabled = Boolean(enabled);
draft.localRules = localRules;
return { selectRuleId: currentGlobalRegexRuleId };
},
{ refresh },
);
return;
}
_updateCurrentTaskProfile(
(draft) => {
const localRules = Array.isArray(draft.regex?.localRules)
? [...draft.regex.localRules]
: [];
const rule = localRules.find((item) => item.id === ruleId);
if (!rule) return null;
rule.enabled = Boolean(enabled);
draft.regex = {
...(draft.regex || {}),
localRules,
};
return { selectRuleId: currentTaskProfileRuleId };
},
{ refresh },
);
}
function _persistSelectedBlockField(target, refresh) {
const field = target.dataset.blockField;
if (!field) return;