pixelon 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1618,8 +1618,47 @@ function parseContent(content) {
1618
1618
  return null;
1619
1619
  }
1620
1620
 
1621
+ const TERMINAL_STATUS = /* @__PURE__ */ new Set(["answered", "cancelled", "expired"]);
1621
1622
  const DISPLAY_COMPONENTS = /* @__PURE__ */ new Set(["card", "badge", "reactive_form", "carousel"]);
1622
1623
 
1624
+ const AnsweredSummary = ({ component }) => {
1625
+ const status = component.status;
1626
+ const values = component.selected_values ?? [];
1627
+ const labels = values.map((v) => v.label).filter(Boolean).join(", ");
1628
+ let icon = "\u2713";
1629
+ let text = labels || "Respondido";
1630
+ let color = "#3a8a3a";
1631
+ if (status === "cancelled") {
1632
+ icon = "\u2715";
1633
+ text = "Cancelado";
1634
+ color = "#999";
1635
+ } else if (status === "expired") {
1636
+ icon = "\u231B";
1637
+ text = "Expir\xF3";
1638
+ color = "#b58a00";
1639
+ }
1640
+ return /* @__PURE__ */ jsxs(
1641
+ "div",
1642
+ {
1643
+ style: {
1644
+ display: "inline-flex",
1645
+ alignItems: "center",
1646
+ gap: "6px",
1647
+ padding: "6px 12px",
1648
+ borderRadius: "999px",
1649
+ background: "rgba(0,0,0,0.04)",
1650
+ color,
1651
+ fontSize: "0.875rem",
1652
+ fontWeight: 500
1653
+ },
1654
+ children: [
1655
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: icon }),
1656
+ /* @__PURE__ */ jsx("span", { children: text })
1657
+ ]
1658
+ }
1659
+ );
1660
+ };
1661
+
1623
1662
  const useAppHeight$5 = () => {
1624
1663
  useEffect(() => {
1625
1664
  const setDrawerHeight = () => {
@@ -1796,13 +1835,14 @@ function useComponentInteraction({
1796
1835
  }) {
1797
1836
  const [isConfirmed, setIsConfirmed] = useState(false);
1798
1837
  const emitInteraction = useCallback(
1799
- (value, label) => {
1838
+ (value, label, actionId) => {
1800
1839
  eventBus?.emit(CHAT_EVENT.COMPONENT_INTERACTION, {
1801
1840
  fieldName,
1802
1841
  value,
1803
1842
  label,
1804
1843
  componentType,
1805
- assistantMessageId
1844
+ assistantMessageId,
1845
+ actionId
1806
1846
  });
1807
1847
  setIsConfirmed(true);
1808
1848
  },
@@ -1811,6 +1851,114 @@ function useComponentInteraction({
1811
1851
  return { emitInteraction, isConfirmed };
1812
1852
  }
1813
1853
 
1854
+ const FETCH_TIMEOUT_MS = 12e4;
1855
+ function fetchViaEventBus(eventBus, service, url, method, body) {
1856
+ const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1857
+ const r = Math.random() * 16 | 0;
1858
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1859
+ });
1860
+ return new Promise((resolve, reject) => {
1861
+ const timeoutId = setTimeout(() => {
1862
+ unsubscribe();
1863
+ reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
1864
+ }, FETCH_TIMEOUT_MS);
1865
+ const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
1866
+ if (response.requestId !== requestId) return;
1867
+ clearTimeout(timeoutId);
1868
+ unsubscribe();
1869
+ if (response.httpError) {
1870
+ reject(new Error(response.httpError));
1871
+ } else {
1872
+ resolve(response.data);
1873
+ }
1874
+ });
1875
+ eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
1876
+ requestId,
1877
+ service,
1878
+ url,
1879
+ method,
1880
+ ...body ? { body } : {}
1881
+ });
1882
+ });
1883
+ }
1884
+
1885
+ function mapResponseToOptions(data, endpoint) {
1886
+ let items = [];
1887
+ if (endpoint.response_path) {
1888
+ const parts = endpoint.response_path.split(".");
1889
+ let current = data;
1890
+ for (const part of parts) {
1891
+ if (current != null && typeof current === "object") {
1892
+ current = current[part];
1893
+ } else {
1894
+ return [];
1895
+ }
1896
+ }
1897
+ items = Array.isArray(current) ? current : [];
1898
+ } else if (Array.isArray(data)) {
1899
+ items = data;
1900
+ } else {
1901
+ return [];
1902
+ }
1903
+ return items.filter((item) => item != null && typeof item === "object").map((item) => ({
1904
+ value: String(item[endpoint.value_field] ?? ""),
1905
+ label: String(item[endpoint.label_field] ?? "")
1906
+ }));
1907
+ }
1908
+
1909
+ function normalizeBody(body) {
1910
+ if (!body) return void 0;
1911
+ const normalized = { ...body };
1912
+ for (const [key, val] of Object.entries(normalized)) {
1913
+ if (/s_id$/.test(key) && typeof val === "string") {
1914
+ normalized[key] = [val];
1915
+ }
1916
+ }
1917
+ return normalized;
1918
+ }
1919
+ function useSelectEndpoint(endpoint, eventBus) {
1920
+ const [options, setOptions] = useState([]);
1921
+ const [status, setStatus] = useState(endpoint ? "loading" : "idle");
1922
+ const [errorMessage, setErrorMessage] = useState("");
1923
+ const fetchedRef = useRef(false);
1924
+ const endpointRef = useRef(endpoint);
1925
+ endpointRef.current = endpoint;
1926
+ const doFetch = useCallback(async () => {
1927
+ const ep = endpointRef.current;
1928
+ if (!ep || fetchedRef.current) return;
1929
+ fetchedRef.current = true;
1930
+ if (!eventBus) {
1931
+ setOptions([
1932
+ { value: "mock-1", label: "Mock Option 1" },
1933
+ { value: "mock-2", label: "Mock Option 2" }
1934
+ ]);
1935
+ setStatus("idle");
1936
+ return;
1937
+ }
1938
+ try {
1939
+ const body = normalizeBody(ep.body);
1940
+ if (ep.method.toUpperCase() === "POST" && !body) {
1941
+ setErrorMessage("Faltan par\xE1metros para cargar opciones");
1942
+ setStatus("error");
1943
+ return;
1944
+ }
1945
+ const data = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, body);
1946
+ const mapped = mapResponseToOptions(data, ep);
1947
+ setOptions(mapped);
1948
+ setStatus("idle");
1949
+ } catch (err) {
1950
+ setErrorMessage(err instanceof Error ? err.message : "Error al cargar opciones");
1951
+ setStatus("error");
1952
+ }
1953
+ }, [eventBus]);
1954
+ useEffect(() => {
1955
+ if (endpoint && !fetchedRef.current) {
1956
+ doFetch();
1957
+ }
1958
+ }, [endpoint, doFetch]);
1959
+ return { options, status, errorMessage };
1960
+ }
1961
+
1814
1962
  const MOBILE_BREAKPOINT$4 = 640;
1815
1963
  const useIsMobile$5 = () => {
1816
1964
  const [isMobile, setIsMobile] = useState(
@@ -1834,10 +1982,15 @@ const SelectWithConfirm = ({
1834
1982
  const [searchValue, setSearchValue] = useState("");
1835
1983
  const searchInputRef = useRef(null);
1836
1984
  const isMobile = useIsMobile$5();
1837
- const { field_name, value, ...componentProps } = component.props;
1838
- const options = componentProps.options || [];
1985
+ const { field_name, value, endpoint: endpointProp, ...componentProps } = component.props;
1986
+ const endpoint = endpointProp;
1987
+ const { options: endpointOptions, status: endpointStatus, errorMessage } = useSelectEndpoint(endpoint, eventBus);
1988
+ const staticOptions = componentProps.options || [];
1989
+ const options = endpoint ? endpointOptions : staticOptions;
1839
1990
  const placeholder = componentProps.placeholder || "Seleccionar...";
1840
1991
  const label = componentProps.label || "";
1992
+ const isLoading = endpointStatus === "loading";
1993
+ const isError = endpointStatus === "error";
1841
1994
  const { emitInteraction, isConfirmed } = useComponentInteraction({
1842
1995
  eventBus,
1843
1996
  fieldName: component.props.field_name,
@@ -1857,6 +2010,18 @@ const SelectWithConfirm = ({
1857
2010
  }
1858
2011
  }, [isDrawerOpen]);
1859
2012
  if (isConfirmed) return null;
2013
+ if (isLoading) {
2014
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-select-with-confirm", children: [
2015
+ label && /* @__PURE__ */ jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
2016
+ /* @__PURE__ */ jsx("div", { className: "px-chat-select-with-confirm__loading", children: "Cargando opciones..." })
2017
+ ] });
2018
+ }
2019
+ if (isError) {
2020
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-select-with-confirm", children: [
2021
+ label && /* @__PURE__ */ jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
2022
+ /* @__PURE__ */ jsx("div", { className: "px-chat-select-with-confirm__error", children: errorMessage })
2023
+ ] });
2024
+ }
1860
2025
  if (isMobile) {
1861
2026
  return /* @__PURE__ */ jsxs("div", { className: "px-chat-select-with-confirm", children: [
1862
2027
  label && /* @__PURE__ */ jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
@@ -1976,8 +2141,8 @@ const SelectWithConfirm = ({
1976
2141
  /* @__PURE__ */ jsx(
1977
2142
  Select$2,
1978
2143
  {
1979
- options: [],
1980
2144
  ...componentProps,
2145
+ options,
1981
2146
  variant: "chat",
1982
2147
  mode: "single",
1983
2148
  showSearch: true,
@@ -2514,6 +2679,22 @@ const ConfirmButton = ({
2514
2679
  assistantMessageId
2515
2680
  });
2516
2681
  if (isConfirmed) return null;
2682
+ const actions = component.actions;
2683
+ if (actions && actions.length > 0) {
2684
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: actions.map((a) => /* @__PURE__ */ jsx(
2685
+ Button$1,
2686
+ {
2687
+ variant: a.intent === "primary" ? "primary" : "secondary",
2688
+ onClick: () => emitInteraction(
2689
+ a.emits_value ? component.props.value : a.value,
2690
+ a.label,
2691
+ a.id
2692
+ ),
2693
+ children: a.label
2694
+ },
2695
+ a.id
2696
+ )) });
2697
+ }
2517
2698
  return /* @__PURE__ */ jsx(
2518
2699
  Button$1,
2519
2700
  {
@@ -3137,7 +3318,7 @@ const SelectField$1 = ({ field, value, disabled, loading, options, onChange, dra
3137
3318
  ),
3138
3319
  searchValue && /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3139
3320
  ] }),
3140
- /* @__PURE__ */ jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3321
+ /* @__PURE__ */ jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3141
3322
  const isSelected = opt.value === value;
3142
3323
  return /* @__PURE__ */ jsxs(
3143
3324
  "li",
@@ -3258,7 +3439,7 @@ const SelectMultipleField = ({ field, value, disabled, loading, options, onChang
3258
3439
  ),
3259
3440
  searchValue && /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3260
3441
  ] }),
3261
- /* @__PURE__ */ jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3442
+ /* @__PURE__ */ jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3262
3443
  const isChecked = selected.includes(opt.value);
3263
3444
  return /* @__PURE__ */ jsxs(
3264
3445
  "li",
@@ -3356,6 +3537,200 @@ const DateField = ({ field, value, disabled, onChange, drawerPlacement }) => {
3356
3537
  ] });
3357
3538
  };
3358
3539
 
3540
+ const PREFIX$3 = "px-chat-currency";
3541
+ const ChatCurrencyDisplay = ({
3542
+ value,
3543
+ currencyCode,
3544
+ currencySymbol,
3545
+ showCode = true,
3546
+ size = "md",
3547
+ className
3548
+ }) => {
3549
+ const { locale, currency: configCurrency } = useChatConfig();
3550
+ const code = currencyCode || configCurrency?.code || "COP";
3551
+ const symbol = currencySymbol || configCurrency?.symbol || "$";
3552
+ const formatted = useMemo(() => {
3553
+ if (value === null || value === void 0 || value === "") return null;
3554
+ const num = typeof value === "string" ? parseFloat(value) : value;
3555
+ if (isNaN(num)) return null;
3556
+ try {
3557
+ return new Intl.NumberFormat(locale || "es-CO", {
3558
+ minimumFractionDigits: 2,
3559
+ maximumFractionDigits: 2
3560
+ }).format(num);
3561
+ } catch {
3562
+ return num.toFixed(2);
3563
+ }
3564
+ }, [value, locale]);
3565
+ if (!formatted) return /* @__PURE__ */ jsx("span", { className: `${PREFIX$3} ${PREFIX$3}--empty`, children: "-" });
3566
+ const classes = [PREFIX$3, `${PREFIX$3}--${size}`, className].filter(Boolean).join(" ");
3567
+ return /* @__PURE__ */ jsxs("span", { className: classes, children: [
3568
+ /* @__PURE__ */ jsx("span", { className: `${PREFIX$3}__symbol`, children: symbol }),
3569
+ " ",
3570
+ /* @__PURE__ */ jsx("span", { className: `${PREFIX$3}__value`, children: formatted }),
3571
+ showCode && /* @__PURE__ */ jsxs(Fragment, { children: [
3572
+ " ",
3573
+ /* @__PURE__ */ jsx("span", { className: `${PREFIX$3}__code`, children: code })
3574
+ ] })
3575
+ ] });
3576
+ };
3577
+ ChatCurrencyDisplay.displayName = "ChatCurrencyDisplay";
3578
+
3579
+ function resolveVariable(template, formValues, fieldStates) {
3580
+ if (!template.startsWith("$")) return template;
3581
+ const withoutDollar = template.slice(1);
3582
+ const dotIndex = withoutDollar.indexOf(".");
3583
+ const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3584
+ const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3585
+ const value = formValues[fieldName];
3586
+ const state = fieldStates[fieldName];
3587
+ if (!prop) return value;
3588
+ if (prop === "label" && state) {
3589
+ return state.label || "";
3590
+ }
3591
+ if (prop === "labels" && state && Array.isArray(value)) {
3592
+ return value.map((v) => {
3593
+ const opt = state.options.find((o) => o.value === v);
3594
+ return opt?.label || v;
3595
+ }).join(", ");
3596
+ }
3597
+ if (state) {
3598
+ const selectedOpt = state.options.find((o) => o.value === value);
3599
+ if (selectedOpt && prop in selectedOpt) {
3600
+ return selectedOpt[prop];
3601
+ }
3602
+ }
3603
+ return "";
3604
+ }
3605
+ function resolveBody(obj, formValues, fieldStates) {
3606
+ if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3607
+ if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3608
+ if (obj !== null && typeof obj === "object") {
3609
+ const resolved = {};
3610
+ for (const [key, value] of Object.entries(obj)) {
3611
+ resolved[key] = resolveBody(value, formValues, fieldStates);
3612
+ }
3613
+ return resolved;
3614
+ }
3615
+ return obj;
3616
+ }
3617
+ function resolveParams(params, formValues, fieldStates) {
3618
+ const resolved = {};
3619
+ for (const [key, template] of Object.entries(params)) {
3620
+ resolved[key] = resolveVariable(template, formValues, fieldStates);
3621
+ }
3622
+ return resolved;
3623
+ }
3624
+ function resolveSummaryRows(rows, formValues, fieldStates) {
3625
+ return rows.map((row) => ({
3626
+ label: row.label,
3627
+ value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3628
+ }));
3629
+ }
3630
+
3631
+ const CostSummaryField = ({
3632
+ field,
3633
+ values,
3634
+ fieldStates,
3635
+ onCostResolved,
3636
+ eventBus,
3637
+ onCancel
3638
+ }) => {
3639
+ const [totalCost, setTotalCost] = useState(null);
3640
+ const [loading, setLoading] = useState(true);
3641
+ const [error, setError] = useState("");
3642
+ const selectedItems = useMemo(() => {
3643
+ if (!field.items_from) return [];
3644
+ const { source_field, label_key, value_key } = field.items_from;
3645
+ const sourceState = fieldStates[source_field];
3646
+ const selectedValues = values[source_field];
3647
+ if (!sourceState?.options || !selectedValues) return [];
3648
+ const selectedArr = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
3649
+ return sourceState.options.filter((opt) => selectedArr.includes(opt.value)).map((opt) => ({
3650
+ label: String(opt[label_key] || opt.label || ""),
3651
+ value: Number(opt[value_key] || 0)
3652
+ }));
3653
+ }, [field.items_from, fieldStates, values]);
3654
+ const fetchedRef = useRef(false);
3655
+ useEffect(() => {
3656
+ if (!field.endpoint || !eventBus || fetchedRef.current) {
3657
+ if (!field.endpoint || !eventBus) setLoading(false);
3658
+ return;
3659
+ }
3660
+ fetchedRef.current = true;
3661
+ const body = resolveBody(field.endpoint.body || {}, values, fieldStates);
3662
+ setLoading(true);
3663
+ setError("");
3664
+ fetchViaEventBus(eventBus, field.endpoint.service, field.endpoint.url, field.endpoint.method, body).then((data) => {
3665
+ const resp = data;
3666
+ if (resp?.notification_type && String(resp.notification_type).toUpperCase() !== "SUCCESS") {
3667
+ setError(String(resp.message || "Error al calcular costo"));
3668
+ return;
3669
+ }
3670
+ const responseData = resp?.response;
3671
+ const totalField = field.endpoint.total_field || "total_cost";
3672
+ const cost = Number(responseData?.[totalField] ?? resp?.[totalField] ?? 0);
3673
+ setTotalCost(cost);
3674
+ if (onCostResolved) {
3675
+ try {
3676
+ const formatted = new Intl.NumberFormat("es-CO", { style: "currency", currency: "COP" }).format(cost);
3677
+ onCostResolved(field.name, formatted);
3678
+ } catch {
3679
+ onCostResolved(field.name, `$${cost}`);
3680
+ }
3681
+ }
3682
+ }).catch((err) => {
3683
+ setError(err instanceof Error ? err.message : "Error al calcular costo");
3684
+ }).finally(() => setLoading(false));
3685
+ }, [field.endpoint, eventBus]);
3686
+ const handleCancel = () => {
3687
+ if (field.on_cancel && eventBus) {
3688
+ if (field.on_cancel.type === "system_message") {
3689
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3690
+ chatId: "",
3691
+ formTitle: "",
3692
+ source: "reactive_form",
3693
+ phase: "cancel",
3694
+ data: { message: field.on_cancel.message },
3695
+ service: "",
3696
+ url: "",
3697
+ method: ""
3698
+ });
3699
+ }
3700
+ }
3701
+ onCancel();
3702
+ };
3703
+ if (loading) {
3704
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--loading", children: [
3705
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3706
+ /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary__loader", children: [
3707
+ /* @__PURE__ */ jsx("span", { className: "px-chat-rf__spinner" }),
3708
+ /* @__PURE__ */ jsx("span", { children: "Calculando costo..." })
3709
+ ] })
3710
+ ] });
3711
+ }
3712
+ if (error) {
3713
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--error", children: [
3714
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3715
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__error", children: error }),
3716
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" })
3717
+ ] });
3718
+ }
3719
+ return /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary", children: [
3720
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3721
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__items", children: selectedItems.map((item, i) => /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary__item", children: [
3722
+ /* @__PURE__ */ jsx("span", { className: "px-chat-rf__cost-summary__item-label", children: item.label }),
3723
+ /* @__PURE__ */ jsx(ChatCurrencyDisplay, { value: item.value, size: "sm" })
3724
+ ] }, i)) }),
3725
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__divider" }),
3726
+ /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__cost-summary__total", children: [
3727
+ /* @__PURE__ */ jsx("span", { className: "px-chat-rf__cost-summary__total-label", children: "Total" }),
3728
+ /* @__PURE__ */ jsx(ChatCurrencyDisplay, { value: totalCost, size: "lg" })
3729
+ ] }),
3730
+ /* @__PURE__ */ jsx("div", { className: "px-chat-rf__cost-summary__actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" }) })
3731
+ ] });
3732
+ };
3733
+
3359
3734
  const checkIcon = /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx("path", { d: "M2.5 6L5 8.5L9.5 3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
3360
3735
  const calendarIcon = /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
3361
3736
  /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "11", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }),
