213 lines
7.5 KiB
JavaScript
213 lines
7.5 KiB
JavaScript
/* global window */
|
|
// AgentUI common UI utilities (DRY helpers shared by editor.html and pm-ui.js)
|
|
(function (w) {
|
|
'use strict';
|
|
|
|
const AU = {};
|
|
|
|
// HTML escaping for safe text/attribute insertion
|
|
AU.escapeHtml = function escapeHtml(s) {
|
|
const str = String(s ?? '');
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'");
|
|
};
|
|
|
|
// Attribute-safe escape (keeps quotes escaped; conservative)
|
|
AU.escAttr = function escAttr(v) {
|
|
const s = String(v ?? '');
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'");
|
|
};
|
|
|
|
// Text-node escape (keeps quotes as-is for readability)
|
|
AU.escText = function escText(v) {
|
|
const s = String(v ?? '');
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
};
|
|
|
|
// DRY helper: sync Drawflow node data + mirror into DOM.__data with deep copy
|
|
AU.updateNodeDataAndDom = function updateNodeDataAndDom(editor, id, data) {
|
|
try { editor && typeof editor.updateNodeDataFromId === 'function' && editor.updateNodeDataFromId(id, data); } catch (_) {}
|
|
try {
|
|
const el = document.querySelector('#node-' + id);
|
|
if (el) el.__data = JSON.parse(JSON.stringify(data));
|
|
} catch (_) {}
|
|
};
|
|
|
|
// Double rAF helper: waits for two animation frames; returns Promise or accepts callback
|
|
AU.nextRaf2 = function nextRaf2(cb) {
|
|
try {
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
if (typeof cb === 'function') {
|
|
requestAnimationFrame(() => { requestAnimationFrame(() => { try { cb(); } catch (_) {} }); });
|
|
return;
|
|
}
|
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
} else {
|
|
if (typeof cb === 'function') { setTimeout(() => { try { cb(); } catch (_) {} }, 32); return; }
|
|
return new Promise((resolve) => setTimeout(resolve, 32));
|
|
}
|
|
} catch (_) {
|
|
if (typeof cb === 'function') { try { cb(); } catch (__ ) {} }
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
|
|
// Heuristic: looks like long base64 payload
|
|
AU.isProbablyBase64 = function isProbablyBase64(s) {
|
|
try {
|
|
if (typeof s !== 'string') return false;
|
|
if (s.length < 64) return false;
|
|
return /^[A-Za-z0-9+/=\r\n]+$/.test(s);
|
|
} catch { return false; }
|
|
};
|
|
|
|
AU.trimBase64 = function trimBase64(s, maxLen = 180) {
|
|
try {
|
|
const str = String(s ?? '');
|
|
if (str.length > maxLen) {
|
|
return str.slice(0, maxLen) + `... (trimmed ${str.length - maxLen})`;
|
|
}
|
|
return str;
|
|
} catch { return String(s ?? ''); }
|
|
};
|
|
|
|
// Flatten JSON-like object into [path, stringValue] pairs
|
|
// Includes special handling for backend preview objects: { "__truncated__": true, "preview": "..." }
|
|
AU.flattenObject = function flattenObject(obj, prefix = '') {
|
|
const out = [];
|
|
if (obj == null) return out;
|
|
if (typeof obj !== 'object') {
|
|
out.push([prefix, String(obj)]);
|
|
return out;
|
|
}
|
|
try {
|
|
const entries = Object.entries(obj);
|
|
for (const [k, v] of entries) {
|
|
const p = prefix ? `${prefix}.${k}` : k;
|
|
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
// Special preview shape from backend
|
|
if (Object.prototype.hasOwnProperty.call(v, '__truncated__') && Object.prototype.hasOwnProperty.call(v, 'preview')) {
|
|
out.push([p, String(v.preview ?? '')]);
|
|
continue;
|
|
}
|
|
out.push(...AU.flattenObject(v, p));
|
|
} else {
|
|
try {
|
|
const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 0);
|
|
out.push([p, s]);
|
|
} catch {
|
|
out.push([p, String(v)]);
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Fallback best-effort
|
|
try { out.push([prefix, JSON.stringify(obj)]); } catch { out.push([prefix, String(obj)]); }
|
|
}
|
|
return out;
|
|
};
|
|
|
|
// Format headers dictionary into text lines "Key: Value"
|
|
AU.fmtHeaders = function fmtHeaders(h) {
|
|
try {
|
|
const keys = Object.keys(h || {});
|
|
return keys.map(k => `${k}: ${String(h[k])}`).join('\n');
|
|
} catch { return ''; }
|
|
};
|
|
|
|
// Build HTTP request preview text
|
|
AU.buildReqText = function buildReqText(x) {
|
|
if (!x) return '';
|
|
const head = `${x.method || 'POST'} ${x.url || '/'} HTTP/1.1`;
|
|
const host = (() => {
|
|
try { const u = new URL(x.url); return `Host: ${u.host}`; } catch { return ''; }
|
|
})();
|
|
const hs = AU.fmtHeaders(x.headers || {});
|
|
const body = String(x.body_text || '').trim();
|
|
return [head, host, hs, '', body].filter(Boolean).join('\n');
|
|
};
|
|
|
|
// Build HTTP response preview text
|
|
AU.buildRespText = function buildRespText(x) {
|
|
if (!x) return '';
|
|
const head = `HTTP/1.1 ${x.status || 0}`;
|
|
const hs = AU.fmtHeaders(x.headers || {});
|
|
const body = String(x.body_text || '').trim();
|
|
return [head, hs, '', body].filter(Boolean).join('\n');
|
|
};
|
|
|
|
// Unified fetch helper with timeout and JSON handling
|
|
AU.apiFetch = async function apiFetch(url, opts) {
|
|
const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
const o = opts || {};
|
|
const method = String(o.method || 'GET').toUpperCase();
|
|
const expectJson = (o.expectJson !== false); // default true
|
|
const headers = Object.assign({}, o.headers || {});
|
|
let body = o.body;
|
|
const timeoutMs = Number.isFinite(o.timeoutMs) ? o.timeoutMs : 15000;
|
|
|
|
const hasAbort = (typeof AbortController !== 'undefined');
|
|
const ctrl = hasAbort ? new AbortController() : null;
|
|
let to = null;
|
|
if (ctrl) {
|
|
try { to = setTimeout(() => { try { ctrl.abort(); } catch(_){} }, timeoutMs); } catch(_) {}
|
|
}
|
|
|
|
try {
|
|
if (expectJson) {
|
|
if (!headers['Accept'] && !headers['accept']) headers['Accept'] = 'application/json';
|
|
}
|
|
if (body != null) {
|
|
const isForm = (typeof FormData !== 'undefined' && body instanceof FormData);
|
|
const isBlob = (typeof Blob !== 'undefined' && body instanceof Blob);
|
|
if (typeof body === 'object' && !isForm && !isBlob) {
|
|
body = JSON.stringify(body);
|
|
if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/json';
|
|
}
|
|
}
|
|
|
|
const res = await fetch(url, { method, headers, body, signal: ctrl ? ctrl.signal : undefined });
|
|
const ct = String(res.headers && res.headers.get ? (res.headers.get('Content-Type') || '') : '');
|
|
const isJsonCt = /application\/json/i.test(ct);
|
|
|
|
let data = null;
|
|
if (expectJson || isJsonCt) {
|
|
try { data = await res.json(); } catch (_) { data = null; }
|
|
} else {
|
|
try { data = await res.text(); } catch (_) { data = null; }
|
|
}
|
|
|
|
const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
try { console.debug('[AU.apiFetch]', { method, url, status: res.status, ms: Math.round(t1 - t0) }); } catch(_) {}
|
|
|
|
if (!res.ok) {
|
|
const msg = (data && typeof data === 'object' && data.error) ? String(data.error) : `HTTP ${res.status}`;
|
|
const err = new Error(`apiFetch: ${msg}`);
|
|
err.status = res.status;
|
|
err.data = data;
|
|
err.url = url;
|
|
throw err;
|
|
}
|
|
|
|
return data;
|
|
} finally {
|
|
if (to) { try { clearTimeout(to); } catch(_) {} }
|
|
}
|
|
};
|
|
|
|
// Expose
|
|
try { w.AU = AU; } catch (_) {}
|
|
try { w.nextRaf2 = AU.nextRaf2; } catch (_) {}
|
|
})(window); |