myoperator-ui 0.0.225 → 0.0.226

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.
Files changed (2) hide show
  1. package/dist/index.js +946 -248
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14346,7 +14346,8 @@ export type { BrandIconProps } from "./icon";
14346
14346
  "badge",
14347
14347
  "button",
14348
14348
  "dialog",
14349
- "dropdown-menu"
14349
+ "dropdown-menu",
14350
+ "tooltip"
14350
14351
  ],
14351
14352
  isMultiFile: true,
14352
14353
  directory: "bots",
@@ -14533,6 +14534,12 @@ import {
14533
14534
  DialogTitle,
14534
14535
  } from "../dialog";
14535
14536
  import { Button } from "../button";
14537
+ import {
14538
+ Tooltip,
14539
+ TooltipContent,
14540
+ TooltipProvider,
14541
+ TooltipTrigger,
14542
+ } from "../tooltip";
14536
14543
  import { BOT_TYPE, type CreateBotModalProps, type BotType } from "./types";
14537
14544
 
14538
14545
  interface BotTypeOption {
@@ -14554,137 +14561,230 @@ const BOT_TYPE_OPTIONS: BotTypeOption[] = [
14554
14561
  },
14555
14562
  ];
14556
14563
 
14557
- export const CreateBotModal = React.forwardRef(({ open, onOpenChange, onSubmit, isLoading, className }: CreateBotModalProps, ref: React.Ref<HTMLDivElement>) => {
14558
- const [name, setName] = React.useState("");
14559
- const [selectedType, setSelectedType] = React.useState<BotType>("chatbot");
14564
+ function getFirstEnabledBotType(
14565
+ chatbotDisabled: boolean,
14566
+ voicebotDisabled: boolean
14567
+ ): BotType {
14568
+ if (!chatbotDisabled) return "chatbot";
14569
+ if (!voicebotDisabled) return "voicebot";
14570
+ return "chatbot";
14571
+ }
14560
14572
 
14561
- React.useEffect(() => {
14562
- if (!open) {
14563
- setName("");
14564
- setSelectedType("chatbot");
14565
- }
14566
- }, [open]);
14573
+ function isBotTypeDisabled(
14574
+ type: BotType,
14575
+ chatbotDisabled: boolean,
14576
+ voicebotDisabled: boolean
14577
+ ): boolean {
14578
+ return (
14579
+ (type === "chatbot" && chatbotDisabled) ||
14580
+ (type === "voicebot" && voicebotDisabled)
14581
+ );
14582
+ }
14567
14583
 
14568
- const handleSubmit = () => {
14569
- if (!name.trim()) return;
14570
- const typeValue = selectedType === "chatbot" ? BOT_TYPE.CHAT : BOT_TYPE.VOICE;
14571
- onSubmit?.({ name: name.trim(), type: typeValue });
14572
- };
14584
+ export const CreateBotModal = React.forwardRef(
14585
+ (
14586
+ {
14587
+ open,
14588
+ onOpenChange,
14589
+ onSubmit,
14590
+ isLoading,
14591
+ chatbotDisabled = false,
14592
+ voicebotDisabled = false,
14593
+ chatbotDisabledTooltip,
14594
+ voicebotDisabledTooltip,
14595
+ className,
14596
+ }: CreateBotModalProps,
14597
+ ref: React.Ref<HTMLDivElement>
14598
+ ) => {
14599
+ const [name, setName] = React.useState("");
14600
+ const [selectedType, setSelectedType] = React.useState<BotType>("chatbot");
14573
14601
 
14574
- const handleClose = () => {
14575
- onOpenChange(false);
14576
- };
14602
+ const chatD = Boolean(chatbotDisabled);
14603
+ const voiceD = Boolean(voicebotDisabled);
14577
14604
 
14578
- return (
14579
- <Dialog open={open} onOpenChange={onOpenChange}>
14580
- <DialogContent ref={ref} size="sm" className={cn("mx-3 max-h-[90vh] overflow-y-auto w-[calc(100%-1.5rem)] sm:mx-auto sm:w-full", className)}>
14581
- <DialogHeader>
14582
- <DialogTitle>Create AI bot</DialogTitle>
14583
- </DialogHeader>
14605
+ React.useEffect(() => {
14606
+ if (!open) {
14607
+ setName("");
14608
+ setSelectedType(getFirstEnabledBotType(chatD, voiceD));
14609
+ return;
14610
+ }
14611
+ setSelectedType((prev) => {
14612
+ if (!isBotTypeDisabled(prev, chatD, voiceD)) return prev;
14613
+ return getFirstEnabledBotType(chatD, voiceD);
14614
+ });
14615
+ }, [open, chatD, voiceD]);
14584
14616
 
14585
- <div className="flex flex-col gap-4 sm:gap-6">
14586
- {/* Name field */}
14587
- <div className="flex flex-col gap-1.5">
14588
- <label
14589
- htmlFor="bot-name"
14590
- className="flex items-center gap-0.5 text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]"
14591
- >
14592
- Name
14593
- <span className="text-xs text-semantic-error-primary">*</span>
14594
- </label>
14595
- <input
14596
- id="bot-name"
14597
- type="text"
14598
- value={name}
14599
- onChange={(e) => setName(e.target.value)}
14600
- placeholder="Enter bot name"
14601
- className={cn(
14602
- "w-full h-10 px-4 py-2.5 text-sm rounded border border-solid",
14603
- "border-semantic-border-input bg-semantic-bg-primary",
14604
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
14605
- "outline-none hover:border-semantic-border-input-focus",
14606
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
14607
- )}
14608
- />
14609
- </div>
14617
+ const selectedTypeBlocked = isBotTypeDisabled(selectedType, chatD, voiceD);
14610
14618
 
14611
- {/* Bot type selection */}
14612
- <div className="flex flex-col gap-2">
14613
- <span className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
14614
- Select Bot Type
14615
- </span>
14616
- <div className="flex flex-col gap-3 sm:flex-row sm:gap-3">
14617
- {BOT_TYPE_OPTIONS.map(({ id, label, description }) => {
14618
- const isSelected = selectedType === id;
14619
- return (
14620
- <button
14621
- key={id}
14622
- type="button"
14623
- onClick={() => setSelectedType(id)}
14624
- className={cn(
14625
- "flex flex-col items-start gap-2 sm:gap-2.5 p-3 rounded-lg border border-solid text-left flex-1 min-h-[100px] sm:h-[134px] justify-center min-w-0",
14626
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus",
14627
- isSelected
14628
- ? "bg-semantic-brand-surface border-semantic-brand shadow-sm"
14629
- : "bg-semantic-bg-primary border-semantic-border-layout hover:bg-semantic-bg-hover"
14630
- )}
14631
- aria-pressed={isSelected}
14632
- >
14633
- <div
14634
- className={cn(
14635
- "flex items-center justify-center size-[34px] rounded-lg",
14636
- isSelected
14637
- ? "bg-semantic-bg-primary"
14638
- : "bg-semantic-info-surface-subtle"
14639
- )}
14640
- >
14641
- {id === "chatbot" ? (
14642
- <MessageSquare className="size-5 text-semantic-text-secondary" />
14643
- ) : (
14644
- <Phone className="size-5 text-semantic-text-secondary" />
14645
- )}
14646
- </div>
14647
- <div className="flex flex-col gap-1">
14648
- <p className="m-0 text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
14649
- {label}
14650
- </p>
14651
- <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
14652
- {description}
14653
- </p>
14654
- </div>
14655
- </button>
14656
- );
14657
- })}
14619
+ const handleSubmit = () => {
14620
+ if (!name.trim() || selectedTypeBlocked) return;
14621
+ const typeValue =
14622
+ selectedType === "chatbot" ? BOT_TYPE.CHAT : BOT_TYPE.VOICE;
14623
+ onSubmit?.({ name: name.trim(), type: typeValue });
14624
+ };
14625
+
14626
+ const handleClose = () => {
14627
+ onOpenChange(false);
14628
+ };
14629
+
14630
+ return (
14631
+ <Dialog open={open} onOpenChange={onOpenChange}>
14632
+ <DialogContent
14633
+ ref={ref}
14634
+ size="sm"
14635
+ className={cn(
14636
+ "mx-3 max-h-[90vh] overflow-y-auto w-[calc(100%-1.5rem)] sm:mx-auto sm:w-full",
14637
+ className
14638
+ )}
14639
+ >
14640
+ <DialogHeader>
14641
+ <DialogTitle>Create AI bot</DialogTitle>
14642
+ </DialogHeader>
14643
+
14644
+ <div className="flex flex-col gap-4 sm:gap-6">
14645
+ {/* Name field */}
14646
+ <div className="flex flex-col gap-1.5">
14647
+ <label
14648
+ htmlFor="bot-name"
14649
+ className="flex items-center gap-0.5 text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]"
14650
+ >
14651
+ Name
14652
+ <span className="text-xs text-semantic-error-primary">*</span>
14653
+ </label>
14654
+ <input
14655
+ id="bot-name"
14656
+ type="text"
14657
+ value={name}
14658
+ onChange={(e) => setName(e.target.value)}
14659
+ placeholder="Enter bot name"
14660
+ className={cn(
14661
+ "w-full h-10 px-4 py-2.5 text-sm rounded border border-solid",
14662
+ "border-semantic-border-input bg-semantic-bg-primary",
14663
+ "text-semantic-text-primary placeholder:text-semantic-text-muted",
14664
+ "outline-none hover:border-semantic-border-input-focus",
14665
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
14666
+ )}
14667
+ />
14658
14668
  </div>
14659
14669
 
14660
- {/* Helper text */}
14661
- <div className="flex items-center gap-1.5 px-3 py-2.5 rounded bg-semantic-bg-ui">
14662
- <Info className="size-4 text-semantic-text-secondary shrink-0" />
14663
- <p className="m-0 text-xs text-semantic-text-secondary">
14664
- This setting cannot be changed once selected.
14665
- </p>
14670
+ {/* Bot type selection */}
14671
+ <div className="flex flex-col gap-2">
14672
+ <span className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
14673
+ Select Bot Type
14674
+ </span>
14675
+ <TooltipProvider delayDuration={200}>
14676
+ <div className="flex flex-col gap-3 sm:flex-row sm:gap-3">
14677
+ {BOT_TYPE_OPTIONS.map(({ id, label, description }) => {
14678
+ const optionDisabled = isBotTypeDisabled(id, chatD, voiceD);
14679
+ const isSelected = selectedType === id && !optionDisabled;
14680
+ const disabledTooltip =
14681
+ id === "chatbot"
14682
+ ? chatbotDisabledTooltip
14683
+ : voicebotDisabledTooltip;
14684
+ const showTooltip =
14685
+ optionDisabled &&
14686
+ disabledTooltip != null &&
14687
+ disabledTooltip.trim() !== "";
14688
+
14689
+ const baseButtonClass = cn(
14690
+ "flex flex-col items-start gap-2 sm:gap-2.5 p-3 rounded-lg border border-solid text-left flex-1 min-h-[100px] sm:h-[134px] justify-center min-w-0 w-full",
14691
+ "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus",
14692
+ optionDisabled
14693
+ ? "cursor-not-allowed opacity-50 pointer-events-none bg-semantic-bg-primary border-semantic-border-layout"
14694
+ : isSelected
14695
+ ? "bg-semantic-brand-surface border-semantic-brand shadow-sm"
14696
+ : "bg-semantic-bg-primary border-semantic-border-layout hover:bg-semantic-bg-hover"
14697
+ );
14698
+
14699
+ const button = (
14700
+ <button
14701
+ type="button"
14702
+ disabled={optionDisabled}
14703
+ onClick={() => {
14704
+ if (!optionDisabled) setSelectedType(id);
14705
+ }}
14706
+ className={baseButtonClass}
14707
+ aria-pressed={isSelected}
14708
+ aria-disabled={optionDisabled}
14709
+ >
14710
+ <div
14711
+ className={cn(
14712
+ "flex items-center justify-center size-[34px] rounded-lg",
14713
+ isSelected
14714
+ ? "bg-semantic-bg-primary"
14715
+ : "bg-semantic-info-surface-subtle"
14716
+ )}
14717
+ >
14718
+ {id === "chatbot" ? (
14719
+ <MessageSquare className="size-5 text-semantic-text-secondary" />
14720
+ ) : (
14721
+ <Phone className="size-5 text-semantic-text-secondary" />
14722
+ )}
14723
+ </div>
14724
+ <div className="flex flex-col gap-1">
14725
+ <p className="m-0 text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
14726
+ {label}
14727
+ </p>
14728
+ <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
14729
+ {description}
14730
+ </p>
14731
+ </div>
14732
+ </button>
14733
+ );
14734
+
14735
+ if (showTooltip) {
14736
+ return (
14737
+ <Tooltip key={id}>
14738
+ <TooltipTrigger asChild>
14739
+ <span className="flex flex-1 min-w-0 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus">
14740
+ {button}
14741
+ </span>
14742
+ </TooltipTrigger>
14743
+ <TooltipContent side="top">
14744
+ <p className="m-0">{disabledTooltip}</p>
14745
+ </TooltipContent>
14746
+ </Tooltip>
14747
+ );
14748
+ }
14749
+
14750
+ return (
14751
+ <React.Fragment key={id}>
14752
+ {button}
14753
+ </React.Fragment>
14754
+ );
14755
+ })}
14756
+ </div>
14757
+ </TooltipProvider>
14758
+
14759
+ {/* Helper text */}
14760
+ <div className="flex items-center gap-1.5 px-3 py-2.5 rounded bg-semantic-bg-ui">
14761
+ <Info className="size-4 text-semantic-text-secondary shrink-0" />
14762
+ <p className="m-0 text-xs text-semantic-text-secondary">
14763
+ This setting cannot be changed once selected.
14764
+ </p>
14765
+ </div>
14666
14766
  </div>
14667
14767
  </div>
14668
- </div>
14669
14768
 
14670
- {/* Footer actions */}
14671
- <div className="flex flex-col-reverse gap-3 sm:flex-row sm:gap-4 justify-end mt-2">
14672
- <Button variant="outline" onClick={handleClose}>
14673
- Cancel
14674
- </Button>
14675
- <Button
14676
- variant="default"
14677
- onClick={handleSubmit}
14678
- disabled={!name.trim() || isLoading}
14679
- loading={isLoading}
14680
- >
14681
- Create
14682
- </Button>
14683
- </div>
14684
- </DialogContent>
14685
- </Dialog>
14686
- );
14687
- });
14769
+ {/* Footer actions */}
14770
+ <div className="flex flex-col-reverse gap-3 sm:flex-row sm:gap-4 justify-end mt-2">
14771
+ <Button variant="outline" onClick={handleClose}>
14772
+ Cancel
14773
+ </Button>
14774
+ <Button
14775
+ variant="default"
14776
+ onClick={handleSubmit}
14777
+ disabled={!name.trim() || isLoading || selectedTypeBlocked}
14778
+ loading={isLoading}
14779
+ >
14780
+ Create
14781
+ </Button>
14782
+ </div>
14783
+ </DialogContent>
14784
+ </Dialog>
14785
+ );
14786
+ }
14787
+ );
14688
14788
 
