Refactor Ena Planner to iframe settings and harden save ack flow
This commit is contained in:
@@ -1,242 +1,188 @@
|
||||
/* Ena Planner v0.5 — collapsible, clean */
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--card: #1b1b1b;
|
||||
--line: #343434;
|
||||
--muted: #a8a8a8;
|
||||
--text: #e9e9e9;
|
||||
--ok: #85d48b;
|
||||
--err: #f48f8f;
|
||||
--btn: #2c2c2c;
|
||||
--primary: #355fcf;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 14px;
|
||||
}
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ff9800;
|
||||
}
|
||||
.dot.ok { background: #4caf50; }
|
||||
|
||||
/* ===== Settings panel inside inline-drawer ===== */
|
||||
#ena_planner_panel {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tab {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
user-select: none;
|
||||
}
|
||||
.tab.active {
|
||||
opacity: 1;
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
|
||||
#ena_planner_panel label {
|
||||
font-size: 12px;
|
||||
opacity: .9;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.col {
|
||||
flex: 1;
|
||||
min-width: 230px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #d0d0d0;
|
||||
font-size: 13px;
|
||||
}
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
background: #111;
|
||||
color: #efefef;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 7px 8px;
|
||||
}
|
||||
textarea {
|
||||
min-height: 110px;
|
||||
resize: vertical;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 3px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn {
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 6px;
|
||||
background: var(--btn);
|
||||
color: #fff;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
}
|
||||
.status {
|
||||
min-height: 18px;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
white-space: pre-wrap;
|
||||
color: var(--ok);
|
||||
}
|
||||
.status.error { color: var(--err); }
|
||||
|
||||
#ena_planner_panel input[type="text"],
|
||||
#ena_planner_panel input[type="password"],
|
||||
#ena_planner_panel input[type="number"],
|
||||
#ena_planner_panel select,
|
||||
#ena_planner_panel textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.prompt-block {
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.prompt-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.prompt-head-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-col {
|
||||
flex: 1 1 220px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-col.wide {
|
||||
flex: 1 1 100%;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
#ena_planner_panel .ep-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-tab {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--SmartThemeBorderColor, #333);
|
||||
opacity: .85;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-tab.active {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, .06);
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-hint {
|
||||
font-size: 11px;
|
||||
opacity: .7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-hint-box {
|
||||
font-size: 12px;
|
||||
opacity: .85;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid rgba(255, 255, 255, .08);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#ena_planner_panel .ep-divider {
|
||||
margin: 10px 0;
|
||||
border-top: 1px dashed rgba(255, 255, 255, .15);
|
||||
}
|
||||
|
||||
/* Inline badge (in drawer header) */
|
||||
.ep-badge-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
opacity: .9;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ep-badge-inline .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #888;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ep-badge-inline.ok .dot {
|
||||
background: #2ecc71;
|
||||
}
|
||||
|
||||
.ep-badge-inline.warn .dot {
|
||||
background: #f39c12;
|
||||
}
|
||||
|
||||
/* Prompt block */
|
||||
.ep-prompt-block {
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.ep-prompt-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ===== Log modal ===== */
|
||||
.ep-log-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, .65);
|
||||
z-index: 99999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ep-log-modal.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ep-log-modal .ep-log-card {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(980px, 96vw);
|
||||
height: min(82vh, 900px);
|
||||
background: rgba(20, 20, 20, .95);
|
||||
border: 1px solid rgba(255, 255, 255, .15);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ep-log-modal .ep-log-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ep-log-modal .ep-log-head .title {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ep-log-modal .ep-log-body {
|
||||
overflow: auto;
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid rgba(255, 255, 255, .08);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.ep-log-item {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.ep-log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ep-log-item .meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
opacity: .85;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ep-log-item .ep-log-error {
|
||||
color: #ffb3b3;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ep-log-item details {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ep-log-item details summary {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
opacity: .85;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* Issue #3: proper log formatting with line breaks */
|
||||
.ep-log-pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid rgba(255, 255, 255, .06);
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
.log-list {
|
||||
max-height: 62vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.log-item {
|
||||
border-bottom: 1px solid #2f2f2f;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.log-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
gap: 8px;
|
||||
}
|
||||
.log-error {
|
||||
margin: 5px 0;
|
||||
color: var(--err);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.log-pre {
|
||||
margin-top: 6px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid #2f2f2f;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
653
modules/ena-planner/ena-planner.html
Normal file
653
modules/ena-planner/ena-planner.html
Normal file
@@ -0,0 +1,653 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ena Planner</title>
|
||||
<link rel="stylesheet" href="./ena-planner.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="top">
|
||||
<strong>Ena Planner</strong>
|
||||
<span id="stateBadge" class="badge"><span class="dot"></span><span>Disabled</span></span>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="general">General</div>
|
||||
<div class="tab" data-tab="api">API</div>
|
||||
<div class="tab" data-tab="prompt">Prompt</div>
|
||||
<div class="tab" data-tab="debug">Debug</div>
|
||||
<div class="tab" data-tab="logs">Logs</div>
|
||||
</div>
|
||||
|
||||
<div class="panel active" data-panel="general">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Enabled</label>
|
||||
<select id="ep_enabled"><option value="true">Enabled</option><option value="false">Disabled</option></select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>Skip if input already contains <plot></label>
|
||||
<select id="ep_skip_plot"><option value="true">Yes</option><option value="false">No</option></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Include global worldbooks</label>
|
||||
<select id="ep_include_global_wb"><option value="false">No</option><option value="true">Yes</option></select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>Exclude worldbook entries with position=4</label>
|
||||
<select id="ep_wb_pos4"><option value="true">Yes</option><option value="false">No</option></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Worldbook name excludes (comma separated)</label>
|
||||
<input id="ep_wb_exclude_names" type="text" placeholder="mvu_update">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>Recent plot count</label>
|
||||
<input id="ep_plot_n" type="number" min="0" step="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Chat exclude tags (comma separated)</label>
|
||||
<input id="ep_exclude_tags" type="text" placeholder="ActionOptions, UpdateVariable, StatusPlaceHolderImpl">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Persist logs</label>
|
||||
<select id="ep_logs_persist"><option value="true">Yes</option><option value="false">No</option></select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>Log max count</label>
|
||||
<input id="ep_logs_max" type="number" min="1" max="200" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" data-panel="api">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Channel</label>
|
||||
<select id="ep_api_channel">
|
||||
<option value="openai">OpenAI compatible</option>
|
||||
<option value="gemini">Gemini compatible</option>
|
||||
<option value="claude">Claude compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label>Prefix mode</label>
|
||||
<select id="ep_prefix_mode"><option value="auto">Auto</option><option value="custom">Custom</option></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>API base URL</label><input id="ep_api_base" type="text" placeholder="https://..."></div>
|
||||
<div class="col"><label>Custom prefix</label><input id="ep_prefix_custom" type="text" placeholder="/v1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>API key</label><input id="ep_api_key" type="password" placeholder="sk-..."></div>
|
||||
<div class="col"><label>Model</label><input id="ep_model" type="text" placeholder="model-name"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>Stream</label><select id="ep_stream"><option value="true">Enabled</option><option value="false">Disabled</option></select></div>
|
||||
<div class="col"><label>Temperature</label><input id="ep_temp" type="number" step="0.1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>Top P</label><input id="ep_top_p" type="number" step="0.05"></div>
|
||||
<div class="col"><label>Top K</label><input id="ep_top_k" type="number" step="1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>Presence penalty</label><input id="ep_pp" type="text"></div>
|
||||
<div class="col"><label>Frequency penalty</label><input id="ep_fp" type="text"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><label>Max tokens</label><input id="ep_mt" type="text"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="ep_fetch_models" class="btn">Fetch models</button>
|
||||
<button id="ep_test_conn" class="btn">Test connection</button>
|
||||
</div>
|
||||
<div class="hint">Models preview: <span id="ep_models_preview">Not loaded</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" data-panel="prompt">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Template</label>
|
||||
<select id="ep_tpl_select"><option value="">-- Select template --</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="ep_tpl_save" class="btn">Save</button>
|
||||
<button id="ep_tpl_saveas" class="btn">Save as</button>
|
||||
<button id="ep_tpl_delete" class="btn">Delete</button>
|
||||
</div>
|
||||
|
||||
<div id="ep_prompt_list"></div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="ep_add_prompt" class="btn">Add block</button>
|
||||
<button id="ep_reset_prompt" class="btn">Reset default</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" data-panel="debug">
|
||||
<div class="card">
|
||||
<div class="actions">
|
||||
<button id="ep_debug_worldbook" class="btn">Debug worldbook</button>
|
||||
<button id="ep_debug_char" class="btn">Debug character</button>
|
||||
<button id="ep_test_planner" class="btn">Run planner test</button>
|
||||
</div>
|
||||
<pre id="ep_debug_output" class="log-pre" style="display:none;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" data-panel="logs">
|
||||
<div class="card">
|
||||
<div class="actions">
|
||||
<button id="ep_open_logs" class="btn">Refresh logs</button>
|
||||
<button id="ep_log_export" class="btn">Export JSON</button>
|
||||
<button id="ep_log_clear" class="btn">Clear</button>
|
||||
</div>
|
||||
<div id="ep_log_body" class="log-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="ep_save" class="btn primary">Save</button>
|
||||
<button id="ep_close" class="btn">Close</button>
|
||||
</div>
|
||||
<div id="ep_status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PARENT_ORIGIN = (() => {
|
||||
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
|
||||
})();
|
||||
|
||||
const post = (type, payload) => parent.postMessage({ type, payload }, PARENT_ORIGIN);
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const $$ = (sel) => document.querySelectorAll(sel);
|
||||
|
||||
let cfg = null;
|
||||
let logs = [];
|
||||
let pendingSave = null;
|
||||
|
||||
function setStatus(text, isError = false) {
|
||||
const el = $('ep_status');
|
||||
el.textContent = text || '';
|
||||
el.className = `status${isError ? ' error' : ''}`;
|
||||
}
|
||||
|
||||
function setSaveButtonBusy(busy) {
|
||||
const btn = $('ep_save');
|
||||
if (!btn) return;
|
||||
if (busy) {
|
||||
btn.disabled = true;
|
||||
btn.dataset.originalText = btn.textContent;
|
||||
btn.textContent = 'Saving...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.originalText || 'Save';
|
||||
delete btn.dataset.originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function startPendingSave(requestId, pendingText = 'Saving...') {
|
||||
pendingSave = {
|
||||
requestId,
|
||||
timer: setTimeout(() => {
|
||||
if (!pendingSave || pendingSave.requestId !== requestId) return;
|
||||
pendingSave = null;
|
||||
setSaveButtonBusy(false);
|
||||
setStatus('Save timeout after 3s', true);
|
||||
}, 3000)
|
||||
};
|
||||
setSaveButtonBusy(true);
|
||||
setStatus(pendingText);
|
||||
}
|
||||
|
||||
function setBadge(enabled) {
|
||||
const badge = $('stateBadge');
|
||||
badge.innerHTML = `<span class="dot${enabled ? ' ok' : ''}"></span><span>${enabled ? 'Enabled' : 'Disabled'}</span>`;
|
||||
}
|
||||
|
||||
function activateTab(tabId) {
|
||||
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabId));
|
||||
$$('.panel').forEach((p) => p.classList.toggle('active', p.dataset.panel === tabId));
|
||||
if (tabId === 'logs') post('xb-ena:logs-request');
|
||||
}
|
||||
|
||||
function toBool(v, fallback = false) {
|
||||
if (v === true || v === false) return v;
|
||||
if (v === 'true') return true;
|
||||
if (v === 'false') return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toNum(v, fallback = 0) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function arrToCsv(arr) {
|
||||
return Array.isArray(arr) ? arr.join(', ') : '';
|
||||
}
|
||||
|
||||
function csvToArr(text) {
|
||||
return String(text || '').split(',').map((x) => x.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function createPromptBlockElement(block, idx, total) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'prompt-block';
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'prompt-head';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'prompt-head-left';
|
||||
|
||||
const name = document.createElement('input');
|
||||
name.type = 'text';
|
||||
name.placeholder = 'Block name';
|
||||
name.value = block.name || '';
|
||||
name.style.minWidth = '180px';
|
||||
name.addEventListener('change', () => {
|
||||
block.name = name.value || '';
|
||||
});
|
||||
|
||||
const role = document.createElement('select');
|
||||
['system', 'user', 'assistant'].forEach((r) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r;
|
||||
opt.textContent = r;
|
||||
opt.selected = (block.role || 'system') === r;
|
||||
role.appendChild(opt);
|
||||
});
|
||||
role.addEventListener('change', () => {
|
||||
block.role = role.value;
|
||||
});
|
||||
|
||||
left.append(name, role);
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.style.display = 'flex';
|
||||
right.style.gap = '6px';
|
||||
|
||||
const up = document.createElement('button');
|
||||
up.className = 'btn';
|
||||
up.textContent = 'Up';
|
||||
up.disabled = idx === 0;
|
||||
up.addEventListener('click', () => {
|
||||
if (idx === 0) return;
|
||||
const list = cfg.promptBlocks;
|
||||
[list[idx - 1], list[idx]] = [list[idx], list[idx - 1]];
|
||||
renderPromptList();
|
||||
});
|
||||
|
||||
const down = document.createElement('button');
|
||||
down.className = 'btn';
|
||||
down.textContent = 'Down';
|
||||
down.disabled = idx === total - 1;
|
||||
down.addEventListener('click', () => {
|
||||
if (idx >= total - 1) return;
|
||||
const list = cfg.promptBlocks;
|
||||
[list[idx], list[idx + 1]] = [list[idx + 1], list[idx]];
|
||||
renderPromptList();
|
||||
});
|
||||
|
||||
const del = document.createElement('button');
|
||||
del.className = 'btn';
|
||||
del.textContent = 'Delete';
|
||||
del.addEventListener('click', () => {
|
||||
cfg.promptBlocks.splice(idx, 1);
|
||||
renderPromptList();
|
||||
});
|
||||
|
||||
right.append(up, down, del);
|
||||
|
||||
const content = document.createElement('textarea');
|
||||
content.value = block.content || '';
|
||||
content.placeholder = 'Prompt block content';
|
||||
content.addEventListener('change', () => {
|
||||
block.content = content.value || '';
|
||||
});
|
||||
|
||||
head.append(left, right);
|
||||
wrap.append(head, content);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderTemplateSelect(selected = '') {
|
||||
const sel = $('ep_tpl_select');
|
||||
sel.textContent = '';
|
||||
const first = document.createElement('option');
|
||||
first.value = '';
|
||||
first.textContent = '-- Select template --';
|
||||
sel.appendChild(first);
|
||||
|
||||
const names = Object.keys(cfg?.promptTemplates || {});
|
||||
names.forEach((name) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
opt.selected = name === selected;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPromptList() {
|
||||
const list = $('ep_prompt_list');
|
||||
list.textContent = '';
|
||||
const blocks = cfg?.promptBlocks || [];
|
||||
|
||||
if (!blocks.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'hint';
|
||||
empty.textContent = 'No prompt blocks';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
blocks.forEach((block, idx) => {
|
||||
list.appendChild(createPromptBlockElement(block, idx, blocks.length));
|
||||
});
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
const body = $('ep_log_body');
|
||||
body.textContent = '';
|
||||
|
||||
if (!Array.isArray(logs) || logs.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'hint';
|
||||
empty.textContent = 'No logs';
|
||||
body.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
logs.forEach((item) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'log-item';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'log-meta';
|
||||
const left = document.createElement('span');
|
||||
left.textContent = `${item.time || '-'} | ${item.ok ? 'OK' : 'FAIL'}`;
|
||||
const right = document.createElement('span');
|
||||
right.textContent = item.model || '-';
|
||||
meta.append(left, right);
|
||||
row.appendChild(meta);
|
||||
|
||||
if (item.error) {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'log-error';
|
||||
err.textContent = String(item.error);
|
||||
row.appendChild(err);
|
||||
}
|
||||
|
||||
const req = document.createElement('details');
|
||||
const reqSm = document.createElement('summary');
|
||||
reqSm.textContent = 'Request messages';
|
||||
const reqPre = document.createElement('pre');
|
||||
reqPre.className = 'log-pre';
|
||||
reqPre.textContent = JSON.stringify(item.requestMessages || [], null, 2);
|
||||
req.append(reqSm, reqPre);
|
||||
row.appendChild(req);
|
||||
|
||||
const raw = document.createElement('details');
|
||||
const rawSm = document.createElement('summary');
|
||||
rawSm.textContent = 'Raw reply';
|
||||
const rawPre = document.createElement('pre');
|
||||
rawPre.className = 'log-pre';
|
||||
rawPre.textContent = String(item.rawReply || '');
|
||||
raw.append(rawSm, rawPre);
|
||||
row.appendChild(raw);
|
||||
|
||||
const filtered = document.createElement('details');
|
||||
filtered.open = true;
|
||||
const filteredSm = document.createElement('summary');
|
||||
filteredSm.textContent = 'Filtered reply';
|
||||
const filteredPre = document.createElement('pre');
|
||||
filteredPre.className = 'log-pre';
|
||||
filteredPre.textContent = String(item.filteredReply || '');
|
||||
filtered.append(filteredSm, filteredPre);
|
||||
row.appendChild(filtered);
|
||||
|
||||
body.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function applyConfig(nextCfg) {
|
||||
cfg = nextCfg || {};
|
||||
logs = Array.isArray(cfg.logs) ? cfg.logs : [];
|
||||
|
||||
$('ep_enabled').value = String(toBool(cfg.enabled, true));
|
||||
$('ep_skip_plot').value = String(toBool(cfg.skipIfPlotPresent, true));
|
||||
$('ep_include_global_wb').value = String(toBool(cfg.includeGlobalWorldbooks, false));
|
||||
$('ep_wb_pos4').value = String(toBool(cfg.excludeWorldbookPosition4, true));
|
||||
$('ep_wb_exclude_names').value = arrToCsv(cfg.worldbookExcludeNames);
|
||||
$('ep_plot_n').value = String(toNum(cfg.plotCount, 2));
|
||||
$('ep_exclude_tags').value = arrToCsv(cfg.chatExcludeTags);
|
||||
$('ep_logs_persist').value = String(toBool(cfg.logsPersist, true));
|
||||
$('ep_logs_max').value = String(toNum(cfg.logsMax, 20));
|
||||
|
||||
const api = cfg.api || {};
|
||||
$('ep_api_channel').value = api.channel || 'openai';
|
||||
$('ep_prefix_mode').value = api.prefixMode || 'auto';
|
||||
$('ep_api_base').value = api.baseUrl || '';
|
||||
$('ep_prefix_custom').value = api.customPrefix || '';
|
||||
$('ep_api_key').value = api.apiKey || '';
|
||||
$('ep_model').value = api.model || '';
|
||||
$('ep_stream').value = String(toBool(api.stream, false));
|
||||
$('ep_temp').value = String(toNum(api.temperature, 1));
|
||||
$('ep_top_p').value = String(toNum(api.top_p, 1));
|
||||
$('ep_top_k').value = String(toNum(api.top_k, 0));
|
||||
$('ep_pp').value = api.presence_penalty ?? '';
|
||||
$('ep_fp').value = api.frequency_penalty ?? '';
|
||||
$('ep_mt').value = api.max_tokens ?? '';
|
||||
|
||||
setBadge(toBool(cfg.enabled, true));
|
||||
|
||||
renderTemplateSelect();
|
||||
renderPromptList();
|
||||
renderLogs();
|
||||
}
|
||||
|
||||
function collectPatch() {
|
||||
const next = structuredClone(cfg || {});
|
||||
|
||||
next.enabled = toBool($('ep_enabled').value, true);
|
||||
next.skipIfPlotPresent = toBool($('ep_skip_plot').value, true);
|
||||
next.includeGlobalWorldbooks = toBool($('ep_include_global_wb').value, false);
|
||||
next.excludeWorldbookPosition4 = toBool($('ep_wb_pos4').value, true);
|
||||
next.worldbookExcludeNames = csvToArr($('ep_wb_exclude_names').value);
|
||||
next.plotCount = Math.max(0, Math.floor(toNum($('ep_plot_n').value, 2)));
|
||||
next.chatExcludeTags = csvToArr($('ep_exclude_tags').value);
|
||||
next.logsPersist = toBool($('ep_logs_persist').value, true);
|
||||
next.logsMax = Math.max(1, Math.min(200, Math.floor(toNum($('ep_logs_max').value, 20))));
|
||||
|
||||
next.api = next.api || {};
|
||||
next.api.channel = $('ep_api_channel').value;
|
||||
next.api.prefixMode = $('ep_prefix_mode').value;
|
||||
next.api.baseUrl = $('ep_api_base').value.trim();
|
||||
next.api.customPrefix = $('ep_prefix_custom').value.trim();
|
||||
next.api.apiKey = $('ep_api_key').value;
|
||||
next.api.model = $('ep_model').value.trim();
|
||||
next.api.stream = toBool($('ep_stream').value, false);
|
||||
next.api.temperature = toNum($('ep_temp').value, 1);
|
||||
next.api.top_p = toNum($('ep_top_p').value, 1);
|
||||
next.api.top_k = Math.floor(toNum($('ep_top_k').value, 0));
|
||||
next.api.presence_penalty = $('ep_pp').value.trim();
|
||||
next.api.frequency_penalty = $('ep_fp').value.trim();
|
||||
next.api.max_tokens = $('ep_mt').value.trim();
|
||||
|
||||
next.promptBlocks = Array.isArray(cfg?.promptBlocks) ? cfg.promptBlocks : [];
|
||||
next.promptTemplates = cfg?.promptTemplates || {};
|
||||
return next;
|
||||
}
|
||||
|
||||
function bindUiEvents() {
|
||||
$$('.tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => activateTab(tab.dataset.tab));
|
||||
});
|
||||
|
||||
$('ep_enabled').addEventListener('change', () => setBadge(toBool($('ep_enabled').value, true)));
|
||||
|
||||
$('ep_add_prompt').addEventListener('click', () => {
|
||||
cfg.promptBlocks = cfg.promptBlocks || [];
|
||||
cfg.promptBlocks.push({
|
||||
id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
role: 'system',
|
||||
name: 'New block',
|
||||
content: ''
|
||||
});
|
||||
renderPromptList();
|
||||
});
|
||||
|
||||
$('ep_reset_prompt').addEventListener('click', () => {
|
||||
if (!confirm('Reset prompt blocks to default?')) return;
|
||||
if (pendingSave) return;
|
||||
const requestId = `ena_reset_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
startPendingSave(requestId, 'Resetting prompt blocks...');
|
||||
post('xb-ena:reset-prompt-default', { requestId });
|
||||
});
|
||||
|
||||
$('ep_tpl_select').addEventListener('change', () => {
|
||||
const name = $('ep_tpl_select').value;
|
||||
if (!name) return;
|
||||
const blocks = cfg?.promptTemplates?.[name];
|
||||
if (!Array.isArray(blocks)) return;
|
||||
cfg.promptBlocks = structuredClone(blocks);
|
||||
renderPromptList();
|
||||
setStatus(`Template loaded: ${name}`);
|
||||
});
|
||||
|
||||
$('ep_tpl_save').addEventListener('click', () => {
|
||||
const name = $('ep_tpl_select').value;
|
||||
if (!name) return setStatus('Select a template first', true);
|
||||
cfg.promptTemplates = cfg.promptTemplates || {};
|
||||
cfg.promptTemplates[name] = structuredClone(cfg.promptBlocks || []);
|
||||
renderTemplateSelect(name);
|
||||
setStatus(`Template saved: ${name}`);
|
||||
});
|
||||
|
||||
$('ep_tpl_saveas').addEventListener('click', () => {
|
||||
const name = prompt('Template name');
|
||||
if (!name) return;
|
||||
cfg.promptTemplates = cfg.promptTemplates || {};
|
||||
cfg.promptTemplates[name] = structuredClone(cfg.promptBlocks || []);
|
||||
renderTemplateSelect(name);
|
||||
setStatus(`Template saved as: ${name}`);
|
||||
});
|
||||
|
||||
$('ep_tpl_delete').addEventListener('click', () => {
|
||||
const name = $('ep_tpl_select').value;
|
||||
if (!name) return;
|
||||
if (!confirm(`Delete template \"${name}\"?`)) return;
|
||||
delete cfg.promptTemplates[name];
|
||||
renderTemplateSelect('');
|
||||
setStatus(`Template deleted: ${name}`);
|
||||
});
|
||||
|
||||
$('ep_open_logs').addEventListener('click', () => post('xb-ena:logs-request'));
|
||||
$('ep_log_clear').addEventListener('click', () => post('xb-ena:logs-clear'));
|
||||
$('ep_log_export').addEventListener('click', () => {
|
||||
const blob = new Blob([JSON.stringify(logs || [], null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ena-planner-logs-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
$('ep_fetch_models').addEventListener('click', () => post('xb-ena:fetch-models'));
|
||||
$('ep_test_conn').addEventListener('click', () => post('xb-ena:fetch-models'));
|
||||
$('ep_debug_worldbook').addEventListener('click', () => post('xb-ena:debug-worldbook'));
|
||||
$('ep_debug_char').addEventListener('click', () => post('xb-ena:debug-char'));
|
||||
$('ep_test_planner').addEventListener('click', () => post('xb-ena:run-test', { text: '(test input) Please plan the next story step.' }));
|
||||
|
||||
$('ep_save').addEventListener('click', () => {
|
||||
if (pendingSave) return;
|
||||
const requestId = `ena_save_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const patch = collectPatch();
|
||||
startPendingSave(requestId, 'Saving...');
|
||||
post('xb-ena:save-config', { requestId, patch });
|
||||
});
|
||||
$('ep_close').addEventListener('click', () => post('xb-ena:close'));
|
||||
}
|
||||
|
||||
window.addEventListener('message', (ev) => {
|
||||
if (ev.origin !== PARENT_ORIGIN) return;
|
||||
const { type, payload } = ev.data || {};
|
||||
|
||||
if (type === 'xb-ena:config' || type === 'xb-ena:config-saved') {
|
||||
applyConfig(payload || {});
|
||||
if (type === 'xb-ena:config-saved') {
|
||||
const requestId = payload?.requestId || '';
|
||||
if (pendingSave && pendingSave.requestId === requestId) {
|
||||
clearTimeout(pendingSave.timer);
|
||||
pendingSave = null;
|
||||
setSaveButtonBusy(false);
|
||||
setStatus('Saved');
|
||||
}
|
||||
}
|
||||
} else if (type === 'xb-ena:config-save-error') {
|
||||
const requestId = payload?.requestId || '';
|
||||
if (pendingSave && pendingSave.requestId === requestId) {
|
||||
clearTimeout(pendingSave.timer);
|
||||
pendingSave = null;
|
||||
setSaveButtonBusy(false);
|
||||
}
|
||||
setStatus(payload?.message || 'Save failed', true);
|
||||
} else if (type === 'xb-ena:test-done') {
|
||||
setStatus('Planner test completed');
|
||||
} else if (type === 'xb-ena:test-error') {
|
||||
setStatus(payload?.message || 'Planner test failed', true);
|
||||
} else if (type === 'xb-ena:logs') {
|
||||
logs = Array.isArray(payload?.logs) ? payload.logs : [];
|
||||
renderLogs();
|
||||
} else if (type === 'xb-ena:models') {
|
||||
const models = Array.isArray(payload?.models) ? payload.models : [];
|
||||
const preview = models.slice(0, 8).join(', ');
|
||||
$('ep_models_preview').textContent = models.length ? `${models.length} models: ${preview}` : 'No models';
|
||||
setStatus(`Fetched ${models.length} models`);
|
||||
} else if (type === 'xb-ena:models-error') {
|
||||
setStatus(payload?.message || 'Model fetch failed', true);
|
||||
} else if (type === 'xb-ena:debug-output') {
|
||||
const out = $('ep_debug_output');
|
||||
out.style.display = '';
|
||||
out.textContent = String(payload?.output || '');
|
||||
}
|
||||
});
|
||||
|
||||
bindUiEvents();
|
||||
post('xb-ena:ready');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user