@@ -3371,7 +3746,7 @@ const formIcon = /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBo
3371
3746
  ] });
3372
3747
  const chevronDown = /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3373
3748
  const chevronRight = /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx("path", { d: "M4.5 3L7.5 6L4.5 9", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3374
- const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom" }) => {
3749
+ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom", eventBus }) => {
3375
3750
  const {
3376
3751
  formStatus,
3377
3752
  setFormStatus,
@@ -3387,6 +3762,7 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3387
3762
  handleNext,
3388
3763
  handleBack,
3389
3764
  handleSubmit,
3765
+ isSubmitting,
3390
3766
  getDependencies,
3391
3767
  allDepsResolved,
3392
3768
  resolveSummaryRows
@@ -3446,11 +3822,25 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3446
3822
  const renderSummaryActions = () => {
3447
3823
  const actions = summary.actions;
3448
3824
  if (!actions || actions.length === 0) {
3449
- return /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--primary px-chat-rf__btn--full", onClick: handleSubmit, children: submit.label });
3825
+ return /* @__PURE__ */ jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--primary px-chat-rf__btn--full", onClick: handleSubmit, disabled: isSubmitting, children: isSubmitting ? /* @__PURE__ */ jsx("span", { className: "px-chat-rf__spinner" }) : submit.label });
3450
3826
  }
3451
3827
  const actionHandlers = {
3452
3828
  submit: handleSubmit,
3453
- close: () => setFormStatus("closed"),
3829
+ close: () => {
3830
+ if (eventBus) {
3831
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3832
+ chatId: "",
3833
+ formTitle: "",
3834
+ source: "reactive_form",
3835
+ phase: "cancel",
3836
+ data: { message: "El usuario cancel\xF3 el agendamiento de la cita." },
3837
+ service: "",
3838
+ url: "",
3839
+ method: ""
3840
+ });
3841
+ }
3842
+ setFormStatus("closed");
3843
+ },
3454
3844
  back: handleBack
3455
3845
  };
3456
3846
  return /* @__PURE__ */ jsx("div", { className: "px-chat-rf__nav", children: actions.map((act, i) => /* @__PURE__ */ jsx(
@@ -3460,7 +3850,8 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3460
3850
  className: `px-chat-rf__btn px-chat-rf__btn--${act.variant || "primary"}`,
3461
3851
  onClick: actionHandlers[act.action] || (() => {
3462
3852
  }),
3463
- children: act.label
3853
+ disabled: act.action === "submit" && isSubmitting,
3854
+ children: act.action === "submit" && isSubmitting ? /* @__PURE__ */ jsx("span", { className: "px-chat-rf__spinner" }) : act.label
3464
3855
  },
3465
3856
  i
3466
3857
  )) });
@@ -3493,8 +3884,15 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3493
3884
  )
