myoperator-ui 0.0.210-beta.9 → 0.0.210

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 +455 -256
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13604,17 +13604,10 @@ export type {
13604
13604
  {
13605
13605
  name: "ivr-bot-config.tsx",
13606
13606
  content: prefixTailwindClasses(`import * as React from "react";
13607
- import { Info } from "lucide-react";
13608
13607
  import { cn } from "../../../lib/utils";
13609
13608
  import { Button } from "../button";
13610
13609
  import { Badge } from "../badge";
13611
13610
  import { PageHeader } from "../page-header";
13612
- import {
13613
- Accordion,
13614
- AccordionItem,
13615
- AccordionTrigger,
13616
- AccordionContent,
13617
- } from "../accordion";
13618
13611
  import { BotIdentityCard } from "./bot-identity-card";
13619
13612
  import { BotBehaviorCard } from "./bot-behavior-card";
13620
13613
  import { KnowledgeBaseCard } from "./knowledge-base-card";
@@ -13628,115 +13621,8 @@ import type {
13628
13621
  IvrBotConfigData,
13629
13622
  CreateFunctionData,
13630
13623
  } from "./types";
13624
+ import { FallbackPromptsCard } from "./fallback-prompts-card";
13631
13625
 
13632
- // \u2500\u2500\u2500 Styled Textarea (still used by FallbackPromptsAccordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13633
- function StyledTextarea({
13634
- placeholder,
13635
- value,
13636
- rows = 3,
13637
- onChange,
13638
- onBlur,
13639
- disabled,
13640
- className,
13641
- }: {
13642
- placeholder?: string;
13643
- value?: string;
13644
- rows?: number;
13645
- onChange?: (v: string) => void;
13646
- onBlur?: (v: string) => void;
13647
- disabled?: boolean;
13648
- className?: string;
13649
- }) {
13650
- return (
13651
- <textarea
13652
- value={value ?? ""}
13653
- rows={rows}
13654
- onChange={(e) => onChange?.(e.target.value)}
13655
- onBlur={(e) => onBlur?.(e.target.value)}
13656
- placeholder={placeholder}
13657
- disabled={disabled}
13658
- className={cn(
13659
- "w-full px-4 py-2.5 text-base rounded border resize-none",
13660
- "border-semantic-border-input bg-semantic-bg-primary",
13661
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
13662
- "outline-none hover:border-semantic-border-input-focus",
13663
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
13664
- disabled && "opacity-50 cursor-not-allowed",
13665
- className
13666
- )}
13667
- />
13668
- );
13669
- }
13670
-
13671
- // \u2500\u2500\u2500 Field wrapper (still used by FallbackPromptsAccordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13672
- function Field({
13673
- label,
13674
- children,
13675
- }: {
13676
- label: string;
13677
- children: React.ReactNode;
13678
- }) {
13679
- return (
13680
- <div className="flex flex-col gap-1.5">
13681
- <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
13682
- {label}
13683
- </label>
13684
- {children}
13685
- </div>
13686
- );
13687
- }
13688
-
13689
- // \u2500\u2500\u2500 Fallback Prompts (accordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13690
- function FallbackPromptsAccordion({
13691
- data,
13692
- onChange,
13693
- onAgentBusyPromptBlur,
13694
- onNoExtensionFoundPromptBlur,
13695
- disabled,
13696
- }: {
13697
- data: Partial<IvrBotConfigData>;
13698
- onChange: (patch: Partial<IvrBotConfigData>) => void;
13699
- onAgentBusyPromptBlur?: (value: string) => void;
13700
- onNoExtensionFoundPromptBlur?: (value: string) => void;
13701
- disabled?: boolean;
13702
- }) {
13703
- return (
13704
- <div className="bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden">
13705
- <Accordion type="single">
13706
- <AccordionItem value="fallback">
13707
- <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
13708
- <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
13709
- Fallback Prompts
13710
- <Info className="size-3.5 text-semantic-text-muted shrink-0" />
13711
- </span>
13712
- </AccordionTrigger>
13713
- <AccordionContent>
13714
- <div className="px-4 pt-4 pb-2 flex flex-col gap-6 sm:px-6 sm:pt-6">
13715
- <Field label="Agent Busy Prompt">
13716
- <StyledTextarea
13717
- value={data.agentBusyPrompt ?? ""}
13718
- onChange={(v) => onChange({ agentBusyPrompt: v })}
13719
- onBlur={onAgentBusyPromptBlur}
13720
- placeholder="Executives are busy at the moment, we will connect you soon."
13721
- disabled={disabled}
13722
- />
13723
- </Field>
13724
- <Field label="No Extension Found">
13725
- <StyledTextarea
13726
- value={data.noExtensionPrompt ?? ""}
13727
- onChange={(v) => onChange({ noExtensionPrompt: v })}
13728
- onBlur={onNoExtensionFoundPromptBlur}
13729
- placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
13730
- disabled={disabled}
13731
- />
13732
- </Field>
13733
- </div>
13734
- </AccordionContent>
13735
- </AccordionItem>
13736
- </Accordion>
13737
- </div>
13738
- );
13739
- }
13740
13626
 
13741
13627
  // \u2500\u2500\u2500 Default data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13742
13628
  const DEFAULT_DATA: IvrBotConfigData = {
@@ -13780,8 +13666,14 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13780
13666
  onDeleteFunction,
13781
13667
  onTestApi,
13782
13668
  functionsInfoTooltip,
13669
+ knowledgeBaseInfoTooltip,
13783
13670
  functionPromptMinLength,
13784
13671
  functionPromptMaxLength,
13672
+ functionEditData,
13673
+ systemPromptMaxLength,
13674
+ onSystemPromptBlur,
13675
+ onAgentBusyPromptBlur,
13676
+ onNoExtensionFoundPromptBlur,
13785
13677
  onBack,
13786
13678
  onPlayVoice,
13787
13679
  onPauseVoice,
@@ -13797,9 +13689,6 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13797
13689
  silenceTimeoutMax,
13798
13690
  callEndThresholdMin,
13799
13691
  callEndThresholdMax,
13800
- onSystemPromptBlur,
13801
- onAgentBusyPromptBlur,
13802
- onNoExtensionFoundPromptBlur,
13803
13692
  className,
13804
13693
  },
13805
13694
  ref
@@ -13809,6 +13698,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13809
13698
  ...initialData,
13810
13699
  });
13811
13700
  const [createFnOpen, setCreateFnOpen] = React.useState(false);
13701
+ const [editFnOpen, setEditFnOpen] = React.useState(false);
13812
13702
  const [uploadOpen, setUploadOpen] = React.useState(false);
13813
13703
 
13814
13704
  const update = (patch: Partial<IvrBotConfigData>) =>
@@ -13820,6 +13710,16 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13820
13710
  onCreateFunction?.(fnData);
13821
13711
  };
13822
13712
 
13713
+ const handleEditFunction = (id: string) => {
13714
+ onEditFunction?.(id);
13715
+ setEditFnOpen(true);
13716
+ };
13717
+
13718
+ const handleEditFunctionSubmit = (fnData: CreateFunctionData) => {
13719
+ onCreateFunction?.(fnData);
13720
+ setEditFnOpen(false);
13721
+ };
13722
+
13823
13723
  return (
13824
13724
  <div ref={ref} className={cn("flex flex-col min-h-screen bg-semantic-bg-primary", className)}>
13825
13725
  {/* Page header */}
@@ -13876,13 +13776,22 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13876
13776
  <BotBehaviorCard
13877
13777
  data={data}
13878
13778
  onChange={update}
13879
- onBlur={onSystemPromptBlur}
13779
+ onSystemPromptBlur={onSystemPromptBlur}
13880
13780
  sessionVariables={sessionVariables}
13781
+ maxLength={systemPromptMaxLength}
13881
13782
  disabled={disabled}
13882
13783
  />
13883
- <FallbackPromptsAccordion
13884
- data={data}
13885
- onChange={update}
13784
+ <FallbackPromptsCard
13785
+ data={{
13786
+ agentBusyPrompt: data.agentBusyPrompt,
13787
+ noExtensionFoundPrompt: data.noExtensionPrompt,
13788
+ }}
13789
+ onChange={(patch) =>
13790
+ update({
13791
+ ...(patch.agentBusyPrompt !== undefined && { agentBusyPrompt: patch.agentBusyPrompt }),
13792
+ ...(patch.noExtensionFoundPrompt !== undefined && { noExtensionPrompt: patch.noExtensionFoundPrompt }),
13793
+ })
13794
+ }
13886
13795
  onAgentBusyPromptBlur={onAgentBusyPromptBlur}
13887
13796
  onNoExtensionFoundPromptBlur={onNoExtensionFoundPromptBlur}
13888
13797
  disabled={disabled}
@@ -13895,6 +13804,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13895
13804
  files={data.knowledgeBaseFiles}
13896
13805
  onAdd={() => setUploadOpen(true)}
13897
13806
  onDownload={onDownloadKnowledgeFile}
13807
+ infoTooltip={knowledgeBaseInfoTooltip}
13898
13808
  disabled={disabled}
13899
13809
  onDelete={(id) => {
13900
13810
  update({
@@ -13908,7 +13818,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13908
13818
  <FunctionsCard
13909
13819
  functions={data.functions}
13910
13820
  onAddFunction={() => setCreateFnOpen(true)}
13911
- onEditFunction={onEditFunction}
13821
+ onEditFunction={handleEditFunction}
13912
13822
  infoTooltip={functionsInfoTooltip}
13913
13823
  disabled={disabled}
13914
13824
  onDeleteFunction={(id) => {
@@ -13946,6 +13856,18 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13946
13856
  promptMaxLength={functionPromptMaxLength}
13947
13857
  />
13948
13858
 
13859
+ {/* Edit Function Modal */}
13860
+ <CreateFunctionModal
13861
+ open={editFnOpen}
13862
+ onOpenChange={setEditFnOpen}
13863
+ onSubmit={handleEditFunctionSubmit}
13864
+ onTestApi={onTestApi}
13865
+ initialData={functionEditData}
13866
+ isEditing
13867
+ promptMinLength={functionPromptMinLength}
13868
+ promptMaxLength={functionPromptMaxLength}
13869
+ />
13870
+
13949
13871
  {/* File Upload Modal */}
13950
13872
  <FileUploadModal
13951
13873
  open={uploadOpen}
@@ -13989,13 +13911,27 @@ const BODY_MAX = 4000;
13989
13911
  const URL_MAX = 500;
13990
13912
  const HEADER_KEY_MAX = 512;
13991
13913
  const HEADER_VALUE_MAX = 2048;
13992
- const QUERY_KEY_MAX = 512;
13993
- const QUERY_VALUE_MAX = 2048;
13994
13914
 
13995
13915
  const FUNCTION_NAME_REGEX = /^(?!_+$)(?=.*[a-zA-Z])[a-zA-Z][a-zA-Z0-9_]*$/;
13996
13916
  const URL_REGEX = /^https?:\\/\\//;
13997
13917
  const HEADER_KEY_REGEX = /^[!#$%&'*+\\-.^_\`|~0-9a-zA-Z]+$/;
13998
- const QUERY_KEY_REGEX = /^[a-zA-Z0-9_.\\-~]+$/;
13918
+ // Query parameter validation (aligned with apiIntegrationSchema.queryParams)
13919
+ const QUERY_PARAM_KEY_MAX = 512;
13920
+ const QUERY_PARAM_VALUE_MAX = 2048;
13921
+ const QUERY_PARAM_KEY_PATTERN = /^[a-zA-Z0-9_.\\-~]+$/;
13922
+
13923
+ function validateQueryParamKey(key: string): string | undefined {
13924
+ if (!key.trim()) return "Query param key is required";
13925
+ if (key.length > QUERY_PARAM_KEY_MAX) return "key cannot exceed 512 characters.";
13926
+ if (!QUERY_PARAM_KEY_PATTERN.test(key)) return "Invalid query parameter key.";
13927
+ return undefined;
13928
+ }
13929
+
13930
+ function validateQueryParamValue(value: string): string | undefined {
13931
+ if (!value.trim()) return "Query param value is required";
13932
+ if (value.length > QUERY_PARAM_VALUE_MAX) return "value cannot exceed 2048 characters.";
13933
+ return undefined;
13934
+ }
13999
13935
 
14000
13936
  function generateId() {
14001
13937
  return Math.random().toString(36).slice(2, 9);
@@ -14019,10 +13955,13 @@ const textareaCls = cn(
14019
13955
  );
14020
13956
 
14021
13957
  // \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
13958
+ type RowErrors = { key?: string; value?: string };
13959
+
14022
13960
  function KeyValueTable({
14023
13961
  rows,
14024
13962
  onChange,
14025
13963
  label,
13964
+ getRowErrors,
14026
13965
  keyMaxLength,
14027
13966
  valueMaxLength,
14028
13967
  keyRegex,
@@ -14031,17 +13970,18 @@ function KeyValueTable({
14031
13970
  rows: KeyValuePair[];
14032
13971
  onChange: (rows: KeyValuePair[]) => void;
14033
13972
  label: string;
13973
+ getRowErrors?: (row: KeyValuePair) => RowErrors;
14034
13974
  keyMaxLength?: number;
14035
13975
  valueMaxLength?: number;
14036
13976
  keyRegex?: RegExp;
14037
13977
  keyRegexError?: string;
14038
13978
  }) {
14039
13979
  const update = (id: string, patch: Partial<KeyValuePair>) => {
14040
- const processed = { ...patch };
14041
- if (processed.key !== undefined) {
14042
- processed.key = processed.key.replace(/ /g, "-");
13980
+ // Replace spaces with hyphens in key values
13981
+ if (patch.key !== undefined) {
13982
+ patch = { ...patch, key: patch.key.replace(/ /g, "-") };
14043
13983
  }
14044
- onChange(rows.map((r) => (r.id === id ? { ...r, ...processed } : r)));
13984
+ onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
14045
13985
  };
14046
13986
 
14047
13987
  const remove = (id: string) => onChange(rows.filter((r) => r.id !== id));
@@ -14049,105 +13989,109 @@ function KeyValueTable({
14049
13989
  const add = () =>
14050
13990
  onChange([...rows, { id: generateId(), key: "", value: "" }]);
14051
13991
 
13992
+ const getErrors = (row: KeyValuePair): RowErrors => {
13993
+ if (getRowErrors) return getRowErrors(row);
13994
+ // Inline validation from keyRegex prop when no getRowErrors provided
13995
+ const errors: RowErrors = {};
13996
+ if (keyRegex && row.key.trim() && !keyRegex.test(row.key)) {
13997
+ errors.key = keyRegexError ?? "Invalid key format";
13998
+ }
13999
+ return errors;
14000
+ };
14001
+
14002
+ // Reusable delete row action \u2014 same placement and styling as KeyValueRow / knowledge-base-card
14003
+ const deleteRowButtonClass =
14004
+ "text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors shrink-0";
14005
+
14052
14006
  return (
14053
14007
  <div className="flex flex-col gap-1.5">
14054
14008
  <span className="text-xs text-semantic-text-muted">{label}</span>
14055
14009
  <div className="border border-semantic-border-layout rounded overflow-hidden">
14056
- {/* Column headers \u2014 desktop only */}
14010
+ {/* Column headers \u2014 desktop only; border-r on Key cell defines column boundary */}
14057
14011
  <div className="hidden sm:flex bg-semantic-bg-ui border-b border-semantic-border-layout">
14058
- <div className="flex-1 px-3 py-2 text-xs font-semibold text-semantic-text-muted border-r border-semantic-border-layout">
14012
+ <div className="flex-1 min-w-0 px-3 py-2 text-xs font-semibold text-semantic-text-muted border-r border-semantic-border-layout">
14059
14013
  Key
14060
14014
  </div>
14061
- <div className="flex-[2] px-3 py-2 text-xs font-semibold text-semantic-text-muted">
14015
+ <div className="flex-[2] min-w-0 px-3 py-2 text-xs font-semibold text-semantic-text-muted">
14062
14016
  Value
14063
14017
  </div>
14064
- <div className="w-10 shrink-0" />
14018
+ <div className="w-10 shrink-0" aria-hidden="true" />
14065
14019
  </div>
14066
14020
 
14067
- {/* Filled rows */}
14068
- {rows.map((row) => (
14069
- <div
14070
- key={row.id}
14071
- className="border-b border-semantic-border-layout last:border-b-0"
14072
- >
14073
- {/* Mobile: label + input pairs stacked */}
14074
- <div className="flex sm:hidden flex-col">
14075
- <div className="flex flex-col px-3 pt-2.5 pb-1 gap-0.5">
14076
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14021
+ {/* Filled rows \u2014 same flex ratio (flex-1 / flex-2 / w-10) so middle border aligns with header */}
14022
+ {rows.map((row) => {
14023
+ const errors = getErrors(row);
14024
+ return (
14025
+ <div
14026
+ key={row.id}
14027
+ className="border-b border-semantic-border-layout last:border-b-0 flex items-center min-h-0"
14028
+ >
14029
+ {/* Key column \u2014 border-r on column (not input) so it aligns with header */}
14030
+ <div className="flex-1 flex flex-col min-w-0 sm:border-r sm:border-semantic-border-layout">
14031
+ <span className="sm:hidden px-3 pt-2.5 pb-0.5 text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14077
14032
  Key
14078
14033
  </span>
14079
14034
  <input
14080
14035
  type="text"
14081
14036
  value={row.key}
14082
- maxLength={keyMaxLength}
14083
14037
  onChange={(e) => update(row.id, { key: e.target.value })}
14084
14038
  placeholder="Key"
14085
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
14039
+ maxLength={keyMaxLength}
14040
+ className={cn(
14041
+ "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",
14042
+ errors.key && "border-semantic-error-primary"
14043
+ )}
14044
+ aria-invalid={Boolean(errors.key)}
14045
+ aria-describedby={errors.key ? \`err-key-\${row.id}\` : undefined}
14086
14046
  />
14087
- {keyRegex && row.key && !keyRegex.test(row.key) && (
14088
- <p className="m-0 text-xs text-semantic-error-primary">{keyRegexError ?? "Invalid key format"}</p>
14047
+ {errors.key && (
14048
+ <p id={\`err-key-\${row.id}\`} className="m-0 px-3 pt-0.5 text-xs text-semantic-error-primary">
14049
+ {errors.key}
14050
+ </p>
14089
14051
  )}
14090
14052
  </div>
14091
- <div className="h-px bg-semantic-border-layout mx-3" />
14092
- <div className="flex items-start gap-2 px-3 py-2.5">
14093
- <div className="flex flex-col flex-1 gap-0.5">
14094
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14095
- Value
14096
- </span>
14097
- <input
14098
- type="text"
14099
- value={row.value}
14100
- maxLength={valueMaxLength}
14101
- onChange={(e) => update(row.id, { value: e.target.value })}
14102
- placeholder="Type {{ to add variables"
14103
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
14104
- />
14105
- </div>
14106
- <button
14107
- type="button"
14108
- onClick={() => remove(row.id)}
14109
- className="mt-4 size-8 flex items-center justify-center text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface rounded transition-colors shrink-0"
14110
- aria-label="Delete row"
14111
- >
14112
- <Trash2 className="size-3.5" />
14113
- </button>
14114
- </div>
14115
- </div>
14116
14053
 
14117
- {/* Desktop: side-by-side */}
14118
- <div className="hidden sm:flex flex-col">
14119
- <div className="flex">
14120
- <input
14121
- type="text"
14122
- value={row.key}
14123
- maxLength={keyMaxLength}
14124
- onChange={(e) => update(row.id, { key: e.target.value })}
14125
- placeholder="Key"
14126
- className="flex-1 px-3 py-2.5 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-semantic-bg-primary border-r border-semantic-border-layout outline-none focus:bg-semantic-bg-hover"
14127
- />
14054
+ {/* Value column */}
14055
+ <div className="flex-[2] flex flex-col min-w-0">
14056
+ <span className="sm:hidden px-3 pt-2.5 pb-0.5 text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14057
+ Value
14058
+ </span>
14128
14059
  <input
14129
14060
  type="text"
14130
14061
  value={row.value}
14131
- maxLength={valueMaxLength}
14132
14062
  onChange={(e) => update(row.id, { value: e.target.value })}
14133
14063
  placeholder="Type {{ to add variables"
14134
- className="flex-[2] 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"
14064
+ maxLength={valueMaxLength}
14065
+ className={cn(
14066
+ "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",
14067
+ errors.value && "border-semantic-error-primary"
14068
+ )}
14069
+ aria-invalid={Boolean(errors.value)}
14070
+ aria-describedby={errors.value ? \`err-value-\${row.id}\` : undefined}
14135
14071
  />
14136
- <button
14072
+ {errors.value && (
14073
+ <p id={\`err-value-\${row.id}\`} className="m-0 px-3 pt-0.5 text-xs text-semantic-error-primary">
14074
+ {errors.value}
14075
+ </p>
14076
+ )}
14077
+ </div>
14078
+
14079
+ {/* Action column \u2014 delete aligned with row (same as KeyValueRow / knowledge-base-card) */}
14080
+ <div className="w-10 sm:w-10 flex items-center justify-center shrink-0 self-stretch border-l border-semantic-border-layout sm:border-l-0">
14081
+ <Button
14137
14082
  type="button"
14083
+ variant="ghost"
14084
+ size="icon"
14138
14085
  onClick={() => remove(row.id)}
14139
- className="w-10 flex items-center justify-center text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors shrink-0"
14086
+ className={cn("rounded-md", deleteRowButtonClass)}
14140
14087
  aria-label="Delete row"
14141
14088
  >
14142
- <Trash2 className="size-3.5" />
14143
- </button>
14089
+ <Trash2 className="size-4" />
14090
+ </Button>
14144
14091
  </div>
14145
- {keyRegex && row.key && !keyRegex.test(row.key) && (
14146
- <p className="m-0 px-3 py-1 text-xs text-semantic-error-primary">{keyRegexError ?? "Invalid key format"}</p>
14147
- )}
14148
14092
  </div>
14149
- </div>
14150
- ))}
14093
+ );
14094
+ })}
14151
14095
 
14152
14096
  {/* Add row \u2014 always visible */}
14153
14097
  <button
@@ -14174,6 +14118,8 @@ export const CreateFunctionModal = React.forwardRef<
14174
14118
  onOpenChange,
14175
14119
  onSubmit,
14176
14120
  onTestApi,
14121
+ initialData,
14122
+ isEditing = false,
14177
14123
  promptMinLength = 100,
14178
14124
  promptMaxLength = 1000,
14179
14125
  initialStep = 1,
@@ -14184,39 +14130,61 @@ export const CreateFunctionModal = React.forwardRef<
14184
14130
  ) => {
14185
14131
  const [step, setStep] = React.useState<1 | 2>(initialStep);
14186
14132
 
14187
- const [name, setName] = React.useState("");
14188
- const [prompt, setPrompt] = React.useState("");
14133
+ const [name, setName] = React.useState(initialData?.name ?? "");
14134
+ const [prompt, setPrompt] = React.useState(initialData?.prompt ?? "");
14189
14135
 
14190
- const [method, setMethod] = React.useState<HttpMethod>("GET");
14191
- const [url, setUrl] = React.useState("");
14136
+ const [method, setMethod] = React.useState<HttpMethod>(initialData?.method ?? "GET");
14137
+ const [url, setUrl] = React.useState(initialData?.url ?? "");
14192
14138
  const [activeTab, setActiveTab] =
14193
14139
  React.useState<FunctionTabType>(initialTab);
14194
- const [headers, setHeaders] = React.useState<KeyValuePair[]>([]);
14195
- const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>([]);
14196
- const [body, setBody] = React.useState("");
14140
+ const [headers, setHeaders] = React.useState<KeyValuePair[]>(initialData?.headers ?? []);
14141
+ const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>(initialData?.queryParams ?? []);
14142
+ const [body, setBody] = React.useState(initialData?.body ?? "");
14197
14143
  const [apiResponse, setApiResponse] = React.useState("");
14198
14144
  const [isTesting, setIsTesting] = React.useState(false);
14199
-
14200
- // Validation errors (shown on blur)
14145
+ const [step2SubmitAttempted, setStep2SubmitAttempted] = React.useState(false);
14201
14146
  const [nameError, setNameError] = React.useState("");
14202
14147
  const [urlError, setUrlError] = React.useState("");
14203
14148
  const [bodyError, setBodyError] = React.useState("");
14204
14149
 
14150
+ // Sync form state from initialData each time the modal opens
14151
+ React.useEffect(() => {
14152
+ if (open) {
14153
+ setStep(initialStep);
14154
+ setName(initialData?.name ?? "");
14155
+ setPrompt(initialData?.prompt ?? "");
14156
+ setMethod(initialData?.method ?? "GET");
14157
+ setUrl(initialData?.url ?? "");
14158
+ setActiveTab(initialTab);
14159
+ setHeaders(initialData?.headers ?? []);
14160
+ setQueryParams(initialData?.queryParams ?? []);
14161
+ setBody(initialData?.body ?? "");
14162
+ setApiResponse("");
14163
+ setStep2SubmitAttempted(false);
14164
+ setNameError("");
14165
+ setUrlError("");
14166
+ setBodyError("");
14167
+ }
14168
+ // Re-run only when modal opens; intentionally exclude deep deps to avoid mid-session resets
14169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14170
+ }, [open]);
14171
+
14205
14172
  const reset = React.useCallback(() => {
14206
14173
  setStep(initialStep);
14207
- setName("");
14208
- setPrompt("");
14209
- setMethod("GET");
14210
- setUrl("");
14174
+ setName(initialData?.name ?? "");
14175
+ setPrompt(initialData?.prompt ?? "");
14176
+ setMethod(initialData?.method ?? "GET");
14177
+ setUrl(initialData?.url ?? "");
14211
14178
  setActiveTab(initialTab);
14212
- setHeaders([]);
14213
- setQueryParams([]);
14214
- setBody("");
14179
+ setHeaders(initialData?.headers ?? []);
14180
+ setQueryParams(initialData?.queryParams ?? []);
14181
+ setBody(initialData?.body ?? "");
14215
14182
  setApiResponse("");
14183
+ setStep2SubmitAttempted(false);
14216
14184
  setNameError("");
14217
14185
  setUrlError("");
14218
14186
  setBodyError("");
14219
- }, [initialStep, initialTab]);
14187
+ }, [initialData, initialStep, initialTab]);
14220
14188
 
14221
14189
  const handleClose = React.useCallback(() => {
14222
14190
  reset();
@@ -14232,26 +14200,26 @@ export const CreateFunctionModal = React.forwardRef<
14232
14200
  }
14233
14201
  }, [supportsBody, activeTab]);
14234
14202
 
14235
- const validateName = (v: string) => {
14236
- if (v && !FUNCTION_NAME_REGEX.test(v)) {
14203
+ const validateName = (value: string) => {
14204
+ if (value.trim() && !FUNCTION_NAME_REGEX.test(value.trim())) {
14237
14205
  setNameError("Must start with a letter and contain only letters, numbers, and underscores");
14238
14206
  } else {
14239
14207
  setNameError("");
14240
14208
  }
14241
14209
  };
14242
14210
 
14243
- const validateUrl = (v: string) => {
14244
- if (v && !URL_REGEX.test(v)) {
14211
+ const validateUrl = (value: string) => {
14212
+ if (value.trim() && !URL_REGEX.test(value.trim())) {
14245
14213
  setUrlError("URL must start with http:// or https://");
14246
14214
  } else {
14247
14215
  setUrlError("");
14248
14216
  }
14249
14217
  };
14250
14218
 
14251
- const validateBody = (v: string) => {
14252
- if (v.trim()) {
14219
+ const validateBody = (value: string) => {
14220
+ if (value.trim()) {
14253
14221
  try {
14254
- JSON.parse(v);
14222
+ JSON.parse(value.trim());
14255
14223
  setBodyError("");
14256
14224
  } catch {
14257
14225
  setBodyError("Body must be valid JSON");
@@ -14262,10 +14230,24 @@ export const CreateFunctionModal = React.forwardRef<
14262
14230
  };
14263
14231
 
14264
14232
  const handleNext = () => {
14265
- if (isStep1Valid) setStep(2);
14233
+ if (name.trim() && prompt.trim().length >= promptMinLength) setStep(2);
14266
14234
  };
14267
14235
 
14236
+ const queryParamsHaveErrors = (rows: KeyValuePair[]): boolean =>
14237
+ rows.some((row) => {
14238
+ const hasInput = row.key.trim() !== "" || row.value.trim() !== "";
14239
+ if (!hasInput) return false;
14240
+ return (
14241
+ validateQueryParamKey(row.key) !== undefined ||
14242
+ validateQueryParamValue(row.value) !== undefined
14243
+ );
14244
+ });
14245
+
14268
14246
  const handleSubmit = () => {
14247
+ if (step === 2) {
14248
+ setStep2SubmitAttempted(true);
14249
+ if (queryParamsHaveErrors(queryParams)) return;
14250
+ }
14269
14251
  const data: CreateFunctionData = {
14270
14252
  name: name.trim(),
14271
14253
  prompt: prompt.trim(),
@@ -14297,16 +14279,17 @@ export const CreateFunctionModal = React.forwardRef<
14297
14279
  }
14298
14280
  };
14299
14281
 
14300
- const isNameValid = name.trim().length > 0 && FUNCTION_NAME_REGEX.test(name.trim());
14301
- const isStep1Valid = isNameValid && prompt.trim().length >= promptMinLength;
14302
-
14303
- const hasHeaderKeyErrors = headers.some(
14304
- (r) => r.key && !HEADER_KEY_REGEX.test(r.key)
14282
+ const headersHaveKeyErrors = headers.some(
14283
+ (row) => row.key.trim() && HEADER_KEY_REGEX && !HEADER_KEY_REGEX.test(row.key)
14305
14284
  );
14306
- const hasQueryKeyErrors = queryParams.some(
14307
- (r) => r.key && !QUERY_KEY_REGEX.test(r.key)
14308
- );
14309
- const isStep2Valid = !urlError && !bodyError && !hasHeaderKeyErrors && !hasQueryKeyErrors;
14285
+
14286
+ const isStep1Valid =
14287
+ name.trim().length > 0 &&
14288
+ FUNCTION_NAME_REGEX.test(name.trim()) &&
14289
+ prompt.trim().length >= promptMinLength;
14290
+
14291
+ const isStep2Valid =
14292
+ !urlError && !bodyError && !headersHaveKeyErrors && !queryParamsHaveErrors(queryParams);
14310
14293
 
14311
14294
  const tabLabels: Record<FunctionTabType, string> = {
14312
14295
  header: \`Header (\${headers.length})\`,
@@ -14333,7 +14316,7 @@ export const CreateFunctionModal = React.forwardRef<
14333
14316
  {/* \u2500\u2500 Header \u2500\u2500 */}
14334
14317
  <div className="flex items-center justify-between px-4 py-4 border-b border-semantic-border-layout shrink-0 sm:px-6">
14335
14318
  <DialogTitle className="text-base font-semibold text-semantic-text-primary">
14336
- Create Function
14319
+ {isEditing ? "Edit Function" : "Create Function"}
14337
14320
  </DialogTitle>
14338
14321
  <button
14339
14322
  type="button"
@@ -14502,7 +14485,7 @@ export const CreateFunctionModal = React.forwardRef<
14502
14485
  keyMaxLength={HEADER_KEY_MAX}
14503
14486
  valueMaxLength={HEADER_VALUE_MAX}
14504
14487
  keyRegex={HEADER_KEY_REGEX}
14505
- keyRegexError="Header key contains invalid characters"
14488
+ keyRegexError="Invalid header key. Use only alphanumeric and !#$%&'*+-.^_\`|~ characters."
14506
14489
  />
14507
14490
  )}
14508
14491
  {activeTab === "queryParams" && (
@@ -14510,10 +14493,16 @@ export const CreateFunctionModal = React.forwardRef<
14510
14493
  rows={queryParams}
14511
14494
  onChange={setQueryParams}
14512
14495
  label="Query parameter"
14513
- keyMaxLength={QUERY_KEY_MAX}
14514
- valueMaxLength={QUERY_VALUE_MAX}
14515
- keyRegex={QUERY_KEY_REGEX}
14516
- keyRegexError="Query param key contains invalid characters"
14496
+ keyMaxLength={QUERY_PARAM_KEY_MAX}
14497
+ valueMaxLength={QUERY_PARAM_VALUE_MAX}
14498
+ getRowErrors={(row) => {
14499
+ const hasInput = row.key.trim() !== "" || row.value.trim() !== "";
14500
+ if (!hasInput && !step2SubmitAttempted) return {};
14501
+ return {
14502
+ key: validateQueryParamKey(row.key),
14503
+ value: validateQueryParamValue(row.value),
14504
+ };
14505
+ }}
14517
14506
  />
14518
14507
  )}
14519
14508
  {activeTab === "body" && (
@@ -14985,9 +14974,11 @@ export interface BotBehaviorCardProps {
14985
14974
  /** Callback when any field changes */
14986
14975
  onChange: (patch: Partial<BotBehaviorData>) => void;
14987
14976
  /** Called when the system prompt textarea loses focus */
14988
- onBlur?: (value: string) => void;
14977
+ onSystemPromptBlur?: (value: string) => void;
14989
14978
  /** Session variables shown as insertable chips */
14990
14979
  sessionVariables?: string[];
14980
+ /** Maximum character length for the system prompt textarea (default: 25000) */
14981
+ maxLength?: number;
14991
14982
  /** Disables all fields in the card (view mode) */
14992
14983
  disabled?: boolean;
14993
14984
  /** Additional className for the card */
@@ -15075,15 +15066,16 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15075
15066
  {
15076
15067
  data,
15077
15068
  onChange,
15078
- onBlur,
15069
+ onSystemPromptBlur,
15079
15070
  sessionVariables = DEFAULT_SESSION_VARIABLES,
15071
+ maxLength = 25000,
15080
15072
  disabled,
15081
15073
  className,
15082
15074
  },
15083
15075
  ref
15084
15076
  ) => {
15085
15077
  const prompt = data.systemPrompt ?? "";
15086
- const MAX = 25000;
15078
+ const MAX = maxLength;
15087
15079
 
15088
15080
  const insertVariable = (variable: string) => {
15089
15081
  onChange({ systemPrompt: prompt + variable });
@@ -15103,7 +15095,7 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15103
15095
  onChange={(v) => {
15104
15096
  if (v.length <= MAX) onChange({ systemPrompt: v });
15105
15097
  }}
15106
- onBlur={onBlur}
15098
+ onBlur={onSystemPromptBlur}
15107
15099
  placeholder="You are a helpful assistant. Always start by greeting the user politely: 'Hello! Welcome. How can I assist you today?'"
15108
15100
  disabled={disabled}
15109
15101
  className="pb-8"
@@ -15149,6 +15141,13 @@ export { BotBehaviorCard };
15149
15141
  import { Download, Trash2, Plus, Info } from "lucide-react";
15150
15142
  import { cn } from "../../../lib/utils";
15151
15143
  import { Badge } from "../badge";
15144
+ import {
15145
+ Tooltip,
15146
+ TooltipContent,
15147
+ TooltipProvider,
15148
+ TooltipTrigger,
15149
+ } from "../tooltip";
15150
+ import { BOT_KNOWLEDGE_STATUS } from "./types";
15152
15151
  import type { KnowledgeBaseFile } from "./types";
15153
15152
 
15154
15153
  // \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
@@ -15162,6 +15161,8 @@ export interface KnowledgeBaseCardProps {
15162
15161
  onDownload?: (id: string) => void;
15163
15162
  /** Called when user clicks the delete button on a file */
15164
15163
  onDelete?: (id: string) => void;
15164
+ /** Hover text shown on the info icon next to the "Knowledge Base" title */
15165
+ infoTooltip?: string;
15165
15166
  /** Disables all interactive elements in the card (view mode) */
15166
15167
  disabled?: boolean;
15167
15168
  /** Additional className */
@@ -15170,13 +15171,13 @@ export interface KnowledgeBaseCardProps {
15170
15171
 
15171
15172
  // \u2500\u2500\u2500 Status config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15172
15173
 
15173
- type BadgeVariant = "active" | "destructive";
15174
- const STATUS_CONFIG: Record<string, { label: string; variant: BadgeVariant }> =
15175
- {
15176
- training: { label: "Training", variant: "active" },
15177
- trained: { label: "Trained", variant: "active" },
15178
- error: { label: "Error", variant: "destructive" },
15179
- };
15174
+ type BadgeVariant = "default" | "active" | "destructive";
15175
+ const STATUS_CONFIG: Record<BOT_KNOWLEDGE_STATUS, { label: string; variant: BadgeVariant }> = {
15176
+ [BOT_KNOWLEDGE_STATUS.PENDING]: { label: "Pending", variant: "default" },
15177
+ [BOT_KNOWLEDGE_STATUS.READY]: { label: "Ready", variant: "active" },
15178
+ [BOT_KNOWLEDGE_STATUS.PROCESSING]: { label: "Processing", variant: "active" },
15179
+ [BOT_KNOWLEDGE_STATUS.FAILED]: { label: "Failed", variant: "destructive" },
15180
+ };
15180
15181
 
15181
15182
  // \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
15182
15183
 
@@ -15187,6 +15188,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15187
15188
  onAdd,
15188
15189
  onDownload,
15189
15190
  onDelete,
15191
+ infoTooltip,
15190
15192
  disabled,
15191
15193
  className,
15192
15194
  },
@@ -15206,7 +15208,18 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15206
15208
  <h2 className="m-0 text-base font-semibold text-semantic-text-primary">
15207
15209
  Knowledge Base
15208
15210
  </h2>
15209
- <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15211
+ {infoTooltip ? (
15212
+ <TooltipProvider delayDuration={200}>
15213
+ <Tooltip>
15214
+ <TooltipTrigger asChild>
15215
+ <Info className="size-3.5 text-semantic-text-muted shrink-0 cursor-help" />
15216
+ </TooltipTrigger>
15217
+ <TooltipContent>{infoTooltip}</TooltipContent>
15218
+ </Tooltip>
15219
+ </TooltipProvider>
15220
+ ) : (
15221
+ <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15222
+ )}
15210
15223
  </div>
15211
15224
  <button
15212
15225
  type="button"
@@ -15227,7 +15240,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15227
15240
  ) : (
15228
15241
  <div className="flex flex-col divide-y divide-semantic-border-layout">
15229
15242
  {files.map((file) => {
15230
- const status = STATUS_CONFIG[file.status] ?? STATUS_CONFIG.training;
15243
+ const status = STATUS_CONFIG[file.status] ?? STATUS_CONFIG[BOT_KNOWLEDGE_STATUS.PENDING];
15231
15244
  return (
15232
15245
  <div
15233
15246
  key={file.id}
@@ -15761,6 +15774,164 @@ const AdvancedSettingsCard = React.forwardRef<HTMLDivElement, AdvancedSettingsCa
15761
15774
  AdvancedSettingsCard.displayName = "AdvancedSettingsCard";
15762
15775
 
15763
15776
  export { AdvancedSettingsCard };
15777
+ `, prefix)
15778
+ },
15779
+ {
15780
+ name: "fallback-prompts-card.tsx",
15781
+ content: prefixTailwindClasses(`import * as React from "react";
15782
+ import { Info } from "lucide-react";
15783
+ import { cn } from "../../../lib/utils";
15784
+ import {
15785
+ Accordion,
15786
+ AccordionItem,
15787
+ AccordionTrigger,
15788
+ AccordionContent,
15789
+ } from "../accordion";
15790
+
15791
+ // \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
15792
+
15793
+ export interface FallbackPromptsData {
15794
+ agentBusyPrompt: string;
15795
+ noExtensionFoundPrompt: string;
15796
+ }
15797
+
15798
+ export interface FallbackPromptsCardProps {
15799
+ /** Current prompt text values */
15800
+ data: Partial<FallbackPromptsData>;
15801
+ /** Callback when any field changes */
15802
+ onChange: (patch: Partial<FallbackPromptsData>) => void;
15803
+ /** Called when the Agent Busy Prompt textarea loses focus */
15804
+ onAgentBusyPromptBlur?: (value: string) => void;
15805
+ /** Called when the No Extension Found textarea loses focus */
15806
+ onNoExtensionFoundPromptBlur?: (value: string) => void;
15807
+ /** Maximum character length for each prompt field (default: 25000) */
15808
+ maxLength?: number;
15809
+ /** Disables all fields in the card (view mode) */
15810
+ disabled?: boolean;
15811
+ /** Opens the accordion by default (default: false) */
15812
+ defaultOpen?: boolean;
15813
+ /** Additional className */
15814
+ className?: string;
15815
+ }
15816
+
15817
+ // \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
15818
+
15819
+ function PromptField({
15820
+ label,
15821
+ value,
15822
+ placeholder,
15823
+ maxLength,
15824
+ disabled,
15825
+ onChange,
15826
+ onBlur,
15827
+ rows = 2,
15828
+ }: {
15829
+ label: string;
15830
+ value: string;
15831
+ placeholder?: string;
15832
+ maxLength: number;
15833
+ disabled?: boolean;
15834
+ onChange: (value: string) => void;
15835
+ onBlur?: (value: string) => void;
15836
+ rows?: number;
15837
+ }) {
15838
+ return (
15839
+ <div className="flex flex-col gap-1.5">
15840
+ <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
15841
+ {label}
15842
+ </label>
15843
+ <textarea
15844
+ value={value}
15845
+ placeholder={placeholder}
15846
+ maxLength={maxLength}
15847
+ disabled={disabled}
15848
+ rows={rows}
15849
+ onChange={(e) => onChange(e.target.value)}
15850
+ onBlur={(e) => onBlur?.(e.target.value)}
15851
+ className={cn(
15852
+ "w-full resize-none rounded border border-semantic-border-layout bg-semantic-bg-primary px-3 py-2.5 text-base text-semantic-text-primary placeholder:text-semantic-text-muted outline-none transition-all",
15853
+ "focus:outline-none focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
15854
+ disabled && "cursor-not-allowed opacity-50"
15855
+ )}
15856
+ />
15857
+ <p className="m-0 text-xs text-semantic-text-muted text-right">
15858
+ {value.length}/{maxLength}
15859
+ </p>
15860
+ </div>
15861
+ );
15862
+ }
15863
+
15864
+ // \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
15865
+
15866
+ const FallbackPromptsCard = React.forwardRef<
15867
+ HTMLDivElement,
15868
+ FallbackPromptsCardProps
15869
+ >(
15870
+ (
15871
+ {
15872
+ data,
15873
+ onChange,
15874
+ onAgentBusyPromptBlur,
15875
+ onNoExtensionFoundPromptBlur,
15876
+ maxLength = 25000,
15877
+ disabled,
15878
+ defaultOpen = false,
15879
+ className,
15880
+ },
15881
+ ref
15882
+ ) => {
15883
+ return (
15884
+ <div
15885
+ ref={ref}
15886
+ className={cn(
15887
+ "bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden",
15888
+ className
15889
+ )}
15890
+ >
15891
+ <Accordion
15892
+ type="single"
15893
+ defaultValue={defaultOpen ? ["fallback"] : []}
15894
+ >
15895
+ <AccordionItem value="fallback">
15896
+ <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
15897
+ <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
15898
+ Fallback Prompts
15899
+ <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15900
+ </span>
15901
+ </AccordionTrigger>
15902
+ <AccordionContent>
15903
+ <div className="flex flex-col gap-6 px-4 pt-4 sm:px-6 sm:pt-5">
15904
+ <PromptField
15905
+ label="Agent Busy Prompt"
15906
+ value={data.agentBusyPrompt ?? ""}
15907
+ placeholder="Executives are busy at the moment, we will connect you soon."
15908
+ maxLength={maxLength}
15909
+ disabled={disabled}
15910
+ onChange={(v) => onChange({ agentBusyPrompt: v })}
15911
+ onBlur={onAgentBusyPromptBlur}
15912
+ rows={2}
15913
+ />
15914
+ <PromptField
15915
+ label="No Extension Found"
15916
+ value={data.noExtensionFoundPrompt ?? ""}
15917
+ placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
15918
+ maxLength={maxLength}
15919
+ disabled={disabled}
15920
+ onChange={(v) => onChange({ noExtensionFoundPrompt: v })}
15921
+ onBlur={onNoExtensionFoundPromptBlur}
15922
+ rows={4}
15923
+ />
15924
+ </div>
15925
+ </AccordionContent>
15926
+ </AccordionItem>
15927
+ </Accordion>
15928
+ </div>
15929
+ );
15930
+ }
15931
+ );
15932
+ FallbackPromptsCard.displayName = "FallbackPromptsCard";
15933
+
15934
+ export { FallbackPromptsCard };
15764
15935
  `, prefix)
15765
15936
  },
15766
15937
  {
@@ -15771,7 +15942,16 @@ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
15771
15942
 
15772
15943
  export type FunctionTabType = "header" | "queryParams" | "body";
15773
15944
 
15774
- export type KnowledgeFileStatus = "training" | "trained" | "error";
15945
+ export const BOT_KNOWLEDGE_STATUS = {
15946
+ PENDING: "pending",
15947
+ READY: "ready",
15948
+ PROCESSING: "processing",
15949
+ FAILED: "failed",
15950
+ } as const;
15951
+
15952
+ export type BOT_KNOWLEDGE_STATUS = typeof BOT_KNOWLEDGE_STATUS[keyof typeof BOT_KNOWLEDGE_STATUS];
15953
+
15954
+ export type KnowledgeFileStatus = BOT_KNOWLEDGE_STATUS;
15775
15955
 
15776
15956
  export interface KeyValuePair {
15777
15957
  id: string;
@@ -15815,6 +15995,10 @@ export interface CreateFunctionModalProps {
15815
15995
  onOpenChange: (open: boolean) => void;
15816
15996
  onSubmit?: (data: CreateFunctionData) => void;
15817
15997
  onTestApi?: (step2: CreateFunctionStep2Data) => Promise<string>;
15998
+ /** Pre-fills all fields \u2014 use when opening the modal to edit an existing function */
15999
+ initialData?: Partial<CreateFunctionData>;
16000
+ /** When true, changes the modal title to "Edit Function" */
16001
+ isEditing?: boolean;
15818
16002
  /** Minimum character length for the prompt field (default: 100) */
15819
16003
  promptMinLength?: number;
15820
16004
  /** Maximum character length for the prompt field (default: 1000) */
@@ -15866,17 +16050,32 @@ export interface IvrBotConfigProps {
15866
16050
  onDownloadKnowledgeFile?: (fileId: string) => void;
15867
16051
  onDeleteKnowledgeFile?: (fileId: string) => void;
15868
16052
  onCreateFunction?: (data: CreateFunctionData) => void;
15869
- /** Called when user edits a custom function */
16053
+ /** Called when user edits a custom function. Receives the function id. */
15870
16054
  onEditFunction?: (id: string) => void;
15871
16055
  /** Called when user deletes a custom function */
15872
16056
  onDeleteFunction?: (id: string) => void;
15873
16057
  onTestApi?: (step2: CreateFunctionStep2Data) => Promise<string>;
15874
16058
  /** Hover text for the info icon in the Functions card header */
15875
16059
  functionsInfoTooltip?: string;
16060
+ /** Hover text for the info icon in the Knowledge Base card header */
16061
+ knowledgeBaseInfoTooltip?: string;
15876
16062
  /** Minimum character length for the function prompt (default: 100) */
15877
16063
  functionPromptMinLength?: number;
15878
16064
  /** Maximum character length for the function prompt (default: 1000) */
15879
16065
  functionPromptMaxLength?: number;
16066
+ /**
16067
+ * Pre-filled data shown when the edit function modal opens.
16068
+ * Pass when your app fetches full function data after onEditFunction fires.
16069
+ */
16070
+ functionEditData?: Partial<CreateFunctionData>;
16071
+ /** Max character length for the "How It Behaves" system prompt (default: 25000) */
16072
+ systemPromptMaxLength?: number;
16073
+ /** Called when the system prompt textarea loses focus */
16074
+ onSystemPromptBlur?: (value: string) => void;
16075
+ /** Called when the Agent Busy Prompt textarea loses focus */
16076
+ onAgentBusyPromptBlur?: (value: string) => void;
16077
+ /** Called when the No Extension Found textarea loses focus */
16078
+ onNoExtensionFoundPromptBlur?: (value: string) => void;
15880
16079
  onBack?: () => void;
15881
16080
  /** Called when the play icon is clicked on a voice option */
15882
16081
  onPlayVoice?: (voiceValue: string) => void;
@@ -15902,12 +16101,6 @@ export interface IvrBotConfigProps {
15902
16101
  /** Override call end threshold bounds */
15903
16102
  callEndThresholdMin?: number;
15904
16103
  callEndThresholdMax?: number;
15905
- /** Called when the system prompt textarea loses focus */
15906
- onSystemPromptBlur?: (value: string) => void;
15907
- /** Called when the Agent Busy Prompt textarea loses focus */
15908
- onAgentBusyPromptBlur?: (value: string) => void;
15909
- /** Called when the No Extension Found textarea loses focus */
15910
- onNoExtensionFoundPromptBlur?: (value: string) => void;
15911
16104
  className?: string;
15912
16105
  }
15913
16106
 
@@ -15929,6 +16122,12 @@ export { KnowledgeBaseCard } from "./knowledge-base-card";
15929
16122
  export { FunctionsCard } from "./functions-card";
15930
16123
  export { FrustrationHandoverCard } from "./frustration-handover-card";
15931
16124
  export { AdvancedSettingsCard } from "./advanced-settings-card";
16125
+ export { FallbackPromptsCard } from "./fallback-prompts-card";
16126
+ export type {
16127
+ FallbackPromptsData,
16128
+ FallbackPromptsCardProps,
16129
+ } from "./fallback-prompts-card";
16130
+ export { BOT_KNOWLEDGE_STATUS } from "./types";
15932
16131
  export { CreateFunctionModal } from "./create-function-modal";
15933
16132
  export { FileUploadModal } from "../file-upload-modal";
15934
16133
  export { IvrBotConfig } from "./ivr-bot-config";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-ui",
3
- "version": "0.0.210-beta.9",
3
+ "version": "0.0.210",
4
4
  "description": "CLI for adding myOperator UI components to your project",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",