import { Alert, Box, Button, Chip, Divider, MenuItem, Paper, Select, Stack, StructuredDialog, TextField, Typography } from "@exotel-npm-dev/signal-design-system"; import { useMemo, useState, type MutableRefObject } from "react"; import type { AafClient } from "../aaf/gwtAafSdk"; import { applyContextToSample, type AafEventContext } from "../utils/eventContext"; import { AAF_API_NAMESPACES, AAF_METHODS, methodSubtitle, type AafApiNamespace, type AafMethodSpec } from "../utils/methodCatalog"; import { safeJson } from "../utils/safeJson"; type MethodResult = | { kind: "ok"; ts: number; value: unknown } | { kind: "error"; ts: number; message: string }; function formatTs(ts: number): string { const d = new Date(ts); const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); const ss = String(d.getSeconds()).padStart(2, "0"); const ms = String(d.getMilliseconds()).padStart(3, "0"); return `${hh}:${mm}:${ss}.${ms}`; } function tryParseJsonArgs(text: string): unknown[] | { error: string } { const t = text.trim(); if (t === "") return []; try { const parsed = JSON.parse(t); return Array.isArray(parsed) ? parsed : [parsed]; } catch (e) { return { error: e instanceof Error ? e.message : String(e) }; } } /** * `aaf_sdk.js` calls are spread verbatim — `client[api][method](...args)`. The catalogue * already owns the correct argument shape, and other `globalData` methods (`update`, * `getTickets`, `addNote`, `bulkUpdate`, `bulkAction`, `transfer`, `invoke`) take * positional `(dataObject: string, …)`, so we must not coerce here. */ function callApiMethod( client: AafClient, api: AafApiNamespace, method: string, args: unknown[] ): Promise { const apiObj = (client as unknown as Record>)[api]; if (!apiObj || typeof apiObj !== "object") { return Promise.reject( new Error(`Namespace client.${api} is not available on this AAF SDK build.`) ); } const fn = apiObj[method]; if (typeof fn !== "function") { return Promise.reject(new Error(`Method client.${api}.${method} is not a function.`)); } let out: unknown; try { out = (fn as (...a: unknown[]) => unknown).apply(apiObj, args); } catch (e) { return Promise.reject(e); } if (out && typeof (out as Promise).then === "function") return out as Promise; return Promise.resolve(out); } function MethodCard({ method, ctx, selected, onToggle, clientRef }: { method: AafMethodSpec; ctx: AafEventContext; selected: boolean; onToggle: () => void; clientRef: MutableRefObject; }) { const initial = useMemo(() => applyContextToSample(method.sampleRequest, ctx), [method.sampleRequest, ctx]); const [text, setText] = useState(initial); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const onRun = () => { const client = clientRef.current; if (!client) { setResult({ kind: "error", ts: Date.now(), message: "AmeyoClient not ready yet." }); return; } const parsed = tryParseJsonArgs(text); if (!Array.isArray(parsed)) { setResult({ kind: "error", ts: Date.now(), message: `Invalid JSON: ${parsed.error}` }); return; } setBusy(true); setResult(null); callApiMethod(client, method.api, method.name, parsed) .then((value) => setResult({ kind: "ok", ts: Date.now(), value })) .catch((err: unknown) => setResult({ kind: "error", ts: Date.now(), message: err instanceof Error ? err.message : safeJson(err) }) ) .finally(() => setBusy(false)); }; return ( {method.description} {methodSubtitle(method)} {selected && ( Sample request — JSON array of arguments. Edit values and Run. Placeholders like{" "} {"{{customerId}}"} are filled from recent events. setText(e.target.value)} sx={{ "& textarea": { fontFamily: "monospace", fontSize: 12 } }} /> {result && ( <> {formatTs(result.ts)} {result.kind === "ok" ? safeJson(result.value) : result.message} )} )} ); } export function InvokeMethodPanel({ ctx, clientRef }: { ctx: AafEventContext; clientRef: MutableRefObject; }) { const [filter, setFilter] = useState(""); const [selectedKey, setSelectedKey] = useState(null); const [manualOpen, setManualOpen] = useState(false); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); if (!q) return AAF_METHODS; return AAF_METHODS.filter( (m) => methodSubtitle(m).toLowerCase().includes(q) || m.description.toLowerCase().includes(q) ); }, [filter]); return ( setFilter(e.target.value)} sx={{ minWidth: 240, flex: 1 }} /> Each method runs against the live AmeyoClient. Switch ids / phone numbers to ones your campaign actually has — server returns 121/122 if the values are not valid. {filtered.map((m, idx) => { const key = `${m.api}.${m.name}#${idx}`; return ( setSelectedKey((prev) => (prev === key ? null : key))} clientRef={clientRef} /> ); })} setManualOpen(false)} clientRef={clientRef} ctx={ctx} /> ); } function ManualInvokeDialog({ open, onClose, clientRef, ctx }: { open: boolean; onClose: () => void; clientRef: MutableRefObject; ctx: AafEventContext; }) { const [api, setApi] = useState("globalData"); const [methodName, setMethodName] = useState("get"); const [body, setBody] = useState('[\n "loggedInUser"\n]'); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const onRun = () => { const client = clientRef.current; if (!client) { setResult({ kind: "error", ts: Date.now(), message: "AmeyoClient not ready yet." }); return; } const parsed = tryParseJsonArgs(applyContextToSample(body, ctx)); if (!Array.isArray(parsed)) { setResult({ kind: "error", ts: Date.now(), message: `Invalid JSON: ${parsed.error}` }); return; } const name = methodName.trim(); if (!name) { setResult({ kind: "error", ts: Date.now(), message: "Method name is required." }); return; } setBusy(true); setResult(null); callApiMethod(client, api, name, parsed) .then((value) => setResult({ kind: "ok", ts: Date.now(), value })) .catch((err: unknown) => setResult({ kind: "error", ts: Date.now(), message: err instanceof Error ? err.message : safeJson(err) }) ) .finally(() => setBusy(false)); }; return ( } > API namespace Method name (e.g. get, trigger, register) setMethodName(e.target.value)} placeholder="get" /> Arguments — JSON array (placeholders like {"{{customerId}}"} are substituted) setBody(e.target.value)} sx={{ "& textarea": { fontFamily: "monospace", fontSize: 12 } }} /> {result && ( <> {formatTs(result.ts)} {result.kind === "ok" ? safeJson(result.value) : result.message} )} ); } export { ManualInvokeDialog };