myoperator-ui 0.0.210-beta.0 → 0.0.210-beta.10

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 +437 -516
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2194,172 +2194,6 @@ export const ReadableField = React.forwardRef<HTMLDivElement, ReadableFieldProps
2194
2194
  );
2195
2195
 
2196
2196
  ReadableField.displayName = "ReadableField";
2197
- `, prefix)
2198
- }
2199
- ]
2200
- },
2201
- "textarea": {
2202
- name: "textarea",
2203
- description: "A multi-line text area with label, helper text, error state, and character count support",
2204
- category: "form",
2205
- dependencies: [
2206
- "class-variance-authority",
2207
- "clsx",
2208
- "tailwind-merge"
2209
- ],
2210
- files: [
2211
- {
2212
- name: "textarea.tsx",
2213
- content: prefixTailwindClasses(`import * as React from "react";
2214
- import { cva, type VariantProps } from "class-variance-authority";
2215
-
2216
- import { cn } from "../../lib/utils";
2217
-
2218
- const textAreaVariants = cva(
2219
- "w-full rounded bg-semantic-bg-primary px-4 py-3 text-base text-semantic-text-primary outline-none transition-all resize-y placeholder:text-semantic-text-placeholder disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[var(--color-neutral-50)]",
2220
- {
2221
- variants: {
2222
- state: {
2223
- default:
2224
- "border border-semantic-border-input focus:outline-none focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
2225
- error:
2226
- "border border-semantic-error-primary/40 focus:outline-none focus:border-semantic-error-primary/60 focus:shadow-[0_0_0_1px_rgba(240,68,56,0.1)]",
2227
- },
2228
- },
2229
- defaultVariants: {
2230
- state: "default",
2231
- },
2232
- }
2233
- );
2234
-
2235
- export interface TextAreaProps
2236
- extends React.ComponentProps<"textarea">,
2237
- VariantProps<typeof textAreaVariants> {
2238
- /** Label text displayed above the textarea */
2239
- label?: string;
2240
- /** Shows red asterisk next to label when true */
2241
- required?: boolean;
2242
- /** Helper text displayed below the textarea */
2243
- helperText?: string;
2244
- /** Error message - shows error state with red styling */
2245
- error?: string;
2246
- /** Shows character count when maxLength is set */
2247
- showCount?: boolean;
2248
- /** Additional class for the wrapper container */
2249
- wrapperClassName?: string;
2250
- /** Additional class for the label */
2251
- labelClassName?: string;
2252
- }
2253
-
2254
- const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
2255
- (
2256
- {
2257
- className,
2258
- wrapperClassName,
2259
- labelClassName,
2260
- state,
2261
- label,
2262
- required,
2263
- helperText,
2264
- error,
2265
- showCount,
2266
- maxLength,
2267
- value,
2268
- defaultValue,
2269
- onChange,
2270
- disabled,
2271
- id,
2272
- rows = 4,
2273
- ...props
2274
- },
2275
- ref
2276
- ) => {
2277
- const [internalValue, setInternalValue] = React.useState(
2278
- defaultValue ?? ""
2279
- );
2280
-
2281
- const isControlled = value !== undefined;
2282
- const currentValue = isControlled ? value : internalValue;
2283
- const derivedState = error ? "error" : (state ?? "default");
2284
-
2285
- const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
2286
- if (!isControlled) setInternalValue(e.target.value);
2287
- onChange?.(e);
2288
- };
2289
-
2290
- const generatedId = React.useId();
2291
- const textAreaId = id || generatedId;
2292
- const helperId = \`\${textAreaId}-helper\`;
2293
- const errorId = \`\${textAreaId}-error\`;
2294
- const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined;
2295
- const charCount = String(currentValue).length;
2296
-
2297
- return (
2298
- <div className={cn("flex flex-col gap-1.5", wrapperClassName)}>
2299
- {label && (
2300
- <label
2301
- htmlFor={textAreaId}
2302
- className={cn(
2303
- "text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]",
2304
- labelClassName
2305
- )}
2306
- >
2307
- {label}
2308
- {required && (
2309
- <span className="text-semantic-error-primary ml-0.5">*</span>
2310
- )}
2311
- </label>
2312
- )}
2313
-
2314
- <textarea
2315
- ref={ref}
2316
- id={textAreaId}
2317
- rows={rows}
2318
- className={cn(textAreaVariants({ state: derivedState, className }))}
2319
- disabled={disabled}
2320
- maxLength={maxLength}
2321
- value={isControlled ? value : undefined}
2322
- defaultValue={!isControlled ? defaultValue : undefined}
2323
- onChange={handleChange}
2324
- aria-invalid={!!error}
2325
- aria-describedby={ariaDescribedBy}
2326
- {...props}
2327
- />
2328
-
2329
- {(error || helperText || (showCount && maxLength)) && (
2330
- <div className="flex justify-between items-start gap-2">
2331
- {error ? (
2332
- <span id={errorId} className="text-xs text-semantic-error-primary">
2333
- {error}
2334
- </span>
2335
- ) : helperText ? (
2336
- <span id={helperId} className="text-xs text-semantic-text-muted">
2337
- {helperText}
2338
- </span>
2339
- ) : (
2340
- <span />
2341
- )}
2342
- {showCount && maxLength && (
2343
- <span
2344
- className={cn(
2345
- "text-xs",
2346
- charCount > maxLength
2347
- ? "text-semantic-error-primary"
2348
- : "text-semantic-text-muted"
2349
- )}
2350
- >
2351
- {charCount}/{maxLength}
2352
- </span>
2353
- )}
2354
- </div>
2355
- )}
2356
- </div>
2357
- );
2358
- }
2359
- );
2360
- TextArea.displayName = "TextArea";
2361
-
2362
- export { TextArea, textAreaVariants };
2363
2197
  `, prefix)
2364
2198
  }
2365
2199
  ]
