HadTavern 0.01: Gemini/Claude fixes; UI _origId reuse; docs; .bat open
This commit is contained in:
191
static/js/pm-ui.js
Normal file
191
static/js/pm-ui.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/* global window, document */
|
||||
// AgentUI Prompt Manager UI extracted from editor.html
|
||||
// Exposes window.PM.setupProviderCallPMUI(editor, id)
|
||||
// Depends on DOM elements rendered by editor.html inspector:
|
||||
// #pm-list, #pm-editor, #pm-name, #pm-role, #pm-prompt, #pm-save, #pm-cancel
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
function setupProviderCallPMUI(editor, id) {
|
||||
try {
|
||||
const n2 = editor.getNodeFromId(id);
|
||||
if (!n2) return;
|
||||
const d2 = n2.data || {};
|
||||
if (!Array.isArray(d2.blocks)) d2.blocks = [];
|
||||
|
||||
// Ensure node.data and DOM __data always reflect latest blocks
|
||||
function syncNodeDataBlocks() {
|
||||
try {
|
||||
const n = editor.getNodeFromId(id);
|
||||
if (!n) return;
|
||||
// Готовим новые данные с глубокой копией blocks
|
||||
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
||||
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
||||
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
|
||||
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
} catch (e) {}
|
||||
}
|
||||
// Initial sync to attach blocks into __data for toPipelineJSON
|
||||
syncNodeDataBlocks();
|
||||
|
||||
const listEl = document.getElementById('pm-list');
|
||||
const addBtn = document.getElementById('pm-add');
|
||||
const editorBox = document.getElementById('pm-editor');
|
||||
const nameInp = document.getElementById('pm-name');
|
||||
const roleSel = document.getElementById('pm-role');
|
||||
const promptTxt = document.getElementById('pm-prompt');
|
||||
const saveBtn = document.getElementById('pm-save');
|
||||
const cancelBtn = document.getElementById('pm-cancel');
|
||||
let editingId = null;
|
||||
|
||||
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
|
||||
|
||||
// Drag&Drop через SortableJS (если доступен)
|
||||
if (w.Sortable && listEl && !listEl.__sortable) {
|
||||
listEl.__sortable = new w.Sortable(listEl, {
|
||||
animation: 150,
|
||||
handle: '.pm-handle',
|
||||
onEnd(evt) {
|
||||
const oldIndex = evt.oldIndex;
|
||||
const newIndex = evt.newIndex;
|
||||
if (oldIndex === newIndex) return;
|
||||
const moved = d2.blocks.splice(oldIndex, 1)[0];
|
||||
d2.blocks.splice(newIndex, 0, moved);
|
||||
d2.blocks.forEach((b, i) => b.order = i);
|
||||
syncNodeDataBlocks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sortAndReindex() {
|
||||
d2.blocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
d2.blocks.forEach((b, i) => b.order = i);
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
sortAndReindex();
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = '';
|
||||
d2.blocks.forEach((b, i) => {
|
||||
const domId = b.id || ('b' + i);
|
||||
const li = document.createElement('li');
|
||||
li.draggable = true;
|
||||
li.dataset.id = domId;
|
||||
li.style.display = 'flex';
|
||||
li.style.alignItems = 'center';
|
||||
li.style.gap = '6px';
|
||||
li.style.padding = '4px 0';
|
||||
li.innerHTML = `
|
||||
<span class="pm-handle" style="cursor:grab;">☰</span>
|
||||
<input type="checkbox" class="pm-enabled" ${b.enabled !== false ? 'checked' : ''} title="enabled"/>
|
||||
<span class="pm-name" style="flex:1">${(b.name || ('Block ' + (i + 1))).replace(/</g, '<')}</span>
|
||||
<span class="pm-role" style="opacity:.8">${b.role || 'user'}</span>
|
||||
<button class="pm-edit" title="Редактировать">✎</button>
|
||||
<button class="pm-del" title="Удалить">🗑</button>
|
||||
`;
|
||||
// DnD via HTML5 fallback as well (kept for compatibility)
|
||||
li.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', domId); });
|
||||
li.addEventListener('dragover', e => { e.preventDefault(); });
|
||||
li.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
const srcId = e.dataTransfer.getData('text/plain');
|
||||
const tgtId = domId;
|
||||
if (!srcId || srcId === tgtId) return;
|
||||
const srcIdx = d2.blocks.findIndex(x => (x.id || '') === srcId);
|
||||
const tgtIdx = d2.blocks.findIndex(x => (x.id || '') === tgtId);
|
||||
if (srcIdx < 0 || tgtIdx < 0) return;
|
||||
const [moved] = d2.blocks.splice(srcIdx, 1);
|
||||
d2.blocks.splice(tgtIdx, 0, moved);
|
||||
sortAndReindex();
|
||||
renderList();
|
||||
syncNodeDataBlocks();
|
||||
});
|
||||
// toggle
|
||||
li.querySelector('.pm-enabled').addEventListener('change', ev => {
|
||||
b.enabled = ev.target.checked;
|
||||
syncNodeDataBlocks();
|
||||
});
|
||||
// edit
|
||||
li.querySelector('.pm-edit').addEventListener('click', () => {
|
||||
openEditor(b);
|
||||
});
|
||||
// delete
|
||||
li.querySelector('.pm-del').addEventListener('click', () => {
|
||||
const idx = d2.blocks.indexOf(b);
|
||||
if (idx >= 0) d2.blocks.splice(idx, 1);
|
||||
sortAndReindex();
|
||||
renderList();
|
||||
syncNodeDataBlocks();
|
||||
if (editingId && editingId === (b.id || null)) {
|
||||
if (editorBox) editorBox.style.display = 'none';
|
||||
editingId = null;
|
||||
}
|
||||
});
|
||||
listEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function openEditor(b) {
|
||||
// Гарантируем наличие id у редактируемого блока
|
||||
if (!b.id) {
|
||||
b.id = 'b' + Date.now().toString(36);
|
||||
syncNodeDataBlocks();
|
||||
}
|
||||
editingId = b.id;
|
||||
if (editorBox) editorBox.style.display = '';
|
||||
if (nameInp) nameInp.value = b.name || '';
|
||||
if (roleSel) roleSel.value = (b.role || 'user');
|
||||
if (promptTxt) promptTxt.value = b.prompt || '';
|
||||
}
|
||||
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', () => {
|
||||
const idv = 'b' + Date.now().toString(36);
|
||||
const nb = { id: idv, name: 'New Block', role: 'system', prompt: '', enabled: true, order: d2.blocks.length };
|
||||
d2.blocks.push(nb);
|
||||
sortAndReindex();
|
||||
renderList();
|
||||
syncNodeDataBlocks();
|
||||
openEditor(nb);
|
||||
});
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => {
|
||||
if (!editingId) { if (editorBox) editorBox.style.display = 'none'; return; }
|
||||
const b = d2.blocks.find(x => (x.id || null) === editingId);
|
||||
if (b) {
|
||||
b.name = nameInp ? nameInp.value : b.name;
|
||||
b.role = roleSel ? roleSel.value : b.role || 'user';
|
||||
b.prompt = promptTxt ? promptTxt.value : b.prompt;
|
||||
// Пересоберём массив, чтобы избежать проблем с мутацией по ссылке
|
||||
d2.blocks = d2.blocks.map(x => (x.id === b.id ? ({ ...b }) : x));
|
||||
}
|
||||
if (editorBox) editorBox.style.display = 'none';
|
||||
editingId = null;
|
||||
renderList();
|
||||
syncNodeDataBlocks();
|
||||
// попытка автосохранения пайплайна, если доступна глобальная функция
|
||||
try { (typeof w.savePipeline === 'function') && w.savePipeline(); } catch (e) {}
|
||||
try { (typeof w.status === 'function') && w.status('Блок сохранён в pipeline.json'); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
if (editorBox) editorBox.style.display = 'none';
|
||||
editingId = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Первичная отрисовка
|
||||
renderList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
w.PM = {
|
||||
setupProviderCallPMUI
|
||||
};
|
||||
})(window);
|
||||
Reference in New Issue
Block a user