Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
728 lines
32 KiB
HTML
728 lines
32 KiB
HTML
{{define "workflow.html"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{.Data.ChannelTitle}} — {{.InstanceName}}</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/favicon.svg?v={{.Version}}">
|
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}/favicon.ico?v={{.Version}}">
|
|
<link rel="stylesheet" href="{{.BasePath}}/css/variables.css?v={{.Version}}">
|
|
<link rel="stylesheet" href="{{.BasePath}}/css/primitives.css?v={{.Version}}">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: var(--font); background: var(--bg); color: var(--text); }
|
|
|
|
.wf-shell { display: flex; flex-direction: column; height: 100vh; }
|
|
|
|
.wf-header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-surface);
|
|
}
|
|
.wf-header h1 { font-size: 18px; font-weight: 600; }
|
|
.wf-header p { font-size: 13px; color: var(--text-2); margin-top: 4px; }
|
|
|
|
.wf-stage-info {
|
|
padding: 8px 24px; font-size: 12px; color: var(--text-2);
|
|
background: var(--bg-surface); border-bottom: 1px solid var(--border);
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
}
|
|
.wf-progress {
|
|
display: flex; gap: 4px;
|
|
}
|
|
.wf-progress-dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
background: var(--border);
|
|
}
|
|
.wf-progress-dot.active { background: var(--accent); }
|
|
.wf-progress-dot.done { background: var(--text-3); }
|
|
|
|
/* ── Form ───────────────────────── */
|
|
.wf-form {
|
|
flex: 1; overflow-y: auto; padding: 24px;
|
|
max-width: 640px; margin: 0 auto; width: 100%;
|
|
}
|
|
.wf-form-field { margin-bottom: 16px; }
|
|
.wf-form-field label {
|
|
display: block; font-size: 13px; font-weight: 600;
|
|
margin-bottom: 4px; color: var(--text);
|
|
}
|
|
.wf-form-field label .required { color: var(--danger, #e74c3c); margin-left: 2px; }
|
|
.wf-form-field input, .wf-form-field select, .wf-form-field textarea {
|
|
width: 100%; padding: 8px 12px; font-size: 14px;
|
|
background: var(--input-bg); color: var(--text);
|
|
border: 1px solid var(--border); border-radius: 6px;
|
|
font-family: var(--font);
|
|
}
|
|
.wf-form-field input:focus, .wf-form-field select:focus, .wf-form-field textarea:focus {
|
|
outline: none; border-color: var(--accent);
|
|
}
|
|
.wf-form-field textarea { resize: vertical; min-height: 80px; }
|
|
.wf-form-field .field-error {
|
|
font-size: 12px; color: var(--danger, #e74c3c); margin-top: 4px;
|
|
}
|
|
.wf-form-field.has-error input,
|
|
.wf-form-field.has-error select,
|
|
.wf-form-field.has-error textarea {
|
|
border-color: var(--danger, #e74c3c);
|
|
}
|
|
.wf-form-field .field-hint {
|
|
font-size: 12px; color: var(--text-3); margin-top: 2px;
|
|
}
|
|
.wf-form-check {
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.wf-form-check input[type="checkbox"] {
|
|
width: auto; margin: 0;
|
|
}
|
|
|
|
.wf-form-submit {
|
|
margin-top: 24px;
|
|
}
|
|
.wf-form-submit button {
|
|
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
padding: 10px 24px; font-weight: 600; cursor: pointer; font-size: 14px;
|
|
}
|
|
.wf-form-submit button:hover { background: var(--accent-hover); }
|
|
.wf-form-submit button:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
.wf-form-success {
|
|
text-align: center; padding: 40px 24px;
|
|
}
|
|
.wf-form-success h3 { font-size: 18px; margin-bottom: 8px; }
|
|
.wf-form-success p { color: var(--text-2); }
|
|
|
|
/* ── Chat ───────────────────────── */
|
|
.wf-chat {
|
|
flex: 1; overflow-y: auto; padding: 16px 24px;
|
|
}
|
|
.wf-chat .message {
|
|
margin-bottom: 12px; padding: 10px 14px;
|
|
border-radius: 8px; max-width: 80%;
|
|
}
|
|
.wf-chat .message.user { background: var(--accent-dim); margin-left: auto; }
|
|
.wf-chat .message.assistant { background: var(--bg-raised); }
|
|
.wf-chat .message .meta { font-size: 11px; color: var(--text-3); margin-bottom: 4px; }
|
|
.wf-chat .message .content { font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
|
|
|
|
.wf-input {
|
|
padding: 12px 24px; border-top: 1px solid var(--border);
|
|
background: var(--bg-surface); display: flex; gap: 8px;
|
|
}
|
|
.wf-input textarea {
|
|
flex: 1; resize: none; background: var(--input-bg); color: var(--text);
|
|
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
font-family: var(--font); font-size: 14px; min-height: 42px; max-height: 160px;
|
|
}
|
|
.wf-input textarea:focus { outline: none; border-color: var(--accent); }
|
|
.wf-input button {
|
|
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
padding: 0 20px; font-weight: 600; cursor: pointer; font-size: 14px;
|
|
}
|
|
.wf-input button:hover { background: var(--accent-hover); }
|
|
.wf-input button:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
.wf-session-info {
|
|
padding: 8px 24px; font-size: 12px; color: var(--text-3);
|
|
border-top: 1px solid var(--border); background: var(--bg);
|
|
}
|
|
|
|
/* ── Form+Chat layout ───────────── */
|
|
.wf-body-split { display: flex; flex: 1; overflow: hidden; }
|
|
.wf-body-split .wf-form { flex: 1; border-right: 1px solid var(--border); overflow-y: auto; }
|
|
.wf-body-split .wf-chat-col { flex: 1; display: flex; flex-direction: column; }
|
|
.wf-body-split .wf-chat { flex: 1; }
|
|
|
|
/* ── Branding (v0.35.0) ────────── */
|
|
.wf-branding-logo { max-height: 32px; margin-bottom: 4px; }
|
|
.wf-branding-tagline { font-size: 13px; color: var(--text-2); margin-top: 2px; }
|
|
|
|
/* ── Progressive Forms (v0.35.0) ── */
|
|
.wf-fieldset-nav {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
margin-bottom: 16px; padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.wf-fieldset-nav .step-label { font-size: 14px; font-weight: 600; }
|
|
.wf-fieldset-nav .step-counter { font-size: 12px; color: var(--text-3); }
|
|
.wf-fieldset-buttons { display: flex; gap: 8px; margin-top: 16px; }
|
|
.wf-fieldset-buttons button {
|
|
padding: 8px 20px; border-radius: 8px; font-size: 14px; cursor: pointer;
|
|
font-weight: 600; border: 1px solid var(--border);
|
|
background: var(--bg-surface); color: var(--text);
|
|
}
|
|
.wf-fieldset-buttons button.primary {
|
|
background: var(--accent); color: #fff; border-color: var(--accent);
|
|
}
|
|
.wf-fieldset-buttons button.primary:hover { background: var(--accent-hover); }
|
|
|
|
/* ── Conditional field (hidden) ── */
|
|
.wf-form-field.cond-hidden { display: none; }
|
|
</style>
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
|
var mode = p.theme || 'system';
|
|
var resolved = mode;
|
|
if (mode === 'system') resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
document.documentElement.setAttribute('data-theme', resolved);
|
|
} catch(e) {}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="wf-shell">
|
|
<div class="wf-header" id="wfHeader">
|
|
<h1>{{.Data.ChannelTitle}}</h1>
|
|
{{if .Data.ChannelDescription}}<p>{{.Data.ChannelDescription}}</p>{{end}}
|
|
</div>
|
|
|
|
{{if .Data.StageName}}
|
|
<div class="wf-stage-info">
|
|
<span>{{.Data.StageName}}</span>
|
|
{{if gt .Data.TotalStages 1}}
|
|
<div class="wf-progress" id="progressDots"></div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Stage surface mount point (v0.30.2) -->
|
|
<!-- Modes: form_only, form_chat, chat_only, review, or custom surface via SurfacePkgID -->
|
|
|
|
{{if .Data.SurfacePkgID}}
|
|
<div id="customSurfaceMount" style="flex:1;overflow-y:auto"></div>
|
|
{{else if eq .Data.StageMode "form_only"}}
|
|
<div class="wf-form" id="formArea"></div>
|
|
{{else if eq .Data.StageMode "form_chat"}}
|
|
<div class="wf-body-split">
|
|
<div class="wf-form" id="formArea"></div>
|
|
<div class="wf-chat-col">
|
|
<div class="wf-chat" id="chatMessages"></div>
|
|
<div class="wf-input">
|
|
<textarea id="chatInput" placeholder="Type a message…" rows="1"
|
|
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"></textarea>
|
|
<button id="sendBtn" onclick="sendMessage()">Send</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{else if eq .Data.StageMode "review"}}
|
|
<div id="reviewArea" style="flex:1;overflow-y:auto;display:flex">
|
|
<div id="reviewDataPanel" style="flex:1;overflow-y:auto;border-right:1px solid var(--border)"></div>
|
|
<div id="reviewActionPanel" style="flex:1;overflow-y:auto;display:flex;flex-direction:column"></div>
|
|
</div>
|
|
{{else}}
|
|
<div class="wf-chat" id="chatMessages"></div>
|
|
<div class="wf-input">
|
|
<textarea id="chatInput" placeholder="Type a message…" rows="1"
|
|
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"></textarea>
|
|
<button id="sendBtn" onclick="sendMessage()">Send</button>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="wf-session-info">
|
|
{{if .Data.SurfacePkgID}}Session:{{else if eq .Data.StageMode "form_only"}}Submitting as{{else if eq .Data.StageMode "review"}}Reviewing as{{else}}Chatting as{{end}} <strong>{{.Data.SessionName}}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const BASE = '{{.BasePath}}';
|
|
const CHAN_ID = '{{.Data.ChannelID}}';
|
|
const SESSION_ID = '{{.Data.SessionID}}';
|
|
const STAGE_MODE = '{{.Data.StageMode}}';
|
|
const TOTAL_STAGES = {{.Data.TotalStages}};
|
|
const CURRENT_STAGE = {{.Data.CurrentStage}};
|
|
const SURFACE_PKG_ID = '{{.Data.SurfacePkgID}}';
|
|
|
|
var FORM_TPL = null;
|
|
try { FORM_TPL = JSON.parse('{{.Data.FormTemplateJSON}}' || 'null'); } catch(e) {}
|
|
|
|
// v0.35.0: Branding
|
|
var BRANDING = null;
|
|
try { BRANDING = JSON.parse('{{.Data.BrandingJSON}}' || 'null'); } catch(e) {}
|
|
|
|
// v0.35.0: Apply branding
|
|
(function() {
|
|
if (!BRANDING) return;
|
|
var header = document.getElementById('wfHeader');
|
|
if (!header) return;
|
|
if (BRANDING.accent_color) {
|
|
document.documentElement.style.setProperty('--accent', BRANDING.accent_color);
|
|
}
|
|
if (BRANDING.logo_url) {
|
|
var img = document.createElement('img');
|
|
img.src = BRANDING.logo_url;
|
|
img.className = 'wf-branding-logo';
|
|
img.alt = '';
|
|
header.insertBefore(img, header.firstChild);
|
|
}
|
|
if (BRANDING.tagline) {
|
|
var tag = document.createElement('div');
|
|
tag.className = 'wf-branding-tagline';
|
|
tag.textContent = BRANDING.tagline;
|
|
header.appendChild(tag);
|
|
}
|
|
})();
|
|
|
|
// ── Progress dots ──────────────────
|
|
(function() {
|
|
var dotsEl = document.getElementById('progressDots');
|
|
if (!dotsEl || TOTAL_STAGES <= 1) return;
|
|
for (var i = 0; i < TOTAL_STAGES; i++) {
|
|
var dot = document.createElement('div');
|
|
dot.className = 'wf-progress-dot';
|
|
if (i < CURRENT_STAGE) dot.className += ' done';
|
|
if (i === CURRENT_STAGE) dot.className += ' active';
|
|
dotsEl.appendChild(dot);
|
|
}
|
|
})();
|
|
|
|
// ── Form rendering (v0.35.0: progressive forms + conditional fields) ──
|
|
var _fieldsetIndex = 0;
|
|
var _fieldsetCount = 0;
|
|
|
|
if ((STAGE_MODE === 'form_only' || STAGE_MODE === 'form_chat') && FORM_TPL) {
|
|
var formArea = document.getElementById('formArea');
|
|
if (FORM_TPL.fieldsets && FORM_TPL.fieldsets.length > 0) {
|
|
renderProgressiveForm(formArea, FORM_TPL);
|
|
} else if (FORM_TPL.fields && FORM_TPL.fields.length > 0) {
|
|
renderForm(formArea, FORM_TPL);
|
|
}
|
|
}
|
|
|
|
function renderForm(container, tpl) {
|
|
var html = '';
|
|
for (var i = 0; i < tpl.fields.length; i++) {
|
|
var f = tpl.fields[i];
|
|
html += renderField(f);
|
|
}
|
|
html += '<div class="wf-form-submit"><button id="formSubmitBtn" onclick="submitForm()">Submit</button></div>';
|
|
container.innerHTML = html;
|
|
applyConditionalFields(tpl.fields);
|
|
}
|
|
|
|
function renderProgressiveForm(container, tpl) {
|
|
_fieldsetCount = tpl.fieldsets.length;
|
|
_fieldsetIndex = 0;
|
|
var html = '';
|
|
for (var s = 0; s < tpl.fieldsets.length; s++) {
|
|
var fs = tpl.fieldsets[s];
|
|
var display = s === 0 ? '' : ' style="display:none"';
|
|
html += '<div class="wf-fieldset" id="fieldset_' + s + '"' + display + '>';
|
|
html += '<div class="wf-fieldset-nav"><span class="step-label">' + escHtml(fs.label) + '</span>';
|
|
html += '<span class="step-counter">Step ' + (s + 1) + ' of ' + _fieldsetCount + '</span></div>';
|
|
for (var i = 0; i < fs.fields.length; i++) {
|
|
html += renderField(fs.fields[i]);
|
|
}
|
|
html += '<div class="wf-fieldset-buttons">';
|
|
if (s > 0) html += '<button onclick="showFieldset(' + (s - 1) + ')">Back</button>';
|
|
if (s < tpl.fieldsets.length - 1) {
|
|
html += '<button class="primary" onclick="showFieldset(' + (s + 1) + ')">Next</button>';
|
|
} else {
|
|
html += '<button class="primary" id="formSubmitBtn" onclick="submitForm()">Submit</button>';
|
|
}
|
|
html += '</div></div>';
|
|
}
|
|
container.innerHTML = html;
|
|
// Apply conditional fields for all fieldsets
|
|
for (var s = 0; s < tpl.fieldsets.length; s++) {
|
|
applyConditionalFields(tpl.fieldsets[s].fields);
|
|
}
|
|
}
|
|
|
|
window.showFieldset = function(idx) {
|
|
if (idx < 0 || idx >= _fieldsetCount) return;
|
|
document.getElementById('fieldset_' + _fieldsetIndex).style.display = 'none';
|
|
document.getElementById('fieldset_' + idx).style.display = '';
|
|
_fieldsetIndex = idx;
|
|
};
|
|
|
|
function applyConditionalFields(fields) {
|
|
for (var i = 0; i < fields.length; i++) {
|
|
var f = fields[i];
|
|
if (!f.condition) continue;
|
|
var when = f.condition.when;
|
|
var srcEl = document.getElementById('ff_' + when);
|
|
if (!srcEl) continue;
|
|
(function(field) {
|
|
var handler = function() { evaluateCondition(field); };
|
|
srcEl.addEventListener('change', handler);
|
|
srcEl.addEventListener('input', handler);
|
|
handler(); // initial evaluation
|
|
})(f);
|
|
}
|
|
}
|
|
|
|
function evaluateCondition(f) {
|
|
var cond = f.condition;
|
|
var srcEl = document.getElementById('ff_' + cond.when);
|
|
var tgtEl = document.querySelector('.wf-form-field[data-key="' + f.key + '"]');
|
|
if (!srcEl || !tgtEl) return;
|
|
|
|
var val = srcEl.type === 'checkbox' ? srcEl.checked : srcEl.value;
|
|
var op = cond.op || 'eq';
|
|
var visible = false;
|
|
|
|
switch (op) {
|
|
case 'exists': visible = val !== '' && val !== false; break;
|
|
case 'eq': visible = String(val) === String(cond.value); break;
|
|
case 'neq': visible = String(val) !== String(cond.value); break;
|
|
case 'in': visible = Array.isArray(cond.value) && cond.value.map(String).indexOf(String(val)) >= 0; break;
|
|
default: visible = true;
|
|
}
|
|
|
|
if (visible) {
|
|
tgtEl.classList.remove('cond-hidden');
|
|
} else {
|
|
tgtEl.classList.add('cond-hidden');
|
|
}
|
|
}
|
|
|
|
function renderField(f) {
|
|
var req = f.required ? '<span class="required">*</span>' : '';
|
|
var ph = f.placeholder ? ' placeholder="' + escAttr(f.placeholder) + '"' : '';
|
|
var inner = '';
|
|
|
|
switch (f.type) {
|
|
case 'text':
|
|
case 'email':
|
|
case 'date':
|
|
inner = '<input type="' + f.type + '" id="ff_' + f.key + '"' + ph + '>';
|
|
break;
|
|
case 'number':
|
|
var attrs = ph;
|
|
if (f.validation) {
|
|
if (f.validation.min != null) attrs += ' min="' + f.validation.min + '"';
|
|
if (f.validation.max != null) attrs += ' max="' + f.validation.max + '"';
|
|
if (f.validation.step != null) attrs += ' step="' + f.validation.step + '"';
|
|
}
|
|
inner = '<input type="number" id="ff_' + f.key + '"' + attrs + '>';
|
|
break;
|
|
case 'textarea':
|
|
inner = '<textarea id="ff_' + f.key + '"' + ph + ' rows="4"></textarea>';
|
|
break;
|
|
case 'select':
|
|
var opts = '<option value="">— Select —</option>';
|
|
if (f.options) {
|
|
for (var j = 0; j < f.options.length; j++) {
|
|
opts += '<option value="' + escAttr(f.options[j].value) + '">' + escHtml(f.options[j].label) + '</option>';
|
|
}
|
|
}
|
|
inner = '<select id="ff_' + f.key + '">' + opts + '</select>';
|
|
break;
|
|
case 'checkbox':
|
|
return '<div class="wf-form-field" data-key="' + f.key + '">' +
|
|
'<div class="wf-form-check">' +
|
|
'<input type="checkbox" id="ff_' + f.key + '">' +
|
|
'<label for="ff_' + f.key + '">' + escHtml(f.label) + req + '</label>' +
|
|
'</div>' +
|
|
'<div class="field-error" id="err_' + f.key + '"></div>' +
|
|
'</div>';
|
|
case 'file':
|
|
var accept = (f.validation && f.validation.accept) ? ' accept="' + escAttr(f.validation.accept) + '"' : '';
|
|
inner = '<input type="file" id="ff_' + f.key + '"' + accept + '>';
|
|
break;
|
|
default:
|
|
inner = '<input type="text" id="ff_' + f.key + '"' + ph + '>';
|
|
}
|
|
|
|
return '<div class="wf-form-field" data-key="' + f.key + '">' +
|
|
'<label for="ff_' + f.key + '">' + escHtml(f.label) + req + '</label>' +
|
|
inner +
|
|
'<div class="field-error" id="err_' + f.key + '"></div>' +
|
|
'</div>';
|
|
}
|
|
|
|
window.submitForm = async function() {
|
|
if (!FORM_TPL || !FORM_TPL.fields) return;
|
|
var btn = document.getElementById('formSubmitBtn');
|
|
if (btn) btn.disabled = true;
|
|
|
|
// Clear previous errors
|
|
document.querySelectorAll('.wf-form-field').forEach(function(el) {
|
|
el.classList.remove('has-error');
|
|
});
|
|
document.querySelectorAll('.field-error').forEach(function(el) {
|
|
el.textContent = '';
|
|
});
|
|
|
|
// Collect form data (v0.35.0: get all fields from fieldsets or top-level)
|
|
var allFields = FORM_TPL.fields || [];
|
|
if (FORM_TPL.fieldsets && FORM_TPL.fieldsets.length > 0) {
|
|
allFields = [];
|
|
for (var s = 0; s < FORM_TPL.fieldsets.length; s++) {
|
|
allFields = allFields.concat(FORM_TPL.fieldsets[s].fields);
|
|
}
|
|
}
|
|
var data = {};
|
|
for (var i = 0; i < allFields.length; i++) {
|
|
var f = allFields[i];
|
|
var el = document.getElementById('ff_' + f.key);
|
|
if (!el) continue;
|
|
// v0.35.0: Skip hidden conditional fields
|
|
var fieldWrap = document.querySelector('.wf-form-field[data-key="' + f.key + '"]');
|
|
if (fieldWrap && fieldWrap.classList.contains('cond-hidden')) continue;
|
|
if (f.type === 'checkbox') {
|
|
data[f.key] = el.checked;
|
|
} else if (f.type === 'number') {
|
|
data[f.key] = el.value !== '' ? parseFloat(el.value) : null;
|
|
} else {
|
|
data[f.key] = el.value;
|
|
}
|
|
}
|
|
|
|
try {
|
|
var resp = await fetch(BASE + '/api/v1/w/' + CHAN_ID + '/form-submit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ data: data }),
|
|
});
|
|
var result = await resp.json();
|
|
if (!resp.ok) {
|
|
if (result.errors) {
|
|
showErrors(result.errors);
|
|
} else if (result.error) {
|
|
alert(result.error);
|
|
}
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
// Success
|
|
if (result.status === 'advanced' || result.status === 'completed') {
|
|
showFormSuccess(result.status === 'completed'
|
|
? 'Thank you! Your submission is complete.'
|
|
: 'Submitted! Moving to the next step...');
|
|
if (result.status === 'advanced') {
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
}
|
|
} else {
|
|
showFormSuccess('Your response has been submitted.');
|
|
}
|
|
} catch(e) {
|
|
alert('Network error: ' + e.message);
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
function showErrors(errors) {
|
|
for (var i = 0; i < errors.length; i++) {
|
|
var e = errors[i];
|
|
var errEl = document.getElementById('err_' + e.key);
|
|
if (errEl) errEl.textContent = e.message;
|
|
var fieldEl = document.querySelector('.wf-form-field[data-key="' + e.key + '"]');
|
|
if (fieldEl) fieldEl.classList.add('has-error');
|
|
}
|
|
}
|
|
|
|
function showFormSuccess(msg) {
|
|
var formArea = document.getElementById('formArea');
|
|
if (formArea) {
|
|
formArea.innerHTML = '<div class="wf-form-success"><h3>Submitted</h3><p>' + escHtml(msg) + '</p></div>';
|
|
}
|
|
}
|
|
|
|
// ── Chat ───────────────────────────
|
|
var chatEl = document.getElementById('chatMessages');
|
|
var inputEl = document.getElementById('chatInput');
|
|
var sendBtn = document.getElementById('sendBtn');
|
|
var sending = false;
|
|
|
|
function addMessage(role, content, name) {
|
|
if (!chatEl) return;
|
|
var div = document.createElement('div');
|
|
div.className = 'message ' + role;
|
|
var meta = name ? '<div class="meta">' + escHtml(name) + '</div>' : '';
|
|
div.innerHTML = meta + '<div class="content">' + escHtml(content) + '</div>';
|
|
chatEl.appendChild(div);
|
|
chatEl.scrollTop = chatEl.scrollHeight;
|
|
}
|
|
|
|
function escHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function escAttr(s) {
|
|
return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
window.sendMessage = async function() {
|
|
if (!inputEl || !sendBtn) return;
|
|
var text = inputEl.value.trim();
|
|
if (!text || sending) return;
|
|
sending = true;
|
|
sendBtn.disabled = true;
|
|
inputEl.value = '';
|
|
|
|
addMessage('user', text, '{{.Data.SessionName}}');
|
|
|
|
try {
|
|
var resp = await fetch(BASE + '/api/v1/w/' + CHAN_ID + '/completions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ channel_id: CHAN_ID, content: text, stream: false }),
|
|
});
|
|
if (resp.ok) {
|
|
var data = await resp.json();
|
|
if (data.content) {
|
|
addMessage('assistant', data.content, data.persona_name || 'Assistant');
|
|
}
|
|
} else {
|
|
var err = await resp.json().catch(function() { return {}; });
|
|
addMessage('assistant', 'Error: ' + (err.error || resp.statusText));
|
|
}
|
|
} catch(e) {
|
|
addMessage('assistant', 'Network error: ' + e.message);
|
|
}
|
|
|
|
sending = false;
|
|
sendBtn.disabled = false;
|
|
inputEl.focus();
|
|
};
|
|
|
|
// Auto-resize textarea
|
|
if (inputEl) {
|
|
inputEl.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.min(this.scrollHeight, 160) + 'px';
|
|
});
|
|
}
|
|
|
|
// ── Custom surface (v0.37.14: legacy registry removed) ──────────
|
|
if (SURFACE_PKG_ID) {
|
|
var mount = document.getElementById('customSurfaceMount');
|
|
if (mount) {
|
|
mount.innerHTML = '<div style="padding:24px;color:var(--danger)">Custom workflow surfaces require an updated package format. The legacy surface registry has been removed.</div>';
|
|
}
|
|
}
|
|
|
|
function loadScript(src) {
|
|
return new Promise(function(resolve, reject) {
|
|
var s = document.createElement('script');
|
|
s.src = src;
|
|
s.onload = resolve;
|
|
s.onerror = function() { reject(new Error('Failed to load ' + src)); };
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
// ── Review surface (v0.35.0: structured review with side-by-side + comments) ──
|
|
if (STAGE_MODE === 'review') {
|
|
var dataPanel = document.getElementById('reviewDataPanel');
|
|
var actionPanel = document.getElementById('reviewActionPanel');
|
|
if (dataPanel && actionPanel) {
|
|
loadReviewSurface(dataPanel, actionPanel);
|
|
}
|
|
}
|
|
|
|
async function loadReviewSurface(dataPanel, actionPanel) {
|
|
dataPanel.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text-3)">Loading\u2026</div>';
|
|
|
|
// Left panel: structured data card
|
|
var html = '<div style="padding:24px">';
|
|
html += '<h3 style="margin-bottom:16px">Collected Data</h3>';
|
|
|
|
try {
|
|
var resp = await fetch(BASE + '/api/v1/channels/' + CHAN_ID + '/workflow/status', {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (resp.ok) {
|
|
var status = await resp.json();
|
|
if (status.stage_data && typeof status.stage_data === 'object') {
|
|
html += '<table style="width:100%;border-collapse:collapse">';
|
|
for (var key in status.stage_data) {
|
|
if (!status.stage_data.hasOwnProperty(key) || key.startsWith('_')) continue;
|
|
html += '<tr style="border-bottom:1px solid var(--border)">';
|
|
html += '<td style="padding:8px;font-weight:600;font-size:13px;width:30%;vertical-align:top">' + escHtml(key) + '</td>';
|
|
var val = status.stage_data[key];
|
|
var display = typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
|
|
html += '<td style="padding:8px;font-size:13px;white-space:pre-wrap">' + escHtml(display) + '</td>';
|
|
html += '</tr>';
|
|
}
|
|
html += '</table>';
|
|
} else {
|
|
html += '<p style="color:var(--text-3)">No data collected yet.</p>';
|
|
}
|
|
}
|
|
} catch(e) {
|
|
html += '<p style="color:var(--danger)">Failed to load review data.</p>';
|
|
}
|
|
html += '</div>';
|
|
dataPanel.innerHTML = html;
|
|
|
|
// Right panel: comment input + approve/reject buttons
|
|
var actHtml = '<div style="padding:24px;flex:1;display:flex;flex-direction:column">';
|
|
actHtml += '<h3 style="margin-bottom:16px">Review Actions</h3>';
|
|
actHtml += '<div style="margin-bottom:16px">';
|
|
actHtml += '<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">Add Comment</label>';
|
|
actHtml += '<textarea id="reviewComment" rows="3" style="width:100%;padding:8px;font-size:14px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text);font-family:var(--font);resize:vertical" placeholder="Optional comment\u2026"></textarea>';
|
|
actHtml += '</div>';
|
|
actHtml += '<div style="display:flex;gap:8px">';
|
|
actHtml += '<button id="reviewAdvanceBtn" style="background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Approve & Advance</button>';
|
|
actHtml += '<button id="reviewRejectBtn" style="background:var(--danger,#e74c3c);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Reject</button>';
|
|
actHtml += '</div>';
|
|
actHtml += '<div style="font-size:12px;color:var(--text-3);margin-top:8px">Ctrl+Enter: Approve · Ctrl+Shift+Enter: Reject</div>';
|
|
actHtml += '</div>';
|
|
actionPanel.innerHTML = actHtml;
|
|
|
|
// Keyboard shortcuts (v0.35.0)
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.ctrlKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (e.shiftKey) {
|
|
document.getElementById('reviewRejectBtn').click();
|
|
} else {
|
|
document.getElementById('reviewAdvanceBtn').click();
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('reviewAdvanceBtn').addEventListener('click', async function() {
|
|
this.disabled = true;
|
|
try {
|
|
var r = await fetch(BASE + '/api/v1/channels/' + CHAN_ID + '/workflow/advance', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ data: {} }),
|
|
});
|
|
if (r.ok) {
|
|
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Approved</h3><p style="color:var(--text-2)">Stage advanced.</p></div>';
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
} else {
|
|
var err = await r.json().catch(function() { return {}; });
|
|
alert('Failed: ' + (err.error || 'unknown'));
|
|
document.getElementById('reviewAdvanceBtn').disabled = false;
|
|
}
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
});
|
|
|
|
document.getElementById('reviewRejectBtn').addEventListener('click', async function() {
|
|
var comment = document.getElementById('reviewComment').value;
|
|
var reason = comment || prompt('Rejection reason:');
|
|
if (!reason) return;
|
|
this.disabled = true;
|
|
try {
|
|
var r = await fetch(BASE + '/api/v1/channels/' + CHAN_ID + '/workflow/reject', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ reason: reason }),
|
|
});
|
|
if (r.ok) {
|
|
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Rejected</h3><p style="color:var(--text-2)">Sent back for revision.</p></div>';
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
} else {
|
|
var err = await r.json().catch(function() { return {}; });
|
|
alert('Failed: ' + (err.error || 'unknown'));
|
|
document.getElementById('reviewRejectBtn').disabled = false;
|
|
}
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|