myoperator-ui 0.0.213 → 0.0.214

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 +815 -261
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12890,10 +12890,16 @@ export type { BrandIconProps } from "./icon";
12890
12890
  {
12891
12891
  name: "bot-card.tsx",
12892
12892
  content: prefixTailwindClasses(`import * as React from "react";
12893
- import { MessageSquare, Phone } from "lucide-react";
12893
+ import { MessageSquare, Phone, MoreVertical, Pencil, Trash2 } from "lucide-react";
12894
12894
  import { cn } from "../../../lib/utils";
12895
12895
  import { Badge } from "../badge";
12896
- import { BotListAction } from "./bot-list-action";
12896
+ import {
12897
+ DropdownMenu,
12898
+ DropdownMenuTrigger,
12899
+ DropdownMenuContent,
12900
+ DropdownMenuItem,
12901
+ DropdownMenuSeparator,
12902
+ } from "../dropdown-menu";
12897
12903
  import type { Bot, BotCardProps, BotType } from "./types";
12898
12904
 
12899
12905
  const DEFAULT_TYPE_LABELS: Record<BotType, string> = {
@@ -12967,11 +12973,38 @@ export const BotCard = React.forwardRef<HTMLDivElement, BotCardProps>(
12967
12973
  </Badge>
12968
12974
 
12969
12975
  <span data-bot-card-action className="inline-flex" onClick={(e) => e.stopPropagation()}>
12970
- <BotListAction
12971
- align="end"
12972
- onEdit={() => onEdit?.(bot.id)}
12973
- onDelete={() => onDelete?.(bot.id)}
12974
- />
12976
+ <DropdownMenu>
12977
+ <DropdownMenuTrigger asChild>
12978
+ <button
12979
+ type="button"
12980
+ className="p-2 min-h-[44px] min-w-[44px] sm:p-1 sm:min-h-0 sm:min-w-0 rounded hover:bg-semantic-bg-hover text-semantic-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus flex items-center justify-center touch-manipulation"
12981
+ aria-label="More options"
12982
+ >
12983
+ <MoreVertical className="size-4 shrink-0" />
12984
+ </button>
12985
+ </DropdownMenuTrigger>
12986
+ <DropdownMenuContent align="end" className="min-w-[160px]">
12987
+ <DropdownMenuItem
12988
+ className="flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm"
12989
+ onSelect={(e) => { e.preventDefault(); onEdit?.(bot.id); }}
12990
+ >
12991
+ <Pencil className="size-4 shrink-0" />
12992
+ <span>Edit</span>
12993
+ </DropdownMenuItem>
12994
+ {onDelete && (
12995
+ <>
12996
+ <DropdownMenuSeparator />
12997
+ <DropdownMenuItem
12998
+ className="flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm text-semantic-error-primary focus:bg-semantic-error-surface focus:text-semantic-error-primary"
12999
+ onSelect={(e) => { e.preventDefault(); onDelete(bot.id); }}
13000
+ >
13001
+ <Trash2 className="size-4 shrink-0 text-semantic-error-primary" />
13002
+ <span>Delete</span>
13003
+ </DropdownMenuItem>
13004
+ </>
13005
+ )}
13006
+ </DropdownMenuContent>
13007
+ </DropdownMenu>
12975
13008
  </span>
12976
13009
  </div>
12977
13010
  </div>
@@ -13004,9 +13037,11 @@ export const BotCard = React.forwardRef<HTMLDivElement, BotCardProps>(
13004
13037
  Last Published
13005
13038
  </span>
13006
13039
  )}
13007
- {bot.lastPublishedBy && bot.lastPublishedDate ? (
13040
+ {(bot.lastPublishedBy || bot.lastPublishedDate) ? (
13008
13041
  <p className="m-0 text-xs sm:text-sm text-semantic-text-muted truncate">
13009
- {bot.lastPublishedBy} | {bot.lastPublishedDate}
13042
+ {bot.lastPublishedBy
13043
+ ? \`\${bot.lastPublishedBy} | \${bot.lastPublishedDate ?? "\u2014"}\`
13044
+ : bot.lastPublishedDate}
13010
13045
  </p>
13011
13046
  ) : bot.status !== "draft" ? (
13012
13047
  <p className="m-0 text-xs sm:text-sm text-semantic-text-muted">\u2014</p>
@@ -13060,17 +13095,20 @@ export const CreateBotModal = React.forwardRef<
13060
13095
  const [name, setName] = React.useState("");
13061
13096
  const [selectedType, setSelectedType] = React.useState<BotType>("chatbot");
13062
13097
 
13098
+ React.useEffect(() => {
13099
+ if (!open) {
13100
+ setName("");
13101
+ setSelectedType("chatbot");
13102
+ }
13103
+ }, [open]);
13104
+
13063
13105
  const handleSubmit = () => {
13064
13106
  if (!name.trim()) return;
13065
13107
  const typeValue = selectedType === "chatbot" ? BOT_TYPE.CHAT : BOT_TYPE.VOICE;
13066
13108
  onSubmit?.({ name: name.trim(), type: typeValue });
13067
- setName("");
13068
- setSelectedType("chatbot");
13069
13109
  };
13070
13110
 
13071
13111
  const handleClose = () => {
13072
- setName("");
13073
- setSelectedType("chatbot");
13074
13112
  onOpenChange(false);
13075
13113
  };
13076
13114
 
@@ -13576,9 +13614,9 @@ export const BotListCreateCard = React.forwardRef<
13576
13614
  type="button"
13577
13615
  onClick={onClick}
13578
13616
  className={cn(
13579
- "flex flex-col items-center justify-center gap-2 sm:gap-3 p-3 sm:p-2.5 rounded-[5px] min-h-[180px] sm:min-h-[207px] w-full min-w-0 max-w-full",
13580
- "bg-semantic-info-surface-subtle border border-dashed border-semantic-border-layout",
13581
- "cursor-pointer transition-colors hover:bg-semantic-bg-hover hover:border-semantic-border-input",
13617
+ "relative flex flex-col items-center justify-center gap-2 sm:gap-3 p-3 sm:p-2.5 rounded-[5px] min-h-[180px] sm:min-h-[207px] w-full min-w-0 max-w-full",
13618
+ "bg-semantic-info-surface-subtle",
13619
+ "group cursor-pointer",
13582
13620
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus",
13583
13621
  "self-stretch justify-self-stretch",
13584
13622
  className
@@ -13586,6 +13624,21 @@ export const BotListCreateCard = React.forwardRef<
13586
13624
  aria-label={label}
13587
13625
  {...props}
13588
13626
  >
13627
+ <svg
13628
+ className="pointer-events-none absolute inset-0 h-full w-full"
13629
+ aria-hidden="true"
13630
+ >
13631
+ <rect
13632
+ x="0.5"
13633
+ y="0.5"
13634
+ style={{ width: "calc(100% - 1px)", height: "calc(100% - 1px)" }}
13635
+ rx="4.5"
13636
+ fill="none"
13637
+ strokeWidth="1"
13638
+ strokeDasharray="6 6"
13639
+ className="stroke-[#c0c3ca] group-hover:stroke-[#717680] transition-colors duration-150"
13640
+ />
13641
+ </svg>
13589
13642
  <Plus className="size-4 text-semantic-text-secondary shrink-0" />
13590
13643
  <span className="text-sm font-semibold leading-5 text-semantic-text-secondary text-center tracking-[0.014px]">
13591
13644
  {label}
@@ -13620,81 +13673,6 @@ export const BotListGrid = React.forwardRef<HTMLDivElement, BotListGridProps>(
13620
13673
  );
13621
13674
 
13622
13675
  BotListGrid.displayName = "BotListGrid";
13623
- `, prefix)
13624
- },
13625
- {
13626
- name: "bot-list-action.tsx",
13627
- content: prefixTailwindClasses(`import * as React from "react";
13628
- import { MoreVertical, Pencil, Trash2 } from "lucide-react";
13629
- import { cn } from "../../../lib/utils";
13630
- import {
13631
- DropdownMenu,
13632
- DropdownMenuTrigger,
13633
- DropdownMenuContent,
13634
- DropdownMenuItem,
13635
- DropdownMenuSeparator,
13636
- } from "../dropdown-menu";
13637
- import type { BotListActionProps } from "./types";
13638
-
13639
- const defaultTrigger = (
13640
- <button
13641
- type="button"
13642
- className="p-2 min-h-[44px] min-w-[44px] sm:p-1 sm:min-h-0 sm:min-w-0 rounded hover:bg-semantic-bg-hover text-semantic-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-border-focus flex items-center justify-center touch-manipulation"
13643
- aria-label="More options"
13644
- >
13645
- <MoreVertical className="size-4 shrink-0" />
13646
- </button>
13647
- );
13648
-
13649
- export const BotListAction = React.forwardRef<HTMLDivElement, BotListActionProps>(
13650
- (
13651
- {
13652
- onEdit,
13653
- onDelete,
13654
- trigger = defaultTrigger,
13655
- align = "end",
13656
- className,
13657
- ...props
13658
- },
13659
- ref
13660
- ) => {
13661
- return (
13662
- <div ref={ref} className={cn("inline-flex", className)} {...props}>
13663
- <DropdownMenu>
13664
- <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
13665
- <DropdownMenuContent
13666
- align={align}
13667
- className="min-w-[160px] max-w-[min(100vw-2rem,320px)] max-h-[min(70vh,400px)] overflow-y-auto rounded-lg border border-semantic-border-layout bg-semantic-bg-ui p-1 shadow-lg"
13668
- >
13669
- <DropdownMenuItem
13670
- className="flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm text-semantic-text-primary outline-none transition-colors focus:bg-semantic-bg-hover focus:text-semantic-text-primary"
13671
- onSelect={(e) => {
13672
- e.preventDefault();
13673
- onEdit?.();
13674
- }}
13675
- >
13676
- <Pencil className="size-4 shrink-0 text-semantic-text-primary" />
13677
- <span>Edit</span>
13678
- </DropdownMenuItem>
13679
- <DropdownMenuSeparator className="my-1 bg-semantic-border-layout" />
13680
- <DropdownMenuItem
13681
- className="flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm text-semantic-error-primary outline-none transition-colors focus:bg-semantic-error-surface focus:text-semantic-error-primary"
13682
- onSelect={(e) => {
13683
- e.preventDefault();
13684
- onDelete?.();
13685
- }}
13686
- >
13687
- <Trash2 className="size-4 shrink-0 text-semantic-error-primary" />
13688
- <span>Delete</span>
13689
- </DropdownMenuItem>
13690
- </DropdownMenuContent>
13691
- </DropdownMenu>
13692
- </div>
13693
- );
13694
- }
13695
- );
13696
-
13697
- BotListAction.displayName = "BotListAction";
13698
13676
  `, prefix)
13699
13677
  },
