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.js CHANGED
@@ -1639,8 +1639,47 @@ function parseContent(content) {
1639
1639
  return null;
1640
1640
  }
1641
1641
 
1642
+ const TERMINAL_STATUS = /* @__PURE__ */ new Set(["answered", "cancelled", "expired"]);
1642
1643
  const DISPLAY_COMPONENTS = /* @__PURE__ */ new Set(["card", "badge", "reactive_form", "carousel"]);
1643
1644
 
1645
+ const AnsweredSummary = ({ component }) => {
1646
+ const status = component.status;
1647
+ const values = component.selected_values ?? [];
1648
+ const labels = values.map((v) => v.label).filter(Boolean).join(", ");
1649
+ let icon = "\u2713";
1650
+ let text = labels || "Respondido";
1651
+ let color = "#3a8a3a";
1652
+ if (status === "cancelled") {
1653
+ icon = "\u2715";
1654
+ text = "Cancelado";
1655
+ color = "#999";
1656
+ } else if (status === "expired") {
1657
+ icon = "\u231B";
1658
+ text = "Expir\xF3";
1659
+ color = "#b58a00";
1660
+ }
1661
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1662
+ "div",
1663
+ {
1664
+ style: {
1665
+ display: "inline-flex",
1666
+ alignItems: "center",
1667
+ gap: "6px",
1668
+ padding: "6px 12px",
1669
+ borderRadius: "999px",
1670
+ background: "rgba(0,0,0,0.04)",
1671
+ color,
1672
+ fontSize: "0.875rem",
1673
+ fontWeight: 500
1674
+ },
1675
+ children: [
1676
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: icon }),
1677
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: text })
1678
+ ]
1679
+ }
1680
+ );
1681
+ };
1682
+
1644
1683
  const useAppHeight$5 = () => {
1645
1684
  React.useEffect(() => {
1646
1685
  const setDrawerHeight = () => {
@@ -1817,13 +1856,14 @@ function useComponentInteraction({
1817
1856
  }) {
1818
1857
  const [isConfirmed, setIsConfirmed] = React.useState(false);
1819
1858
  const emitInteraction = React.useCallback(
1820
- (value, label) => {
1859
+ (value, label, actionId) => {
1821
1860
  eventBus?.emit(CHAT_EVENT.COMPONENT_INTERACTION, {
1822
1861
  fieldName,
1823
1862
  value,
1824
1863
  label,
1825
1864
  componentType,
1826
- assistantMessageId
1865
+ assistantMessageId,
1866
+ actionId
1827
1867
  });
1828
1868
  setIsConfirmed(true);
1829
1869
  },
@@ -1832,6 +1872,114 @@ function useComponentInteraction({
1832
1872
  return { emitInteraction, isConfirmed };
1833
1873
  }
1834
1874
 
1875
+ const FETCH_TIMEOUT_MS = 12e4;
1876
+ function fetchViaEventBus(eventBus, service, url, method, body) {
1877
+ const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1878
+ const r = Math.random() * 16 | 0;
1879
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1880
+ });
1881
+ return new Promise((resolve, reject) => {
1882
+ const timeoutId = setTimeout(() => {
1883
+ unsubscribe();
1884
+ reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
1885
+ }, FETCH_TIMEOUT_MS);
1886
+ const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
1887
+ if (response.requestId !== requestId) return;
1888
+ clearTimeout(timeoutId);
1889
+ unsubscribe();
1890
+ if (response.httpError) {
1891
+ reject(new Error(response.httpError));
1892
+ } else {
1893
+ resolve(response.data);
1894
+ }
1895
+ });
1896
+ eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
1897
+ requestId,
1898
+ service,
1899
+ url,
1900
+ method,
1901
+ ...body ? { body } : {}
1902
+ });
1903
+ });
1904
+ }
1905
+
1906
+ function mapResponseToOptions(data, endpoint) {
1907
+ let items = [];
1908
+ if (endpoint.response_path) {
1909
+ const parts = endpoint.response_path.split(".");
1910
+ let current = data;
1911
+ for (const part of parts) {
1912
+ if (current != null && typeof current === "object") {
1913
+ current = current[part];
1914
+ } else {
1915
+ return [];
1916
+ }
1917
+ }
1918
+ items = Array.isArray(current) ? current : [];
1919
+ } else if (Array.isArray(data)) {
1920
+ items = data;
1921
+ } else {
1922
+ return [];
1923
+ }
1924
+ return items.filter((item) => item != null && typeof item === "object").map((item) => ({
1925
+ value: String(item[endpoint.value_field] ?? ""),
1926
+ label: String(item[endpoint.label_field] ?? "")
1927
+ }));
1928
+ }
1929
+
1930
+ function normalizeBody(body) {
1931
+ if (!body) return void 0;
1932
+ const normalized = { ...body };
1933
+ for (const [key, val] of Object.entries(normalized)) {
1934
+ if (/s_id$/.test(key) && typeof val === "string") {
1935
+ normalized[key] = [val];
1936
+ }
1937
+ }
1938
+ return normalized;
1939
+ }
1940
+ function useSelectEndpoint(endpoint, eventBus) {
1941
+ const [options, setOptions] = React.useState([]);
1942
+ const [status, setStatus] = React.useState(endpoint ? "loading" : "idle");
1943
+ const [errorMessage, setErrorMessage] = React.useState("");
1944
+ const fetchedRef = React.useRef(false);
1945
+ const endpointRef = React.useRef(endpoint);
1946
+ endpointRef.current = endpoint;
1947
+ const doFetch = React.useCallback(async () => {
1948
+ const ep = endpointRef.current;
1949
+ if (!ep || fetchedRef.current) return;
1950
+ fetchedRef.current = true;
1951
+ if (!eventBus) {
1952
+ setOptions([
1953
+ { value: "mock-1", label: "Mock Option 1" },
1954
+ { value: "mock-2", label: "Mock Option 2" }
1955
+ ]);
1956
+ setStatus("idle");
1957
+ return;
1958
+ }
1959
+ try {
1960
+ const body = normalizeBody(ep.body);
1961
+ if (ep.method.toUpperCase() === "POST" && !body) {
1962
+ setErrorMessage("Faltan par\xE1metros para cargar opciones");
1963
+ setStatus("error");
1964
+ return;
1965
+ }
1966
+ const data = await fetchViaEventBus(eventBus, ep.service, ep.url, ep.method, body);
1967
+ const mapped = mapResponseToOptions(data, ep);
1968
+ setOptions(mapped);
1969
+ setStatus("idle");
1970
+ } catch (err) {
1971
+ setErrorMessage(err instanceof Error ? err.message : "Error al cargar opciones");
1972
+ setStatus("error");
1973
+ }
1974
+ }, [eventBus]);
1975
+ React.useEffect(() => {
1976
+ if (endpoint && !fetchedRef.current) {
1977
+ doFetch();
1978
+ }
1979
+ }, [endpoint, doFetch]);
1980
+ return { options, status, errorMessage };
1981
+ }
1982
+
1835
1983
  const MOBILE_BREAKPOINT$4 = 640;
1836
1984
  const useIsMobile$5 = () => {
1837
1985
  const [isMobile, setIsMobile] = React.useState(
@@ -1855,10 +2003,15 @@ const SelectWithConfirm = ({
1855
2003
  const [searchValue, setSearchValue] = React.useState("");
1856
2004
  const searchInputRef = React.useRef(null);
1857
2005
  const isMobile = useIsMobile$5();
1858
- const { field_name, value, ...componentProps } = component.props;
1859
- const options = componentProps.options || [];
2006
+ const { field_name, value, endpoint: endpointProp, ...componentProps } = component.props;
2007
+ const endpoint = endpointProp;
2008
+ const { options: endpointOptions, status: endpointStatus, errorMessage } = useSelectEndpoint(endpoint, eventBus);
2009
+ const staticOptions = componentProps.options || [];
2010
+ const options = endpoint ? endpointOptions : staticOptions;
1860
2011
  const placeholder = componentProps.placeholder || "Seleccionar...";
1861
2012
  const label = componentProps.label || "";
2013
+ const isLoading = endpointStatus === "loading";
2014
+ const isError = endpointStatus === "error";
1862
2015
  const { emitInteraction, isConfirmed } = useComponentInteraction({
1863
2016
  eventBus,
1864
2017
  fieldName: component.props.field_name,
@@ -1878,6 +2031,18 @@ const SelectWithConfirm = ({
1878
2031
  }
1879
2032
  }, [isDrawerOpen]);
1880
2033
  if (isConfirmed) return null;
2034
+ if (isLoading) {
2035
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
2036
+ label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
2037
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-select-with-confirm__loading", children: "Cargando opciones..." })
2038
+ ] });
2039
+ }
2040
+ if (isError) {
2041
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
2042
+ label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
2043
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-select-with-confirm__error", children: errorMessage })
2044
+ ] });
2045
+ }
1881
2046
  if (isMobile) {
1882
2047
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-select-with-confirm", children: [
1883
2048
  label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "px-chat-select-with-confirm__label", children: label }),