14689
14789
  CreateBotModal.displayName = "CreateBotModal";
14690
14790
  `, prefix)
@@ -14707,6 +14807,10 @@ export const CreateBotFlow = React.forwardRef(
14707
14807
  {
14708
14808
  createCardLabel = "Create new bot",
14709
14809
  onSubmit,
14810
+ chatbotDisabled,
14811
+ voicebotDisabled,
14812
+ chatbotDisabledTooltip,
14813
+ voicebotDisabledTooltip,
14710
14814
  className,
14711
14815
  ...props
14712
14816
  }: CreateBotFlowProps,
@@ -14734,6 +14838,10 @@ export const CreateBotFlow = React.forwardRef(
14734
14838
  <CreateBotModal
14735
14839
  open={modalOpen}
14736
14840
  onOpenChange={setModalOpen}
14841
+ chatbotDisabled={chatbotDisabled}
14842
+ voicebotDisabled={voicebotDisabled}
14843
+ chatbotDisabledTooltip={chatbotDisabledTooltip}
14844
+ voicebotDisabledTooltip={voicebotDisabledTooltip}
14737
14845
  onSubmit={(data) => {
14738
14846
  onSubmit?.(data);
14739
14847
  setModalOpen(false);
@@ -14764,6 +14872,10 @@ export function EditBotFlow({
14764
14872
  searchPlaceholder = "Search bot...",
14765
14873
  createCardLabel = "Create new bot",
14766
14874
  typeLabels,
14875
+ chatbotDisabled,
14876
+ voicebotDisabled,
14877
+ chatbotDisabledTooltip,
14878
+ voicebotDisabledTooltip,
14767
14879
  onBotDelete,
14768
14880
  onCreateBotSubmit,
14769
14881
  onSearch,
@@ -14803,6 +14915,10 @@ export function EditBotFlow({
14803
14915
  searchPlaceholder={searchPlaceholder}
14804
14916
  createCardLabel={createCardLabel}
14805
14917
  typeLabels={typeLabels}
14918
+ chatbotDisabled={chatbotDisabled}
14919
+ voicebotDisabled={voicebotDisabled}
14920
+ chatbotDisabledTooltip={chatbotDisabledTooltip}
14921
+ voicebotDisabledTooltip={voicebotDisabledTooltip}
14806
14922
  onBotEdit={handleEdit}
14807
14923
  onBotDelete={onBotDelete}
14808
14924
  onCreateBotSubmit={onCreateBotSubmit}
@@ -14840,6 +14956,10 @@ export const BotList = React.forwardRef(
14840
14956
  subtitle = "Create & manage AI bots",
14841
14957
  searchPlaceholder = "Search bot...",
14842
14958
  createCardLabel = "Create new bot",
14959
+ chatbotDisabled,
14960
+ voicebotDisabled,
14961
+ chatbotDisabledTooltip,
14962
+ voicebotDisabledTooltip,
14843
14963
  className,
14844
14964
  ...props
14845
14965
  }: BotListProps,
@@ -14879,6 +14999,10 @@ export const BotList = React.forwardRef(
14879
14999
  <CreateBotModal
14880
15000
  open={createModalOpen}
14881
15001
  onOpenChange={setCreateModalOpen}
15002
+ chatbotDisabled={chatbotDisabled}
15003
+ voicebotDisabled={voicebotDisabled}
15004
+ chatbotDisabledTooltip={chatbotDisabledTooltip}
15005
+ voicebotDisabledTooltip={voicebotDisabledTooltip}
14882
15006
  onSubmit={(data) => {
14883
15007
  onCreateBotSubmit?.(data);
14884
15008
  setCreateModalOpen(false);
@@ -14919,6 +15043,10 @@ export const BotList = React.forwardRef(
14919
15043
  <CreateBotModal
14920
15044
  open={createModalOpen}
14921
15045
  onOpenChange={setCreateModalOpen}
15046
+ chatbotDisabled={chatbotDisabled}
15047
+ voicebotDisabled={voicebotDisabled}
15048
+ chatbotDisabledTooltip={chatbotDisabledTooltip}
15049
+ voicebotDisabledTooltip={voicebotDisabledTooltip}
14922
15050
  onSubmit={(data) => {
14923
15051
  onCreateBotSubmit?.(data);
14924
15052
  setCreateModalOpen(false);
@@ -15189,6 +15317,18 @@ export interface CreateBotModalProps {
15189
15317
  onSubmit?: (data: { name: string; type: BOT_TYPE }) => void;
15190
15318
  /** Shows loading spinner on Create button and disables it (e.g. while API call is in flight) */
15191
15319
  isLoading?: boolean;
15320
+ /** When true, Chat bot type cannot be selected */
15321
+ chatbotDisabled?: boolean;
15322
+ /** When true, Voice bot type cannot be selected */
15323
+ voicebotDisabled?: boolean;
15324
+ /**
15325
+ * Shown on hover/focus when Chat bot is disabled. Tooltip is not rendered when omitted or empty.
15326
+ */
15327
+ chatbotDisabledTooltip?: string;
15328
+ /**
15329
+ * Shown on hover/focus when Voice bot is disabled. Tooltip is not rendered when omitted or empty.
15330
+ */
15331
+ voicebotDisabledTooltip?: string;
15192
15332
  className?: string;
15193
15333
  }
15194
15334
 
@@ -15227,9 +15367,19 @@ export interface BotListGridProps
15227
15367
  children: React.ReactNode;
15228
15368
  }
15229
15369
 
15370
+ /** Props forwarded to CreateBotModal for bot-type gating (optional). */
15371
+ export type CreateBotModalTypeOptionsProps = Pick<
15372
+ CreateBotModalProps,
15373
+ | "chatbotDisabled"
15374
+ | "voicebotDisabled"
15375
+ | "chatbotDisabledTooltip"
15376
+ | "voicebotDisabledTooltip"
15377
+ >;
15378
+
15230
15379
  /** Props for CreateBotFlow: create card + Create Bot modal (no header). */
15231
15380
  export interface CreateBotFlowProps
15232
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onSubmit"> {
15381
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onSubmit">,
15382
+ CreateBotModalTypeOptionsProps {
15233
15383
  /** Create new bot card label */
15234
15384
  createCardLabel?: string;
15235
15385
  /** Called when Create Bot modal is submitted with { name, type } */
@@ -15237,7 +15387,7 @@ export interface CreateBotFlowProps
15237
15387
  }
15238
15388
 
15239
15389
  /** Props for EditBotFlow: bot list + config view when Edit is clicked. */
15240
- export interface EditBotFlowProps {
15390
+ export interface EditBotFlowProps extends CreateBotModalTypeOptionsProps {
15241
15391
  /** Bots to show in the list (e.g. first 2 for demo) */
15242
15392
  bots: Bot[];
15243
15393
  /** Page title */
@@ -15265,7 +15415,8 @@ export interface EditBotFlowProps {
15265
15415
  }
15266
15416
 
15267
15417
  export interface BotListProps
15268
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "title" | "children"> {
15418
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "title" | "children">,
15419
+ CreateBotModalTypeOptionsProps {
15269
15420
  /** List of bots to display */
15270
15421
  bots?: Bot[];
15271
15422
  /** Override type badge labels for all cards (e.g. { chatbot: "Chat", voicebot: "Voice" }). Per-bot bot.typeLabel still wins. */
@@ -15299,7 +15450,12 @@ export type { BotCardProps } from "./types";
15299
15450
  export { CreateBotModal } from "./create-bot-modal";
15300
15451
  export { CreateBotFlow } from "./create-bot-flow";
15301
15452
  export { EditBotFlow } from "./edit-bot-flow";
15302
- export type { CreateBotModalProps, CreateBotFlowProps, EditBotFlowProps } from "./types";
15453
+ export type {
15454
+ CreateBotModalProps,
15455
+ CreateBotModalTypeOptionsProps,
15456
+ CreateBotFlowProps,
15457
+ EditBotFlowProps,
15458
+ } from "./types";
15303
15459
 
15304
15460
  export { BotList } from "./bot-list";
15305
15461
  export { BotListHeader } from "./bot-list-header";
@@ -15570,14 +15726,14 @@ const FileUploadModal = React.forwardRef(
15570
15726
  size="default"
15571
15727
  hideCloseButton
15572
15728
  className={cn(
15573
- "max-w-[min(660px,calc(100vw-2rem))] rounded-xl p-4 gap-0 sm:p-6",
15729
+ "max-w-[min(660px,calc(100vw-2rem))] min-w-0 rounded-xl p-4 gap-0 sm:p-6 overflow-x-hidden",
15574
15730
  className
15575
15731
  )}
15576
15732
  {...props}
15577
15733
  >
15578
15734
  {/* Header */}
15579
- <div className="flex items-center justify-between mb-6">
15580
- <DialogTitle className="m-0 text-base font-semibold text-semantic-text-primary">
15735
+ <div className="flex items-center justify-between gap-3 mb-6 min-w-0">
15736
+ <DialogTitle className="m-0 text-base font-semibold text-semantic-text-primary truncate min-w-0 pr-2">
15581
15737
  {title}
15582
15738
  </DialogTitle>
15583
15739
  <DialogDescription className="sr-only">
@@ -15586,20 +15742,20 @@ const FileUploadModal = React.forwardRef(
15586
15742
  <button
15587
15743
  type="button"
15588
15744
  onClick={handleClose}
15589
- className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-semantic-text-primary"
15745
+ className="shrink-0 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-semantic-text-primary"
15590
15746
  aria-label="Close dialog"
15591
15747
  >
15592
15748
  <X className="h-4 w-4" />
15593
15749
  </button>
15594
15750
  </div>
15595
15751
 
15596
- {/* Body */}
15597
- <div className="flex flex-col gap-4 items-end w-full">
15752
+ {/* Body \u2014 stretch so children respect modal width; long filenames need min-w-0 chain */}
15753
+ <div className="flex flex-col gap-4 items-stretch w-full min-w-0 max-w-full">
15598
15754
  {shouldShowSampleDownload && (
15599
15755
  <button
15600
15756
  type="button"
15601
15757
  onClick={onSampleDownload}
15602
- className="flex items-center gap-1.5 text-sm font-semibold text-semantic-text-link hover:opacity-80 transition-opacity"
15758
+ className="self-end flex items-center gap-1.5 text-sm font-semibold text-semantic-text-link hover:opacity-80 transition-opacity"
15603
15759
  >
15604
15760
  <Download className="size-3.5" />
15605
15761
  {sampleDownloadLabel}
@@ -15608,14 +15764,14 @@ const FileUploadModal = React.forwardRef(
15608
15764
 
15609
15765
  {/* Drop zone */}
15610
15766
  <div
15611
- className="w-full border border-dashed border-semantic-border-layout bg-semantic-bg-ui rounded p-4"
15767
+ className="w-full min-w-0 max-w-full border border-dashed border-semantic-border-layout bg-semantic-bg-ui rounded p-4"
15612
15768
  onDrop={(e) => {
15613
15769
  e.preventDefault();
15614
15770
  addFiles(e.dataTransfer.files);
15615
15771
  }}
15616
15772
  onDragOver={(e) => e.preventDefault()}
15617
15773
  >
15618
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
15774
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4 min-w-0">
15619
15775
  <button
15620
15776
  type="button"
15621
15777
  onClick={() => fileInputRef.current?.click()}
@@ -15623,11 +15779,11 @@ const FileUploadModal = React.forwardRef(
15623
15779
  >
15624
15780
  {uploadButtonLabel}
15625
15781
  </button>
15626
- <div className="flex flex-col gap-1">
15627
- <p className="m-0 text-sm text-semantic-text-secondary tracking-[0.035px]">
15782
+ <div className="flex flex-col gap-1 min-w-0 flex-1">
15783
+ <p className="m-0 text-sm text-semantic-text-secondary tracking-[0.035px] break-words">
15628
15784
  {dropDescription}
15629
15785
  </p>
15630
- <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
15786
+ <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px] break-words">
15631
15787
  {formatDescription}
15632
15788
  </p>
15633
15789
  </div>
@@ -15647,15 +15803,18 @@ const FileUploadModal = React.forwardRef(
15647
15803
 
15648
15804
  {/* Upload item list */}
15649
15805
  {items.length > 0 && (
15650
- <div className="flex flex-col gap-2.5 w-full">
15806
+ <div className="flex flex-col gap-2.5 w-full min-w-0 max-w-full">
15651
15807
  {items.map((item) => (
15652
15808
  <div
15653
15809
  key={item.id}
15654
- className="bg-semantic-bg-primary border border-solid border-semantic-border-layout rounded px-4 py-3 flex flex-col gap-2"
15810
+ className="bg-semantic-bg-primary border border-solid border-semantic-border-layout rounded px-4 py-3 flex flex-col gap-2 min-w-0 max-w-full overflow-hidden"
15655
15811
  >
15656
- <div className="flex items-start gap-3">
15657
- <div className="flex flex-col gap-0.5 flex-1 min-w-0">
15658
- <p className="m-0 text-sm text-semantic-text-primary tracking-[0.035px] truncate">
15812
+ <div className="flex items-start gap-3 min-w-0">
15813
+ <div className="flex flex-col gap-0.5 flex-1 min-w-0 overflow-hidden">
15814
+ <p
15815
+ className="m-0 text-sm text-semantic-text-primary tracking-[0.035px] truncate max-w-full"
15816
+ title={item.file.name}
15817
+ >
15659
15818
  {item.status === "uploading"
15660
15819
  ? "Uploading..."
15661
15820
  : item.file.name}
@@ -17860,11 +18019,18 @@ export const IvrBotConfig = React.forwardRef(
17860
18019
  voiceOptions,
17861
18020
  languageOptions,
17862
18021
  sessionVariables,
18022
+ functionVariableGroups,
18023
+ onAddFunctionVariable,
18024
+ onEditFunctionVariable,
17863
18025
  escalationDepartmentOptions,
18026
+ advancedSettingsNumericBounds,
17864
18027
  silenceTimeoutMin,
17865
18028
  silenceTimeoutMax,
17866
18029
  callEndThresholdMin,
17867
18030
  callEndThresholdMax,
18031
+ onAdvancedSettingsChange,
18032
+ onSilenceTimeoutBlur,
18033
+ onCallEndThresholdBlur,
17868
18034
  className,
17869
18035
  }: IvrBotConfigProps,
17870
18036
  ref: React.Ref<HTMLDivElement>
@@ -18017,10 +18183,14 @@ export const IvrBotConfig = React.forwardRef(
18017
18183
  <AdvancedSettingsCard
18018
18184
  data={data}
18019
18185
  onChange={update}
18186
+ numericBounds={advancedSettingsNumericBounds}
18020
18187
  silenceTimeoutMin={silenceTimeoutMin}
18021
18188
  silenceTimeoutMax={silenceTimeoutMax}
18022
18189
  callEndThresholdMin={callEndThresholdMin}
18023
18190
  callEndThresholdMax={callEndThresholdMax}
18191
+ onAdvancedSettingsChange={onAdvancedSettingsChange}
18192
+ onSilenceTimeoutBlur={onSilenceTimeoutBlur}
18193
+ onCallEndThresholdBlur={onCallEndThresholdBlur}
18024
18194
  disabled={disabled}
18025
18195
  />
18026
18196
  </div>
@@ -18035,6 +18205,9 @@ export const IvrBotConfig = React.forwardRef(
18035
18205
  promptMinLength={functionPromptMinLength}
18036
18206
  promptMaxLength={functionPromptMaxLength}
18037
18207
  sessionVariables={sessionVariables}
18208
+ variableGroups={functionVariableGroups}
18209
+ onAddVariable={onAddFunctionVariable}
18210
+ onEditVariable={onEditFunctionVariable}
18038
18211
  />
18039
18212
 
18040
18213
  {/* Edit Function Modal */}
@@ -18048,6 +18221,9 @@ export const IvrBotConfig = React.forwardRef(
18048
18221
  promptMinLength={functionPromptMinLength}
18049
18222
  promptMaxLength={functionPromptMaxLength}
18050
18223
  sessionVariables={sessionVariables}
18224
+ variableGroups={functionVariableGroups}
18225
+ onAddVariable={onAddFunctionVariable}
18226
+ onEditVariable={onEditFunctionVariable}
18051
18227
  disabled={disabled}
18052
18228
  />
18053
18229
 
@@ -18165,7 +18341,7 @@ export function headerRowsHaveSubmitErrors(rows: KeyValuePair[]): boolean {
18165
18341
  {
18166
18342
  name: "create-function-modal.tsx",
18167
18343
  content: prefixTailwindClasses(`import * as React from "react";
