Files
HadTavern/static/js/utils.js

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);