@@ -1997,8 +2162,8 @@ const SelectWithConfirm = ({
1997
2162
  /* @__PURE__ */ jsxRuntime.jsx(
1998
2163
  Select$2,
1999
2164
  {
2000
- options: [],
2001
2165
  ...componentProps,
2166
+ options,
2002
2167
  variant: "chat",
2003
2168
  mode: "single",
2004
2169
  showSearch: true,
@@ -2535,6 +2700,22 @@ const ConfirmButton = ({
2535
2700
  assistantMessageId
2536
2701
  });
2537
2702
  if (isConfirmed) return null;
2703
+ const actions = component.actions;
2704
+ if (actions && actions.length > 0) {
2705
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
2706
+ Button$1,
2707
+ {
2708
+ variant: a.intent === "primary" ? "primary" : "secondary",
2709
+ onClick: () => emitInteraction(
2710
+ a.emits_value ? component.props.value : a.value,
2711
+ a.label,
2712
+ a.id
2713
+ ),
2714
+ children: a.label
2715
+ },
2716
+ a.id
2717
+ )) });
2718
+ }
2538
2719
  return /* @__PURE__ */ jsxRuntime.jsx(
2539
2720
  Button$1,
2540
2721
  {
@@ -3158,7 +3339,7 @@ const SelectField$1 = ({ field, value, disabled, loading, options, onChange, dra
3158
3339
  ),
3159
3340
  searchValue && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsxRuntime.jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3160
3341
  ] }),