13700
13678
  {
@@ -13785,18 +13763,6 @@ export interface BotListGridProps
13785
13763
  children: React.ReactNode;
13786
13764
  }
13787
13765
 
13788
- export interface BotListActionProps
13789
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
13790
- /** Called when Edit is selected */
13791
- onEdit?: () => void;
13792
- /** Called when Delete is selected */
13793
- onDelete?: () => void;
13794
- /** Custom trigger element; defaults to three-dot icon button */
13795
- trigger?: React.ReactNode;
13796
- /** Content alignment relative to trigger */
13797
- align?: "start" | "center" | "end";
13798
- }
13799
-
13800
13766
  /** Props for CreateBotFlow: create card + Create Bot modal (no header). */
13801
13767
  export interface CreateBotFlowProps
13802
13768
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "onSubmit"> {
@@ -13876,7 +13842,6 @@ export { BotListHeader } from "./bot-list-header";
13876
13842
  export { BotListSearch } from "./bot-list-search";
13877
13843
  export { BotListCreateCard } from "./bot-list-create-card";
13878
13844
  export { BotListGrid } from "./bot-list-grid";
13879
- export { BotListAction } from "./bot-list-action";
13880
13845
  export { BOT_TYPE } from "./types";
13881
13846
  export type {
13882
13847
  BotListProps,
@@ -13884,7 +13849,6 @@ export type {
13884
13849
  BotListSearchProps,
13885
13850
  BotListCreateCardProps,
13886
13851
  BotListGridProps,
13887
- BotListActionProps,
13888
13852
  Bot,
13889
13853
  BotType,
13890
13854
  BotStatus,
@@ -14451,9 +14415,13 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14451
14415
  onSampleFileDownload,
14452
14416
  onDownloadKnowledgeFile,
14453
14417
  onDeleteKnowledgeFile,
14418
+ knowledgeDownloadDisabled,
14419
+ knowledgeDeleteDisabled,
14454
14420
  onCreateFunction,
14455
14421
  onEditFunction,
14456
14422
  onDeleteFunction,
14423
+ functionEditDisabled,
14424
+ functionDeleteDisabled,
14457
14425
  onTestApi,
14458
14426
  functionsInfoTooltip,
14459
14427
  knowledgeBaseInfoTooltip,
@@ -14594,29 +14562,33 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14594
14562
  files={data.knowledgeBaseFiles}
14595
14563
  onAdd={() => setUploadOpen(true)}
14596
14564
  onDownload={onDownloadKnowledgeFile}
14597
- infoTooltip={knowledgeBaseInfoTooltip}
14598
- disabled={disabled}
14599
- onDelete={(id) => {
14565
+ onDelete={onDeleteKnowledgeFile ? (id) => {
14600
14566
  update({
14601
14567
  knowledgeBaseFiles: data.knowledgeBaseFiles.filter(
14602
14568
  (f) => f.id !== id
14603
14569
  ),
14604
14570
  });
14605
- onDeleteKnowledgeFile?.(id);
14606
- }}
14571
+ onDeleteKnowledgeFile(id);
14572
+ } : undefined}
14573
+ infoTooltip={knowledgeBaseInfoTooltip}
14574
+ disabled={disabled}
14575
+ downloadDisabled={knowledgeDownloadDisabled}
14576
+ deleteDisabled={knowledgeDeleteDisabled}
14607
14577
  />
14608
14578
  <FunctionsCard
14609
14579
  functions={data.functions}
14610
14580
  onAddFunction={() => setCreateFnOpen(true)}
14611
- onEditFunction={handleEditFunction}
14612
- infoTooltip={functionsInfoTooltip}
14613
- disabled={disabled}
14614
- onDeleteFunction={(id) => {
14581
+ onEditFunction={onEditFunction ? handleEditFunction : undefined}
14582
+ onDeleteFunction={onDeleteFunction ? (id) => {
14615
14583
  update({
14616
14584
  functions: data.functions.filter((f) => f.id !== id),
14617
14585
  });
14618
- onDeleteFunction?.(id);
14619
- }}
14586
+ onDeleteFunction(id);
14587
+ } : undefined}
14588
+ infoTooltip={functionsInfoTooltip}
14589
+ disabled={disabled}
14590
+ editDisabled={functionEditDisabled}
14591
+ deleteDisabled={functionDeleteDisabled}
14620
14592
  />
14621
14593
  <FrustrationHandoverCard
14622
14594
  data={data}
@@ -14644,6 +14616,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14644
14616
  onTestApi={onTestApi}
14645
14617
  promptMinLength={functionPromptMinLength}
14646
14618
  promptMaxLength={functionPromptMaxLength}
14619
+ sessionVariables={sessionVariables}
14647
14620
  />
14648
14621
 
14649
14622
  {/* Edit Function Modal */}
@@ -14656,6 +14629,8 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14656
14629
  isEditing
14657
14630
  promptMinLength={functionPromptMinLength}
14658
14631
  promptMaxLength={functionPromptMaxLength}
14632
+ sessionVariables={sessionVariables}
14633
+ disabled={disabled}
14659
14634
  />
14660
14635
 
14661
14636
  {/* File Upload Modal */}
@@ -14710,6 +14685,12 @@ const QUERY_PARAM_KEY_MAX = 512;
14710
14685
  const QUERY_PARAM_VALUE_MAX = 2048;
14711
14686
  const QUERY_PARAM_KEY_PATTERN = /^[a-zA-Z0-9_.\\-~]+$/;
14712
14687
 
14688
+ const DEFAULT_SESSION_VARIABLES = [
14689
+ "{{Caller number}}",
14690
+ "{{Time}}",
14691
+ "{{Contact Details}}",
14692
+ ];
14693
+
14713
14694
  function validateQueryParamKey(key: string): string | undefined {
14714
14695
  if (!key.trim()) return "Query param key is required";
14715
14696
  if (key.length > QUERY_PARAM_KEY_MAX) return "key cannot exceed 512 characters.";
@@ -14727,13 +14708,206 @@ function generateId() {
14727
14708
  return Math.random().toString(36).slice(2, 9);
14728
14709
  }
14729
14710
 
14711
+ // \u2500\u2500 Variable trigger helpers \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
14712
+
14713
+ interface TriggerState {
14714
+ query: string;
14715
+ from: number;
14716
+ to: number;
14717
+ }
14718
+
14719
+ function detectVarTrigger(value: string, cursor: number): TriggerState | null {
14720
+ const before = value.slice(0, cursor);
14721
+ const match = /\\{\\{([^}]*)$/.exec(before);
14722
+ if (!match) return null;
14723
+ return { query: match[1].toLowerCase(), from: match.index, to: cursor };
14724
+ }
14725
+
14726
+ function insertVar(value: string, variable: string, from: number, to: number): string {
14727
+ return value.slice(0, from) + variable + value.slice(to);
14728
+ }
14729
+
14730
+ function extractVarRefs(texts: string[]): string[] {
14731
+ const pattern = /\\{\\{[^}]+\\}\\}/g;
14732
+ const all = texts.flatMap((t) => t.match(pattern) ?? []);
14733
+ return [...new Set(all)];
14734
+ }
14735
+
14736
+ /** Mirror-div technique \u2014 returns { top, left } relative to the element's top-left corner. */
14737
+ function getCaretPixelPos(
14738
+ el: HTMLTextAreaElement | HTMLInputElement,
14739
+ position: number
14740
+ ): { top: number; left: number } {
14741
+ const cs = window.getComputedStyle(el);
14742
+ const mirror = document.createElement("div");
14743
+
14744
+ (
14745
+ [
14746
+ "boxSizing", "width", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
14747
+ "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth",
14748
+ "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontVariant",
14749
+ "letterSpacing", "lineHeight", "textTransform", "wordSpacing", "tabSize",
14750
+ ] as (keyof CSSStyleDeclaration)[]
14751
+ ).forEach((prop) => {
14752
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14753
+ (mirror.style as any)[prop] = cs[prop];
14754
+ });
14755
+
14756
+ mirror.style.whiteSpace = el.tagName === "TEXTAREA" ? "pre-wrap" : "pre";
14757
+ mirror.style.wordWrap = el.tagName === "TEXTAREA" ? "break-word" : "normal";
14758
+ mirror.style.position = "absolute";
14759
+ mirror.style.visibility = "hidden";
14760
+ mirror.style.overflow = "hidden";
14761
+ mirror.style.top = "0";
14762
+ mirror.style.left = "0";
14763
+ mirror.style.width = el.offsetWidth + "px";
14764
+
14765
+ document.body.appendChild(mirror);
14766
+ mirror.appendChild(document.createTextNode(el.value.substring(0, position)));
14767
+
14768
+ const marker = document.createElement("span");
14769
+ marker.textContent = "\\u200b";
14770
+ mirror.appendChild(marker);
14771
+
14772
+ const markerRect = marker.getBoundingClientRect();
14773
+ const mirrorRect = mirror.getBoundingClientRect();
14774
+ document.body.removeChild(mirror);
14775
+
14776
+ const scrollTop = el instanceof HTMLTextAreaElement ? el.scrollTop : 0;
14777
+ return {
14778
+ top: markerRect.top - mirrorRect.top - scrollTop,
14779
+ left: markerRect.left - mirrorRect.left,
14780
+ };
14781
+ }
14782
+
14783
+ // Uses same visual classes as DropdownMenuContent + DropdownMenuItem.
14784
+ // Position is cursor-anchored via getCaretPixelPos.
14785
+ function VarPopup({
14786
+ variables,
14787
+ onSelect,
14788
+ style,
14789
+ }: {
14790
+ variables: string[];
14791
+ onSelect: (v: string) => void;
14792
+ style?: React.CSSProperties;
14793
+ }) {
14794
+ if (variables.length === 0) return null;
14795
+ return (
14796
+ <div
14797
+ role="listbox"
14798
+ style={style}
14799
+ className="absolute z-[9999] min-w-[8rem] max-w-xs overflow-hidden rounded-md border border-semantic-border-layout bg-semantic-bg-primary p-1 text-semantic-text-primary shadow-md"
14800
+ >
14801
+ {variables.map((v) => (
14802
+ <button
14803
+ key={v}
14804
+ type="button"
14805
+ role="option"
14806
+ onMouseDown={(e) => {
14807
+ e.preventDefault(); // keep input focused so blur doesn't close popup first
14808
+ onSelect(v);
14809
+ }}
14810
+ className="relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-semantic-bg-ui focus:bg-semantic-bg-ui"
14811
+ >
14812
+ {v}
14813
+ </button>
14814
+ ))}
14815
+ </div>
14816
+ );
14817
+ }
14818
+
14819
+ // \u2500\u2500 VariableInput \u2014 input with {{ autocomplete \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
14820
+
14821
+ function VariableInput({
14822
+ value,
14823
+ onChange,
14824
+ sessionVariables,
14825
+ placeholder,
14826
+ maxLength,
14827
+ className,
14828
+ inputRef: externalInputRef,
14829
+ ...inputProps
14830
+ }: {
14831
+ value: string;
14832
+ onChange: (v: string) => void;
14833
+ sessionVariables: string[];
14834
+ placeholder?: string;
14835
+ maxLength?: number;
14836
+ className?: string;
14837
+ inputRef?: React.RefObject<HTMLInputElement>;
14838
+ [k: string]: unknown;
14839
+ }) {
14840
+ const internalRef = React.useRef<HTMLInputElement>(null);
14841
+ const inputRef = externalInputRef ?? internalRef;
14842
+ const [trigger, setTrigger] = React.useState<TriggerState | null>(null);
14843
+ const [popupStyle, setPopupStyle] = React.useState<React.CSSProperties | undefined>();
14844
+
14845
+ const filtered = trigger
14846
+ ? sessionVariables.filter((v) => v.toLowerCase().includes(trigger.query))
14847
+ : [];
14848
+
14849
+ const updatePopupPos = (el: HTMLInputElement, cursor: number) => {
14850
+ const caret = getCaretPixelPos(el, cursor);
14851
+ const lineHeight = parseFloat(window.getComputedStyle(el).lineHeight) || 20;
14852
+ const left = Math.min(caret.left, Math.max(0, el.offsetWidth - 320));
14853
+ setPopupStyle({ top: caret.top + lineHeight, left });
14854
+ };
14855
+
14856
+ const clearTrigger = () => {
14857
+ setTrigger(null);
14858
+ setPopupStyle(undefined);
14859
+ };
14860
+
14861
+ const handleSelect = (variable: string) => {
14862
+ if (!trigger) return;
14863
+ onChange(insertVar(value, variable, trigger.from, trigger.to));
14864
+ clearTrigger();
14865
+ requestAnimationFrame(() => {
14866
+ const el = inputRef.current;
14867
+ if (el) {
14868
+ const pos = trigger.from + variable.length;
14869
+ el.focus();
14870
+ el.setSelectionRange(pos, pos);
14871
+ }
14872
+ });
14873
+ };
14874
+
14875
+ return (
14876
+ <div className="relative w-full">
14877
+ <input
14878
+ ref={inputRef}
14879
+ type="text"
14880
+ value={value}
14881
+ placeholder={placeholder}
14882
+ maxLength={maxLength}
14883
+ className={className}
14884
+ onChange={(e) => {
14885
+ onChange(e.target.value);
14886
+ const cursor = e.target.selectionStart ?? e.target.value.length;
14887
+ const t = detectVarTrigger(e.target.value, cursor);
14888
+ setTrigger(t);
14889
+ if (t) updatePopupPos(e.target, cursor);
14890
+ else setPopupStyle(undefined);
14891
+ }}
14892
+ onKeyDown={(e) => {
14893
+ if (e.key === "Escape") clearTrigger();
14894
+ }}
14895
+ onBlur={() => clearTrigger()}
14896
+ {...inputProps}
14897
+ />
14898
+ <VarPopup variables={filtered} onSelect={handleSelect} style={popupStyle} />
14899
+ </div>
14900
+ );
14901
+ }
14902
+
14730
14903
  // \u2500\u2500 Shared input/textarea styles \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
14731
14904
  const inputCls = cn(
14732
14905
  "w-full h-[42px] px-4 text-base rounded border",
14733
14906
  "border-semantic-border-input bg-semantic-bg-primary",
14734
14907
  "text-semantic-text-primary placeholder:text-semantic-text-muted",
14735
14908
  "outline-none hover:border-semantic-border-input-focus",
14736
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
14909
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
14910
+ "disabled:opacity-50 disabled:cursor-not-allowed"
14737
14911
  );
14738
14912
 
14739
14913
  const textareaCls = cn(
@@ -14741,7 +14915,8 @@ const textareaCls = cn(
14741
14915
  "border-semantic-border-input bg-semantic-bg-primary",
14742
14916
  "text-semantic-text-primary placeholder:text-semantic-text-muted",
14743
14917
  "outline-none hover:border-semantic-border-input-focus",
14744
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
14918
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
14919
+ "disabled:opacity-50 disabled:cursor-not-allowed"
14745
14920
  );
14746
14921
 
14747
14922
  // \u2500\u2500 KeyValueTable \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
@@ -14756,6 +14931,8 @@ function KeyValueTable({
14756
14931
  valueMaxLength,
14757
14932
  keyRegex,
14758
14933
  keyRegexError,
14934
+ sessionVariables = [],
14935
+ disabled = false,
14759
14936
  }: {
14760
14937
  rows: KeyValuePair[];
14761
14938
  onChange: (rows: KeyValuePair[]) => void;
@@ -14765,6 +14942,8 @@ function KeyValueTable({
14765
14942
  valueMaxLength?: number;
14766
14943
  keyRegex?: RegExp;
14767
14944
  keyRegexError?: string;
14945
+ sessionVariables?: string[];
14946
+ disabled?: boolean;
14768
14947
  }) {
14769
14948
  const update = (id: string, patch: Partial<KeyValuePair>) => {
14770
14949
  // Replace spaces with hyphens in key values
@@ -14827,8 +15006,10 @@ function KeyValueTable({
14827
15006
  onChange={(e) => update(row.id, { key: e.target.value })}
14828
15007
  placeholder="Key"
14829
15008
  maxLength={keyMaxLength}
15009
+ disabled={disabled}
14830
15010
  className={cn(
14831
15011
  "w-full px-3 py-2.5 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-semantic-bg-primary outline-none focus:bg-semantic-bg-hover",
15012
+ "disabled:opacity-50 disabled:cursor-not-allowed",
14832
15013
  errors.key && "border-semantic-error-primary"
14833
15014
  )}
14834
15015
  aria-invalid={Boolean(errors.key)}
@@ -14841,19 +15022,21 @@ function KeyValueTable({
14841
15022
  )}
14842
15023
  </div>
14843
15024
 
14844
- {/* Value column */}
15025
+ {/* Value column \u2014 uses VariableInput for {{ autocomplete */}
14845
15026
  <div className="flex-[2] flex flex-col min-w-0">
14846
15027
  <span className="sm:hidden px-3 pt-2.5 pb-0.5 text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14847
15028
  Value
14848
15029
  </span>
14849
- <input
14850
- type="text"
15030
+ <VariableInput
14851
15031
  value={row.value}
14852
- onChange={(e) => update(row.id, { value: e.target.value })}
15032
+ onChange={(v) => update(row.id, { value: v })}
15033
+ sessionVariables={sessionVariables}
14853
15034
  placeholder="Type {{ to add variables"
14854
15035
  maxLength={valueMaxLength}
15036
+ disabled={disabled}
14855
15037
  className={cn(
14856
15038
  "w-full px-3 py-2.5 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-semantic-bg-primary outline-none focus:bg-semantic-bg-hover",
15039
+ "disabled:opacity-50 disabled:cursor-not-allowed",
14857
15040
  errors.value && "border-semantic-error-primary"
14858
15041
  )}
14859
15042
  aria-invalid={Boolean(errors.value)}
@@ -14873,6 +15056,7 @@ function KeyValueTable({
14873
15056
  variant="ghost"
14874
15057
  size="icon"
14875
15058
  onClick={() => remove(row.id)}
15059
+ disabled={disabled}
14876
15060
  className={cn("rounded-md", deleteRowButtonClass)}
14877
15061
  aria-label="Delete row"
14878
15062
  >
@@ -14887,7 +15071,11 @@ function KeyValueTable({
14887
15071
  <button
14888
15072
  type="button"
14889
15073
  onClick={add}
14890
- className="w-full flex items-center gap-2 px-3 py-2.5 text-sm text-semantic-text-muted hover:bg-semantic-bg-hover transition-colors"
15074
+ disabled={disabled}
15075
+ className={cn(
15076
+ "w-full flex items-center gap-2 px-3 py-2.5 text-sm text-semantic-text-muted hover:bg-semantic-bg-hover transition-colors",
15077
+ disabled && "opacity-50 cursor-not-allowed"
15078
+ )}
14891
15079
  >
14892
15080
  <Plus className="size-3.5 shrink-0" />
14893
15081
  <span>Add row</span>
@@ -14914,6 +15102,8 @@ export const CreateFunctionModal = React.forwardRef<
14914
15102
  promptMaxLength = 1000,
14915
15103
  initialStep = 1,
14916
15104
  initialTab = "header",
15105
+ sessionVariables = DEFAULT_SESSION_VARIABLES,
15106
+ disabled = false,
14917
15107
  className,
14918
15108
  },
14919
15109
  ref
@@ -14937,6 +15127,46 @@ export const CreateFunctionModal = React.forwardRef<
14937
15127
  const [urlError, setUrlError] = React.useState("");
14938
15128
  const [bodyError, setBodyError] = React.useState("");
14939
15129
 
15130
+ // Variable trigger state for URL and body
15131
+ const urlInputRef = React.useRef<HTMLInputElement>(null);
15132
+ const bodyTextareaRef = React.useRef<HTMLTextAreaElement>(null);
15133
+ const [urlTrigger, setUrlTrigger] = React.useState<TriggerState | null>(null);
15134
+ const [bodyTrigger, setBodyTrigger] = React.useState<TriggerState | null>(null);
15135
+ const [urlPopupStyle, setUrlPopupStyle] = React.useState<React.CSSProperties | undefined>();
15136
+ const [bodyPopupStyle, setBodyPopupStyle] = React.useState<React.CSSProperties | undefined>();
15137
+
15138
+ const filteredUrlVars = urlTrigger
15139
+ ? sessionVariables.filter((v) => v.toLowerCase().includes(urlTrigger.query))
15140
+ : [];
15141
+ const filteredBodyVars = bodyTrigger
15142
+ ? sessionVariables.filter((v) => v.toLowerCase().includes(bodyTrigger.query))
15143
+ : [];
15144
+
15145
+ const computePopupStyle = (
15146
+ el: HTMLTextAreaElement | HTMLInputElement,
15147
+ cursor: number
15148
+ ): React.CSSProperties => {
15149
+ const caret = getCaretPixelPos(el, cursor);
15150
+ const lineHeight = parseFloat(window.getComputedStyle(el).lineHeight) || 20;
15151
+ const left = Math.min(caret.left, Math.max(0, el.offsetWidth - 320));
15152
+ return { top: caret.top + lineHeight, left };
15153
+ };
15154
+
15155
+ // Test variable values \u2014 filled by user before clicking Test API
15156
+ const [testVarValues, setTestVarValues] = React.useState<Record<string, string>>({});
15157
+
15158
+ // Unique {{variable}} refs found across url, body, headers, queryParams
15159
+ const testableVars = React.useMemo(
15160
+ () =>
15161
+ extractVarRefs([
15162
+ url,
15163
+ body,
15164
+ ...headers.map((h) => h.value),
15165
+ ...queryParams.map((q) => q.value),
15166
+ ]),
15167
+ [url, body, headers, queryParams]
15168
+ );
15169
+
14940
15170
  // Sync form state from initialData each time the modal opens
14941
15171
  React.useEffect(() => {
14942
15172
  if (open) {
@@ -14954,6 +15184,11 @@ export const CreateFunctionModal = React.forwardRef<
14954
15184
  setNameError("");
14955
15185
  setUrlError("");
14956
15186
  setBodyError("");
15187
+ setUrlTrigger(null);
15188
+ setBodyTrigger(null);
15189
+ setUrlPopupStyle(undefined);
15190
+ setBodyPopupStyle(undefined);
15191
+ setTestVarValues({});
14957
15192
  }
14958
15193
  // Re-run only when modal opens; intentionally exclude deep deps to avoid mid-session resets
14959
15194
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -14974,6 +15209,11 @@ export const CreateFunctionModal = React.forwardRef<
14974
15209
  setNameError("");
14975
15210
  setUrlError("");
14976
15211
  setBodyError("");
15212
+ setUrlTrigger(null);
15213
+ setBodyTrigger(null);
15214
+ setUrlPopupStyle(undefined);
15215
+ setBodyPopupStyle(undefined);
15216
+ setTestVarValues({});
14977
15217
  }, [initialData, initialStep, initialTab]);
14978
15218
 
14979
15219
  const handleClose = React.useCallback(() => {
@@ -15020,7 +15260,7 @@ export const CreateFunctionModal = React.forwardRef<
15020
15260
  };
15021
15261
 
15022
15262
  const handleNext = () => {
15023
- if (name.trim() && prompt.trim().length >= promptMinLength) setStep(2);
15263
+ if (disabled || (name.trim() && prompt.trim().length >= promptMinLength)) setStep(2);
15024
15264
  };
15025
15265
 
15026
15266
  const queryParamsHaveErrors = (rows: KeyValuePair[]): boolean =>
@@ -15051,16 +15291,20 @@ export const CreateFunctionModal = React.forwardRef<
15051
15291
  handleClose();
15052
15292
  };
15053
15293
 
15294
+ // Substitute {{variable}} references with user-provided test values before calling onTestApi
15295
+ const substituteVars = (text: string) =>
15296
+ text.replace(/\\{\\{[^}]+\\}\\}/g, (match) => testVarValues[match] ?? match);
15297
+
15054
15298
  const handleTestApi = async () => {
15055
15299
  if (!onTestApi) return;
15056
15300
  setIsTesting(true);
15057
15301
  try {
15058
15302
  const step2: CreateFunctionStep2Data = {
15059
15303
  method,
15060
- url,
15061
- headers,
15062
- queryParams,
15063
- body,
15304
+ url: substituteVars(url),
15305
+ headers: headers.map((h) => ({ ...h, value: substituteVars(h.value) })),
15306
+ queryParams: queryParams.map((q) => ({ ...q, value: substituteVars(q.value) })),
15307
+ body: substituteVars(body),
15064
15308
  };
15065
15309
  const response = await onTestApi(step2);
15066
15310
  setApiResponse(response);
@@ -15069,6 +15313,38 @@ export const CreateFunctionModal = React.forwardRef<
15069
15313
  }
15070
15314
  };
15071
15315
 
15316
+ // URL variable insertion
15317
+ const handleUrlVarSelect = (variable: string) => {
15318
+ if (!urlTrigger) return;
15319
+ setUrl(insertVar(url, variable, urlTrigger.from, urlTrigger.to));
15320
+ setUrlTrigger(null);
15321
+ setUrlPopupStyle(undefined);
15322
+ requestAnimationFrame(() => {
15323
+ const el = urlInputRef.current;
15324
+ if (el) {
15325
+ const pos = urlTrigger.from + variable.length;
15326
+ el.focus();
15327
+ el.setSelectionRange(pos, pos);
15328
+ }
15329
+ });
15330
+ };
15331
+
15332
+ // Body variable insertion
15333
+ const handleBodyVarSelect = (variable: string) => {
15334
+ if (!bodyTrigger) return;
15335
+ setBody(insertVar(body, variable, bodyTrigger.from, bodyTrigger.to));
15336
+ setBodyTrigger(null);
15337
+ setBodyPopupStyle(undefined);
15338
+ requestAnimationFrame(() => {
15339
+ const el = bodyTextareaRef.current;
15340
+ if (el) {
15341
+ const pos = bodyTrigger.from + variable.length;
15342
+ el.focus();
15343
+ el.setSelectionRange(pos, pos);
15344
+ }
15345
+ });
15346
+ };
15347
+
15072
15348
  const headersHaveKeyErrors = headers.some(
15073
15349
  (row) => row.key.trim() && HEADER_KEY_REGEX && !HEADER_KEY_REGEX.test(row.key)
15074
15350
  );
@@ -15137,6 +15413,7 @@ export const CreateFunctionModal = React.forwardRef<
15137
15413
  type="text"
15138
15414
  value={name}
15139
15415
  maxLength={FUNCTION_NAME_MAX}
15416
+ disabled={disabled}
15140
15417
  onChange={(e) => {
15141
15418
  setName(e.target.value);
15142
15419
  if (nameError) validateName(e.target.value);
@@ -15167,6 +15444,7 @@ export const CreateFunctionModal = React.forwardRef<
15167
15444
  id="fn-prompt"
15168
15445
  value={prompt}
15169
15446
  maxLength={promptMaxLength}
15447
+ disabled={disabled}
15170
15448
  onChange={(e) => setPrompt(e.target.value)}
15171
15449
  placeholder="Enter the description of the function"
15172
15450
  rows={5}
@@ -15195,7 +15473,7 @@ export const CreateFunctionModal = React.forwardRef<
15195
15473
  </span>
15196
15474
  <div
15197
15475
  className={cn(
15198
- "flex h-[42px] rounded border border-semantic-border-input overflow-hidden bg-semantic-bg-primary",
15476
+ "flex h-[42px] rounded border border-semantic-border-input overflow-visible bg-semantic-bg-primary",
15199
15477
  "hover:border-semantic-border-input-focus",
15200
15478
  "focus-within:border-semantic-border-input-focus focus-within:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
15201
15479
  "transition-shadow"
@@ -15205,10 +15483,14 @@ export const CreateFunctionModal = React.forwardRef<
15205
15483
  <div className="relative shrink-0 border-r border-semantic-border-layout">
15206
15484
  <select
15207
15485
  value={method}
15486
+ disabled={disabled}
15208
15487
  onChange={(e) =>
15209
15488
  setMethod(e.target.value as HttpMethod)
15210
15489
  }
15211
- className="h-full w-[80px] pl-3 pr-7 text-base text-semantic-text-primary bg-transparent outline-none cursor-pointer appearance-none sm:w-[100px]"
15490
+ className={cn(
15491
+ "h-full w-[80px] pl-3 pr-7 text-base text-semantic-text-primary bg-transparent outline-none cursor-pointer appearance-none sm:w-[100px]",
15492
+ disabled && "opacity-50 cursor-not-allowed"
15493
+ )}
15212
15494
  aria-label="HTTP method"
15213
15495
  >
15214
15496
  {HTTP_METHODS.map((m) => (
@@ -15222,19 +15504,39 @@ export const CreateFunctionModal = React.forwardRef<
15222
15504
  aria-hidden="true"
15223
15505
  />
15224
15506
  </div>
15225
- {/* URL input */}
15226
- <input
15227
- type="text"
15228
- value={url}
15229
- maxLength={URL_MAX}
15230
- onChange={(e) => {
15231
- setUrl(e.target.value);
15232
- if (urlError) validateUrl(e.target.value);
15233
- }}
15234
- onBlur={(e) => validateUrl(e.target.value)}
15235
- placeholder="Enter URL or Type {{ to add variables"
15236
- className="flex-1 min-w-0 px-3 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
15237
- />
15507
+ {/* URL input with {{ trigger */}
15508
+ <div className="relative flex-1 min-w-0">
15509
+ <input
15510
+ ref={urlInputRef}
15511
+ type="text"
15512
+ value={url}
15513
+ maxLength={URL_MAX}
15514
+ disabled={disabled}
15515
+ onChange={(e) => {
15516
+ setUrl(e.target.value);
15517
+ if (urlError) validateUrl(e.target.value);
15518
+ const cursor = e.target.selectionStart ?? e.target.value.length;
15519
+ const t = detectVarTrigger(e.target.value, cursor);
15520
+ setUrlTrigger(t);
15521
+ if (t) setUrlPopupStyle(computePopupStyle(e.target, cursor));
15522
+ else setUrlPopupStyle(undefined);
15523
+ }}
15524
+ onKeyDown={(e) => {
15525
+ if (e.key === "Escape") { setUrlTrigger(null); setUrlPopupStyle(undefined); }
15526
+ }}
15527
+ onBlur={(e) => {
15528
+ validateUrl(e.target.value);
15529
+ setUrlTrigger(null);
15530
+ setUrlPopupStyle(undefined);
15531
+ }}
15532
+ placeholder="Enter URL or Type {{ to add variables"
15533
+ className={cn(
15534
+ "h-full w-full px-3 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none",
15535
+ disabled && "opacity-50 cursor-not-allowed"
15536
+ )}
15537
+ />
15538
+ <VarPopup variables={filteredUrlVars} onSelect={handleUrlVarSelect} style={urlPopupStyle} />
15539
+ </div>
15238
15540
  </div>
15239
15541
  {urlError && (
15240
15542
  <p className="m-0 text-xs text-semantic-error-primary">{urlError}</p>
@@ -15276,6 +15578,8 @@ export const CreateFunctionModal = React.forwardRef<
15276
15578
  valueMaxLength={HEADER_VALUE_MAX}
15277
15579
  keyRegex={HEADER_KEY_REGEX}
15278
15580
  keyRegexError="Invalid header key. Use only alphanumeric and !#$%&'*+-.^_\`|~ characters."
15581
+ sessionVariables={sessionVariables}
15582
+ disabled={disabled}
15279
15583
  />
15280
15584
  )}
15281
15585
  {activeTab === "queryParams" && (
@@ -15293,6 +15597,8 @@ export const CreateFunctionModal = React.forwardRef<
15293
15597
  value: validateQueryParamValue(row.value),
15294
15598
  };
15295
15599
  }}
15600
+ sessionVariables={sessionVariables}
15601
+ disabled={disabled}
15296
15602
  />
15297
15603
  )}
15298
15604
  {activeTab === "body" && (
@@ -15302,13 +15608,27 @@ export const CreateFunctionModal = React.forwardRef<
15302
15608
  </span>
15303
15609
  <div className={cn("relative")}>
15304
15610
  <textarea
15611
+ ref={bodyTextareaRef}
15305
15612
  value={body}
15306
15613
  maxLength={BODY_MAX}
15614
+ disabled={disabled}
15307
15615
  onChange={(e) => {
15308
15616
  setBody(e.target.value);
15309
15617
  if (bodyError) validateBody(e.target.value);
15618
+ const cursor = e.target.selectionStart ?? e.target.value.length;
15619
+ const t = detectVarTrigger(e.target.value, cursor);
15620
+ setBodyTrigger(t);
15621
+ if (t) setBodyPopupStyle(computePopupStyle(e.target, cursor));
15622
+ else setBodyPopupStyle(undefined);
15623
+ }}
15624
+ onKeyDown={(e) => {
15625
+ if (e.key === "Escape") { setBodyTrigger(null); setBodyPopupStyle(undefined); }
15626
+ }}
15627
+ onBlur={(e) => {
15628
+ validateBody(e.target.value);
15629
+ setBodyTrigger(null);
15630
+ setBodyPopupStyle(undefined);
15310
15631
  }}
15311
- onBlur={(e) => validateBody(e.target.value)}
15312
15632
  placeholder="Enter request body (JSON). Type {{ to add variables"
15313
15633
  rows={6}
15314
15634
  className={cn(textareaCls, "pb-7")}
@@ -15316,6 +15636,7 @@ export const CreateFunctionModal = React.forwardRef<
15316
15636
  <span className="absolute bottom-2 right-3 text-xs italic text-semantic-text-muted pointer-events-none">
15317
15637
  {body.length}/{BODY_MAX}
15318
15638
  </span>
15639
+ <VarPopup variables={filteredBodyVars} onSelect={handleBodyVarSelect} style={bodyPopupStyle} />
15319
15640
  </div>
15320
15641
  {bodyError && (
15321
15642
  <p className="m-0 text-xs text-semantic-error-primary">{bodyError}</p>
@@ -15333,6 +15654,34 @@ export const CreateFunctionModal = React.forwardRef<
15333
15654
  <div className="border-t border-semantic-border-layout" />
15334
15655
  </div>
15335
15656
 
15657
+ {/* Variable test values \u2014 shown when URL/body/params contain {{variables}} */}
15658
+ {testableVars.length > 0 && (
15659
+ <div className="flex flex-col gap-2">
15660
+ <span className="text-xs text-semantic-text-muted">
15661
+ Variable values for testing
15662
+ </span>
15663
+ {testableVars.map((variable) => (
15664
+ <div key={variable} className="flex items-center gap-3">
15665
+ <span className="text-xs text-semantic-text-muted font-mono shrink-0 min-w-[120px]">
15666
+ {variable}
15667
+ </span>
15668
+ <input
15669
+ type="text"
15670
+ value={testVarValues[variable] ?? ""}
15671
+ onChange={(e) =>
15672
+ setTestVarValues((prev) => ({
15673
+ ...prev,
15674
+ [variable]: e.target.value,
15675
+ }))
15676
+ }
15677
+ placeholder="Enter test value"
15678
+ className={cn(inputCls, "flex-1 h-9 text-sm")}
15679
+ />
15680
+ </div>
15681
+ ))}
15682
+ </div>
15683
+ )}
15684
+
15336
15685
  <button
15337
15686
  type="button"
15338
15687
  onClick={handleTestApi}
@@ -15374,7 +15723,7 @@ export const CreateFunctionModal = React.forwardRef<
15374
15723
  variant="default"
15375
15724
  className="flex-1 sm:flex-none"
15376
15725
  onClick={handleNext}
15377
- disabled={!isStep1Valid}
15726
+ disabled={!disabled && !isStep1Valid}
15378
15727
  >
15379
15728
  Next
15380
15729
  </Button>
@@ -15392,7 +15741,7 @@ export const CreateFunctionModal = React.forwardRef<
15392
15741
  variant="default"
15393
15742
  className="flex-1 sm:flex-none"
15394
15743
  onClick={handleSubmit}
15395
- disabled={!isStep2Valid}
15744
+ disabled={!isStep2Valid || disabled}
15396
15745
  >
15397
15746
  Submit
15398
15747
  </Button>
@@ -15806,9 +16155,16 @@ export interface BotBehaviorCardProps {
15806
16155
  data: Partial<BotBehaviorData>;
15807
16156
  /** Callback when any field changes */
15808
16157
  onChange: (patch: Partial<BotBehaviorData>) => void;
15809
- /** Called when the system prompt textarea loses focus */
16158
+ /**
16159
+ * Called when focus leaves the **entire** prompt section (textarea + session
16160
+ * variable chips). Clicking a chip or the instruction text does NOT trigger
16161
+ * this \u2014 only clicking outside the whole section does.
16162
+ *
16163
+ * Use this to persist the system prompt (e.g. fire an API call) once the
16164
+ * user is done editing, including any variables they just inserted.
16165
+ */
15810
16166
  onSystemPromptBlur?: (value: string) => void;
15811
- /** Session variables shown as insertable chips */
16167
+ /** Session variables shown as insertable chips and in the {{ autocomplete dropdown */
15812
16168
  sessionVariables?: string[];
15813
16169
  /** Maximum character length for the system prompt textarea (default: 5000, per Figma) */
15814
16170
  maxLength?: number;
@@ -15826,6 +16182,115 @@ const DEFAULT_SESSION_VARIABLES = [
15826
16182
  "{{Contact Details}}",
15827
16183
  ];
15828
16184
 
16185
+ // \u2500\u2500\u2500 Variable trigger helpers \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
16186
+
16187
+ interface TriggerState {
16188
+ query: string;
16189
+ from: number;
16190
+ to: number;
16191
+ }
16192
+
16193
+ function detectVarTrigger(value: string, cursor: number): TriggerState | null {
16194
+ const before = value.slice(0, cursor);
16195
+ const match = /\\{\\{([^}]*)$/.exec(before);
16196
+ if (!match) return null;
16197
+ return { query: match[1].toLowerCase(), from: match.index, to: cursor };
16198
+ }
16199
+
16200
+ function insertVar(value: string, variable: string, from: number, to: number): string {
16201
+ return value.slice(0, from) + variable + value.slice(to);
16202
+ }
16203
+
16204
+ /**
16205
+ * Mirror-div technique: create an invisible clone of the element with identical
16206
+ * styles, fill it with text up to the cursor, place a zero-width marker span at
16207
+ * the end, and read the marker's position to get pixel-exact cursor coordinates.
16208
+ * Returns { top, left } relative to the element's own top-left corner.
16209
+ */
16210
+ function getCaretPixelPos(
16211
+ el: HTMLTextAreaElement | HTMLInputElement,
16212
+ position: number
16213
+ ): { top: number; left: number } {
16214
+ const cs = window.getComputedStyle(el);
16215
+ const mirror = document.createElement("div");
16216
+
16217
+ // Copy every style property that affects text layout
16218
+ (
16219
+ [
16220
+ "boxSizing", "width", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
16221
+ "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth",
16222
+ "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontVariant",
16223
+ "letterSpacing", "lineHeight", "textTransform", "wordSpacing", "tabSize",
16224
+ ] as (keyof CSSStyleDeclaration)[]
16225
+ ).forEach((prop) => {
16226
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16227
+ (mirror.style as any)[prop] = cs[prop];
16228
+ });
16229
+
16230
+ // textarea wraps; input does not
16231
+ mirror.style.whiteSpace = el.tagName === "TEXTAREA" ? "pre-wrap" : "pre";
16232
+ mirror.style.wordWrap = el.tagName === "TEXTAREA" ? "break-word" : "normal";
16233
+ mirror.style.position = "absolute";
16234
+ mirror.style.visibility = "hidden";
16235
+ mirror.style.overflow = "hidden";
16236
+ mirror.style.top = "0";
16237
+ mirror.style.left = "0";
16238
+ mirror.style.width = el.offsetWidth + "px";
16239
+
16240
+ document.body.appendChild(mirror);
16241
+ mirror.appendChild(document.createTextNode(el.value.substring(0, position)));
16242
+
16243
+ const marker = document.createElement("span");
16244
+ marker.textContent = "\\u200b"; // zero-width space
16245
+ mirror.appendChild(marker);
16246
+
16247
+ const markerRect = marker.getBoundingClientRect();
16248
+ const mirrorRect = mirror.getBoundingClientRect();
16249
+ document.body.removeChild(mirror);
16250
+
16251
+ const scrollTop = el instanceof HTMLTextAreaElement ? el.scrollTop : 0;
16252
+ return {
16253
+ top: markerRect.top - mirrorRect.top - scrollTop,
16254
+ left: markerRect.left - mirrorRect.left,
16255
+ };
16256
+ }
16257
+
16258
+ // Uses the same visual classes as DropdownMenuContent + DropdownMenuItem.
16259
+ // Position is driven by cursor coordinates from getCaretPixelPos.
16260
+ function VarPopup({
16261
+ variables,
16262
+ onSelect,
16263
+ style,
16264
+ }: {
16265
+ variables: string[];
16266
+ onSelect: (v: string) => void;
16267
+ style?: React.CSSProperties;
16268
+ }) {
16269
+ if (variables.length === 0) return null;
16270
+ return (
16271
+ <div
16272
+ role="listbox"
16273
+ style={style}
16274
+ className="absolute z-[9999] min-w-[8rem] max-w-xs overflow-hidden rounded-md border border-semantic-border-layout bg-semantic-bg-primary p-1 text-semantic-text-primary shadow-md"
16275
+ >
16276
+ {variables.map((v) => (
16277
+ <button
16278
+ key={v}
16279
+ type="button"
16280
+ role="option"
16281
+ onMouseDown={(e) => {
16282
+ e.preventDefault(); // keep textarea focused so blur doesn't close popup first
16283
+ onSelect(v);
16284
+ }}
16285
+ className="relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-semantic-bg-ui focus:bg-semantic-bg-ui"
16286
+ >
16287
+ {v}
16288
+ </button>
16289
+ ))}
16290
+ </div>
16291
+ );
16292
+ }
16293
+
15829
16294
  // \u2500\u2500\u2500 Internal helpers \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
15830
16295
 
15831
16296
  function SectionCard({
@@ -15854,44 +16319,6 @@ function SectionCard({
15854
16319
  );
15855
16320
  }
15856
16321
 
15857
- function StyledTextarea({
15858
- placeholder,
15859
- value,
15860
- rows = 3,
15861
- onChange,
15862
- onBlur,
15863
- disabled,
15864
- className,
15865
- }: {
15866
- placeholder?: string;
15867
- value?: string;
15868
- rows?: number;
15869
- onChange?: (v: string) => void;
15870
- onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
15871
- disabled?: boolean;
15872
- className?: string;
15873
- }) {
15874
- return (
15875
- <textarea
15876
- value={value ?? ""}
15877
- rows={rows}
15878
- onChange={(e) => onChange?.(e.target.value)}
15879
- onBlur={onBlur}
15880
- placeholder={placeholder}
15881
- disabled={disabled}
15882
- className={cn(
15883
- "w-full px-4 py-2.5 text-base rounded border resize-none",
15884
- "border-semantic-border-input bg-semantic-bg-primary",
15885
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
15886
- "outline-none hover:border-semantic-border-input-focus",
15887
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
15888
- disabled && "opacity-50 cursor-not-allowed",
15889
- className
15890
- )}
15891
- />
15892
- );
15893
- }
15894
-
15895
16322
  // \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
15896
16323
 
15897
16324
  const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
@@ -15909,50 +16336,129 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15909
16336
  ) => {
15910
16337
  const prompt = data.systemPrompt ?? "";
15911
16338
  const MAX = maxLength;
15912
- const footerRef = React.useRef<HTMLDivElement>(null);
15913
- /** Set on footer mousedown so blur does not trigger API when user clicked under the input (instruction/chips). */
15914
- const footerClickInProgressRef = React.useRef(false);
16339
+ const sectionRef = React.useRef<HTMLDivElement>(null);
16340
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
16341
+ /** Tracks whether the section has been focused at least once (prevents firing blur on initial render). */
16342
+ const hasFocusedRef = React.useRef(false);
16343
+
16344
+ const [varTrigger, setVarTrigger] = React.useState<TriggerState | null>(null);
16345
+ const [popupStyle, setPopupStyle] = React.useState<React.CSSProperties | undefined>();
16346
+
16347
+ const filteredVars = varTrigger
16348
+ ? sessionVariables.filter((v) =>
16349
+ v.toLowerCase().includes(varTrigger.query)
16350
+ )
16351
+ : [];
16352
+
16353
+ /** Compute popup pixel position anchored to the cursor, clamped within the textarea. */
16354
+ const updatePopupPos = (el: HTMLTextAreaElement, cursor: number) => {
16355
+ const caret = getCaretPixelPos(el, cursor);
16356
+ const lineHeight = parseFloat(window.getComputedStyle(el).lineHeight) || 20;
16357
+ const top = caret.top + lineHeight;
16358
+ // Clamp left so popup (max-w-xs = 320px) doesn't overflow the textarea width
16359
+ const left = Math.min(caret.left, Math.max(0, el.offsetWidth - 320));
16360
+ setPopupStyle({ top, left });
16361
+ };
16362
+
16363
+ const clearTrigger = () => {
16364
+ setVarTrigger(null);
16365
+ setPopupStyle(undefined);
16366
+ };
15915
16367
 
15916
16368
  const insertVariable = (variable: string) => {
15917
16369
  onChange({ systemPrompt: prompt + variable });
15918
16370
  };
15919
16371
 
15920
- const handleSystemPromptBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
15921
- if (!onSystemPromptBlur) return;
15922
- const relatedTarget = e.relatedTarget as Node | null;
15923
- const footerEl = footerRef.current;
15924
- if (footerClickInProgressRef.current) {
15925
- footerClickInProgressRef.current = false;
15926
- return;
16372
+ const handleVarSelect = (variable: string) => {
16373
+ if (!varTrigger) return;
16374
+ const newVal = insertVar(prompt, variable, varTrigger.from, varTrigger.to);
16375
+ if (newVal.length <= MAX) onChange({ systemPrompt: newVal });
16376
+ clearTrigger();
16377
+ // Restore focus and place cursor after inserted variable
16378
+ requestAnimationFrame(() => {
16379
+ const el = textareaRef.current;
16380
+ if (el) {
16381
+ const pos = varTrigger.from + variable.length;
16382
+ el.focus();
16383
+ el.setSelectionRange(pos, pos);
16384
+ }
16385
+ });
16386
+ };
16387
+
16388
+ const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
16389
+ const v = e.target.value;
16390
+ if (v.length <= MAX) {
16391
+ onChange({ systemPrompt: v });
16392
+ const trigger = detectVarTrigger(v, e.target.selectionStart);
16393
+ setVarTrigger(trigger);
16394
+ if (trigger) updatePopupPos(e.target, e.target.selectionStart);
16395
+ else setPopupStyle(undefined);
15927
16396
  }
15928
- if (footerEl && relatedTarget && footerEl.contains(relatedTarget)) {
15929
- return;
16397
+ };
16398
+
16399
+ const handlePromptKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
16400
+ if (e.key === "Escape" && varTrigger) {
16401
+ e.preventDefault();
16402
+ clearTrigger();
15930
16403
  }
15931
- onSystemPromptBlur(e.target.value);
15932
16404
  };
15933
16405
 
15934
- const handleFooterMouseDown = () => {
15935
- footerClickInProgressRef.current = true;
16406
+ /**
16407
+ * Fires when focus enters the prompt section (textarea or any chip button).
16408
+ * We track this so the section-level blur only fires after the user has
16409
+ * actually interacted with the section.
16410
+ */
16411
+ const handleSectionFocus = () => {
16412
+ hasFocusedRef.current = true;
16413
+ };
16414
+
16415
+ /**
16416
+ * Fires when focus leaves any element inside the prompt section.
16417
+ * We check \`relatedTarget\` \u2014 if the new focus target is still inside
16418
+ * this section, we do nothing. Only when focus moves fully outside
16419
+ * do we fire \`onSystemPromptBlur\` with the current prompt value.
16420
+ */
16421
+ const handleSectionBlur = (e: React.FocusEvent<HTMLDivElement>) => {
16422
+ clearTrigger();
16423
+ if (!onSystemPromptBlur || !hasFocusedRef.current) return;
16424
+ const section = sectionRef.current;
16425
+ const next = e.relatedTarget as Node | null;
16426
+ // Focus moved to another element inside this section \u2014 ignore
16427
+ if (section && next && section.contains(next)) return;
16428
+ onSystemPromptBlur(prompt);
15936
16429
  };
15937
16430
 
15938
16431
  return (
15939
16432
  <div ref={ref} className={className}>
15940
16433
  <SectionCard title="How It Behaves">
15941
- <div className="flex flex-col gap-3">
16434
+ {/* onBlur is on this wrapper so clicking chips / instruction text
16435
+ does NOT fire the callback \u2014 only clicking outside fires it. */}
16436
+ <div
16437
+ ref={sectionRef}
16438
+ className="flex flex-col gap-3"
16439
+ onFocus={handleSectionFocus}
16440
+ onBlur={handleSectionBlur}
16441
+ >
15942
16442
  <p className="m-0 text-sm text-semantic-text-muted">
15943
16443
  Define workflows, conditions and handover logic (System prompt)
15944
16444
  </p>
15945
16445
  <div className="relative">
15946
- <StyledTextarea
16446
+ <textarea
16447
+ ref={textareaRef}
15947
16448
  value={prompt}
15948
16449
  rows={6}
15949
- onChange={(v) => {
15950
- if (v.length <= MAX) onChange({ systemPrompt: v });
15951
- }}
15952
- onBlur={handleSystemPromptBlur}
16450
+ onChange={handlePromptChange}
16451
+ onKeyDown={handlePromptKeyDown}
15953
16452
  placeholder="You are a helpful assistant. Always start by greeting the user politely: 'Hello! Welcome. How can I assist you today?'"
15954
16453
  disabled={disabled}
15955
- className="pb-10 pr-[4.5rem]"
16454
+ className={cn(
16455
+ "w-full px-4 py-2.5 text-base rounded border resize-none pb-10 pr-[4.5rem]",
16456
+ "border-semantic-border-input bg-semantic-bg-primary",
16457
+ "text-semantic-text-primary placeholder:text-semantic-text-muted",
16458
+ "outline-none hover:border-semantic-border-input-focus",
16459
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
16460
+ disabled && "opacity-50 cursor-not-allowed"
16461
+ )}
15956
16462
  />
15957
16463
  <span
15958
16464
  className="absolute bottom-3 right-4 text-sm text-semantic-text-muted pointer-events-none"
@@ -15961,12 +16467,9 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15961
16467
  >
15962
16468
  {prompt.length}/{MAX}
15963
16469
  </span>
16470
+ <VarPopup variables={filteredVars} onSelect={handleVarSelect} style={popupStyle} />
15964
16471
  </div>
15965
- <div
15966
- ref={footerRef}
15967
- className="flex flex-col gap-3"
15968
- onMouseDown={handleFooterMouseDown}
15969
- >
16472
+ <div className="flex flex-col gap-3">
15970
16473
  <p className="m-0 flex items-center gap-1.5 text-sm text-semantic-text-muted">
15971
16474
  <Info className="size-4 shrink-0 text-semantic-text-muted" aria-hidden />
15972
16475
  Type {'{{'} to enable dropdown or use the below chips to input variables.
@@ -16022,14 +16525,24 @@ export interface KnowledgeBaseCardProps {
16022
16525
  files: KnowledgeBaseFile[];
16023
16526
  /** Called when user clicks the "+ Files" button */
16024
16527
  onAdd?: () => void;
16025
- /** Called when user clicks the download button on a file */
16528
+ /**
16529
+ * Called when user clicks the download button on a file.
16530
+ * When omitted, the download button is **not rendered**.
16531
+ */
16026
16532
  onDownload?: (id: string) => void;
16027
- /** Called when user clicks the delete button on a file */
16533
+ /**
16534
+ * Called when user clicks the delete button on a file.
16535
+ * When omitted, the delete button is **not rendered**.
16536
+ */
16028
16537
  onDelete?: (id: string) => void;
16029
16538
  /** Hover text shown on the info icon next to the "Knowledge Base" title */
16030
16539
  infoTooltip?: string;
16031
- /** Disables all interactive elements in the card (view mode) */
16540
+ /** Disables the "+ Files" button and other form-level interactions (view mode) */
16032
16541
  disabled?: boolean;
16542
+ /** Independently disables the download button (e.g. user lacks download permission) */
16543
+ downloadDisabled?: boolean;
16544
+ /** Independently disables the delete button (e.g. user lacks delete permission) */
16545
+ deleteDisabled?: boolean;
16033
16546
  /** Additional className */
16034
16547
  className?: string;
16035
16548
  }
@@ -16055,6 +16568,8 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
16055
16568
  onDelete,
16056
16569
  infoTooltip,
16057
16570
  disabled,
16571
+ downloadDisabled,
16572
+ deleteDisabled,
16058
16573
  className,
16059
16574
  },
16060
16575
  ref
@@ -16123,26 +16638,32 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
16123
16638
  {status.label}
16124
16639
  </Badge>
16125
16640
  </div>
16126
- <div className="flex items-center gap-1 shrink-0 ml-2">
16127
- <button
16128
- type="button"
16129
- onClick={() => onDownload?.(file.id)}
16130
- disabled={disabled}
16131
- className={cn("p-2 rounded text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors", disabled && "opacity-50 cursor-not-allowed")}
16132
- aria-label="Download file"
16133
- >
16134
- <Download className="size-4" />
16135
- </button>
16136
- <button
16137
- type="button"
16138
- onClick={() => onDelete?.(file.id)}
16139
- disabled={disabled}
16140
- className={cn("p-2 rounded text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors", disabled && "opacity-50 cursor-not-allowed")}
16141
- aria-label="Delete file"
16142
- >
16143
- <Trash2 className="size-4" />
16144
- </button>
16145
- </div>
16641
+ {(onDownload || onDelete) && (
16642
+ <div className="flex items-center gap-1 shrink-0 ml-2">
16643
+ {onDownload && (
16644
+ <button
16645
+ type="button"
16646
+ onClick={() => onDownload(file.id)}
16647
+ disabled={downloadDisabled}
16648
+ className={cn("p-2 rounded text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors", downloadDisabled && "opacity-50 cursor-not-allowed")}
16649
+ aria-label="Download file"
16650
+ >
16651
+ <Download className="size-4" />
16652
+ </button>
16653
+ )}
16654
+ {onDelete && (
16655
+ <button
16656
+ type="button"
16657
+ onClick={() => onDelete(file.id)}
16658
+ disabled={deleteDisabled}
16659
+ className={cn("p-2 rounded text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors", deleteDisabled && "opacity-50 cursor-not-allowed")}
16660
+ aria-label="Delete file"
16661
+ >
16662
+ <Trash2 className="size-4" />
16663
+ </button>
16664
+ )}
16665
+ </div>
16666
+ )}
16146
16667
  </div>
16147
16668
  );
16148
16669
  })}
@@ -16179,14 +16700,24 @@ export interface FunctionsCardProps {
16179
16700
  functions: FunctionItem[];
16180
16701
  /** Called when user clicks the add function button */
16181
16702
  onAddFunction?: () => void;
16182
- /** Called when user edits a custom (non-built-in) function */
16703
+ /**
16704
+ * Called when user clicks the edit button on a custom function.
16705
+ * When omitted, the edit button is **not rendered**.
16706
+ */
16183
16707
  onEditFunction?: (id: string) => void;
16184
- /** Called when user deletes a custom (non-built-in) function */
16708
+ /**
16709
+ * Called when user clicks the delete button on a custom function.
16710
+ * When omitted, the delete button is **not rendered**.
16711
+ */
16185
16712
  onDeleteFunction?: (id: string) => void;
16186
16713
  /** Hover text shown on the info icon next to the "Functions" title */
16187
16714
  infoTooltip?: string;
16188
- /** Disables all interactive elements in the card (view mode) */
16715
+ /** Disables the "Add Functions" button and other form-level interactions (view mode) */
16189
16716
  disabled?: boolean;
16717
+ /** Independently disables the edit button (e.g. user lacks edit permission) */
16718
+ editDisabled?: boolean;
16719
+ /** Independently disables the delete button (e.g. user lacks delete permission) */
16720
+ deleteDisabled?: boolean;
16190
16721
  /** Additional className */
16191
16722
  className?: string;
16192
16723
  }
@@ -16194,7 +16725,7 @@ export interface FunctionsCardProps {
16194
16725
  // \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
16195
16726
 
16196
16727
  const FunctionsCard = React.forwardRef<HTMLDivElement, FunctionsCardProps>(
16197
- ({ functions, onAddFunction, onEditFunction, onDeleteFunction, infoTooltip, disabled, className }, ref) => {
16728
+ ({ functions, onAddFunction, onEditFunction, onDeleteFunction, infoTooltip, disabled, editDisabled, deleteDisabled, className }, ref) => {
16198
16729
  return (
16199
16730
  <div
16200
16731
  ref={ref}
@@ -16269,24 +16800,28 @@ const FunctionsCard = React.forwardRef<HTMLDivElement, FunctionsCardProps>(
16269
16800
  </Badge>
16270
16801
  ) : (
16271
16802
  <>
16272
- <button
16273
- type="button"
16274
- onClick={() => onEditFunction?.(fn.id)}
16275
- disabled={disabled}
16276
- className={cn("p-1.5 rounded text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors", disabled && "opacity-50 cursor-not-allowed")}
16277
- aria-label={\`Edit \${fn.name}\`}
16278
- >
16279
- <Pencil className="size-4" />
16280
- </button>
16281
- <button
16282
- type="button"
16283
- onClick={() => onDeleteFunction?.(fn.id)}
16284
- disabled={disabled}
16285
- className={cn("p-1.5 rounded text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors", disabled && "opacity-50 cursor-not-allowed")}
16286
- aria-label={\`Delete \${fn.name}\`}
16287
- >
16288
- <Trash2 className="size-4" />
16289
- </button>
16803
+ {onEditFunction && (
16804
+ <button
16805
+ type="button"
16806
+ onClick={() => onEditFunction(fn.id)}
16807
+ disabled={editDisabled}
16808
+ className={cn("p-1.5 rounded text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors", editDisabled && "opacity-50 cursor-not-allowed")}
16809
+ aria-label={\`Edit \${fn.name}\`}
16810
+ >
16811
+ <Pencil className="size-4" />
16812
+ </button>
16813
+ )}
16814
+ {onDeleteFunction && (
16815
+ <button
16816
+ type="button"
16817
+ onClick={() => onDeleteFunction(fn.id)}
16818
+ disabled={deleteDisabled}
16819
+ className={cn("p-1.5 rounded text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors", deleteDisabled && "opacity-50 cursor-not-allowed")}
16820
+ aria-label={\`Delete \${fn.name}\`}
16821
+ >
16822
+ <Trash2 className="size-4" />
16823
+ </button>
16824
+ )}
16290
16825
  </>
16291
16826
  )}
16292
16827
  </div>
@@ -16872,6 +17407,10 @@ export interface CreateFunctionModalProps {
16872
17407
  initialStep?: 1 | 2;
16873
17408
  /** Storybook/testing: start on a specific tab when initialStep=2 */
16874
17409
  initialTab?: FunctionTabType;
17410
+ /** Session variables available for {{ autocomplete in URL, body, header values, and query param values */
17411
+ sessionVariables?: string[];
17412
+ /** When true, all form fields are disabled (view mode) but Next is enabled so user can browse steps */
17413
+ disabled?: boolean;
16875
17414
  className?: string;
16876
17415
  }
16877
17416
 
@@ -16912,13 +17451,23 @@ export interface IvrBotConfigProps {
16912
17451
  /** Called for each file during upload with progress/error handlers. If omitted, uses fake progress. */
16913
17452
  onUploadKnowledgeFile?: (file: File, handlers: UploadProgressHandlers) => Promise<void>;
16914
17453
  onSampleFileDownload?: () => void;
17454
+ /** Called when user downloads a knowledge file. When omitted, download button is hidden. */
16915
17455
  onDownloadKnowledgeFile?: (fileId: string) => void;
17456
+ /** Called when user deletes a knowledge file. When omitted, delete button is hidden. */
16916
17457
  onDeleteKnowledgeFile?: (fileId: string) => void;
17458
+ /** Independently disables the knowledge file download button */
17459
+ knowledgeDownloadDisabled?: boolean;
17460
+ /** Independently disables the knowledge file delete button */
17461
+ knowledgeDeleteDisabled?: boolean;
16917
17462
  onCreateFunction?: (data: CreateFunctionData) => void;
16918
- /** Called when user edits a custom function. Receives the function id. */
17463
+ /** Called when user edits a custom function. When omitted, edit button is hidden. */
16919
17464
  onEditFunction?: (id: string) => void;
16920
- /** Called when user deletes a custom function */
17465
+ /** Called when user deletes a custom function. When omitted, delete button is hidden. */
16921
17466
  onDeleteFunction?: (id: string) => void;
17467
+ /** Independently disables the function edit button */
17468
+ functionEditDisabled?: boolean;
17469
+ /** Independently disables the function delete button */
17470
+ functionDeleteDisabled?: boolean;
16922
17471
  onTestApi?: (step2: CreateFunctionStep2Data) => Promise<string>;
16923
17472
  /** Hover text for the info icon in the Functions card header */
16924
17473
  functionsInfoTooltip?: string;
@@ -16935,7 +17484,12 @@ export interface IvrBotConfigProps {
16935
17484
  functionEditData?: Partial<CreateFunctionData>;
16936
17485
  /** Max character length for the "How It Behaves" system prompt (default: 5000, per Figma) */
16937
17486
  systemPromptMaxLength?: number;
16938
- /** Called when the system prompt textarea loses focus */
17487
+ /**
17488
+ * Called when focus leaves the **entire** "How It Behaves" section
17489
+ * (textarea + session variable chips). Clicking a chip does NOT trigger
17490
+ * this \u2014 only clicking outside the whole section does.
17491
+ * Use this to persist the system prompt via an API call.
17492
+ */
16939
17493
  onSystemPromptBlur?: (value: string) => void;
16940
17494
  /** Called when the Agent Busy Prompt textarea loses focus */
16941
17495
  onAgentBusyPromptBlur?: (value: string) => void;