feat: redesign prompt block editor as single column

This commit is contained in:
Youzini-afk
2026-04-09 20:36:15 +08:00
parent 49535f8772
commit 4d1077754c
2 changed files with 519 additions and 149 deletions

264
style.css
View File

@@ -2683,15 +2683,15 @@
}
.bme-task-editor-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
display: flex;
flex-direction: column;
gap: 14px;
align-items: start;
}
.bme-task-editor-grid > .bme-config-card:last-child {
position: sticky;
top: 0;
position: static;
top: auto;
}
.bme-task-regex-top {
@@ -2812,6 +2812,260 @@
line-height: 1.55;
}
/* ═══════ Single-column Block Row (EW-style) ═══════ */
.bme-task-block-rows {
display: flex;
flex-direction: column;
gap: 4px;
}
.bme-task-block-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-task-block-row:hover {
border-color: rgba(255, 255, 255, 0.12);
}
.bme-task-block-row.is-expanded {
border-color: var(--bme-primary);
box-shadow: 0 0 0 1px var(--bme-primary);
}
.bme-task-block-row.is-disabled {
opacity: 0.5;
}
.bme-task-block-row.dragging {
opacity: 0.35;
}
.bme-task-block-row.drag-over-top {
border-top: 2px solid var(--bme-primary);
margin-top: -1px;
}
.bme-task-block-row.drag-over-bottom {
border-bottom: 2px solid var(--bme-primary);
margin-bottom: -1px;
}
/* Row header */
.bme-task-block-row-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
min-height: 44px;
cursor: pointer;
user-select: none;
flex-wrap: wrap;
}
/* Drag handle */
.bme-task-drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
color: var(--bme-on-surface-dim);
opacity: 0.45;
cursor: grab;
flex-shrink: 0;
transition: opacity 0.15s;
}
.bme-task-drag-handle:hover {
opacity: 1;
}
.bme-task-drag-handle:active {
cursor: grabbing;
}
/* Block type icon */
.bme-task-block-icon {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
font-size: 14px;
flex-shrink: 0;
color: var(--bme-on-surface-dim);
}
/* Block name */
.bme-task-block-name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 600;
color: var(--bme-on-surface);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Badges (role / inject) */
.bme-task-block-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 700;
padding: 2px 7px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.07);
color: var(--bme-on-surface-dim);
white-space: nowrap;
flex-shrink: 0;
text-transform: lowercase;
}
.bme-badge-role-system {
background: rgba(140, 140, 160, 0.18);
color: #b0b0c0;
}
.bme-badge-role-user {
background: rgba(160, 100, 220, 0.2);
color: #c8a0f0;
}
.bme-badge-role-assistant {
background: rgba(80, 180, 120, 0.2);
color: #80d0a0;
}
/* Row spacer */
.bme-task-block-row-spacer {
flex: 1;
}
/* Row action buttons (edit / delete) */
.bme-task-row-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--bme-on-surface-dim);
cursor: pointer;
flex-shrink: 0;
font-size: 13px;
transition: background 0.12s, color 0.12s;
}
.bme-task-row-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--bme-on-surface);
}
.bme-task-row-btn-danger:hover {
background: rgba(220, 80, 80, 0.15);
color: #f08080;
}
/* Inline toggle switch (EW-style yellow) */
.bme-task-row-toggle {
position: relative;
display: inline-flex;
align-items: center;
width: 38px;
height: 22px;
flex-shrink: 0;
cursor: pointer;
}
.bme-task-row-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.bme-task-row-toggle-slider {
position: absolute;
inset: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
transition: background 0.2s;
}
.bme-task-row-toggle-slider::before {
content: "";
position: absolute;
left: 3px;
top: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #888;
transition: transform 0.2s, background 0.2s;
}
.bme-task-row-toggle input:checked + .bme-task-row-toggle-slider {
background: rgba(220, 180, 50, 0.35);
}
.bme-task-row-toggle input:checked + .bme-task-row-toggle-slider::before {
transform: translateX(16px);
background: #e0b830;
}
/* Expand editor area */
.bme-task-block-expand {
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding: 12px;
animation: bme-block-expand-in 0.2s ease;
}
.bme-task-expand-row2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.bme-task-block-expand .bme-config-row {
margin-bottom: 8px;
}
.bme-task-block-expand .bme-config-row:last-child {
margin-bottom: 0;
}
.bme-task-block-expand .bme-config-textarea {
min-height: 120px;
max-height: 400px;
resize: vertical;
}
.bme-task-expand-footer {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
@keyframes bme-block-expand-in {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 800px;
}
}
.bme-regex-preview-screen {
gap: 14px;
}
@@ -3670,7 +3924,7 @@
.bme-config-grid-2,
.bme-theme-card-grid,
.bme-task-field-grid,
.bme-task-editor-grid,
.bme-task-expand-row2,
.bme-task-regex-top,
.bme-task-debug-grid {
grid-template-columns: 1fr;

View File

@@ -280,6 +280,7 @@ let currentConfigSectionId = "toggles";
let currentTaskProfileTaskType = "extract";
let currentTaskProfileTabId = "generation";
let currentTaskProfileBlockId = "";
let currentTaskProfileDragBlockId = "";
let currentTaskProfileRuleId = "";
let showGlobalRegexPanel = false;
let currentGlobalRegexRuleId = "";
@@ -4998,6 +4999,67 @@ function _bindTaskProfileWorkspace() {
workspace.addEventListener("change", (event) => {
_handleTaskProfileWorkspaceChange(event);
});
workspace.addEventListener("dragstart", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const handle = target.closest(".bme-task-drag-handle");
const row = target.closest(".bme-task-block-row");
if (!handle || !(row instanceof HTMLElement)) return;
const blockId = String(row.dataset.blockId || "").trim();
if (!blockId) return;
currentTaskProfileDragBlockId = blockId;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.dropEffect = "move";
event.dataTransfer.setData("text/plain", blockId);
}
window.requestAnimationFrame(() => {
row.classList.add("dragging");
});
});
workspace.addEventListener("dragover", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement) || !currentTaskProfileDragBlockId) return;
const row = target.closest(".bme-task-block-row");
if (!(row instanceof HTMLElement)) return;
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
const position = _getTaskBlockDropPosition(row, event.clientY);
_setTaskBlockDragIndicator(workspace, row, position);
});
workspace.addEventListener("dragleave", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest(".bme-task-block-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-task-block-row");
if (!(row instanceof HTMLElement)) return;
event.preventDefault();
const sourceId =
currentTaskProfileDragBlockId ||
String(event.dataTransfer?.getData("text/plain") || "").trim();
const targetId = String(row.dataset.blockId || "").trim();
const position = _getTaskBlockDropPosition(row, event.clientY);
_clearTaskBlockDragIndicators(workspace);
currentTaskProfileDragBlockId = "";
if (!sourceId || !targetId || sourceId === targetId) return;
_reorderTaskBlocks(sourceId, targetId, position);
});
workspace.addEventListener("dragend", () => {
currentTaskProfileDragBlockId = "";
_clearTaskBlockDragIndicators(workspace);
});
workspace.dataset.bmeBound = "true";
}
@@ -5327,7 +5389,7 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) {
? profile.regex.localRules
: [];
if (!blocks.some((block) => block.id === currentTaskProfileBlockId)) {
if (currentTaskProfileBlockId && !blocks.some((block) => block.id === currentTaskProfileBlockId)) {
currentTaskProfileBlockId = blocks[0]?.id || "";
}
if (!regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) {
@@ -6035,6 +6097,21 @@ async function _handleTaskProfileWorkspaceClick(event) {
currentTaskProfileBlockId = actionEl.dataset.blockId || "";
_refreshTaskProfileWorkspace();
return;
case "toggle-block-expand": {
// Ignore if the click originated from a toggle switch, delete button, or drag handle
const originEl = event.target;
if (originEl.closest(".bme-task-row-toggle") || originEl.closest(".bme-task-row-btn-danger") || originEl.closest(".bme-task-drag-handle")) {
return;
}
const blockId = actionEl.dataset.blockId || "";
if (currentTaskProfileBlockId === blockId) {
currentTaskProfileBlockId = "";
} else {
currentTaskProfileBlockId = blockId;
}
_refreshTaskProfileWorkspace();
return;
}
case "select-regex-rule":
if (_isGlobalRegexPanelTarget(actionEl)) {
currentGlobalRegexRuleId = actionEl.dataset.ruleId || "";
@@ -6085,6 +6162,16 @@ async function _handleTaskProfileWorkspaceClick(event) {
return { selectBlockId: block.id };
});
return;
case "toggle-block-enabled-cb":
_updateCurrentTaskProfile((draft) => {
const blocks = _sortTaskBlocks(draft.blocks);
const block = blocks.find((item) => item.id === actionEl.dataset.blockId);
if (!block) return null;
block.enabled = actionEl.checked;
draft.blocks = _normalizeTaskBlocks(blocks);
return { selectBlockId: currentTaskProfileBlockId };
});
return;
case "delete-block":
_deleteTaskBlock(actionEl.dataset.blockId);
return;
@@ -6365,57 +6452,40 @@ function _renderTaskProfileWorkspace(state) {
}
function _renderTaskPromptTab(state) {
return `
<div class="bme-task-editor-grid">
<div class="bme-config-card">
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">Prompt 块列表</div>
<div class="bme-config-card-subtitle">
通过顺序、启停与角色控制最终请求的编排方式。
<div class="bme-task-toolbar-row">
<div class="bme-task-toolbar-inline">
<button class="bme-config-secondary-btn" data-task-action="add-custom-block" type="button">
+ 自定义块
</button>
<span class="bme-task-action-sep"></span>
<select id="bme-task-builtin-select" class="bme-config-input bme-task-builtin-select">
${state.builtinBlockDefinitions
.map(
(item) => `
<option value="${_escAttr(item.sourceKey)}">
${_escHtml(item.name)}
</option>
`,
)
.join("")}
</select>
<button class="bme-config-secondary-btn" data-task-action="add-builtin-block" type="button">
+ 内置块
</button>
</div>
<span class="bme-task-block-count">${state.blocks.length} 个块</span>
</div>
<div class="bme-task-block-rows">
${state.blocks.length
? state.blocks
.map((block, index) => _renderTaskBlockRow(block, index, state))
.join("")
: `
<div class="bme-task-empty">
当前预设还没有块。可以先新增一个自定义块或内置块。
</div>
</div>
</div>
<div class="bme-task-toolbar-row">
<div class="bme-task-toolbar-inline">
<button class="bme-config-secondary-btn" data-task-action="add-custom-block" type="button">
+ 自定义块
</button>
<span class="bme-task-action-sep"></span>
<select id="bme-task-builtin-select" class="bme-config-input bme-task-builtin-select">
${state.builtinBlockDefinitions
.map(
(item) => `
<option value="${_escAttr(item.sourceKey)}">
${_escHtml(item.name)}
</option>
`,
)
.join("")}
</select>
<button class="bme-config-secondary-btn" data-task-action="add-builtin-block" type="button">
+ 内置块
</button>
</div>
<span class="bme-task-block-count">${state.blocks.length} 个块</span>
</div>
<div class="bme-task-list">
${state.blocks.length
? state.blocks
.map((block, index) => _renderTaskBlockListItem(block, index, state))
.join("")
: `
<div class="bme-task-empty">
当前预设还没有块。可以先新增一个自定义块或内置块。
</div>
`}
</div>
</div>
<div class="bme-config-card">
${_renderTaskBlockEditor(state)}
</div>
`}
</div>
`;
}
@@ -7388,73 +7458,93 @@ function _stringifyDebugValue(value) {
}
}
function _renderTaskBlockListItem(block, index, state) {
const isSelected = block.id === state.selectedBlock?.id;
function _getBlockTypeIcon(type) {
switch (type) {
case "builtin": return `<i class="fa-solid fa-thumbtack"></i>`;
case "legacyPrompt": return `<i class="fa-solid fa-scroll"></i>`;
default: return `<i class="fa-regular fa-file-lines"></i>`;
}
}
function _getInjectModeLabel(mode) {
switch (mode) {
case "append": return "追加";
case "relative":
default: return "相对";
}
}
function _renderTaskBlockRow(block, index, state) {
const isExpanded = block.id === state.selectedBlock?.id;
const roleClass = `bme-badge-role-${block.role || "system"}`;
const disabledClass = block.enabled ? "" : "is-disabled";
const expandedClass = isExpanded ? "is-expanded" : "";
return `
<div class="bme-task-list-entry">
<button
class="bme-task-list-item ${isSelected ? "active" : ""}"
data-task-action="select-block"
data-block-id="${_escAttr(block.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(block.name || _getTaskBlockTypeLabel(block.type))}
</span>
<span class="bme-task-list-meta">
${_escHtml(_getTaskBlockTypeLabel(block.type))} · ${_escHtml(block.role || "system")} · ${block.enabled ? "启用" : "停用"}
</span>
<div
class="bme-task-block-row ${disabledClass} ${expandedClass}"
data-block-id="${_escAttr(block.id)}"
>
<div class="bme-task-block-row-header" data-task-action="toggle-block-expand" data-block-id="${_escAttr(block.id)}">
<span
class="bme-task-drag-handle"
title="拖拽排序"
aria-label="拖拽排序"
draggable="true"
>
<i class="fa-solid fa-grip-vertical"></i>
</span>
</button>
<div class="bme-task-inline-actions">
<span class="bme-task-block-icon">
${_getBlockTypeIcon(block.type)}
</span>
<span class="bme-task-block-name">
${_escHtml(block.name || _getTaskBlockTypeLabel(block.type))}
</span>
<span class="bme-task-block-badge ${roleClass}">
${_escHtml(block.role || "system")}
</span>
<span class="bme-task-block-badge">
${_escHtml(_getInjectModeLabel(block.injectionMode))}
</span>
<span class="bme-task-block-row-spacer"></span>
<button
class="bme-config-secondary-btn bme-task-mini-btn"
data-task-action="move-block-up"
class="bme-task-row-btn"
data-task-action="toggle-block-expand"
data-block-id="${_escAttr(block.id)}"
type="button"
title="编辑"
>
上移
<i class="fa-solid fa-pen"></i>
</button>
<button
class="bme-config-secondary-btn bme-task-mini-btn"
data-task-action="move-block-down"
data-block-id="${_escAttr(block.id)}"
type="button"
>
下移
</button>
<button
class="bme-config-secondary-btn bme-task-mini-btn"
data-task-action="toggle-block-enabled"
data-block-id="${_escAttr(block.id)}"
type="button"
>
${block.enabled ? "停用" : "启用"}
</button>
<button
class="bme-config-secondary-btn bme-task-mini-btn"
class="bme-task-row-btn bme-task-row-btn-danger"
data-task-action="delete-block"
data-block-id="${_escAttr(block.id)}"
type="button"
title="删除"
>
删除
<i class="fa-solid fa-xmark"></i>
</button>
<label class="bme-task-row-toggle" title="${block.enabled ? "已启用" : "已停用"}">
<input
type="checkbox"
data-task-action="toggle-block-enabled-cb"
data-block-id="${_escAttr(block.id)}"
${block.enabled ? "checked" : ""}
/>
<span class="bme-task-row-toggle-slider"></span>
</label>
</div>
${isExpanded ? `
<div class="bme-task-block-expand">
${_renderTaskBlockInlineEditor(block, state)}
</div>
` : ""}
</div>
`;
}
function _renderTaskBlockEditor(state) {
const block = state.selectedBlock;
if (!block) {
return `
<div class="bme-config-card-title">块详情</div>
<div class="bme-config-help">从左侧列表选择一个块进行编辑。</div>
`;
}
function _renderTaskBlockInlineEditor(block, state) {
const builtinOptions = state.builtinBlockDefinitions
.map(
(item) => `
@@ -7474,31 +7564,18 @@ function _renderTaskBlockEditor(state) {
: block.content || "";
return `
<div class="bme-config-card-head">
<div>
<div class="bme-config-card-title">块详情</div>
<div class="bme-config-card-subtitle">
当前块会直接写回到任务预设中。
</div>
</div>
<span class="bme-task-pill">${_escHtml(_getTaskBlockTypeLabel(block.type))}</span>
${block.type === "builtin" ? _helpTip(
(state.builtinBlockDefinitions.find((d) => d.sourceKey === block.sourceKey) || {}).description || ""
) : ""}
</div>
<div class="bme-config-row">
<label>块名称</label>
<input
class="bme-config-input"
type="text"
data-block-field="name"
value="${_escAttr(block.name || "")}"
placeholder="用于工作区显示"
/>
<input
class="bme-config-input"
type="text"
data-block-field="name"
value="${_escAttr(block.name || "")}"
placeholder="用于工作区显示"
/>
</div>
<div class="bme-task-field-grid">
<div class="bme-task-expand-row2">
<div class="bme-config-row">
<label>角色</label>
<select class="bme-config-input" data-block-field="role">
@@ -7528,18 +7605,6 @@ function _renderTaskBlockEditor(state) {
</div>
</div>
<label class="bme-toggle-item bme-task-editor-toggle">
<span class="bme-toggle-copy">
<span class="bme-toggle-title">启用此块</span>
<span class="bme-toggle-desc">停用后会从最终 prompt 编排中跳过。</span>
</span>
<input
type="checkbox"
data-block-field="enabled"
${block.enabled ? "checked" : ""}
/>
</label>
${
block.type === "builtin"
? (() => {
@@ -7552,18 +7617,17 @@ function _renderTaskBlockEditor(state) {
const externalLabel = externalSourceMap[block.sourceKey];
return `
<div class="bme-config-row">
<label>内置来源${_helpTip("运行时自动从任务上下文注入的数据。不同任务类型使用不同来源。")}</label>
<label>内置来源${_helpTip("运行时自动从任务上下文注入的数据。")}</label>
<select class="bme-config-input" data-block-field="sourceKey">
${builtinOptions}
</select>
</div>
${externalLabel
? `<div class="bme-task-note" style="text-align:center;padding:1rem;opacity:0.7;">
此提示词的内容是从其他地方提取的,无法在此处进行编辑。<br/>
来源:<strong>${externalLabel}</strong>
? `<div class="bme-task-note" style="text-align:center;padding:0.75rem;opacity:0.7;">
内容来源:<strong>${externalLabel}</strong>,无法在此编辑。
</div>`
: `<div class="bme-config-row">
<label>覆盖内容(可选)${_helpTip("留空时自动从 sourceKey 对应的上下文数据读取。填写后将覆盖自动注入的内容。")}</label>
<label>覆盖内容(可选)${_helpTip("留空时自动从 sourceKey 对应的上下文数据读取。")}</label>
<textarea
class="bme-config-textarea"
data-block-field="content"
@@ -7579,12 +7643,7 @@ function _renderTaskBlockEditor(state) {
</div>
<div class="bme-config-row">
<label>兼容字段</label>
<input
class="bme-config-input"
type="text"
value="${_escAttr(legacyField || block.sourceField || "")}"
readonly
/>
<input class="bme-config-input" type="text" value="${_escAttr(legacyField || block.sourceField || "")}" readonly />
</div>
<div class="bme-config-row">
<label>兼容 prompt 内容</label>
@@ -7606,6 +7665,12 @@ function _renderTaskBlockEditor(state) {
</div>
`
}
<div class="bme-task-expand-footer">
<button class="bme-config-secondary-btn" data-task-action="toggle-block-expand" data-block-id="${_escAttr(block.id)}" type="button">
<i class="fa-solid fa-chevron-up"></i> 收起
</button>
</div>
`;
}
@@ -7959,6 +8024,57 @@ function _moveTaskBlock(blockId, direction) {
});
}
function _getTaskBlockDropPosition(row, clientY) {
const rect = row.getBoundingClientRect();
return clientY < rect.top + rect.height / 2 ? "before" : "after";
}
function _clearTaskBlockDragIndicators(workspace = document) {
workspace
.querySelectorAll(".bme-task-block-row.dragging, .bme-task-block-row.drag-over-top, .bme-task-block-row.drag-over-bottom")
.forEach((row) => {
row.classList.remove("dragging", "drag-over-top", "drag-over-bottom");
});
}
function _setTaskBlockDragIndicator(workspace, activeRow, position) {
workspace.querySelectorAll(".bme-task-block-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 _reorderTaskBlocks(sourceBlockId, targetBlockId, position = "before") {
if (!sourceBlockId || !targetBlockId || sourceBlockId === targetBlockId) return;
_updateCurrentTaskProfile((draft) => {
const blocks = _sortTaskBlocks(draft.blocks);
const sourceIndex = blocks.findIndex((item) => item.id === sourceBlockId);
const targetIndex = blocks.findIndex((item) => item.id === targetBlockId);
if (sourceIndex < 0 || targetIndex < 0) {
return null;
}
const [sourceBlock] = blocks.splice(sourceIndex, 1);
let insertIndex = targetIndex;
if (sourceIndex < targetIndex) {
insertIndex -= 1;
}
if (position === "after") {
insertIndex += 1;
}
insertIndex = Math.max(0, Math.min(blocks.length, insertIndex));
blocks.splice(insertIndex, 0, sourceBlock);
draft.blocks = blocks.map((block, index) => ({ ...block, order: index }));
return { selectBlockId: sourceBlockId };
});
}
function _deleteTaskBlock(blockId) {
if (!blockId) return;
_updateCurrentTaskProfile((draft) => {