3161
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3342
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3162
3343
  const isSelected = opt.value === value;
3163
3344
  return /* @__PURE__ */ jsxRuntime.jsxs(
3164
3345
  "li",
@@ -3279,7 +3460,7 @@ const SelectMultipleField = ({ field, value, disabled, loading, options, onChang
3279
3460
  ),
3280
3461
  searchValue && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-select-drawer__search-clear", onClick: () => setSearchValue(""), children: /* @__PURE__ */ jsxRuntime.jsx(Icon$1, { type: ICON_TYPES.CLOSE, size: "small" }) })
3281
3462
  ] }),
3282
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3463
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "px-chat-select-drawer__options", role: "listbox", "aria-multiselectable": "true", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-chat-select-drawer__empty", children: field.empty_message || "No hay opciones disponibles" }) : filteredOptions.map((opt) => {
3283
3464
  const isChecked = selected.includes(opt.value);
3284
3465
  return /* @__PURE__ */ jsxRuntime.jsxs(
3285
3466
  "li",
@@ -3377,6 +3558,200 @@ const DateField = ({ field, value, disabled, onChange, drawerPlacement }) => {
3377
3558
  ] });
3378
3559
  };
3379
3560
 
3561
+ const PREFIX$3 = "px-chat-currency";
3562
+ const ChatCurrencyDisplay = ({
3563
+ value,
3564
+ currencyCode,
3565
+ currencySymbol,
3566
+ showCode = true,
3567
+ size = "md",
3568
+ className
3569
+ }) => {
3570
+ const { locale, currency: configCurrency } = useChatConfig();
3571
+ const code = currencyCode || configCurrency?.code || "COP";
3572
+ const symbol = currencySymbol || configCurrency?.symbol || "$";
3573
+ const formatted = React.useMemo(() => {
3574
+ if (value === null || value === void 0 || value === "") return null;
3575
+ const num = typeof value === "string" ? parseFloat(value) : value;
3576
+ if (isNaN(num)) return null;
3577
+ try {
3578
+ return new Intl.NumberFormat(locale || "es-CO", {
3579
+ minimumFractionDigits: 2,
3580
+ maximumFractionDigits: 2
3581
+ }).format(num);
3582
+ } catch {
3583
+ return num.toFixed(2);
3584
+ }
3585
+ }, [value, locale]);
3586
+ if (!formatted) return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3} ${PREFIX$3}--empty`, children: "-" });
3587
+ const classes = [PREFIX$3, `${PREFIX$3}--${size}`, className].filter(Boolean).join(" ");
3588
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: classes, children: [
3589
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__symbol`, children: symbol }),
3590
+ " ",
3591
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__value`, children: formatted }),
3592
+ showCode && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3593
+ " ",
3594
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${PREFIX$3}__code`, children: code })
3595
+ ] })
3596
+ ] });
3597
+ };
3598
+ ChatCurrencyDisplay.displayName = "ChatCurrencyDisplay";
3599
+
3600
+ function resolveVariable(template, formValues, fieldStates) {
3601
+ if (!template.startsWith("$")) return template;
3602
+ const withoutDollar = template.slice(1);
3603
+ const dotIndex = withoutDollar.indexOf(".");
3604
+ const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3605
+ const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3606
+ const value = formValues[fieldName];
3607
+ const state = fieldStates[fieldName];
3608
+ if (!prop) return value;
3609
+ if (prop === "label" && state) {
3610
+ return state.label || "";
3611
+ }
3612
+ if (prop === "labels" && state && Array.isArray(value)) {
3613
+ return value.map((v) => {
3614
+ const opt = state.options.find((o) => o.value === v);
3615
+ return opt?.label || v;
3616
+ }).join(", ");
3617
+ }
3618
+ if (state) {
3619
+ const selectedOpt = state.options.find((o) => o.value === value);
3620
+ if (selectedOpt && prop in selectedOpt) {
3621
+ return selectedOpt[prop];
3622
+ }
3623
+ }
3624
+ return "";
3625
+ }
3626
+ function resolveBody(obj, formValues, fieldStates) {
3627
+ if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3628
+ if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3629
+ if (obj !== null && typeof obj === "object") {
3630
+ const resolved = {};
3631
+ for (const [key, value] of Object.entries(obj)) {
3632
+ resolved[key] = resolveBody(value, formValues, fieldStates);
3633
+ }
3634
+ return resolved;
3635
+ }
3636
+ return obj;
3637
+ }
3638
+ function resolveParams(params, formValues, fieldStates) {
3639
+ const resolved = {};
3640
+ for (const [key, template] of Object.entries(params)) {
3641
+ resolved[key] = resolveVariable(template, formValues, fieldStates);
3642
+ }
3643
+ return resolved;
3644
+ }
3645
+ function resolveSummaryRows(rows, formValues, fieldStates) {
3646
+ return rows.map((row) => ({
3647
+ label: row.label,
3648
+ value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3649
+ }));
3650
+ }
3651
+
3652
+ const CostSummaryField = ({
3653
+ field,
3654
+ values,
3655
+ fieldStates,
3656
+ onCostResolved,
3657
+ eventBus,
3658
+ onCancel
3659
+ }) => {
3660
+ const [totalCost, setTotalCost] = React.useState(null);
3661
+ const [loading, setLoading] = React.useState(true);
3662
+ const [error, setError] = React.useState("");
3663
+ const selectedItems = React.useMemo(() => {
3664
+ if (!field.items_from) return [];
3665
+ const { source_field, label_key, value_key } = field.items_from;
3666
+ const sourceState = fieldStates[source_field];
3667
+ const selectedValues = values[source_field];
3668
+ if (!sourceState?.options || !selectedValues) return [];
3669
+ const selectedArr = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
3670
+ return sourceState.options.filter((opt) => selectedArr.includes(opt.value)).map((opt) => ({
3671
+ label: String(opt[label_key] || opt.label || ""),
3672
+ value: Number(opt[value_key] || 0)
3673
+ }));
3674
+ }, [field.items_from, fieldStates, values]);
3675
+ const fetchedRef = React.useRef(false);
3676
+ React.useEffect(() => {
3677
+ if (!field.endpoint || !eventBus || fetchedRef.current) {
3678
+ if (!field.endpoint || !eventBus) setLoading(false);
3679
+ return;
3680
+ }
3681
+ fetchedRef.current = true;
3682
+ const body = resolveBody(field.endpoint.body || {}, values, fieldStates);
3683
+ setLoading(true);
3684
+ setError("");
3685
+ fetchViaEventBus(eventBus, field.endpoint.service, field.endpoint.url, field.endpoint.method, body).then((data) => {
3686
+ const resp = data;
3687
+ if (resp?.notification_type && String(resp.notification_type).toUpperCase() !== "SUCCESS") {
3688
+ setError(String(resp.message || "Error al calcular costo"));
3689
+ return;
3690
+ }
3691
+ const responseData = resp?.response;
3692
+ const totalField = field.endpoint.total_field || "total_cost";
3693
+ const cost = Number(responseData?.[totalField] ?? resp?.[totalField] ?? 0);
3694
+ setTotalCost(cost);
3695
+ if (onCostResolved) {
3696
+ try {
3697
+ const formatted = new Intl.NumberFormat("es-CO", { style: "currency", currency: "COP" }).format(cost);
3698
+ onCostResolved(field.name, formatted);
3699
+ } catch {
3700
+ onCostResolved(field.name, `$${cost}`);
3701
+ }
3702
+ }
3703
+ }).catch((err) => {
3704
+ setError(err instanceof Error ? err.message : "Error al calcular costo");
3705
+ }).finally(() => setLoading(false));
3706
+ }, [field.endpoint, eventBus]);
3707
+ const handleCancel = () => {
3708
+ if (field.on_cancel && eventBus) {
3709
+ if (field.on_cancel.type === "system_message") {
3710
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3711
+ chatId: "",
3712
+ formTitle: "",
3713
+ source: "reactive_form",
3714
+ phase: "cancel",
3715
+ data: { message: field.on_cancel.message },
3716
+ service: "",
3717
+ url: "",
3718
+ method: ""
3719
+ });
3720
+ }
3721
+ }
3722
+ onCancel();
3723
+ };
3724
+ if (loading) {
3725
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--loading", children: [
3726
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3727
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__loader", children: [
3728
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }),
3729
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Calculando costo..." })
3730
+ ] })
3731
+ ] });
3732
+ }
3733
+ if (error) {
3734
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary px-chat-rf__cost-summary--error", children: [
3735
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3736
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__error", children: error }),
3737
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" })
3738
+ ] });
3739
+ }
3740
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary", children: [
3741
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__title", children: field.label || "Resumen de costos" }),
3742
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__items", children: selectedItems.map((item, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__item", children: [
3743
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__cost-summary__item-label", children: item.label }),
3744
+ /* @__PURE__ */ jsxRuntime.jsx(ChatCurrencyDisplay, { value: item.value, size: "sm" })
3745
+ ] }, i)) }),
3746
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__divider" }),
3747
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__cost-summary__total", children: [
3748
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__cost-summary__total-label", children: "Total" }),
3749
+ /* @__PURE__ */ jsxRuntime.jsx(ChatCurrencyDisplay, { value: totalCost, size: "lg" })
3750
+ ] }),
3751
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__cost-summary__actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--secondary", onClick: handleCancel, children: "Cancelar" }) })
3752
+ ] });
3753
+ };
3754
+
3380
3755
  const checkIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2.5 6L5 8.5L9.5 3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
