204 lines
8.4 KiB
JavaScript
204 lines
8.4 KiB
JavaScript
/* 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;
|
||
|
||
// Безопасное экранирование HTML для вставок в UI
|
||
function pmEscapeHtml(s) {
|
||
const str = String(s ?? '');
|
||
return str
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// Изменения блока применяются только по кнопке «Сохранить» внутри редактора блока.
|
||
|
||
// 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); |