18168
- import { Trash2, ChevronDown, X, Plus, Pencil } from "lucide-react";
18344
+ import { Trash2, ChevronDown, X, Plus, Pencil, CircleAlert } from "lucide-react";
18169
18345
  import { cn } from "../../../lib/utils";
18170
18346
  import {
18171
18347
  Dialog,
@@ -18260,6 +18436,105 @@ function extractVarRefs(texts: string[]): string[] {
18260
18436
  return Array.from(new Set(all));
18261
18437
  }
18262
18438
 
18439
+ /** True if a \`{{\u2026}}\` token in the form matches this variable item (handles \`{{name}}\` vs \`{{function.name}}\` and legacy \`item.name\` with \`function.\` prefix). */
18440
+ function placeholderMatchesVariableItem(placeholder: string, item: VariableItem): boolean {
18441
+ if (item.value && placeholder === item.value) return true;
18442
+ const asDisplayed = \`{{\${item.name}}}\`;
18443
+ const asFunction = \`{{function.\${item.name}}}\`;
18444
+ if (placeholder === asDisplayed || placeholder === asFunction) return true;
18445
+
18446
+ const m = /^\\{\\{([^}]+)\\}\\}$/.exec(placeholder);
18447
+ if (!m) return false;
18448
+ const inner = m[1].trim();
18449
+ if (inner === item.name) return true;
18450
+
18451
+ const bareName = item.name.startsWith("function.") ? item.name.slice("function.".length) : item.name;
18452
+ return inner === bareName || inner === \`function.\${bareName}\`;
18453
+ }
18454
+
18455
+ /** Aliases for the inner text of \`{{\u2026}}\` (e.g. \`function.foo\` \u2194 \`foo\`). */
18456
+ function placeholderInnerAliases(inner: string): string[] {
18457
+ const trimmed = inner.trim();
18458
+ if (!trimmed) return [];
18459
+ const out = new Set<string>([trimmed]);
18460
+ const bare = trimmed.startsWith("function.") ? trimmed.slice("function.".length) : trimmed;
18461
+ out.add(bare);
18462
+ if (!trimmed.startsWith("function.")) {
18463
+ out.add(\`function.\${bare}\`);
18464
+ }
18465
+ return Array.from(out);
18466
+ }
18467
+
18468
+ /** Keys used to store Test API "required" for a function variable name from the form (bare id, no \`{{}}\`). */
18469
+ function placeholderInnerAliasesForBareName(bareName: string): string[] {
18470
+ const trimmed = bareName.trim();
18471
+ if (!trimmed) return [];
18472
+ return placeholderInnerAliases(trimmed);
18473
+ }
18474
+
18475
+ function buildFnVarRequiredMapFromGroups(groups?: VariableGroup[]): Record<string, boolean> {
18476
+ const seeded: Record<string, boolean> = {};
18477
+ for (const g of groups ?? []) {
18478
+ for (const item of g.items) {
18479
+ if (!item.required) continue;
18480
+ const n = item.name.trim();
18481
+ const bare = n.startsWith("function.") ? n.slice("function.".length) : n;
18482
+ for (const key of placeholderInnerAliasesForBareName(bare)) {
18483
+ seeded[key] = true;
18484
+ }
18485
+ }
18486
+ }
18487
+ return seeded;
18488
+ }
18489
+
18490
+ /**
18491
+ * Whether a \`{{\u2026}}\` placeholder is required for Test API.
18492
+ * \`localFnVarRequired\` merges Required from \`variableGroups\` (on open) plus Create/Edit variable saves
18493
+ * so validation works when the parent omits \`variableGroups\` or has not updated it yet after \`onAddVariable\`.
18494
+ */
18495
+ function isPlaceholderRequiredInTest(
18496
+ placeholder: string,
18497
+ variableGroups?: VariableGroup[],
18498
+ localFnVarRequired?: Record<string, boolean>
18499
+ ): boolean {
18500
+ if (localFnVarRequired && Object.keys(localFnVarRequired).length > 0) {
18501
+ const m = /^\\{\\{([^}]+)\\}\\}$/.exec(placeholder.trim());
18502
+ if (m) {
18503
+ for (const alias of placeholderInnerAliases(m[1])) {
18504
+ if (Object.prototype.hasOwnProperty.call(localFnVarRequired, alias)) {
18505
+ return Boolean(localFnVarRequired[alias]);
18506
+ }
18507
+ }
18508
+ }
18509
+ }
18510
+
18511
+ if (!variableGroups?.length) return false;
18512
+ for (const g of variableGroups) {
18513
+ for (const item of g.items) {
18514
+ if (placeholderMatchesVariableItem(placeholder, item)) {
18515
+ return Boolean(item.required);
18516
+ }
18517
+ }
18518
+ }
18519
+ return false;
18520
+ }
18521
+
18522
+ /**
18523
+ * Rewrites \`{{function.oldRaw}}\` and \`{{oldRaw}}\` to the new name everywhere in a string.
18524
+ * Used when saving "Edit variable" so URL, body, headers, and query params stay in sync.
18525
+ */
18526
+ function renameVariableRefsInString(
18527
+ text: string,
18528
+ oldRaw: string,
18529
+ newRaw: string
18530
+ ): string {
18531
+ const prev = oldRaw.trim();
18532
+ const next = newRaw.trim();
18533
+ if (!prev || prev === next) return text;
18534
+ const withFunction = text.split(\`{{function.\${prev}}}\`).join(\`{{function.\${next}}}\`);
18535
+ return withFunction.split(\`{{\${prev}}}\`).join(\`{{\${next}}}\`);
18536
+ }
18537
+
18263
18538
  // \u2500\u2500 Value segment parser \u2014 splits "text {{var}} text" into typed segments \u2500\u2500\u2500\u2500\u2500
18264
18539
 
18265
18540
  type ValueSegment =
@@ -18515,21 +18790,31 @@ function VariableFormModal({
18515
18790
  };
18516
18791
 
18517
18792
  const handleSave = () => {
18793
+ if (!name.trim()) {
18794
+ setNameError(
18795
+ required
18796
+ ? "Value is required for this key"
18797
+ : "Variable name is required"
18798
+ );
18799
+ return;
18800
+ }
18518
18801
  const error = validateName(name);
18519
- if (error || !name.trim()) {
18520
- setNameError(error || "Variable name is required");
18802
+ if (error) {
18803
+ setNameError(error);
18521
18804
  return;
18522
18805
  }
18523
18806
  onSave({ name: name.trim(), description: description.trim() || undefined, required });
18524
18807
  };
18525
18808
 
18809
+ const hasInvalidFormat = Boolean(name.trim() && validateName(name));
18810
+
18526
18811
  return (
18527
18812
  <FormModal
18528
18813
  open={open}
18529
18814
  onOpenChange={onOpenChange}
18530
18815
  title={mode === "create" ? "Create new variable" : "Edit variable"}
18531
18816
  saveButtonText={mode === "create" ? "Save" : "Save Changes"}
18532
- disableSave={!name.trim() || !!nameError}
18817
+ disableSave={hasInvalidFormat}
18533
18818
  onSave={handleSave}
18534
18819
  size="default"
18535
18820
  >
@@ -18546,15 +18831,28 @@ function VariableFormModal({
18546
18831
  onChange={handleNameChange}
18547
18832
  placeholder="e.g., customer_name"
18548
18833
  maxLength={VARIABLE_NAME_MAX}
18549
- className={cn(inputCls, "pr-16")}
18834
+ aria-invalid={Boolean(nameError)}
18835
+ className={cn(
18836
+ inputCls,
18837
+ "pr-16",
18838
+ nameError && "border-semantic-error-primary"
18839
+ )}
18550
18840
  />
18551
18841
  <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-semantic-text-muted pointer-events-none">
18552
18842
  {name.length}/{VARIABLE_NAME_MAX}
18553
18843
  </span>
18554
18844
  </div>
18555
- <span className={cn("text-sm", nameError ? "text-semantic-error-primary" : "text-semantic-text-muted")}>
18556
- {nameError || "Variable name should start with alphabet; Cannot have special characters except underscore (_)"}
18557
- </span>
18845
+ {nameError ? (
18846
+ <p className="m-0 flex items-start gap-1.5 text-sm text-semantic-error-primary">
18847
+ <CircleAlert className="size-4 shrink-0 mt-0.5" aria-hidden />
18848
+ <span>{nameError}</span>
18849
+ </p>
18850
+ ) : (
18851
+ <span className="text-sm text-semantic-text-muted">
18852
+ Variable name should start with alphabet; Cannot have special characters except
18853
+ underscore (_)
18854
+ </span>
18855
+ )}
18558
18856
  </div>
18559
18857
  <TextField
18560
18858
  label="Description (optional)"
@@ -18739,6 +19037,7 @@ function VariableInput({
18739
19037
  {onEditVariable && (
18740
19038
  <button
18741
19039
  type="button"
19040
+ aria-label={\`Edit variable \${seg.name}\`}
18742
19041
  onMouseDown={(e) => {
18743
19042
  e.preventDefault();
18744
19043
  e.stopPropagation();
@@ -19045,6 +19344,14 @@ export const CreateFunctionModal = React.forwardRef(
19045
19344
  /** Field + \`{{\u2026\` range to replace with \`{{name}}\` after create saves */
19046
19345
  const [varInsertContext, setVarInsertContext] = React.useState<VarInsertContext | null>(null);
19047
19346
 
19347
+ /**
19348
+ * Required flags for function variables for Test API: seeded from \`variableGroups\` on open, then
19349
+ * updated when the user saves Create/Edit variable (covers missing/stale parent props).
19350
+ */
19351
+ const [localFnVarRequiredByBareName, setLocalFnVarRequiredByBareName] = React.useState<
19352
+ Record<string, boolean>
19353
+ >({});
19354
+
19048
19355
  const openVariableCreateModal = React.useCallback(() => {
19049
19356
  setVarModalMode("create");
19050
19357
  setVarModalInitialData(undefined);
@@ -19113,10 +19420,59 @@ export const CreateFunctionModal = React.forwardRef(
19113
19420
  setVarInsertContext(null);
19114
19421
  }
19115
19422
 
19423
+ const requiredFlag = Boolean(data.required);
19424
+
19425
+ const applyRequiredToLocalMap = (bareName: string, required: boolean) => {
19426
+ setLocalFnVarRequiredByBareName((prev) => {
19427
+ const next = { ...prev };
19428
+ for (const key of placeholderInnerAliasesForBareName(bareName)) {
19429
+ next[key] = required;
19430
+ }
19431
+ return next;
19432
+ });
19433
+ };
19434
+
19116
19435
  if (varModalMode === "create") {
19117
19436
  onAddVariable?.(data);
19437
+ applyRequiredToLocalMap(trimmedName, requiredFlag);
19118
19438
  } else {
19119
- onEditVariable?.(varModalInitialData?.name ?? "", data);
19439
+ const prevRaw = (varModalInitialData?.name ?? "").trim();
19440
+ if (prevRaw && prevRaw !== trimmedName) {
19441
+ setUrl((u) => renameVariableRefsInString(u, prevRaw, trimmedName));
19442
+ setBody((b) => renameVariableRefsInString(b, prevRaw, trimmedName));
19443
+ setHeaders((rows) =>
19444
+ rows.map((r) => ({
19445
+ ...r,
19446
+ value: renameVariableRefsInString(r.value, prevRaw, trimmedName),
19447
+ }))
19448
+ );
19449
+ setQueryParams((rows) =>
19450
+ rows.map((r) => ({
19451
+ ...r,
19452
+ value: renameVariableRefsInString(r.value, prevRaw, trimmedName),
19453
+ }))
19454
+ );
19455
+ setTestVarValues((prev) => {
19456
+ const next: Record<string, string> = {};
19457
+ for (const [k, v] of Object.entries(prev)) {
19458
+ next[renameVariableRefsInString(k, prevRaw, trimmedName)] = v;
19459
+ }
19460
+ return next;
19461
+ });
19462
+ setLocalFnVarRequiredByBareName((prev) => {
19463
+ const next = { ...prev };
19464
+ for (const key of placeholderInnerAliasesForBareName(prevRaw)) {
19465
+ delete next[key];
19466
+ }
19467
+ for (const key of placeholderInnerAliasesForBareName(trimmedName)) {
19468
+ next[key] = requiredFlag;
19469
+ }
19470
+ return next;
19471
+ });
19472
+ } else {
19473
+ applyRequiredToLocalMap(trimmedName, requiredFlag);
19474
+ }
19475
+ onEditVariable?.(prevRaw, data);
19120
19476
  }
19121
19477
  setVarModalOpen(false);
19122
19478
  };
@@ -19170,7 +19526,8 @@ export const CreateFunctionModal = React.forwardRef(
19170
19526
 
19171
19527
  // Test variable values \u2014 filled by user before clicking Test API
19172
19528
  const [testVarValues, setTestVarValues] = React.useState<Record<string, string>>({});
19173
- const [testVarSubmitAttempted, setTestVarSubmitAttempted] = React.useState(false);
19529
+ /** Set when user clicks Test API \u2014 drives inline errors for empty required variable values only (not Submit). */
19530
+ const [testApiRequiredAttempted, setTestApiRequiredAttempted] = React.useState(false);
19174
19531
 
19175
19532
  // Unique {{variable}} refs found across url, body, headers, queryParams
19176
19533
  const testableVars = React.useMemo(
@@ -19198,7 +19555,7 @@ export const CreateFunctionModal = React.forwardRef(
19198
19555
  setBody(initialData?.body ?? "");
19199
19556
  setApiResponse("");
19200
19557
  setStep2SubmitAttempted(false);
19201
- setTestVarSubmitAttempted(false);
19558
+ setTestApiRequiredAttempted(false);
19202
19559
  setNameError("");
19203
19560
  setUrlError("");
19204
19561
  setBodyError("");
@@ -19207,6 +19564,7 @@ export const CreateFunctionModal = React.forwardRef(
19207
19564
  setUrlPopupStyle(undefined);
19208
19565
  setBodyPopupStyle(undefined);
19209
19566
  setTestVarValues({});
19567
+ setLocalFnVarRequiredByBareName(buildFnVarRequiredMapFromGroups(variableGroups));
19210
19568
  setVarInsertContext(null);
19211
19569
  }
19212
19570
  // Re-run only when modal opens; intentionally exclude deep deps to avoid mid-session resets
@@ -19225,7 +19583,7 @@ export const CreateFunctionModal = React.forwardRef(
19225
19583
  setBody(initialData?.body ?? "");
19226
19584
  setApiResponse("");
19227
19585
  setStep2SubmitAttempted(false);
19228
- setTestVarSubmitAttempted(false);
19586
+ setTestApiRequiredAttempted(false);
19229
19587
  setNameError("");
19230
19588
  setUrlError("");
19231
19589
  setBodyError("");
@@ -19234,8 +19592,9 @@ export const CreateFunctionModal = React.forwardRef(
19234
19592
  setUrlPopupStyle(undefined);
19235
19593
  setBodyPopupStyle(undefined);
19236
19594
  setTestVarValues({});
19595
+ setLocalFnVarRequiredByBareName(buildFnVarRequiredMapFromGroups(variableGroups));
19237
19596
  setVarInsertContext(null);
19238
- }, [initialData, initialStep, initialTab]);
19597
+ }, [initialData, initialStep, initialTab, variableGroups]);
19239
19598
 
19240
19599
  const handleClose = React.useCallback(() => {
19241
19600
  reset();
@@ -19298,15 +19657,18 @@ export const CreateFunctionModal = React.forwardRef(
19298
19657
  text.replace(/\\{\\{[^}]+\\}\\}/g, (match) => testVarValues[match] ?? match);
19299
19658
 
19300
19659
  const handleTestApi = async () => {
19301
- if (!onTestApi) return;
19302
-
19303
- // Validate all test variable values are filled
19304
- if (testableVars.length > 0) {
19305
- setTestVarSubmitAttempted(true);
19306
- const hasEmpty = testableVars.some((v) => !testVarValues[v]?.trim());
19660
+ // Validate all test variable values are filled (always runs, regardless of onTestApi)
19661
+ const requiredTestVars = testableVars.filter((v) =>
19662
+ isPlaceholderRequiredInTest(v, variableGroups, localFnVarRequiredByBareName)
19663
+ );
19664
+ if (requiredTestVars.length > 0) {
19665
+ setTestApiRequiredAttempted(true);
19666
+ const hasEmpty = requiredTestVars.some((v) => !testVarValues[v]?.trim());
19307
19667
  if (hasEmpty) return;
19308
19668
  }
19309
19669
 
19670
+ if (!onTestApi) return;
19671
+
19310
19672
  setIsTesting(true);
19311
19673
  try {
19312
19674
  const step2: CreateFunctionStep2Data = {
@@ -19697,33 +20059,59 @@ export const CreateFunctionModal = React.forwardRef(
19697
20059
  <span className="text-sm text-semantic-text-muted">
19698
20060
  Variable values for testing
19699
20061
  </span>
19700
- {testableVars.map((variable) => {
19701
- const isEmpty = testVarSubmitAttempted && !testVarValues[variable]?.trim();
20062
+ {testableVars.map((variable, varIndex) => {
20063
+ const mustFill = isPlaceholderRequiredInTest(
20064
+ variable,
20065
+ variableGroups,
20066
+ localFnVarRequiredByBareName
20067
+ );
20068
+ const isEmpty =
20069
+ mustFill &&
20070
+ testApiRequiredAttempted &&
20071
+ !testVarValues[variable]?.trim();
20072
+ const testVarErrId = \`fn-test-var-err-\${varIndex}\`;
19702
20073
  return (
19703
20074
  <div key={variable} className="flex flex-col gap-1">
19704
- <div className="flex items-center gap-3">
19705
- <span className="text-sm text-semantic-text-muted font-mono shrink-0 min-w-[120px]">
19706
- {variable}
19707
- </span>
19708
- <input
19709
- type="text"
19710
- value={testVarValues[variable] ?? ""}
19711
- onChange={(e) =>
19712
- setTestVarValues((prev) => ({
19713
- ...prev,
19714
- [variable]: e.target.value,
19715
- }))
19716
- }
19717
- placeholder="Enter test value"
19718
- className={cn(inputCls, "flex-1 h-9 text-sm", isEmpty && "border-semantic-error-primary")}
19719
- aria-invalid={isEmpty}
19720
- />
20075
+ <div className="flex items-start gap-3">
20076
+ <span className="m-0 inline-flex shrink-0 items-center rounded-md bg-semantic-bg-ui px-2.5 py-1.5 text-sm font-mono text-semantic-text-secondary">
20077
+ {variable}
20078
+ </span>
20079
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
20080
+ <input
20081
+ type="text"
20082
+ value={testVarValues[variable] ?? ""}
20083
+ onChange={(e) =>
20084
+ setTestVarValues((prev) => ({
20085
+ ...prev,
20086
+ [variable]: e.target.value,
20087
+ }))
20088
+ }
20089
+ placeholder="Value"
20090
+ className={cn(
20091
+ inputCls,
20092
+ "h-9 text-sm",
20093
+ isEmpty &&
20094
+ "border-semantic-error-primary focus:border-semantic-error-primary focus:shadow-none"
20095
+ )}
20096
+ aria-invalid={isEmpty}
20097
+ aria-describedby={isEmpty ? testVarErrId : undefined}
20098
+ />
20099
+ {isEmpty && (
20100
+ <p
20101
+ id={testVarErrId}
20102
+ className="m-0 flex items-center gap-1.5 text-xs text-semantic-error-primary"
20103
+ >
20104
+ <span
20105
+ className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-semantic-error-primary text-[10px] font-bold leading-none text-semantic-text-inverted"
20106
+ aria-hidden
20107
+ >
20108
+ !
20109
+ </span>
20110
+ <span>Value is required for this key</span>
20111
+ </p>
20112
+ )}
20113
+ </div>
19721
20114
  </div>
19722
- {isEmpty && (
19723
- <p className="m-0 text-sm text-semantic-error-primary pl-[132px]">
19724
- Test value is required
19725
- </p>
19726
- )}
19727
20115
  </div>
19728
20116
  );
19729
20117
  })}
@@ -21047,28 +21435,60 @@ import {
21047
21435
  AccordionTrigger,
21048
21436
  AccordionContent,
21049
21437
  } from "../accordion";
21438
+ import {
21439
+ defaultAdvancedSettingsNumericBounds,
21440
+ type AdvancedSettingsNumericBounds,
21441
+ } from "./advanced-settings-bounds";
21050
21442
 
21051
21443
  // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
21052
21444
 
21053
21445
  export interface AdvancedSettingsData {
21054
- silenceTimeout: number;
21055
- callEndThreshold: number;
21446
+ silenceTimeout?: number;
21447
+ callEndThreshold?: number;
21056
21448
  interruptionHandling: boolean;
21057
21449
  }
21058
21450
 
21451
+ /** Payload when a numeric advanced field finishes a blur validation pass. */
21452
+ export interface AdvancedSettingsNumericFieldBlurDetail {
21453
+ /** Value committed to form state (\`undefined\` if empty after blur). */
21454
+ value: number | undefined;
21455
+ /** \`false\` when the field shows a validation error after blur. */
21456
+ valid: boolean;
21457
+ }
21458
+
21059
21459
  export interface AdvancedSettingsCardProps {
21060
21460
  /** Current form data */
21061
21461
  data: Partial<AdvancedSettingsData>;
21062
- /** Callback when any field changes */
21462
+ /** Callback when any field in this card changes */
21063
21463
  onChange: (patch: Partial<AdvancedSettingsData>) => void;
21064
- /** Min value for silence timeout spinner (default: 1) */
21464
+ /**
21465
+ * Shorthand min/max for both numeric fields. Overridden by explicit
21466
+ * \`silenceTimeoutMin\`, \`silenceTimeoutMax\`, \`callEndThresholdMin\`, or \`callEndThresholdMax\`
21467
+ * when those are passed.
21468
+ */
21469
+ numericBounds?: Partial<AdvancedSettingsNumericBounds>;
21470
+ /** Min value for silence timeout spinner */
21065
21471
  silenceTimeoutMin?: number;
21066
- /** Max value for silence timeout spinner (default: 60) */
21472
+ /** Max value for silence timeout spinner */
21067
21473
  silenceTimeoutMax?: number;
21068
- /** Min value for call end threshold spinner (default: 1) */
21474
+ /** Min value for call end threshold spinner */
21069
21475
  callEndThresholdMin?: number;
21070
- /** Max value for call end threshold spinner (default: 10) */
21476
+ /** Max value for call end threshold spinner */
21071
21477
  callEndThresholdMax?: number;
21478
+ /** When true, an empty value shows a validation error on blur (default: true) */
21479
+ silenceTimeoutRequired?: boolean;
21480
+ /** When true, an empty value shows a validation error on blur (default: true) */
21481
+ callEndThresholdRequired?: boolean;
21482
+ /** Fires after each successful \`onChange\` from this card (including stepper and switch). */
21483
+ onAdvancedSettingsChange?: (patch: Partial<AdvancedSettingsData>) => void;
21484
+ /** Fires when silence timeout input blurs after validation. */
21485
+ onSilenceTimeoutBlur?: (
21486
+ detail: AdvancedSettingsNumericFieldBlurDetail
21487
+ ) => void;
21488
+ /** Fires when call end threshold input blurs after validation. */
21489
+ onCallEndThresholdBlur?: (
21490
+ detail: AdvancedSettingsNumericFieldBlurDetail
21491
+ ) => void;
21072
21492
  /** Disables all fields in the card (view mode) */
21073
21493
  disabled?: boolean;
21074
21494
  /** Additional className */
@@ -21094,54 +21514,196 @@ function Field({
21094
21514
  );
21095
21515
  }
21096
21516
 
21097
- function NumberSpinner({
21517
+ function clamp(n: number, min: number, max: number): number {
21518
+ return Math.min(max, Math.max(min, n));
21519
+ }
21520
+
21521
+ function ValidatedNumberSpinner({
21522
+ id,
21098
21523
  value,
21099
21524
  onChange,
21100
- min = 0,
21101
- max = 999,
21525
+ min,
21526
+ max,
21527
+ required,
21102
21528
  disabled,
21529
+ onBlurCommit,
21103
21530
  }: {
21104
- value: number;
21105
- onChange: (v: number) => void;
21106
- min?: number;
21107
- max?: number;
21531
+ id: string;
21532
+ value: number | undefined;
21533
+ onChange: (v: number | undefined) => void;
21534
+ min: number;
21535
+ max: number;
21536
+ required: boolean;
21108
21537
  disabled?: boolean;
21538
+ onBlurCommit?: (detail: AdvancedSettingsNumericFieldBlurDetail) => void;
21109
21539
  }) {
21540
+ const [inputStr, setInputStr] = React.useState(() =>
21541
+ value === undefined ? "" : String(value)
21542
+ );
21543
+ const [error, setError] = React.useState<string | null>(null);
21544
+ const focusedRef = React.useRef(false);
21545
+ const prevValueRef = React.useRef(value);
21546
+
21547
+ React.useEffect(() => {
21548
+ if (prevValueRef.current !== value) {
21549
+ prevValueRef.current = value;
21550
+ if (!focusedRef.current) {
21551
+ setInputStr(value === undefined ? "" : String(value));
21552
+ setError(null);
21553
+ }
21554
+ }
21555
+ }, [value]);
21556
+
21557
+ const stepBase = (): number | null => {
21558
+ const t = inputStr.trim();
21559
+ if (t !== "") {
21560
+ const n = Number(t);
21561
+ if (Number.isFinite(n)) return n;
21562
+ }
21563
+ if (value !== undefined) return value;
21564
+ return null;
21565
+ };
21566
+
21567
+ const canIncrement = (): boolean => {
21568
+ const b = stepBase();
21569
+ if (b === null) return true;
21570
+ return b < max;
21571
+ };
21572
+
21573
+ const canDecrement = (): boolean => {
21574
+ const b = stepBase();
21575
+ if (b === null) return false;
21576
+ return b > min;
21577
+ };
21578
+
21579
+ const applyStep = (delta: 1 | -1) => {
21580
+ let n = stepBase();
21581
+ if (n === null) {
21582
+ if (delta > 0) n = min - 1;
21583
+ else return;
21584
+ }
21585
+ const next = clamp(n + delta, min, max);
21586
+ setInputStr(String(next));
21587
+ setError(null);
21588
+ onChange(next);
21589
+ };
21590
+
21591
+ const handleBlur = () => {
21592
+ focusedRef.current = false;
21593
+ const trimmed = inputStr.trim();
21594
+ if (trimmed === "") {
21595
+ onChange(undefined);
21596
+ if (required) {
21597
+ setError("This field is required");
21598
+ onBlurCommit?.({ value: undefined, valid: false });
21599
+ } else {
21600
+ setError(null);
21601
+ onBlurCommit?.({ value: undefined, valid: true });
21602
+ }
21603
+ return;
21604
+ }
21605
+ const num = Number(trimmed);
21606
+ if (!Number.isFinite(num)) {
21607
+ setError("Enter a valid number");
21608
+ onBlurCommit?.({ value, valid: false });
21609
+ return;
21610
+ }
21611
+ if (num < min || num > max) {
21612
+ setError(\`Value must be between \${min} and \${max}\`);
21613
+ onBlurCommit?.({ value, valid: false });
21614
+ return;
21615
+ }
21616
+ setError(null);
21617
+ onChange(num);
21618
+ onBlurCommit?.({ value: num, valid: true });
21619
+ };
21620
+
21621
+ const errorId = error ? \`\${id}-error\` : undefined;
21622
+
21110
21623
  return (
21111
- <div className={cn("flex w-full items-center gap-2.5 px-4 py-2.5 border border-solid border-semantic-border-layout bg-semantic-bg-primary rounded", disabled && "opacity-50 cursor-not-allowed")}>
21112
- <input
21113
- type="number"
21114
- value={value}
21115
- min={min}
21116
- max={max}
21117
- disabled={disabled}
21118
- onChange={(e) => onChange(Number(e.target.value))}
21119
- className="flex-1 min-w-0 text-base text-semantic-text-primary bg-transparent outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none disabled:cursor-not-allowed"
21120
- />
21121
- <div className="flex flex-col items-center shrink-0 gap-0.5">
21122
- <button
21123
- type="button"
21124
- onClick={() => onChange(Math.min(max, value + 1))}
21125
- disabled={disabled}
21126
- className="flex items-center justify-center text-semantic-text-muted hover:text-semantic-text-primary transition-colors disabled:cursor-not-allowed"
21127
- aria-label="Increase"
21128
- >
21129
- <ChevronUp className="size-3" />
21130
- </button>
21131
- <button
21132
- type="button"
21133
- onClick={() => onChange(Math.max(min, value - 1))}
21624
+ <div className="flex flex-col gap-1">
21625
+ <div
21626
+ className={cn(
21627
+ "flex w-full items-center gap-2.5 px-4 py-2.5 border border-solid bg-semantic-bg-primary rounded",
21628
+ error
21629
+ ? "border-semantic-error-primary"
21630
+ : "border-semantic-border-layout",
21631
+ disabled && "opacity-50 cursor-not-allowed"
21632
+ )}
21633
+ >
21634
+ <input
21635
+ id={id}
21636
+ type="text"
21637
+ inputMode="numeric"
21638
+ value={inputStr}
21134
21639
  disabled={disabled}
21135
- className="flex items-center justify-center text-semantic-text-muted hover:text-semantic-text-primary transition-colors disabled:cursor-not-allowed"
21136
- aria-label="Decrease"
21137
- >
21138
- <ChevronDown className="size-3" />
21139
- </button>
21640
+ aria-invalid={error ? true : undefined}
21641
+ aria-describedby={errorId}
21642
+ onFocus={() => {
21643
+ focusedRef.current = true;
21644
+ setError(null);
21645
+ }}
21646
+ onBlur={handleBlur}
21647
+ onChange={(e) => setInputStr(e.target.value)}
21648
+ className="flex-1 min-w-0 text-base text-semantic-text-primary bg-transparent outline-none disabled:cursor-not-allowed"
21649
+ />
21650
+ <div className="flex flex-col items-center shrink-0 gap-0.5">
21651
+ <button
21652
+ type="button"
21653
+ onClick={() => applyStep(1)}
21654
+ disabled={disabled || !canIncrement()}
21655
+ className="flex items-center justify-center text-semantic-text-muted hover:text-semantic-text-primary transition-colors disabled:cursor-not-allowed disabled:opacity-40"
21656
+ aria-label="Increase"
21657
+ >
21658
+ <ChevronUp className="size-3" />
21659
+ </button>
21660
+ <button
21661
+ type="button"
21662
+ onClick={() => applyStep(-1)}
21663
+ disabled={disabled || !canDecrement()}
21664
+ className="flex items-center justify-center text-semantic-text-muted hover:text-semantic-text-primary transition-colors disabled:cursor-not-allowed disabled:opacity-40"
21665
+ aria-label="Decrease"
21666
+ >
21667
+ <ChevronDown className="size-3" />
21668
+ </button>
21669
+ </div>
21140
21670
  </div>
21671
+ {error ? (
21672
+ <p
21673
+ id={errorId}
21674
+ role="alert"
21675
+ className="m-0 text-xs text-semantic-error-primary"
21676
+ >
21677
+ {error}
21678
+ </p>
21679
+ ) : null}
21141
21680
  </div>
21142
21681
  );
21143
21682
  }
21144
21683
 
21684
+ function useCorrectOutOfRangeNumeric(
21685
+ raw: number | undefined,
21686
+ min: number,
21687
+ max: number,
21688
+ disabled: boolean | undefined,
21689
+ patchKey: "silenceTimeout" | "callEndThreshold",
21690
+ onChange: (patch: Partial<AdvancedSettingsData>) => void
21691
+ ) {
21692
+ const onChangeRef = React.useRef(onChange);
21693
+ React.useEffect(() => {
21694
+ onChangeRef.current = onChange;
21695
+ });
21696
+
21697
+ React.useEffect(() => {
21698
+ if (disabled || raw === undefined) return;
21699
+ if (raw < min || raw > max) {
21700
+ onChangeRef.current({
21701
+ [patchKey]: clamp(raw, min, max),
21702
+ } as Partial<AdvancedSettingsData>);
21703
+ }
21704
+ }, [raw, min, max, disabled, patchKey]);
21705
+ }
21706
+
21145
21707
  // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
21146
21708
 
21147
21709
  const AdvancedSettingsCard = React.forwardRef(
@@ -21149,15 +21711,63 @@ const AdvancedSettingsCard = React.forwardRef(
21149
21711
  {
21150
21712
  data,
21151
21713
  onChange,
21152
- silenceTimeoutMin = 1,
21153
- silenceTimeoutMax = 60,
21154
- callEndThresholdMin = 1,
21155
- callEndThresholdMax = 10,
21714
+ numericBounds,
21715
+ silenceTimeoutMin: silenceTimeoutMinProp,
21716
+ silenceTimeoutMax: silenceTimeoutMaxProp,
21717
+ callEndThresholdMin: callEndThresholdMinProp,
21718
+ callEndThresholdMax: callEndThresholdMaxProp,
21719
+ silenceTimeoutRequired = true,
21720
+ callEndThresholdRequired = true,
21721
+ onAdvancedSettingsChange,
21722
+ onSilenceTimeoutBlur,
21723
+ onCallEndThresholdBlur,
21156
21724
  disabled,
21157
21725
  className,
21158
21726
  }: AdvancedSettingsCardProps,
21159
21727
  ref: React.Ref<HTMLDivElement>
21160
21728
  ) => {
21729
+ const silenceTimeoutMin =
21730
+ silenceTimeoutMinProp ??
21731
+ numericBounds?.silenceTimeoutMin ??
21732
+ defaultAdvancedSettingsNumericBounds.silenceTimeoutMin;
21733
+ const silenceTimeoutMax =
21734
+ silenceTimeoutMaxProp ??
21735
+ numericBounds?.silenceTimeoutMax ??
21736
+ defaultAdvancedSettingsNumericBounds.silenceTimeoutMax;
21737
+ const callEndThresholdMin =
21738
+ callEndThresholdMinProp ??
21739
+ numericBounds?.callEndThresholdMin ??
21740
+ defaultAdvancedSettingsNumericBounds.callEndThresholdMin;
21741
+ const callEndThresholdMax =
21742
+ callEndThresholdMaxProp ??
21743
+ numericBounds?.callEndThresholdMax ??
21744
+ defaultAdvancedSettingsNumericBounds.callEndThresholdMax;
21745
+
21746
+ const emitPatch = React.useCallback(
21747
+ (patch: Partial<AdvancedSettingsData>) => {
21748
+ onChange(patch);
21749
+ onAdvancedSettingsChange?.(patch);
21750
+ },
21751
+ [onChange, onAdvancedSettingsChange]
21752
+ );
21753
+
21754
+ useCorrectOutOfRangeNumeric(
21755
+ data.silenceTimeout,
21756
+ silenceTimeoutMin,
21757
+ silenceTimeoutMax,
21758
+ disabled,
21759
+ "silenceTimeout",
21760
+ emitPatch
21761
+ );
21762
+ useCorrectOutOfRangeNumeric(
21763
+ data.callEndThreshold,
21764
+ callEndThresholdMin,
21765
+ callEndThresholdMax,
21766
+ disabled,
21767
+ "callEndThreshold",
21768
+ emitPatch
21769
+ );
21770
+
21161
21771
  return (
21162
21772
  <div
21163
21773
  ref={ref}
@@ -21178,28 +21788,31 @@ const AdvancedSettingsCard = React.forwardRef(
21178
21788
  {/* Number fields section */}
21179
21789
  <div className="px-4 pt-4 pb-4 flex flex-col gap-5 border-b border-solid border-semantic-border-layout sm:px-6 sm:pt-5 sm:pb-6">
21180
21790
  <Field label="Silence Timeout (seconds)">
21181
- <NumberSpinner
21182
- value={data.silenceTimeout ?? 15}
21183
- onChange={(v) => onChange({ silenceTimeout: v })}
21791
+ <ValidatedNumberSpinner
21792
+ id="advanced-silence-timeout"
21793
+ value={data.silenceTimeout}
21794
+ onChange={(v) => emitPatch({ silenceTimeout: v })}
21184
21795
  min={silenceTimeoutMin}
21185
21796
  max={silenceTimeoutMax}
21797
+ required={silenceTimeoutRequired}
21186
21798
  disabled={disabled}
21799
+ onBlurCommit={onSilenceTimeoutBlur}
21187
21800
  />
21188
- <p className="m-0 text-xs text-semantic-text-muted">
21189
- Default: 15 seconds
21190
- </p>
21191
21801
  </Field>
21192
21802
 
21193
21803
  <Field label="Call End Threshold">
21194
- <NumberSpinner
21195
- value={data.callEndThreshold ?? 3}
21196
- onChange={(v) => onChange({ callEndThreshold: v })}
21804
+ <ValidatedNumberSpinner
21805
+ id="advanced-call-end-threshold"
21806
+ value={data.callEndThreshold}
21807
+ onChange={(v) => emitPatch({ callEndThreshold: v })}
21197
21808
  min={callEndThresholdMin}
21198
21809
  max={callEndThresholdMax}
21810
+ required={callEndThresholdRequired}
21199
21811
  disabled={disabled}
21812
+ onBlurCommit={onCallEndThresholdBlur}
21200
21813
  />
21201
21814
  <p className="m-0 text-xs text-semantic-text-muted">
21202
- Drop call after n consecutive silences. Default: 3
21815
+ Drop call after n consecutive silences.
21203
21816
  </p>
21204
21817
  </Field>
21205
21818
  </div>
@@ -21217,7 +21830,7 @@ const AdvancedSettingsCard = React.forwardRef(
21217
21830
  <Switch
21218
21831
  checked={data.interruptionHandling ?? true}
21219
21832
  onCheckedChange={(v) =>
21220
- onChange({ interruptionHandling: v })
21833
+ emitPatch({ interruptionHandling: v })
21221
21834
  }
21222
21835
  disabled={disabled}
21223
21836
  />
@@ -21233,6 +21846,39 @@ const AdvancedSettingsCard = React.forwardRef(
21233
21846
  AdvancedSettingsCard.displayName = "AdvancedSettingsCard";
21234
21847
 
21235
21848
  export { AdvancedSettingsCard };
21849
+ `, prefix)
21850
+ },
21851
+ {
21852
+ name: "advanced-settings-bounds.ts",
21853
+ content: prefixTailwindClasses(`/**
21854
+ * Min/max for Advanced Settings numeric fields (silence timeout, call end threshold).
21855
+ * Use with \`numericBounds\` / \`advancedSettingsNumericBounds\` props or edit
21856
+ * \`defaultAdvancedSettingsNumericBounds\` for deployment defaults.
21857
+ */
21858
+ export interface AdvancedSettingsNumericBounds {
21859
+ silenceTimeoutMin: number;
21860
+ silenceTimeoutMax: number;
21861
+ callEndThresholdMin: number;
21862
+ callEndThresholdMax: number;
21863
+ }
21864
+
21865
+ /**
21866
+ * Default min/max for Advanced Settings numeric fields (silence timeout, call end threshold).
21867
+ *
21868
+ * Change these values per client or deployment. You can also override any bound by passing
21869
+ * \`advancedSettingsNumericBounds\`, \`numericBounds\`, or the individual min/max props
21870
+ * on \`IvrBotConfig\` / \`AdvancedSettingsCard\`.
21871
+ */
21872
+ export const defaultAdvancedSettingsNumericBounds: AdvancedSettingsNumericBounds =
21873
+ {
21874
+ silenceTimeoutMin: 3,
21875
+ silenceTimeoutMax: 15,
21876
+ callEndThresholdMin: 1,
21877
+ callEndThresholdMax: 10,
21878
+ };
21879
+
21880
+ export type DefaultAdvancedSettingsNumericBounds =
21881
+ typeof defaultAdvancedSettingsNumericBounds;
21236
21882
  `, prefix)
21237
21883
  },