3381
3756
  const calendarIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
3382
3757
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "3", width: "12", height: "11", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }),
@@ -3392,7 +3767,7 @@ const formIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "
3392
3767
  ] });
3393
3768
  const chevronDown = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3394
3769
  const chevronRight = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.5 3L7.5 6L4.5 9", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round", strokeLinejoin: "round" }) });
3395
- const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom" }) => {
3770
+ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form, drawerPlacement = "bottom", eventBus }) => {
3396
3771
  const {
3397
3772
  formStatus,
3398
3773
  setFormStatus,
@@ -3408,6 +3783,7 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3408
3783
  handleNext,
3409
3784
  handleBack,
3410
3785
  handleSubmit,
3786
+ isSubmitting,
3411
3787
  getDependencies,
3412
3788
  allDepsResolved,
3413
3789
  resolveSummaryRows
@@ -3467,11 +3843,25 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3467
3843
  const renderSummaryActions = () => {
3468
3844
  const actions = summary.actions;
3469
3845
  if (!actions || actions.length === 0) {
3470
- return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "px-chat-rf__btn px-chat-rf__btn--primary px-chat-rf__btn--full", onClick: handleSubmit, children: submit.label });
3846
+ return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }) : submit.label });
3471
3847
  }
3472
3848
  const actionHandlers = {
3473
3849
  submit: handleSubmit,
3474
- close: () => setFormStatus("closed"),
3850
+ close: () => {
3851
+ if (eventBus) {
3852
+ eventBus.emit(CHAT_EVENT.FORM_TRACE, {
3853
+ chatId: "",
3854
+ formTitle: "",
3855
+ source: "reactive_form",
3856
+ phase: "cancel",
3857
+ data: { message: "El usuario cancel\xF3 el agendamiento de la cita." },
3858
+ service: "",
3859
+ url: "",
3860
+ method: ""
3861
+ });
3862
+ }
3863
+ setFormStatus("closed");
3864
+ },
3475
3865
  back: handleBack
3476
3866
  };