3494
3885
  ] }),
3495
3886
  prevStep.fields.map((fname) => {
3496
- const fs = fieldStates[fname];
3497
3887
  const fld = fields.find((f) => f.name === fname);
3888
+ if (fld?.type === "cost_summary") {
3889
+ const costLabel = fieldStates[fname]?.label;
3890
+ return costLabel ? /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__completed-field", children: [
3891
+ /* @__PURE__ */ jsx("span", { className: "px-chat-rf__completed-field-label", children: "Total:" }),
3892
+ /* @__PURE__ */ jsx("span", { className: "px-chat-rf__completed-field-value", children: costLabel })
3893
+ ] }, fname) : null;
3894
+ }
3895
+ const fs = fieldStates[fname];
3498
3896
  return /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__completed-field", children: [
3499
3897
  /* @__PURE__ */ jsxs("span", { className: "px-chat-rf__completed-field-label", children: [
3500
3898
  fld?.label,
@@ -3507,6 +3905,20 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3507
3905
  /* @__PURE__ */ jsxs("div", { className: "px-chat-rf__active-step", children: [
3508
3906
  /* @__PURE__ */ jsx("div", { className: "px-chat-rf__step-title", children: step.title }),
3509
3907
  stepFields.map((field) => {
3908
+ if (field.type === "cost_summary") {
3909
+ return /* @__PURE__ */ jsx(
3910
+ CostSummaryField,
3911
+ {
3912
+ field,
3913
+ values,
3914
+ fieldStates,
3915
+ eventBus,
3916
+ onCancel: () => setFormStatus("closed"),
3917
+ onCostResolved: (name, label) => handleFieldChange(name, label, label)
3918
+ },
3919
+ field.name
3920
+ );
3921
+ }
3510
3922
  const deps = getDependencies(field);
3511
3923
  const isDisabled = !allDepsResolved(deps, values);
3512
3924
  return /* @__PURE__ */ jsx(
@@ -3602,89 +4014,6 @@ function buildInitialValues(fields) {
3602
4014
  return values;
3603
4015
  }
3604
4016
 
3605
- function resolveVariable(template, formValues, fieldStates) {
3606
- if (!template.startsWith("$")) return template;
3607
- const withoutDollar = template.slice(1);
3608
- const dotIndex = withoutDollar.indexOf(".");
3609
- const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3610
- const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3611
- const value = formValues[fieldName];
3612
- const state = fieldStates[fieldName];
3613
- if (!prop) return value;
3614
- if (prop === "label" && state) {
3615
- return state.label || "";
3616
- }
3617
- if (prop === "labels" && state && Array.isArray(value)) {
3618
- return value.map((v) => {
3619
- const opt = state.options.find((o) => o.value === v);
3620
- return opt?.label || v;
3621
- }).join(", ");
3622
- }
3623
- if (state) {
3624
- const selectedOpt = state.options.find((o) => o.value === value);
3625
- if (selectedOpt && prop in selectedOpt) {
3626
- return selectedOpt[prop];
3627
- }
3628
- }
3629
- return "";
3630
- }
3631
- function resolveBody(obj, formValues, fieldStates) {
3632
- if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3633
- if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3634
- if (obj !== null && typeof obj === "object") {
3635
- const resolved = {};
3636
- for (const [key, value] of Object.entries(obj)) {
3637
- resolved[key] = resolveBody(value, formValues, fieldStates);
3638
- }
3639
- return resolved;
3640
- }
3641
- return obj;
3642
- }
3643
- function resolveParams(params, formValues, fieldStates) {
3644
- const resolved = {};
3645
- for (const [key, template] of Object.entries(params)) {
3646
- resolved[key] = resolveVariable(template, formValues, fieldStates);
3647
- }
3648
- return resolved;
3649
- }
3650
- function resolveSummaryRows(rows, formValues, fieldStates) {
3651
- return rows.map((row) => ({
3652
- label: row.label,
3653
- value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3654
- }));
3655
- }
3656
-
3657
- const FETCH_TIMEOUT_MS = 12e4;
3658
- function fetchViaEventBus(eventBus, service, url, method, body) {
3659
- const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3660
- const r = Math.random() * 16 | 0;
3661
- return (c === "x" ? r : r & 3 | 8).toString(16);
3662
- });
3663
- return new Promise((resolve, reject) => {
3664
- const timeoutId = setTimeout(() => {
3665
- unsubscribe();
3666
- reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
3667
- }, FETCH_TIMEOUT_MS);
3668
- const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
3669
- if (response.requestId !== requestId) return;
3670
- clearTimeout(timeoutId);
3671
- unsubscribe();
3672
- if (response.httpError) {
3673
- reject(new Error(response.httpError));
3674
- } else {
3675
- resolve(response.data);
3676
- }
3677
- });
3678
- eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
3679
- requestId,
3680
- service,
3681
- url,
3682
- method,
3683
- ...body ? { body } : {}
3684
- });
3685
- });
3686
- }
3687
-
3688
4017
  function formatTime$1(value, timezone) {
3689
4018
  try {
3690
4019
  const d = new Date(value);
@@ -3692,7 +4021,7 @@ function formatTime$1(value, timezone) {
3692
4021
  return d.toLocaleTimeString([], {
3693
4022
  hour: "2-digit",
3694
4023
  minute: "2-digit",
3695
- hour12: false,
4024
+ hour12: true,
3696
4025
  ...timezone ? { timeZone: timezone } : {}
3697
4026
  });
3698
4027
  } catch {
@@ -4054,7 +4383,10 @@ function useReactiveForm(props) {
4054
4383
  };
4055
4384
  const handleBack = () => setCurrentStep((s) => s - 1);
4056
4385
  const [submitMessage, setSubmitMessage] = useState("");
4386
+ const [isSubmitting, setIsSubmitting] = useState(false);
4057
4387
  const handleSubmit = async () => {
4388
+ if (isSubmitting) return;
4389
+ setIsSubmitting(true);
4058
4390
  const resolvedPayload = resolveBody(submit.payload_map, values, fieldStates);
4059
4391
  if (eventBus) {
4060
4392
  try {
@@ -4077,7 +4409,8 @@ function useReactiveForm(props) {
4077
4409
  submitUrl: submit.url,
4078
4410
  result: data,
4079
4411
  formValues: values,
4080
- formLabels
4412
+ formLabels,
4413
+ onSuccess: submit.on_success
4081
4414
  });
4082
4415
  if (notificationType === "SUCCESS") {
4083
4416
  setSubmitMessage(message || "Completado");
@@ -4093,6 +4426,7 @@ function useReactiveForm(props) {
4093
4426
  const message = err instanceof Error ? err.message : "Error al enviar formulario";
4094
4427
  setSubmitMessage(message);
4095
4428
  setFormStatus("error");
4429
+ setIsSubmitting(false);
4096
4430
  }
4097
4431
  } else {
4098
4432
  console.log("[ReactiveForm] Submit:", values);
@@ -4121,6 +4455,7 @@ function useReactiveForm(props) {
4121
4455
  handleNext,
4122
4456
  handleBack,
4123
4457
  handleSubmit,
4458
+ isSubmitting,
4124
4459
  getDependencies,
4125
4460
  allDepsResolved,
4126
4461
  resolveSummaryRows: resolveRows
@@ -4152,7 +4487,8 @@ const ReactiveForm = ({ component, eventBus }) => {
4152
4487
  {
4153
4488
  ...props,
4154
4489
  form,
4155
- drawerPlacement: isMobile ? "bottom" : "right"
4490
+ drawerPlacement: isMobile ? "bottom" : "right",
4491
+ eventBus
4156
4492
  }
4157
4493
  );
4158
4494
  };
@@ -4818,7 +5154,15 @@ const COMPONENT_MAP = {
4818
5154
  };
4819
5155
  function renderComponent(props) {
4820
5156
  const { component } = props;
4821
- if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5157
+ if (component.status) {
5158
+ if (TERMINAL_STATUS.has(component.status)) {
5159
+ const policy = component.visibility_policy ?? "hide_on_terminal";
5160
+ if (policy === "hide_on_terminal") return null;
5161
+ if (component.selected_values && component.selected_values.length > 0) {
5162
+ return /* @__PURE__ */ jsx(AnsweredSummary, { component });
5163
+ }
5164
+ }
5165
+ } else if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
4822
5166
  return null;
4823
5167
  }
4824
5168
  const Component = COMPONENT_MAP[component.type];