Changeset 0.29.3 (#198)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -121,3 +121,32 @@
|
||||
/* DnD stage reorder feedback */
|
||||
.wf-stage-row.dragging { opacity: 0.4; }
|
||||
.wf-stage-row.drag-over { border-top: 2px solid var(--accent); }
|
||||
|
||||
/* ── v0.29.3: Form builder (admin) ──── */
|
||||
|
||||
.wf-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--bg-surface);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wf-field-row:hover { background: var(--bg-raised); }
|
||||
.wf-field-row.dragging { opacity: 0.4; }
|
||||
.wf-field-row.drag-over { border-top: 2px solid var(--accent); }
|
||||
.wf-field-row input[type="text"],
|
||||
.wf-field-row select {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.wf-field-row .wf-fb-del { opacity: 0; transition: opacity 0.15s; }
|
||||
.wf-field-row:hover .wf-fb-del { opacity: 1; }
|
||||
.badge-accent { background: var(--accent-dim); color: var(--accent); }
|
||||
|
||||
@@ -69,8 +69,22 @@
|
||||
'<textarea id="wfStagePrompt" rows="3" style="width:100%"></textarea></div>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
||||
'<div class="form-group" style="flex:1"><label>Form Template (JSON)</label>' +
|
||||
'<textarea id="wfStageForm" rows="3" style="width:100%;font-family:monospace;font-size:12px" placeholder=\'{"name":{"description":"Full name","required":true}}\'></textarea></div>' +
|
||||
'<div class="form-group" style="flex:1"><label>Stage Mode</label>' +
|
||||
'<select id="wfStageMode" style="width:100%">' +
|
||||
'<option value="chat_only">Chat Only</option>' +
|
||||
'<option value="form_only">Form Only (no AI)</option>' +
|
||||
'<option value="form_chat">Form + Chat</option>' +
|
||||
'</select></div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div id="wfFormBuilderWrap" style="display:none;margin-top:8px">' +
|
||||
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">' +
|
||||
'<label style="margin:0;font-weight:600">Form Fields</label>' +
|
||||
'<label class="toggle-label" style="font-size:12px"><input type="checkbox" id="wfFormJsonToggle"><span class="toggle-track"></span><span>Show JSON</span></label>' +
|
||||
'</div>' +
|
||||
'<div id="wfFormFields"></div>' +
|
||||
'<button class="btn-small" id="wfAddFieldBtn" style="margin-top:4px">+ Add Field</button>' +
|
||||
'<textarea id="wfStageForm" rows="4" style="display:none;width:100%;font-family:monospace;font-size:12px;margin-top:6px" placeholder=\'{"fields":[{"key":"name","type":"text","label":"Full Name","required":true}]}\'></textarea>' +
|
||||
'</div>' +
|
||||
'<div class="form-row" style="margin-top:8px;gap:8px">' +
|
||||
'<button class="btn-small btn-primary" id="wfSaveStageBtn">Save Stage</button>' +
|
||||
@@ -129,6 +143,7 @@
|
||||
var target = document.getElementById('adminDynamic');
|
||||
if (target && !document.getElementById('wfAdminList')) {
|
||||
target.innerHTML = SCAFFOLD_HTML;
|
||||
_wfWireFormBuilder();
|
||||
}
|
||||
|
||||
const list = document.getElementById('wfAdminList');
|
||||
@@ -247,11 +262,13 @@
|
||||
personaLabel = p ? '<span class="badge">' + esc((p.icon ? p.icon + ' ' : '') + p.name) + '</span>' :
|
||||
'<span class="badge">persona</span>';
|
||||
}
|
||||
var modeLabel = s.stage_mode && s.stage_mode !== 'chat_only'
|
||||
? '<span class="badge badge-accent">' + esc(s.stage_mode.replace('_', ' ')) + '</span>' : '';
|
||||
html += '<div class="wf-stage-row" draggable="true" data-id="' + s.id + '" data-idx="' + i + '">' +
|
||||
'<span class="wf-stage-grip">⠿</span>' +
|
||||
'<span class="wf-stage-ord">' + i + '</span>' +
|
||||
'<span class="wf-stage-name">' + esc(s.name) + '</span>' +
|
||||
personaLabel +
|
||||
personaLabel + modeLabel +
|
||||
'<span class="badge">' + (s.history_mode || 'full') + '</span>' +
|
||||
'<button class="btn-small" data-action="_wfEditStage" data-args=\'' + JSON.stringify([s.id]) + '\'>Edit</button>' +
|
||||
'<button class="btn-small btn-danger" data-action="_wfDeleteStage" data-args=\'' + JSON.stringify([s.id]) + '\'>×</button>' +
|
||||
@@ -384,7 +401,10 @@
|
||||
document.getElementById('wfStageHistory').value = 'full';
|
||||
document.getElementById('wfStagePersona').value = '';
|
||||
document.getElementById('wfStagePrompt').value = '';
|
||||
document.getElementById('wfStageMode').value = 'chat_only';
|
||||
document.getElementById('wfStageForm').value = '';
|
||||
document.getElementById('wfFormFields').innerHTML = '';
|
||||
_wfUpdateFormBuilderVisibility();
|
||||
await _wfLoadPersonaSelect('');
|
||||
document.getElementById('wfStageEditor').style.display = '';
|
||||
};
|
||||
@@ -399,11 +419,32 @@
|
||||
var name = document.getElementById('wfStageName').value.trim();
|
||||
if (!name) { UI.toast('Stage name required', 'error'); return; }
|
||||
|
||||
var formRaw = document.getElementById('wfStageForm').value.trim();
|
||||
var stageMode = document.getElementById('wfStageMode').value;
|
||||
var formTemplate = null;
|
||||
if (formRaw) {
|
||||
try { formTemplate = JSON.parse(formRaw); }
|
||||
catch (e) { UI.toast('Invalid JSON in form template', 'error'); return; }
|
||||
|
||||
if (stageMode === 'form_only' || stageMode === 'form_chat') {
|
||||
// Build from visual builder or JSON textarea
|
||||
var jsonToggle = document.getElementById('wfFormJsonToggle');
|
||||
if (jsonToggle && jsonToggle.checked) {
|
||||
var formRaw = document.getElementById('wfStageForm').value.trim();
|
||||
if (formRaw) {
|
||||
try { formTemplate = JSON.parse(formRaw); }
|
||||
catch (e) { UI.toast('Invalid JSON in form template', 'error'); return; }
|
||||
}
|
||||
} else {
|
||||
formTemplate = _wfBuildFormTemplateFromUI();
|
||||
}
|
||||
if (!formTemplate || !formTemplate.fields || formTemplate.fields.length === 0) {
|
||||
UI.toast('Form modes require at least one form field', 'error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// chat_only: still allow optional legacy form template
|
||||
var formRaw = document.getElementById('wfStageForm').value.trim();
|
||||
if (formRaw) {
|
||||
try { formTemplate = JSON.parse(formRaw); }
|
||||
catch (e) { UI.toast('Invalid JSON in form template', 'error'); return; }
|
||||
}
|
||||
}
|
||||
|
||||
var personaId = document.getElementById('wfStagePersona').value || null;
|
||||
@@ -411,6 +452,7 @@
|
||||
var data = {
|
||||
name: name,
|
||||
history_mode: document.getElementById('wfStageHistory').value,
|
||||
stage_mode: stageMode,
|
||||
persona_id: personaId,
|
||||
system_prompt: document.getElementById('wfStagePrompt').value.trim() || undefined,
|
||||
form_template: formTemplate,
|
||||
@@ -446,10 +488,16 @@
|
||||
_editingStageId = stageId;
|
||||
document.getElementById('wfStageName').value = stage.name;
|
||||
document.getElementById('wfStageHistory').value = stage.history_mode || 'full';
|
||||
document.getElementById('wfStageMode').value = stage.stage_mode || 'chat_only';
|
||||
await _wfLoadPersonaSelect(stage.persona_id || '');
|
||||
document.getElementById('wfStagePrompt').value = stage.system_prompt || '';
|
||||
document.getElementById('wfStageForm').value =
|
||||
stage.form_template ? JSON.stringify(stage.form_template, null, 2) : '';
|
||||
|
||||
// Populate form builder from existing template
|
||||
var tpl = stage.form_template;
|
||||
document.getElementById('wfStageForm').value = tpl ? JSON.stringify(tpl, null, 2) : '';
|
||||
_wfPopulateFormBuilder(tpl);
|
||||
_wfUpdateFormBuilderVisibility();
|
||||
|
||||
document.getElementById('wfStageEditor').style.display = '';
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
@@ -468,3 +516,215 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ── Form Builder Helpers ────────────────
|
||||
|
||||
var FIELD_TYPES = ['text', 'email', 'number', 'date', 'textarea', 'select', 'checkbox', 'file'];
|
||||
|
||||
// Show/hide the form builder based on stage_mode
|
||||
function _wfUpdateFormBuilderVisibility() {
|
||||
var mode = document.getElementById('wfStageMode').value;
|
||||
var wrap = document.getElementById('wfFormBuilderWrap');
|
||||
if (wrap) wrap.style.display = (mode === 'form_only' || mode === 'form_chat') ? '' : 'none';
|
||||
}
|
||||
|
||||
// Build a single field row element
|
||||
function _wfCreateFieldRow(field) {
|
||||
field = field || { key: '', type: 'text', label: '', required: false };
|
||||
var row = document.createElement('div');
|
||||
row.className = 'wf-field-row';
|
||||
row.draggable = true;
|
||||
row.innerHTML =
|
||||
'<span class="wf-stage-grip" style="cursor:grab">⠿</span>' +
|
||||
'<input type="text" class="wf-fb-key" value="' + esc(field.key || '') + '" placeholder="key" style="width:80px">' +
|
||||
'<select class="wf-fb-type" style="width:90px">' +
|
||||
FIELD_TYPES.map(function(t) {
|
||||
return '<option value="' + t + '"' + (t === field.type ? ' selected' : '') + '>' + t + '</option>';
|
||||
}).join('') +
|
||||
'</select>' +
|
||||
'<input type="text" class="wf-fb-label" value="' + esc(field.label || '') + '" placeholder="Label" style="flex:1">' +
|
||||
'<label class="toggle-label" style="font-size:12px;white-space:nowrap"><input type="checkbox" class="wf-fb-required"' +
|
||||
(field.required ? ' checked' : '') + '><span class="toggle-track"></span><span>Req</span></label>' +
|
||||
'<button class="btn-small btn-danger wf-fb-del" style="padding:2px 6px">×</button>';
|
||||
|
||||
// Delete handler
|
||||
row.querySelector('.wf-fb-del').onclick = function() {
|
||||
row.remove();
|
||||
_wfSyncFormJSON();
|
||||
};
|
||||
|
||||
// Type change: show options editor for select
|
||||
row.querySelector('.wf-fb-type').onchange = function() {
|
||||
var optWrap = row.querySelector('.wf-fb-options');
|
||||
if (this.value === 'select') {
|
||||
if (!optWrap) {
|
||||
optWrap = document.createElement('div');
|
||||
optWrap.className = 'wf-fb-options';
|
||||
optWrap.style.cssText = 'margin:4px 0 0 28px;font-size:12px';
|
||||
optWrap.innerHTML = '<input type="text" class="wf-fb-opts-input" placeholder="option1, option2, ..." style="width:100%;font-size:12px" value="' +
|
||||
esc((field.options || []).map(function(o) { return typeof o === 'string' ? o : o.value || o.label || ''; }).join(', ')) + '">';
|
||||
row.appendChild(optWrap);
|
||||
}
|
||||
optWrap.style.display = '';
|
||||
} else if (optWrap) {
|
||||
optWrap.style.display = 'none';
|
||||
}
|
||||
_wfSyncFormJSON();
|
||||
};
|
||||
|
||||
// If select type, add options input
|
||||
if (field.type === 'select') {
|
||||
var optWrap = document.createElement('div');
|
||||
optWrap.className = 'wf-fb-options';
|
||||
optWrap.style.cssText = 'margin:4px 0 0 28px;font-size:12px';
|
||||
optWrap.innerHTML = '<input type="text" class="wf-fb-opts-input" placeholder="option1, option2, ..." style="width:100%;font-size:12px" value="' +
|
||||
esc((field.options || []).map(function(o) { return typeof o === 'string' ? o : o.value || o.label || ''; }).join(', ')) + '">';
|
||||
row.appendChild(optWrap);
|
||||
}
|
||||
|
||||
// Sync JSON on any input change
|
||||
row.querySelectorAll('input, select').forEach(function(el) {
|
||||
el.addEventListener('change', _wfSyncFormJSON);
|
||||
el.addEventListener('input', _wfSyncFormJSON);
|
||||
});
|
||||
|
||||
// DnD within field list
|
||||
row.addEventListener('dragstart', function(e) {
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', '');
|
||||
_wfDragFieldSrc = row;
|
||||
});
|
||||
row.addEventListener('dragend', function() {
|
||||
row.classList.remove('dragging');
|
||||
document.querySelectorAll('.wf-field-row').forEach(function(r) { r.classList.remove('drag-over'); });
|
||||
_wfDragFieldSrc = null;
|
||||
});
|
||||
row.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
if (row !== _wfDragFieldSrc) row.classList.add('drag-over');
|
||||
});
|
||||
row.addEventListener('dragleave', function() { row.classList.remove('drag-over'); });
|
||||
row.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
row.classList.remove('drag-over');
|
||||
if (!_wfDragFieldSrc || _wfDragFieldSrc === row) return;
|
||||
var container = document.getElementById('wfFormFields');
|
||||
var rows = Array.from(container.querySelectorAll('.wf-field-row'));
|
||||
var from = rows.indexOf(_wfDragFieldSrc);
|
||||
var to = rows.indexOf(row);
|
||||
if (from < to) {
|
||||
row.parentNode.insertBefore(_wfDragFieldSrc, row.nextSibling);
|
||||
} else {
|
||||
row.parentNode.insertBefore(_wfDragFieldSrc, row);
|
||||
}
|
||||
_wfSyncFormJSON();
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
var _wfDragFieldSrc = null;
|
||||
|
||||
// Populate the form builder from an existing template object
|
||||
function _wfPopulateFormBuilder(tpl) {
|
||||
var container = document.getElementById('wfFormFields');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!tpl || !tpl.fields || !Array.isArray(tpl.fields)) return;
|
||||
|
||||
tpl.fields.forEach(function(f) {
|
||||
// Support both typed objects and legacy string-only fields
|
||||
if (typeof f === 'string') {
|
||||
f = { key: f, type: 'text', label: f, required: false };
|
||||
}
|
||||
container.appendChild(_wfCreateFieldRow(f));
|
||||
});
|
||||
}
|
||||
|
||||
// Build a typed form_template from the visual builder
|
||||
function _wfBuildFormTemplateFromUI() {
|
||||
var container = document.getElementById('wfFormFields');
|
||||
if (!container) return null;
|
||||
var rows = container.querySelectorAll('.wf-field-row');
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
var fields = [];
|
||||
rows.forEach(function(row) {
|
||||
var key = row.querySelector('.wf-fb-key').value.trim();
|
||||
var type = row.querySelector('.wf-fb-type').value;
|
||||
var label = row.querySelector('.wf-fb-label').value.trim();
|
||||
var required = row.querySelector('.wf-fb-required').checked;
|
||||
if (!key) return; // skip empty key fields
|
||||
|
||||
var field = { key: key, type: type, label: label || key, required: required };
|
||||
|
||||
// Parse select options
|
||||
if (type === 'select') {
|
||||
var optsInput = row.querySelector('.wf-fb-opts-input');
|
||||
if (optsInput && optsInput.value.trim()) {
|
||||
field.options = optsInput.value.split(',').map(function(s) {
|
||||
s = s.trim();
|
||||
return { value: s, label: s };
|
||||
}).filter(function(o) { return o.value; });
|
||||
}
|
||||
}
|
||||
fields.push(field);
|
||||
});
|
||||
|
||||
return fields.length > 0 ? { fields: fields } : null;
|
||||
}
|
||||
|
||||
// Sync visual builder → JSON textarea (one-way, when builder is active)
|
||||
function _wfSyncFormJSON() {
|
||||
var jsonToggle = document.getElementById('wfFormJsonToggle');
|
||||
if (!jsonToggle) return;
|
||||
var tpl = _wfBuildFormTemplateFromUI();
|
||||
document.getElementById('wfStageForm').value = tpl ? JSON.stringify(tpl, null, 2) : '';
|
||||
}
|
||||
|
||||
// Wire form builder controls (called once after scaffold injection)
|
||||
function _wfWireFormBuilder() {
|
||||
var modeSelect = document.getElementById('wfStageMode');
|
||||
if (modeSelect) {
|
||||
modeSelect.onchange = _wfUpdateFormBuilderVisibility;
|
||||
}
|
||||
|
||||
var addBtn = document.getElementById('wfAddFieldBtn');
|
||||
if (addBtn) {
|
||||
addBtn.onclick = function() {
|
||||
var container = document.getElementById('wfFormFields');
|
||||
if (container) {
|
||||
container.appendChild(_wfCreateFieldRow());
|
||||
_wfSyncFormJSON();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var jsonToggle = document.getElementById('wfFormJsonToggle');
|
||||
if (jsonToggle) {
|
||||
jsonToggle.onchange = function() {
|
||||
var textarea = document.getElementById('wfStageForm');
|
||||
if (this.checked) {
|
||||
// Sync builder → JSON before showing
|
||||
_wfSyncFormJSON();
|
||||
textarea.style.display = '';
|
||||
} else {
|
||||
// Sync JSON → builder
|
||||
var raw = textarea.value.trim();
|
||||
if (raw) {
|
||||
try {
|
||||
var tpl = JSON.parse(raw);
|
||||
_wfPopulateFormBuilder(tpl);
|
||||
} catch (e) {
|
||||
UI.toast('Invalid JSON — fix before switching to visual mode', 'error');
|
||||
this.checked = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
textarea.style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,16 @@
|
||||
return this._post(`/api/v1/channels/${channelId}/workflow/reject`, { reason });
|
||||
};
|
||||
|
||||
// ── Forms ────────────────────────────────
|
||||
|
||||
API.getWorkflowForm = function(channelId) {
|
||||
return this._get(`/api/v1/w/${channelId}/form`);
|
||||
};
|
||||
|
||||
API.submitWorkflowForm = function(channelId, data) {
|
||||
return this._post(`/api/v1/w/${channelId}/form-submit`, data);
|
||||
};
|
||||
|
||||
// ── Assignments ─────────────────────────
|
||||
|
||||
API.listMyAssignments = function() {
|
||||
|
||||
Reference in New Issue
Block a user