3477
3867
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__nav", children: actions.map((act, i) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -3481,7 +3871,8 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3481
3871
  className: `px-chat-rf__btn px-chat-rf__btn--${act.variant || "primary"}`,
3482
3872
  onClick: actionHandlers[act.action] || (() => {
3483
3873
  }),
3484
- children: act.label
3874
+ disabled: act.action === "submit" && isSubmitting,
3875
+ children: act.action === "submit" && isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__spinner" }) : act.label
3485
3876
  },
3486
3877
  i
3487
3878
  )) });
@@ -3514,8 +3905,15 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3514
3905
  )
3515
3906
  ] }),
3516
3907
  prevStep.fields.map((fname) => {
3517
- const fs = fieldStates[fname];
3518
3908
  const fld = fields.find((f) => f.name === fname);
3909
+ if (fld?.type === "cost_summary") {
3910
+ const costLabel = fieldStates[fname]?.label;
3911
+ return costLabel ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__completed-field", children: [
3912
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__completed-field-label", children: "Total:" }),
3913
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-chat-rf__completed-field-value", children: costLabel })
3914
+ ] }, fname) : null;
3915
+ }
3916
+ const fs = fieldStates[fname];
3519
3917
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__completed-field", children: [
3520
3918
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-chat-rf__completed-field-label", children: [
3521
3919
  fld?.label,