@@ -13770,17 +13604,10 @@ export type {
13770
13604
  {
13771
13605
  name: "ivr-bot-config.tsx",
13772
13606
  content: prefixTailwindClasses(`import * as React from "react";
13773
- import { Info } from "lucide-react";
13774
13607
  import { cn } from "../../../lib/utils";
13775
13608
  import { Button } from "../button";
13776
13609
  import { Badge } from "../badge";
13777
13610
  import { PageHeader } from "../page-header";
13778
- import {
13779
- Accordion,
13780
- AccordionItem,
13781
- AccordionTrigger,
13782
- AccordionContent,
13783
- } from "../accordion";
13784
13611
  import { BotIdentityCard } from "./bot-identity-card";
13785
13612
  import { BotBehaviorCard } from "./bot-behavior-card";
13786
13613
  import { KnowledgeBaseCard } from "./knowledge-base-card";
@@ -13794,106 +13621,8 @@ import type {
13794
13621
  IvrBotConfigData,
13795
13622
  CreateFunctionData,
13796
13623
  } from "./types";
13624
+ import { FallbackPromptsCard } from "./fallback-prompts-card";
13797
13625
 
13798
- // \u2500\u2500\u2500 Styled Textarea (still used by FallbackPromptsAccordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13799
- function StyledTextarea({
13800
- placeholder,
13801
- value,
13802
- rows = 3,
13803
- onChange,
13804
- disabled,
13805
- className,
13806
- }: {
13807
- placeholder?: string;
13808
- value?: string;
13809
- rows?: number;
13810
- onChange?: (v: string) => void;
13811
- disabled?: boolean;
13812
- className?: string;
13813
- }) {
13814
- return (
13815
- <textarea
13816
- value={value ?? ""}
13817
- rows={rows}
13818
- onChange={(e) => onChange?.(e.target.value)}
13819
- placeholder={placeholder}
13820
- disabled={disabled}
13821
- className={cn(
13822
- "w-full px-4 py-2.5 text-base rounded border resize-none",
13823
- "border-semantic-border-input bg-semantic-bg-primary",
13824
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
13825
- "outline-none hover:border-semantic-border-input-focus",
13826
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
13827
- disabled && "opacity-50 cursor-not-allowed",
13828
- className
13829
- )}
13830
- />
13831
- );
13832
- }
13833
-
13834
- // \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
13835
- function Field({
13836
- label,
13837
- children,
13838
- }: {
13839
- label: string;
13840
- children: React.ReactNode;
13841
- }) {
13842
- return (
13843
- <div className="flex flex-col gap-1.5">
13844
- <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
13845
- {label}
13846
- </label>
13847
- {children}
13848
- </div>
13849
- );
13850
- }
13851
-
13852
- // \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
13853
- function FallbackPromptsAccordion({
13854
- data,
13855
- onChange,
13856
- disabled,
13857
- }: {
13858
- data: Partial<IvrBotConfigData>;
13859
- onChange: (patch: Partial<IvrBotConfigData>) => void;
13860
- disabled?: boolean;
13861
- }) {
13862
- return (
13863
- <div className="bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden">
13864
- <Accordion type="single">
13865
- <AccordionItem value="fallback">
13866
- <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
13867
- <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
13868
- Fallback Prompts
13869
- <Info className="size-3.5 text-semantic-text-muted shrink-0" />
13870
- </span>
13871
- </AccordionTrigger>
13872
- <AccordionContent>
13873
- <div className="px-4 pt-4 pb-2 flex flex-col gap-6 sm:px-6 sm:pt-6">
13874
- <Field label="Agent Busy Prompt">
13875
- <StyledTextarea
13876
- value={data.agentBusyPrompt ?? ""}
13877
- onChange={(v) => onChange({ agentBusyPrompt: v })}
13878
- placeholder="Executives are busy at the moment, we will connect you soon."
13879
- disabled={disabled}
13880
- />
13881
- </Field>
13882
- <Field label="No Extension Found">
13883
- <StyledTextarea
13884
- value={data.noExtensionPrompt ?? ""}
13885
- onChange={(v) => onChange({ noExtensionPrompt: v })}
13886
- placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
13887
- disabled={disabled}
13888
- />
13889
- </Field>
13890
- </div>
13891
- </AccordionContent>
13892
- </AccordionItem>
13893
- </Accordion>
13894
- </div>
13895
- );
13896
- }
13897
13626
 
13898
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
13899
13628
  const DEFAULT_DATA: IvrBotConfigData = {
@@ -13937,8 +13666,14 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13937
13666
  onDeleteFunction,
13938
13667
  onTestApi,
13939
13668
  functionsInfoTooltip,
13669
+ knowledgeBaseInfoTooltip,
13940
13670
  functionPromptMinLength,
13941
13671
  functionPromptMaxLength,
13672
+ functionEditData,
13673
+ systemPromptMaxLength,
13674
+ onSystemPromptBlur,
13675
+ onAgentBusyPromptBlur,
13676
+ onNoExtensionFoundPromptBlur,
13942
13677
  onBack,
13943
13678
  onPlayVoice,
13944
13679
  onPauseVoice,
@@ -13963,6 +13698,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13963
13698
  ...initialData,
13964
13699
  });
13965
13700
  const [createFnOpen, setCreateFnOpen] = React.useState(false);
13701
+ const [editFnOpen, setEditFnOpen] = React.useState(false);
13966
13702
  const [uploadOpen, setUploadOpen] = React.useState(false);
13967
13703
 
13968
13704
  const update = (patch: Partial<IvrBotConfigData>) =>
@@ -13974,6 +13710,16 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13974
13710
  onCreateFunction?.(fnData);
13975
13711
  };
13976
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
+
13977
13723
  return (
13978
13724
  <div ref={ref} className={cn("flex flex-col min-h-screen bg-semantic-bg-primary", className)}>
13979
13725
  {/* Page header */}
@@ -14030,10 +13776,26 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14030
13776
  <BotBehaviorCard
14031
13777
  data={data}
14032
13778
  onChange={update}
13779
+ onSystemPromptBlur={onSystemPromptBlur}
14033
13780
  sessionVariables={sessionVariables}
13781
+ maxLength={systemPromptMaxLength}
13782
+ disabled={disabled}
13783
+ />
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
+ }
13795
+ onAgentBusyPromptBlur={onAgentBusyPromptBlur}
13796
+ onNoExtensionFoundPromptBlur={onNoExtensionFoundPromptBlur}
14034
13797
  disabled={disabled}
14035
13798
  />
14036
- <FallbackPromptsAccordion data={data} onChange={update} disabled={disabled} />
14037
13799
  </div>
14038
13800
 
14039
13801
  {/* Right column \u2014 gray panel extending full height */}
@@ -14042,6 +13804,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14042
13804
  files={data.knowledgeBaseFiles}
14043
13805
  onAdd={() => setUploadOpen(true)}
14044
13806
  onDownload={onDownloadKnowledgeFile}
13807
+ infoTooltip={knowledgeBaseInfoTooltip}
14045
13808
  disabled={disabled}
14046
13809
  onDelete={(id) => {
14047
13810
  update({
@@ -14055,7 +13818,7 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14055
13818
  <FunctionsCard
14056
13819
  functions={data.functions}
14057
13820
  onAddFunction={() => setCreateFnOpen(true)}
14058
- onEditFunction={onEditFunction}
13821
+ onEditFunction={handleEditFunction}
14059
13822
  infoTooltip={functionsInfoTooltip}
14060
13823
  disabled={disabled}
14061
13824
  onDeleteFunction={(id) => {
@@ -14093,6 +13856,18 @@ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
14093
13856
  promptMaxLength={functionPromptMaxLength}
14094
13857
  />
14095
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
+
14096
13871
  {/* File Upload Modal */}
14097
13872
  <FileUploadModal
14098
13873
  open={uploadOpen}
@@ -14134,6 +13909,24 @@ const METHODS_WITH_BODY: HttpMethod[] = ["POST", "PUT", "PATCH"];
14134
13909
  const FUNCTION_NAME_MAX = 30;
14135
13910
  const BODY_MAX = 4000;
14136
13911
 
13912
+ // Query parameter validation (aligned with apiIntegrationSchema.queryParams)
13913
+ const QUERY_PARAM_KEY_MAX = 512;
13914
+ const QUERY_PARAM_VALUE_MAX = 2048;
13915
+ const QUERY_PARAM_KEY_PATTERN = /^[a-zA-Z0-9_.\\-~]+$/;
13916
+
13917
+ function validateQueryParamKey(key: string): string | undefined {
13918
+ if (!key.trim()) return "Query param key is required";
13919
+ if (key.length > QUERY_PARAM_KEY_MAX) return "key cannot exceed 512 characters.";
13920
+ if (!QUERY_PARAM_KEY_PATTERN.test(key)) return "Invalid query parameter key.";
13921
+ return undefined;
13922
+ }
13923
+
13924
+ function validateQueryParamValue(value: string): string | undefined {
13925
+ if (!value.trim()) return "Query param value is required";
13926
+ if (value.length > QUERY_PARAM_VALUE_MAX) return "value cannot exceed 2048 characters.";
13927
+ return undefined;
13928
+ }
13929
+
14137
13930
  function generateId() {
14138
13931
  return Math.random().toString(36).slice(2, 9);
14139
13932
  }
@@ -14156,14 +13949,18 @@ const textareaCls = cn(
14156
13949
  );
14157
13950
 
14158
13951
  // \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
13952
+ type RowErrors = { key?: string; value?: string };
13953
+
14159
13954
  function KeyValueTable({
14160
13955
  rows,
14161
13956
  onChange,
14162
13957
  label,
13958
+ getRowErrors,
14163
13959
  }: {
14164
13960
  rows: KeyValuePair[];
14165
13961
  onChange: (rows: KeyValuePair[]) => void;
14166
13962
  label: string;
13963
+ getRowErrors?: (row: KeyValuePair) => RowErrors;
14167
13964
  }) {
14168
13965
  const update = (id: string, patch: Partial<KeyValuePair>) =>
14169
13966
  onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
@@ -14173,31 +13970,39 @@ function KeyValueTable({
14173
13970
  const add = () =>
14174
13971
  onChange([...rows, { id: generateId(), key: "", value: "" }]);
14175
13972
 
13973
+ const getErrors = (row: KeyValuePair): RowErrors =>
13974
+ getRowErrors?.(row) ?? {};
13975
+
13976
+ // Reusable delete row action \u2014 same placement and styling as KeyValueRow / knowledge-base-card
13977
+ const deleteRowButtonClass =
13978
+ "text-semantic-text-muted hover:text-semantic-error-primary hover:bg-semantic-error-surface transition-colors shrink-0";
13979
+
14176
13980
  return (
14177
13981
  <div className="flex flex-col gap-1.5">
14178
13982
  <span className="text-xs text-semantic-text-muted">{label}</span>
14179
13983
  <div className="border border-semantic-border-layout rounded overflow-hidden">
14180
- {/* Column headers \u2014 desktop only */}
13984
+ {/* Column headers \u2014 desktop only; border-r on Key cell defines column boundary */}
14181
13985
  <div className="hidden sm:flex bg-semantic-bg-ui border-b border-semantic-border-layout">
14182
- <div className="flex-1 px-3 py-2 text-xs font-semibold text-semantic-text-muted border-r border-semantic-border-layout">
13986
+ <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">
14183
13987
  Key
14184
13988
  </div>
14185
- <div className="flex-[2] px-3 py-2 text-xs font-semibold text-semantic-text-muted">
13989
+ <div className="flex-[2] min-w-0 px-3 py-2 text-xs font-semibold text-semantic-text-muted">
14186
13990
  Value
14187
13991
  </div>
14188
- <div className="w-10 shrink-0" />
13992
+ <div className="w-10 shrink-0" aria-hidden="true" />
14189
13993
  </div>
14190
13994
 
14191
- {/* Filled rows */}
14192
- {rows.map((row) => (
14193
- <div
14194
- key={row.id}
14195
- className="border-b border-semantic-border-layout last:border-b-0"
14196
- >
14197
- {/* Mobile: label + input pairs stacked */}
14198
- <div className="flex sm:hidden flex-col">
14199
- <div className="flex flex-col px-3 pt-2.5 pb-1 gap-0.5">
14200
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
13995
+ {/* Filled rows \u2014 same flex ratio (flex-1 / flex-2 / w-10) so middle border aligns with header */}
13996
+ {rows.map((row) => {
13997
+ const errors = getErrors(row);
13998
+ return (
13999
+ <div
14000
+ key={row.id}
14001
+ className="border-b border-semantic-border-layout last:border-b-0 flex items-center min-h-0"
14002
+ >
14003
+ {/* Key column \u2014 border-r on column (not input) so it aligns with header */}
14004
+ <div className="flex-1 flex flex-col min-w-0 sm:border-r sm:border-semantic-border-layout">
14005
+ <span className="sm:hidden px-3 pt-2.5 pb-0.5 text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14201
14006
  Key
14202
14007
  </span>
14203
14008
  <input
@@ -14205,61 +14010,62 @@ function KeyValueTable({
14205
14010
  value={row.key}
14206
14011
  onChange={(e) => update(row.id, { key: e.target.value })}
14207
14012
  placeholder="Key"
14208
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
14013
+ maxLength={getRowErrors ? QUERY_PARAM_KEY_MAX : undefined}
14014
+ className={cn(
14015
+ "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",
14016
+ errors.key && "border-semantic-error-primary"
14017
+ )}
14018
+ aria-invalid={Boolean(errors.key)}
14019
+ aria-describedby={errors.key ? \`err-key-\${row.id}\` : undefined}
14209
14020
  />
14021
+ {errors.key && (
14022
+ <p id={\`err-key-\${row.id}\`} className="m-0 px-3 pt-0.5 text-xs text-semantic-error-primary">
14023
+ {errors.key}
14024
+ </p>
14025
+ )}
14210
14026
  </div>
14211
- <div className="h-px bg-semantic-border-layout mx-3" />
14212
- <div className="flex items-start gap-2 px-3 py-2.5">
14213
- <div className="flex flex-col flex-1 gap-0.5">
14214
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14215
- Value
14216
- </span>
14217
- <input
14218
- type="text"
14219
- value={row.value}
14220
- onChange={(e) => update(row.id, { value: e.target.value })}
14221
- placeholder="Type {{ to add variables"
14222
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
14223
- />
14224
- </div>
14225
- <button
14027
+
14028
+ {/* Value column */}
14029
+ <div className="flex-[2] flex flex-col min-w-0">
14030
+ <span className="sm:hidden px-3 pt-2.5 pb-0.5 text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
14031
+ Value
14032
+ </span>
14033
+ <input
14034
+ type="text"
14035
+ value={row.value}
14036
+ onChange={(e) => update(row.id, { value: e.target.value })}
14037
+ placeholder="Type {{ to add variables"
14038
+ maxLength={getRowErrors ? QUERY_PARAM_VALUE_MAX : undefined}
14039
+ className={cn(
14040
+ "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",
14041
+ errors.value && "border-semantic-error-primary"
14042
+ )}
14043
+ aria-invalid={Boolean(errors.value)}
14044
+ aria-describedby={errors.value ? \`err-value-\${row.id}\` : undefined}
14045
+ />
14046
+ {errors.value && (
14047
+ <p id={\`err-value-\${row.id}\`} className="m-0 px-3 pt-0.5 text-xs text-semantic-error-primary">
14048
+ {errors.value}
14049
+ </p>
14050
+ )}
14051
+ </div>
14052
+
14053
+ {/* Action column \u2014 delete aligned with row (same as KeyValueRow / knowledge-base-card) */}
14054
+ <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">
14055
+ <Button
14226
14056
  type="button"
14057
+ variant="ghost"
14058
+ size="icon"
14227
14059
  onClick={() => remove(row.id)}
14228
- 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"
14060
+ className={cn("rounded-md", deleteRowButtonClass)}
14229
14061
  aria-label="Delete row"
14230
14062
  >
14231
- <Trash2 className="size-3.5" />
14232
- </button>
14063
+ <Trash2 className="size-4" />
14064
+ </Button>
14233
14065
  </div>
14234
14066
  </div>
14235
-
14236
- {/* Desktop: side-by-side */}
14237
- <div className="hidden sm:flex">
14238
- <input
14239
- type="text"
14240
- value={row.key}
14241
- onChange={(e) => update(row.id, { key: e.target.value })}
14242
- placeholder="Key"
14243
- 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"
14244
- />
14245
- <input
14246
- type="text"
14247
- value={row.value}
14248
- onChange={(e) => update(row.id, { value: e.target.value })}
14249
- placeholder="Type {{ to add variables"
14250
- 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"
14251
- />
14252
- <button
14253
- type="button"
14254
- onClick={() => remove(row.id)}
14255
- 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"
14256
- aria-label="Delete row"
14257
- >
14258
- <Trash2 className="size-3.5" />
14259
- </button>
14260
- </div>
14261
- </div>
14262
- ))}
14067
+ );
14068
+ })}
14263
14069
 
14264
14070
  {/* Add row \u2014 always visible */}
14265
14071
  <button
@@ -14286,6 +14092,8 @@ export const CreateFunctionModal = React.forwardRef<
14286
14092
  onOpenChange,
14287
14093
  onSubmit,
14288
14094
  onTestApi,
14095
+ initialData,
14096
+ isEditing = false,
14289
14097
  promptMinLength = 100,
14290
14098
  promptMaxLength = 5000,
14291
14099
  initialStep = 1,
@@ -14296,31 +14104,52 @@ export const CreateFunctionModal = React.forwardRef<
14296
14104
  ) => {
14297
14105
  const [step, setStep] = React.useState<1 | 2>(initialStep);
14298
14106
 
14299
- const [name, setName] = React.useState("");
14300
- const [prompt, setPrompt] = React.useState("");
14107
+ const [name, setName] = React.useState(initialData?.name ?? "");
14108
+ const [prompt, setPrompt] = React.useState(initialData?.prompt ?? "");
14301
14109
 
14302
- const [method, setMethod] = React.useState<HttpMethod>("GET");
14303
- const [url, setUrl] = React.useState("");
14110
+ const [method, setMethod] = React.useState<HttpMethod>(initialData?.method ?? "GET");
14111
+ const [url, setUrl] = React.useState(initialData?.url ?? "");
14304
14112
  const [activeTab, setActiveTab] =
14305
14113
  React.useState<FunctionTabType>(initialTab);
14306
- const [headers, setHeaders] = React.useState<KeyValuePair[]>([]);
14307
- const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>([]);
14308
- const [body, setBody] = React.useState("");
14114
+ const [headers, setHeaders] = React.useState<KeyValuePair[]>(initialData?.headers ?? []);
14115
+ const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>(initialData?.queryParams ?? []);
14116
+ const [body, setBody] = React.useState(initialData?.body ?? "");
14309
14117
  const [apiResponse, setApiResponse] = React.useState("");
14310
14118
  const [isTesting, setIsTesting] = React.useState(false);
14119
+ const [step2SubmitAttempted, setStep2SubmitAttempted] = React.useState(false);
14120
+
14121
+ // Sync form state from initialData each time the modal opens
14122
+ React.useEffect(() => {
14123
+ if (open) {
14124
+ setStep(initialStep);
14125
+ setName(initialData?.name ?? "");
14126
+ setPrompt(initialData?.prompt ?? "");
14127
+ setMethod(initialData?.method ?? "GET");
14128
+ setUrl(initialData?.url ?? "");
14129
+ setActiveTab(initialTab);
14130
+ setHeaders(initialData?.headers ?? []);
14131
+ setQueryParams(initialData?.queryParams ?? []);
14132
+ setBody(initialData?.body ?? "");
14133
+ setApiResponse("");
14134
+ setStep2SubmitAttempted(false);
14135
+ }
14136
+ // Re-run only when modal opens; intentionally exclude deep deps to avoid mid-session resets
14137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14138
+ }, [open]);
14311
14139
 
14312
14140
  const reset = React.useCallback(() => {
14313
14141
  setStep(initialStep);
14314
- setName("");
14315
- setPrompt("");
14316
- setMethod("GET");
14317
- setUrl("");
14142
+ setName(initialData?.name ?? "");
14143
+ setPrompt(initialData?.prompt ?? "");
14144
+ setMethod(initialData?.method ?? "GET");
14145
+ setUrl(initialData?.url ?? "");
14318
14146
  setActiveTab(initialTab);
14319
- setHeaders([]);
14320
- setQueryParams([]);
14321
- setBody("");
14147
+ setHeaders(initialData?.headers ?? []);
14148
+ setQueryParams(initialData?.queryParams ?? []);
14149
+ setBody(initialData?.body ?? "");
14322
14150
  setApiResponse("");
14323
- }, [initialStep, initialTab]);
14151
+ setStep2SubmitAttempted(false);
14152
+ }, [initialData, initialStep, initialTab]);
14324
14153
 
14325
14154
  const handleClose = React.useCallback(() => {
14326
14155
  reset();
@@ -14340,7 +14169,21 @@ export const CreateFunctionModal = React.forwardRef<
14340
14169
  if (name.trim() && prompt.trim().length >= promptMinLength) setStep(2);
14341
14170
  };
14342
14171
 
14172
+ const queryParamsHaveErrors = (rows: KeyValuePair[]): boolean =>
14173
+ rows.some((row) => {
14174
+ const hasInput = row.key.trim() !== "" || row.value.trim() !== "";
14175
+ if (!hasInput) return false;
14176
+ return (
14177
+ validateQueryParamKey(row.key) !== undefined ||
14178
+ validateQueryParamValue(row.value) !== undefined
14179
+ );
14180
+ });
14181
+
14343
14182
  const handleSubmit = () => {
14183
+ if (step === 2) {
14184
+ setStep2SubmitAttempted(true);
14185
+ if (queryParamsHaveErrors(queryParams)) return;
14186
+ }
14344
14187
  const data: CreateFunctionData = {
14345
14188
  name: name.trim(),
14346
14189
  prompt: prompt.trim(),
@@ -14400,7 +14243,7 @@ export const CreateFunctionModal = React.forwardRef<
14400
14243
  {/* \u2500\u2500 Header \u2500\u2500 */}
14401
14244
  <div className="flex items-center justify-between px-4 py-4 border-b border-semantic-border-layout shrink-0 sm:px-6">
14402
14245
  <DialogTitle className="text-base font-semibold text-semantic-text-primary">
14403
- Create Function
14246
+ {isEditing ? "Edit Function" : "Create Function"}
14404
14247
  </DialogTitle>
14405
14248
  <button
14406
14249
  type="button"
@@ -14558,6 +14401,14 @@ export const CreateFunctionModal = React.forwardRef<
14558
14401
  rows={queryParams}
14559
14402
  onChange={setQueryParams}
14560
14403
  label="Query parameter"
14404
+ getRowErrors={(row) => {
14405
+ const hasInput = row.key.trim() !== "" || row.value.trim() !== "";
14406
+ if (!hasInput && !step2SubmitAttempted) return {};
14407
+ return {
14408
+ key: validateQueryParamKey(row.key),
14409
+ value: validateQueryParamValue(row.value),
14410
+ };
14411
+ }}
14561
14412
  />
14562
14413
  )}
14563
14414
  {activeTab === "body" && (
@@ -15020,8 +14871,12 @@ export interface BotBehaviorCardProps {
15020
14871
  data: Partial<BotBehaviorData>;
15021
14872
  /** Callback when any field changes */
15022
14873
  onChange: (patch: Partial<BotBehaviorData>) => void;
14874
+ /** Called when the system prompt textarea loses focus */
14875
+ onSystemPromptBlur?: (value: string) => void;
15023
14876
  /** Session variables shown as insertable chips */
15024
14877
  sessionVariables?: string[];
14878
+ /** Maximum character length for the system prompt textarea (default: 25000) */
14879
+ maxLength?: number;
15025
14880
  /** Disables all fields in the card (view mode) */
15026
14881
  disabled?: boolean;
15027
14882
  /** Additional className for the card */
@@ -15069,6 +14924,7 @@ function StyledTextarea({
15069
14924
  value,
15070
14925
  rows = 3,
15071
14926
  onChange,
14927
+ onBlur,
15072
14928
  disabled,
15073
14929
  className,
15074
14930
  }: {
@@ -15076,6 +14932,7 @@ function StyledTextarea({
15076
14932
  value?: string;
15077
14933
  rows?: number;
15078
14934
  onChange?: (v: string) => void;
14935
+ onBlur?: (v: string) => void;
15079
14936
  disabled?: boolean;
15080
14937
  className?: string;
15081
14938
  }) {
@@ -15084,6 +14941,7 @@ function StyledTextarea({
15084
14941
  value={value ?? ""}
15085
14942
  rows={rows}
15086
14943
  onChange={(e) => onChange?.(e.target.value)}
14944
+ onBlur={(e) => onBlur?.(e.target.value)}
15087
14945
  placeholder={placeholder}
15088
14946
  disabled={disabled}
15089
14947
  className={cn(
@@ -15106,14 +14964,16 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15106
14964
  {
15107
14965
  data,
15108
14966
  onChange,
14967
+ onSystemPromptBlur,
15109
14968
  sessionVariables = DEFAULT_SESSION_VARIABLES,
14969
+ maxLength = 25000,
15110
14970
  disabled,
15111
14971
  className,
15112
14972
  },
15113
14973
  ref
15114
14974
  ) => {
15115
14975
  const prompt = data.systemPrompt ?? "";
15116
- const MAX = 25000;
14976
+ const MAX = maxLength;
15117
14977
 
15118
14978
  const insertVariable = (variable: string) => {
15119
14979
  onChange({ systemPrompt: prompt + variable });
@@ -15133,6 +14993,7 @@ const BotBehaviorCard = React.forwardRef<HTMLDivElement, BotBehaviorCardProps>(
15133
14993
  onChange={(v) => {
15134
14994
  if (v.length <= MAX) onChange({ systemPrompt: v });
15135
14995
  }}
14996
+ onBlur={onSystemPromptBlur}
15136
14997
  placeholder="You are a helpful assistant. Always start by greeting the user politely: 'Hello! Welcome. How can I assist you today?'"
15137
14998
  disabled={disabled}
15138
14999
  className="pb-8"
@@ -15178,6 +15039,13 @@ export { BotBehaviorCard };
15178
15039
  import { Download, Trash2, Plus, Info } from "lucide-react";
15179
15040
  import { cn } from "../../../lib/utils";
15180
15041
  import { Badge } from "../badge";
15042
+ import {
15043
+ Tooltip,
15044
+ TooltipContent,
15045
+ TooltipProvider,
15046
+ TooltipTrigger,
15047
+ } from "../tooltip";
15048
+ import { BOT_KNOWLEDGE_STATUS } from "./types";
15181
15049
  import type { KnowledgeBaseFile } from "./types";
15182
15050
 
15183
15051
  // \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
@@ -15191,6 +15059,8 @@ export interface KnowledgeBaseCardProps {
15191
15059
  onDownload?: (id: string) => void;
15192
15060
  /** Called when user clicks the delete button on a file */
15193
15061
  onDelete?: (id: string) => void;
15062
+ /** Hover text shown on the info icon next to the "Knowledge Base" title */
15063
+ infoTooltip?: string;
15194
15064
  /** Disables all interactive elements in the card (view mode) */
15195
15065
  disabled?: boolean;
15196
15066
  /** Additional className */
@@ -15199,13 +15069,13 @@ export interface KnowledgeBaseCardProps {
15199
15069
 
15200
15070
  // \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
15201
15071
 
15202
- type BadgeVariant = "active" | "destructive";
15203
- const STATUS_CONFIG: Record<string, { label: string; variant: BadgeVariant }> =
15204
- {
15205
- training: { label: "Training", variant: "active" },
15206
- trained: { label: "Trained", variant: "active" },
15207
- error: { label: "Error", variant: "destructive" },
15208
- };
15072
+ type BadgeVariant = "default" | "active" | "destructive";
15073
+ const STATUS_CONFIG: Record<BOT_KNOWLEDGE_STATUS, { label: string; variant: BadgeVariant }> = {
15074
+ [BOT_KNOWLEDGE_STATUS.PENDING]: { label: "Pending", variant: "default" },
15075
+ [BOT_KNOWLEDGE_STATUS.READY]: { label: "Ready", variant: "active" },
15076
+ [BOT_KNOWLEDGE_STATUS.PROCESSING]: { label: "Processing", variant: "active" },
15077
+ [BOT_KNOWLEDGE_STATUS.FAILED]: { label: "Failed", variant: "destructive" },
15078
+ };
15209
15079
 
15210
15080
  // \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
15211
15081
 
@@ -15216,6 +15086,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15216
15086
  onAdd,
15217
15087
  onDownload,
15218
15088
  onDelete,
15089
+ infoTooltip,
15219
15090
  disabled,
15220
15091
  className,
15221
15092
  },
@@ -15235,7 +15106,18 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15235
15106
  <h2 className="m-0 text-base font-semibold text-semantic-text-primary">
15236
15107
  Knowledge Base
15237
15108
  </h2>
15238
- <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15109
+ {infoTooltip ? (
15110
+ <TooltipProvider delayDuration={200}>
15111
+ <Tooltip>
15112
+ <TooltipTrigger asChild>
15113
+ <Info className="size-3.5 text-semantic-text-muted shrink-0 cursor-help" />
15114
+ </TooltipTrigger>
15115
+ <TooltipContent>{infoTooltip}</TooltipContent>
15116
+ </Tooltip>
15117
+ </TooltipProvider>
15118
+ ) : (
15119
+ <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15120
+ )}
15239
15121
  </div>
15240
15122
  <button
15241
15123
  type="button"
@@ -15256,7 +15138,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
15256
15138
  ) : (
15257
15139
  <div className="flex flex-col divide-y divide-semantic-border-layout">
15258
15140
  {files.map((file) => {
15259
- const status = STATUS_CONFIG[file.status] ?? STATUS_CONFIG.training;
15141
+ const status = STATUS_CONFIG[file.status] ?? STATUS_CONFIG[BOT_KNOWLEDGE_STATUS.PENDING];
15260
15142
  return (
15261
15143
  <div
15262
15144
  key={file.id}
@@ -15790,6 +15672,164 @@ const AdvancedSettingsCard = React.forwardRef<HTMLDivElement, AdvancedSettingsCa
15790
15672
  AdvancedSettingsCard.displayName = "AdvancedSettingsCard";
15791
15673
 
15792
15674
  export { AdvancedSettingsCard };
15675
+ `, prefix)
15676
+ },
15677
+ {
15678
+ name: "fallback-prompts-card.tsx",
15679
+ content: prefixTailwindClasses(`import * as React from "react";
15680
+ import { Info } from "lucide-react";
15681
+ import { cn } from "../../../lib/utils";
15682
+ import {
15683
+ Accordion,
15684
+ AccordionItem,
15685
+ AccordionTrigger,
15686
+ AccordionContent,
15687
+ } from "../accordion";
15688
+
15689
+ // \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
15690
+
15691
+ export interface FallbackPromptsData {
15692
+ agentBusyPrompt: string;
15693
+ noExtensionFoundPrompt: string;
15694
+ }
15695
+
15696
+ export interface FallbackPromptsCardProps {
15697
+ /** Current prompt text values */
15698
+ data: Partial<FallbackPromptsData>;
15699
+ /** Callback when any field changes */
15700
+ onChange: (patch: Partial<FallbackPromptsData>) => void;
15701
+ /** Called when the Agent Busy Prompt textarea loses focus */
15702
+ onAgentBusyPromptBlur?: (value: string) => void;
15703
+ /** Called when the No Extension Found textarea loses focus */
15704
+ onNoExtensionFoundPromptBlur?: (value: string) => void;
15705
+ /** Maximum character length for each prompt field (default: 25000) */
15706
+ maxLength?: number;
15707
+ /** Disables all fields in the card (view mode) */
15708
+ disabled?: boolean;
15709
+ /** Opens the accordion by default (default: false) */
15710
+ defaultOpen?: boolean;
15711
+ /** Additional className */
15712
+ className?: string;
15713
+ }
15714
+
15715
+ // \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
15716
+
15717
+ function PromptField({
15718
+ label,
15719
+ value,
15720
+ placeholder,
15721
+ maxLength,
15722
+ disabled,
15723
+ onChange,
15724
+ onBlur,
15725
+ rows = 2,
15726
+ }: {
15727
+ label: string;
15728
+ value: string;
15729
+ placeholder?: string;
15730
+ maxLength: number;
15731
+ disabled?: boolean;
15732
+ onChange: (value: string) => void;
15733
+ onBlur?: (value: string) => void;
15734
+ rows?: number;
15735
+ }) {
15736
+ return (
15737
+ <div className="flex flex-col gap-1.5">
15738
+ <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
15739
+ {label}
15740
+ </label>
15741
+ <textarea
15742
+ value={value}
15743
+ placeholder={placeholder}
15744
+ maxLength={maxLength}
15745
+ disabled={disabled}
15746
+ rows={rows}
15747
+ onChange={(e) => onChange(e.target.value)}
15748
+ onBlur={(e) => onBlur?.(e.target.value)}
15749
+ className={cn(
15750
+ "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",
15751
+ "focus:outline-none focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
15752
+ disabled && "cursor-not-allowed opacity-50"
15753
+ )}
15754
+ />
15755
+ <p className="m-0 text-xs text-semantic-text-muted text-right">
15756
+ {value.length}/{maxLength}
15757
+ </p>
15758
+ </div>
15759
+ );
15760
+ }
15761
+
15762
+ // \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
15763
+
15764
+ const FallbackPromptsCard = React.forwardRef<
15765
+ HTMLDivElement,
15766
+ FallbackPromptsCardProps
15767
+ >(
15768
+ (
15769
+ {
15770
+ data,
15771
+ onChange,
15772
+ onAgentBusyPromptBlur,
15773
+ onNoExtensionFoundPromptBlur,
15774
+ maxLength = 25000,
15775
+ disabled,
15776
+ defaultOpen = false,
15777
+ className,
15778
+ },
15779
+ ref
15780
+ ) => {
15781
+ return (
15782
+ <div
15783
+ ref={ref}
15784
+ className={cn(
15785
+ "bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden",
15786
+ className
15787
+ )}
15788
+ >
15789
+ <Accordion
15790
+ type="single"
15791
+ defaultValue={defaultOpen ? ["fallback"] : []}
15792
+ >
15793
+ <AccordionItem value="fallback">
15794
+ <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
15795
+ <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
15796
+ Fallback Prompts
15797
+ <Info className="size-3.5 text-semantic-text-muted shrink-0" />
15798
+ </span>
15799
+ </AccordionTrigger>
15800
+ <AccordionContent>
15801
+ <div className="flex flex-col gap-6 px-4 pt-4 sm:px-6 sm:pt-5">
15802
+ <PromptField
15803
+ label="Agent Busy Prompt"
15804
+ value={data.agentBusyPrompt ?? ""}
15805
+ placeholder="Executives are busy at the moment, we will connect you soon."
15806
+ maxLength={maxLength}
15807
+ disabled={disabled}
15808
+ onChange={(v) => onChange({ agentBusyPrompt: v })}
15809
+ onBlur={onAgentBusyPromptBlur}
15810
+ rows={2}
15811
+ />
15812
+ <PromptField
15813
+ label="No Extension Found"
15814
+ value={data.noExtensionFoundPrompt ?? ""}
15815
+ placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
15816
+ maxLength={maxLength}
15817
+ disabled={disabled}
15818
+ onChange={(v) => onChange({ noExtensionFoundPrompt: v })}
15819
+ onBlur={onNoExtensionFoundPromptBlur}
15820
+ rows={4}
15821
+ />
15822
+ </div>
15823
+ </AccordionContent>
15824
+ </AccordionItem>
15825
+ </Accordion>
15826
+ </div>
15827
+ );
15828
+ }
15829
+ );
15830
+ FallbackPromptsCard.displayName = "FallbackPromptsCard";
15831
+
15832
+ export { FallbackPromptsCard };
15793
15833
  `, prefix)
15794
15834
  },
15795
15835
  {
@@ -15800,7 +15840,16 @@ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
15800
15840
 
15801
15841
  export type FunctionTabType = "header" | "queryParams" | "body";
15802
15842
 
15803
- export type KnowledgeFileStatus = "training" | "trained" | "error";
15843
+ export const BOT_KNOWLEDGE_STATUS = {
15844
+ PENDING: "pending",
15845
+ READY: "ready",
15846
+ PROCESSING: "processing",
15847
+ FAILED: "failed",
15848
+ } as const;
15849
+
15850
+ export type BOT_KNOWLEDGE_STATUS = typeof BOT_KNOWLEDGE_STATUS[keyof typeof BOT_KNOWLEDGE_STATUS];
15851
+
15852
+ export type KnowledgeFileStatus = BOT_KNOWLEDGE_STATUS;
15804
15853
 
15805
15854
  export interface KeyValuePair {
15806
15855
  id: string;
@@ -15844,6 +15893,10 @@ export interface CreateFunctionModalProps {
15844
15893
  onOpenChange: (open: boolean) => void;
15845
15894
  onSubmit?: (data: CreateFunctionData) => void;
15846
15895
  onTestApi?: (step2: CreateFunctionStep2Data) => Promise<string>;
15896
+ /** Pre-fills all fields \u2014 use when opening the modal to edit an existing function */
15897
+ initialData?: Partial<CreateFunctionData>;
15898
+ /** When true, changes the modal title to "Edit Function" */
15899
+ isEditing?: boolean;
15847
15900
  /** Minimum character length for the prompt field (default: 100) */
15848
15901
  promptMinLength?: number;
15849
15902
  /** Maximum character length for the prompt field (default: 5000) */
@@ -15895,17 +15948,32 @@ export interface IvrBotConfigProps {
15895
15948
  onDownloadKnowledgeFile?: (fileId: string) => void;
15896
15949
  onDeleteKnowledgeFile?: (fileId: string) => void;
15897
15950
  onCreateFunction?: (data: CreateFunctionData) => void;
15898
- /** Called when user edits a custom function */
15951
+ /** Called when user edits a custom function. Receives the function id. */
15899
15952
  onEditFunction?: (id: string) => void;
15900
15953
  /** Called when user deletes a custom function */
15901
15954
  onDeleteFunction?: (id: string) => void;
15902
15955
  onTestApi?: (step2: CreateFunctionStep2Data) => Promise<string>;
15903
15956
  /** Hover text for the info icon in the Functions card header */
15904
15957
  functionsInfoTooltip?: string;
15958
+ /** Hover text for the info icon in the Knowledge Base card header */
15959
+ knowledgeBaseInfoTooltip?: string;
15905
15960
  /** Minimum character length for the function prompt (default: 100) */
15906
15961
  functionPromptMinLength?: number;
15907
15962
  /** Maximum character length for the function prompt (default: 5000) */
15908
15963
  functionPromptMaxLength?: number;
15964
+ /**
15965
+ * Pre-filled data shown when the edit function modal opens.
15966
+ * Pass when your app fetches full function data after onEditFunction fires.
15967
+ */
15968
+ functionEditData?: Partial<CreateFunctionData>;
15969
+ /** Max character length for the "How It Behaves" system prompt (default: 25000) */
15970
+ systemPromptMaxLength?: number;
15971
+ /** Called when the system prompt textarea loses focus */
15972
+ onSystemPromptBlur?: (value: string) => void;
15973
+ /** Called when the Agent Busy Prompt textarea loses focus */
15974
+ onAgentBusyPromptBlur?: (value: string) => void;
15975
+ /** Called when the No Extension Found textarea loses focus */
15976
+ onNoExtensionFoundPromptBlur?: (value: string) => void;
15909
15977
  onBack?: () => void;
15910
15978
  /** Called when the play icon is clicked on a voice option */
15911
15979
  onPlayVoice?: (voiceValue: string) => void;
@@ -15952,6 +16020,12 @@ export { KnowledgeBaseCard } from "./knowledge-base-card";
15952
16020
  export { FunctionsCard } from "./functions-card";
15953
16021
  export { FrustrationHandoverCard } from "./frustration-handover-card";
15954
16022
  export { AdvancedSettingsCard } from "./advanced-settings-card";
16023
+ export { FallbackPromptsCard } from "./fallback-prompts-card";
16024
+ export type {
16025
+ FallbackPromptsData,
16026
+ FallbackPromptsCardProps,
16027
+ } from "./fallback-prompts-card";
16028
+ export { BOT_KNOWLEDGE_STATUS } from "./types";
15955
16029
  export { CreateFunctionModal } from "./create-function-modal";
15956
16030
  export { FileUploadModal } from "../file-upload-modal";
15957
16031
  export { IvrBotConfig } from "./ivr-bot-config";
@@ -16631,159 +16705,6 @@ export interface WalletTopupProps {
16631
16705
  name: "index.ts",
16632
16706
  content: prefixTailwindClasses(`export { WalletTopup } from "./wallet-topup";
16633
16707
  export type { WalletTopupProps, AmountOption } from "./types";
16634
- `, prefix)
16635
- }
16636
- ]
16637
- },
16638
- "fallback-prompts-panel": {
16639
- name: "fallback-prompts-panel",
16640
- description: "An accordion card for configuring IVR bot fallback prompt messages (agent busy and no extension found)",
16641
- category: "custom",
16642
- dependencies: [
16643
- "clsx",
16644
- "tailwind-merge",
16645
- "lucide-react"
16646
- ],
16647
- internalDependencies: [
16648
- "accordion",
16649
- "textarea"
16650
- ],
16651
- isMultiFile: true,
16652
- directory: "fallback-prompts-panel",
16653
- mainFile: "fallback-prompts-panel.tsx",
16654
- files: [
16655
- {
16656
- name: "fallback-prompts-panel.tsx",
16657
- content: prefixTailwindClasses(`import * as React from "react";
16658
- import { Info } from "lucide-react";
16659
- import { cn } from "../../../lib/utils";
16660
- import { TextArea } from "../textarea";
16661
- import {
16662
- Accordion,
16663
- AccordionItem,
16664
- AccordionTrigger,
16665
- AccordionContent,
16666
- } from "../accordion";
16667
- import type { FallbackPromptsBaseProps } from "./types";
16668
-
16669
- export interface FallbackPromptsPanelProps
16670
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">,
16671
- FallbackPromptsBaseProps {}
16672
-
16673
- const AGENT_BUSY_PLACEHOLDER =
16674
- "Executives are busy at the moment, we will connect you soon.";
16675
- const NO_EXTENSION_PLACEHOLDER =
16676
- "Sorry, the requested extension is currently unavailable. Let me help you directly.";
16677
-
16678
- const FallbackPromptsPanel = React.forwardRef<
16679
- HTMLDivElement,
16680
- FallbackPromptsPanelProps
16681
- >(
16682
- (
16683
- {
16684
- data,
16685
- onChange,
16686
- tooltipContent = "Configure the messages your bot speaks when it cannot reach an agent or extension.",
16687
- defaultOpen = true,
16688
- disabled,
16689
- className,
16690
- ...rest
16691
- },
16692
- ref
16693
- ) => {
16694
- return (
16695
- <div
16696
- ref={ref}
16697
- className={cn(
16698
- "bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden",
16699
- className
16700
- )}
16701
- {...rest}
16702
- >
16703
- <Accordion
16704
- type="single"
16705
- defaultValue={defaultOpen ? ["fallback-prompts"] : []}
16706
- >
16707
- <AccordionItem value="fallback-prompts">
16708
- <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline hover:bg-transparent sm:px-6 sm:py-5">
16709
- <div className="flex items-center gap-1.5">
16710
- <span className="text-base font-semibold text-semantic-text-primary">
16711
- Fallback Prompts
16712
- </span>
16713
- <span
16714
- title={tooltipContent}
16715
- className="flex items-center text-semantic-text-muted cursor-default"
16716
- aria-label={tooltipContent}
16717
- >
16718
- <Info className="size-3.5" />
16719
- </span>
16720
- </div>
16721
- </AccordionTrigger>
16722
-
16723
- <AccordionContent>
16724
- <div className="flex flex-col gap-4 px-4 pt-4 sm:gap-6 sm:px-6 sm:pt-6">
16725
- <TextArea
16726
- label="Agent Busy Prompt"
16727
- placeholder={AGENT_BUSY_PLACEHOLDER}
16728
- value={data.agentBusyPrompt ?? ""}
16729
- onChange={(e) =>
16730
- onChange({ agentBusyPrompt: e.target.value })
16731
- }
16732
- rows={3}
16733
- disabled={disabled}
16734
- />
16735
-
16736
- <TextArea
16737
- label="No Extension Found"
16738
- placeholder={NO_EXTENSION_PLACEHOLDER}
16739
- value={data.noExtensionFound ?? ""}
16740
- onChange={(e) =>
16741
- onChange({ noExtensionFound: e.target.value })
16742
- }
16743
- rows={3}
16744
- disabled={disabled}
16745
- />
16746
- </div>
16747
- </AccordionContent>
16748
- </AccordionItem>
16749
- </Accordion>
16750
- </div>
16751
- );
16752
- }
16753
- );
16754
- FallbackPromptsPanel.displayName = "FallbackPromptsPanel";
16755
-
16756
- export { FallbackPromptsPanel };
16757
- `, prefix)
16758
- },
16759
- {
16760
- name: "types.ts",
16761
- content: prefixTailwindClasses(`export interface FallbackPromptsData {
16762
- /** Text spoken when all agents are busy */
16763
- agentBusyPrompt: string;
16764
- /** Text spoken when a dialed extension is not found */
16765
- noExtensionFound: string;
16766
- }
16767
-
16768
- export interface FallbackPromptsBaseProps {
16769
- /** Current prompt values */
16770
- data: Partial<FallbackPromptsData>;
16771
- /** Callback fired when any prompt value changes */
16772
- onChange: (patch: Partial<FallbackPromptsData>) => void;
16773
- /** Tooltip content shown next to the title */
16774
- tooltipContent?: string;
16775
- /** Opens the panel expanded by default */
16776
- defaultOpen?: boolean;
16777
- /** Disables all textareas */
16778
- disabled?: boolean;
16779
- }
16780
- `, prefix)
16781
- },
16782
- {
16783
- name: "index.ts",
16784
- content: prefixTailwindClasses(`export { FallbackPromptsPanel } from "./fallback-prompts-panel";
16785
- export type { FallbackPromptsPanelProps } from "./fallback-prompts-panel";
16786
- export type { FallbackPromptsData, FallbackPromptsBaseProps } from "./types";
16787
16708
  `, prefix)
16788
16709
  }
16789
16710
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-ui",
3
- "version": "0.0.210-beta.0",
3
+ "version": "0.0.210-beta.10",
4
4
  "description": "CLI for adding myOperator UI components to your project",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",