Files
HadTavern/static/js/pm-ui.js

209 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 {
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
w.AU.updateNodeDataAndDom(editor, id, newData);
} else {
editor.updateNodeDataFromId(id, newData);
const el2 = document.querySelector(`#node-${id}`);
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
}
} catch (e) {}
} 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;
// Безопасное экранирование HTML для вставок в UI
function pmEscapeHtml(s) {
const str = String(s ?? '');
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
// 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';
const nameDisp = pmEscapeHtml(b.name || ('Block ' + (i + 1)));
const roleDisp = pmEscapeHtml(b.role || 'user');
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">${nameDisp}</span>
<span class="pm-role" style="opacity:.8">${roleDisp}</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);