@@ -3528,6 +3926,20 @@ const ReactiveFormView = ({ title, trigger, steps, fields, submit, summary, form
3528
3926
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-chat-rf__active-step", children: [
3529
3927
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-chat-rf__step-title", children: step.title }),
3530
3928
  stepFields.map((field) => {
3929
+ if (field.type === "cost_summary") {
3930
+ return /* @__PURE__ */ jsxRuntime.jsx(
3931
+ CostSummaryField,
3932
+ {
3933
+ field,
3934
+ values,
3935
+ fieldStates,
3936
+ eventBus,
3937
+ onCancel: () => setFormStatus("closed"),
3938
+ onCostResolved: (name, label) => handleFieldChange(name, label, label)
3939
+ },
3940
+ field.name
3941
+ );
3942
+ }
3531
3943
  const deps = getDependencies(field);
3532
3944
  const isDisabled = !allDepsResolved(deps, values);
3533
3945
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3623,89 +4035,6 @@ function buildInitialValues(fields) {
3623
4035
  return values;
3624
4036
  }
3625
4037
 
3626
- function resolveVariable(template, formValues, fieldStates) {
3627
- if (!template.startsWith("$")) return template;
3628
- const withoutDollar = template.slice(1);
3629
- const dotIndex = withoutDollar.indexOf(".");
3630
- const fieldName = dotIndex >= 0 ? withoutDollar.slice(0, dotIndex) : withoutDollar;
3631
- const prop = dotIndex >= 0 ? withoutDollar.slice(dotIndex + 1) : null;
3632
- const value = formValues[fieldName];
3633
- const state = fieldStates[fieldName];
3634
- if (!prop) return value;
3635
- if (prop === "label" && state) {
3636
- return state.label || "";
3637
- }
3638
- if (prop === "labels" && state && Array.isArray(value)) {
3639
- return value.map((v) => {
3640
- const opt = state.options.find((o) => o.value === v);
3641
- return opt?.label || v;
3642
- }).join(", ");
3643
- }
3644
- if (state) {
3645
- const selectedOpt = state.options.find((o) => o.value === value);
3646
- if (selectedOpt && prop in selectedOpt) {
3647
- return selectedOpt[prop];
3648
- }
3649
- }
3650
- return "";
3651
- }
3652
- function resolveBody(obj, formValues, fieldStates) {
3653
- if (typeof obj === "string") return resolveVariable(obj, formValues, fieldStates);
3654
- if (Array.isArray(obj)) return obj.map((item) => resolveBody(item, formValues, fieldStates));
3655
- if (obj !== null && typeof obj === "object") {
3656
- const resolved = {};
3657
- for (const [key, value] of Object.entries(obj)) {
3658
- resolved[key] = resolveBody(value, formValues, fieldStates);
3659
- }
3660
- return resolved;
3661
- }
3662
- return obj;
3663
- }
3664
- function resolveParams(params, formValues, fieldStates) {
3665
- const resolved = {};
3666
- for (const [key, template] of Object.entries(params)) {
3667
- resolved[key] = resolveVariable(template, formValues, fieldStates);
3668
- }
3669
- return resolved;
3670
- }
3671
- function resolveSummaryRows(rows, formValues, fieldStates) {
3672
- return rows.map((row) => ({
3673
- label: row.label,
3674
- value: String(resolveVariable(row.value, formValues, fieldStates) ?? "")
3675
- }));
3676
- }
3677
-
3678
- const FETCH_TIMEOUT_MS = 12e4;
3679
- function fetchViaEventBus(eventBus, service, url, method, body) {
3680
- const requestId = globalThis.crypto?.randomUUID?.() ?? "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3681
- const r = Math.random() * 16 | 0;
3682
- return (c === "x" ? r : r & 3 | 8).toString(16);
3683
- });
3684
- return new Promise((resolve, reject) => {
3685
- const timeoutId = setTimeout(() => {
3686
- unsubscribe();
3687
- reject(new Error(`Timeout: no response for ${url} after ${FETCH_TIMEOUT_MS}ms`));
3688
- }, FETCH_TIMEOUT_MS);
3689
- const unsubscribe = eventBus.on(CHAT_EVENT.FETCH_RESPONSE, (response) => {
3690
- if (response.requestId !== requestId) return;
3691
- clearTimeout(timeoutId);
3692
- unsubscribe();
3693
- if (response.httpError) {
3694
- reject(new Error(response.httpError));
3695
- } else {
3696
- resolve(response.data);
3697
- }
3698
- });
3699
- eventBus.emit(CHAT_EVENT.FETCH_REQUEST, {
3700
- requestId,
3701
- service,
3702
- url,
3703
- method,
3704
- ...body ? { body } : {}
3705
- });
3706
- });
3707
- }
3708
-
3709
4038
  function formatTime$1(value, timezone) {
3710
4039
  try {
3711
4040
  const d = new Date(value);
@@ -3713,7 +4042,7 @@ function formatTime$1(value, timezone) {
3713
4042
  return d.toLocaleTimeString([], {
3714
4043
  hour: "2-digit",
3715
4044
  minute: "2-digit",
3716
- hour12: false,
4045
+ hour12: true,
3717
4046
  ...timezone ? { timeZone: timezone } : {}
3718
4047
  });
3719
4048
  } catch {
@@ -4075,7 +4404,10 @@ function useReactiveForm(props) {
4075
4404
  };
4076
4405
  const handleBack = () => setCurrentStep((s) => s - 1);
4077
4406
  const [submitMessage, setSubmitMessage] = React.useState("");
4407
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
4078
4408
  const handleSubmit = async () => {
4409
+ if (isSubmitting) return;
4410
+ setIsSubmitting(true);
4079
4411
  const resolvedPayload = resolveBody(submit.payload_map, values, fieldStates);
4080
4412
  if (eventBus) {
4081
4413
  try {
@@ -4098,7 +4430,8 @@ function useReactiveForm(props) {
4098
4430
  submitUrl: submit.url,
4099
4431
  result: data,
4100
4432
  formValues: values,
4101
- formLabels
4433
+ formLabels,
4434
+ onSuccess: submit.on_success
4102
4435
  });
4103
4436
  if (notificationType === "SUCCESS") {
4104
4437
  setSubmitMessage(message || "Completado");
@@ -4114,6 +4447,7 @@ function useReactiveForm(props) {
4114
4447
  const message = err instanceof Error ? err.message : "Error al enviar formulario";
4115
4448
  setSubmitMessage(message);
4116
4449
  setFormStatus("error");
4450
+ setIsSubmitting(false);
4117
4451
  }
4118
4452
  } else {
4119
4453
  console.log("[ReactiveForm] Submit:", values);
@@ -4142,6 +4476,7 @@ function useReactiveForm(props) {
4142
4476
  handleNext,
4143
4477
  handleBack,
4144
4478
  handleSubmit,
4479
+ isSubmitting,
4145
4480
  getDependencies,
4146
4481
  allDepsResolved,
4147
4482
  resolveSummaryRows: resolveRows
@@ -4173,7 +4508,8 @@ const ReactiveForm = ({ component, eventBus }) => {
4173
4508
  {
4174
4509
  ...props,
4175
4510
  form,
4176
- drawerPlacement: isMobile ? "bottom" : "right"
4511
+ drawerPlacement: isMobile ? "bottom" : "right",
4512
+ eventBus
4177
4513
  }
4178
4514
  );
4179
4515
  };
@@ -4839,7 +5175,15 @@ const COMPONENT_MAP = {
4839
5175
  };
4840
5176
  function renderComponent(props) {
4841
5177
  const { component } = props;
4842
- if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5178
+ if (component.status) {
5179
+ if (TERMINAL_STATUS.has(component.status)) {
5180
+ const policy = component.visibility_policy ?? "hide_on_terminal";
5181
+ if (policy === "hide_on_terminal") return null;
5182
+ if (component.selected_values && component.selected_values.length > 0) {
5183
+ return /* @__PURE__ */ jsxRuntime.jsx(AnsweredSummary, { component });
5184
+ }
5185
+ }
5186
+ } else if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
4843
5187
  return null;
4844
5188
  }
4845
5189
  const Component = COMPONENT_MAP[component.type];