21238
21884
  {
@@ -21393,6 +22039,11 @@ export { FallbackPromptsCard };
21393
22039
  {
21394
22040
  name: "types.ts",
21395
22041
  content: prefixTailwindClasses(`import type { UploadProgressHandlers } from "../file-upload-modal";
22042
+ import type { AdvancedSettingsNumericBounds } from "./advanced-settings-bounds";
22043
+ import type {
22044
+ AdvancedSettingsData,
22045
+ AdvancedSettingsNumericFieldBlurDetail,
22046
+ } from "./advanced-settings-card";
21396
22047
 
21397
22048
  export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
21398
22049
 
@@ -21501,7 +22152,10 @@ export interface CreateFunctionModalProps {
21501
22152
  initialTab?: FunctionTabType;
21502
22153
  /** Session variables available for {{ autocomplete in URL, body, header values, and query param values */
21503
22154
  sessionVariables?: string[];
21504
- /** Grouped variables shown in the {{ autocomplete popup (overrides flat list display when provided) */
22155
+ /**
22156
+ * Grouped variables for the {{ autocomplete popup (overrides flat list display when provided).
22157
+ * Items with \`required: true\` are validated when the user clicks Test API (inline errors under each empty field).
22158
+ */
21505
22159
  variableGroups?: VariableGroup[];
21506
22160
  /**
21507
22161
  * Called when user saves a new variable from the autocomplete popup.
@@ -21510,7 +22164,11 @@ export interface CreateFunctionModalProps {
21510
22164
  * so it appears in the dropdown on the next open.
21511
22165
  */
21512
22166
  onAddVariable?: (data: VariableFormData) => void;
21513
- /** Called when user edits a variable from the autocomplete popup */
22167
+ /**
22168
+ * Called when the user saves "Edit variable". The modal already renames
22169
+ * \`{{function.name}}\` / \`{{name}}\` across URL, body, headers, query params, and test values.
22170
+ * Update your \`variableGroups\` (and persist to your backend) using \`originalName\` \u2192 \`data.name\`.
22171
+ */
21514
22172
  onEditVariable?: (originalName: string, data: VariableFormData) => void;
21515
22173
  /** When true, all form fields are disabled (view mode) but Next is enabled so user can browse steps */
21516
22174
  disabled?: boolean;
@@ -21530,8 +22188,10 @@ export interface IvrBotConfigData {
21530
22188
  functions: FunctionItem[];
21531
22189
  frustrationHandoverEnabled: boolean;
21532
22190
  escalationDepartment: string;
21533
- silenceTimeout: number;
21534
- callEndThreshold: number;
22191
+ /** Undefined when the field was cleared; validate before save/publish. */
22192
+ silenceTimeout?: number;
22193
+ /** Undefined when the field was cleared; validate before save/publish. */
22194
+ callEndThreshold?: number;
21535
22195
  interruptionHandling: boolean;
21536
22196
  }
21537
22197
 
@@ -21618,17 +22278,49 @@ export interface IvrBotConfigProps {
21618
22278
  languageOptions?: SelectOption[];
21619
22279
  /** Override session variable chips for BotBehaviorCard */
21620
22280
  sessionVariables?: string[];
22281
+ /**
22282
+ * Function-scoped variables for Create / Edit Function modal (\`{{\` autocomplete; \`required\` applies to Test API validation only).
22283
+ * Pass the same groups your app persists; items with \`required: true\` block Test API until test values are filled for placeholders used in the request.
22284
+ */
22285
+ functionVariableGroups?: VariableGroup[];
22286
+ /** When set with \`functionVariableGroups\`, called after the user saves a new variable from the modal. */
22287
+ onAddFunctionVariable?: (data: VariableFormData) => void;
22288
+ /** When set with \`functionVariableGroups\`, called after the user saves an edited variable. */
22289
+ onEditFunctionVariable?: (originalName: string, data: VariableFormData) => void;
21621
22290
  /** Override escalation department options for FrustrationHandoverCard */
21622
22291
  escalationDepartmentOptions?: SelectOption[];
21623
- /** Override silence timeout bounds */
22292
+ /**
22293
+ * Shorthand min/max for Advanced Settings numeric fields. Individual
22294
+ * \`silenceTimeoutMin\` / \`silenceTimeoutMax\` / \`callEndThresholdMin\` / \`callEndThresholdMax\`
22295
+ * override corresponding entries when set.
22296
+ */
22297
+ advancedSettingsNumericBounds?: Partial<AdvancedSettingsNumericBounds>;
22298
+ /** Override silence timeout min (after \`advancedSettingsNumericBounds\`) */
21624
22299
  silenceTimeoutMin?: number;
22300
+ /** Override silence timeout max (after \`advancedSettingsNumericBounds\`) */
21625
22301
  silenceTimeoutMax?: number;
21626
- /** Override call end threshold bounds */
22302
+ /** Override call end threshold min (after \`advancedSettingsNumericBounds\`) */
21627
22303
  callEndThresholdMin?: number;
22304
+ /** Override call end threshold max (after \`advancedSettingsNumericBounds\`) */
21628
22305
  callEndThresholdMax?: number;
22306
+ /**
22307
+ * Fires when any Advanced Settings field changes (numeric commit, stepper, interruption toggle).
22308
+ */
22309
+ onAdvancedSettingsChange?: (patch: Partial<AdvancedSettingsData>) => void;
22310
+ /** Fires when silence timeout blurs after validation (see \`AdvancedSettingsNumericFieldBlurDetail\`). */
22311
+ onSilenceTimeoutBlur?: (
22312
+ detail: AdvancedSettingsNumericFieldBlurDetail
22313
+ ) => void;
22314
+ /** Fires when call end threshold blurs after validation. */
22315
+ onCallEndThresholdBlur?: (
22316
+ detail: AdvancedSettingsNumericFieldBlurDetail
22317
+ ) => void;
21629
22318
  className?: string;
21630
22319
  }
21631
22320
 
22321
+ export type { AdvancedSettingsNumericBounds } from "./advanced-settings-bounds";
22322
+ export type { AdvancedSettingsNumericFieldBlurDetail } from "./advanced-settings-card";
22323
+
21632
22324
  // \u2500\u2500\u2500 File Upload Modal (re-exported from shared module) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
21633
22325
 
21634
22326
  export type {
@@ -21647,6 +22339,12 @@ export { KnowledgeBaseCard } from "./knowledge-base-card";
21647
22339
  export { FunctionsCard } from "./functions-card";
21648
22340
  export { FrustrationHandoverCard } from "./frustration-handover-card";
21649
22341
  export { AdvancedSettingsCard } from "./advanced-settings-card";
22342
+ export {
22343
+ defaultAdvancedSettingsNumericBounds,
22344
+ type AdvancedSettingsNumericBounds,
22345
+ type DefaultAdvancedSettingsNumericBounds,
22346
+ } from "./advanced-settings-bounds";
22347
+ export type { AdvancedSettingsNumericFieldBlurDetail } from "./advanced-settings-card";
21650
22348
  export { FallbackPromptsCard } from "./fallback-prompts-card";
21651
22349
  export type {
21652
22350
  FallbackPromptsData,