myoperator-ui 0.0.206 → 0.0.207

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 +1205 -1172
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13041,9 +13041,9 @@ export type { BotListProps, Bot, BotType } from "./types";
13041
13041
  }
13042
13042
  ]
13043
13043
  },
13044
- "ivr-bot": {
13045
- name: "ivr-bot",
13046
- description: "IVR/Voicebot configuration page with Create Function modal (2-step wizard)",
13044
+ "file-upload-modal": {
13045
+ name: "file-upload-modal",
13046
+ description: "A reusable file upload modal with drag-and-drop, progress tracking, and error handling",
13047
13047
  category: "custom",
13048
13048
  dependencies: [
13049
13049
  "clsx",
@@ -13051,1250 +13051,1333 @@ export type { BotListProps, Bot, BotType } from "./types";
13051
13051
  "lucide-react"
13052
13052
  ],
13053
13053
  internalDependencies: [
13054
- "button",
13055
- "badge",
13056
- "switch",
13057
- "accordion",
13058
13054
  "dialog",
13059
- "select",
13060
- "creatable-select",
13061
- "creatable-multi-select",
13062
- "page-header",
13063
- "tag"
13055
+ "button"
13064
13056
  ],
13065
13057
  isMultiFile: true,
13066
- directory: "ivr-bot",
13067
- mainFile: "ivr-bot-config.tsx",
13058
+ directory: "file-upload-modal",
13059
+ mainFile: "file-upload-modal.tsx",
13068
13060
  files: [
13069
13061
  {
13070
- name: "ivr-bot-config.tsx",
13062
+ name: "file-upload-modal.tsx",
13071
13063
  content: prefixTailwindClasses(`import * as React from "react";
13072
- import { Info } from "lucide-react";
13064
+ import { Download, Trash2, X, XCircle } from "lucide-react";
13073
13065
  import { cn } from "../../../lib/utils";
13074
13066
  import { Button } from "../button";
13075
- import { Badge } from "../badge";
13076
- import { PageHeader } from "../page-header";
13077
13067
  import {
13078
- Accordion,
13079
- AccordionItem,
13080
- AccordionTrigger,
13081
- AccordionContent,
13082
- } from "../accordion";
13083
- import { BotIdentityCard } from "./bot-identity-card";
13084
- import { BotBehaviorCard } from "./bot-behavior-card";
13085
- import { KnowledgeBaseCard } from "./knowledge-base-card";
13086
- import { FunctionsCard } from "./functions-card";
13087
- import { FrustrationHandoverCard } from "./frustration-handover-card";
13088
- import { AdvancedSettingsCard } from "./advanced-settings-card";
13089
- import { CreateFunctionModal } from "./create-function-modal";
13068
+ Dialog,
13069
+ DialogContent,
13070
+ DialogTitle,
13071
+ DialogDescription,
13072
+ } from "../dialog";
13090
13073
  import type {
13091
- IvrBotConfigProps,
13092
- IvrBotConfigData,
13093
- CreateFunctionData,
13074
+ FileUploadModalProps,
13075
+ UploadItem,
13076
+ UploadStatus,
13094
13077
  } from "./types";
13095
13078
 
13096
- // \u2500\u2500\u2500 Styled Textarea (still used by FallbackPromptsAccordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13097
- function StyledTextarea({
13098
- placeholder,
13099
- value,
13100
- rows = 3,
13101
- onChange,
13102
- className,
13103
- }: {
13104
- placeholder?: string;
13105
- value?: string;
13106
- rows?: number;
13107
- onChange?: (v: string) => void;
13108
- className?: string;
13109
- }) {
13110
- return (
13111
- <textarea
13112
- value={value ?? ""}
13113
- rows={rows}
13114
- onChange={(e) => onChange?.(e.target.value)}
13115
- placeholder={placeholder}
13116
- className={cn(
13117
- "w-full px-4 py-2.5 text-base rounded border resize-none",
13118
- "border-semantic-border-input bg-semantic-bg-primary",
13119
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
13120
- "outline-none hover:border-semantic-border-input-focus",
13121
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
13122
- className
13123
- )}
13124
- />
13125
- );
13126
- }
13079
+ const DEFAULT_ACCEPTED = ".doc,.docx,.pdf,.csv,.xls,.xlsx,.txt";
13080
+ const DEFAULT_FORMAT_DESC =
13081
+ "Max file size 100 MB (Supported Format: .docs, .pdf, .csv, .xls, .xlxs, .txt)";
13127
13082
 
13128
- // \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
13129
- function Field({
13130
- label,
13131
- children,
13132
- }: {
13133
- label: string;
13134
- children: React.ReactNode;
13135
- }) {
13136
- return (
13137
- <div className="flex flex-col gap-1.5">
13138
- <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
13139
- {label}
13140
- </label>
13141
- {children}
13142
- </div>
13143
- );
13083
+ function generateId() {
13084
+ return Math.random().toString(36).slice(2, 9);
13144
13085
  }
13145
13086
 
13146
- // \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
13147
- function FallbackPromptsAccordion({
13148
- data,
13149
- onChange,
13150
- }: {
13151
- data: Partial<IvrBotConfigData>;
13152
- onChange: (patch: Partial<IvrBotConfigData>) => void;
13153
- }) {
13154
- return (
13155
- <div className="bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden">
13156
- <Accordion type="single">
13157
- <AccordionItem value="fallback">
13158
- <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
13159
- <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
13160
- Fallback Prompts
13161
- <Info className="size-3.5 text-semantic-text-muted shrink-0" />
13162
- </span>
13163
- </AccordionTrigger>
13164
- <AccordionContent>
13165
- <div className="px-4 pt-4 pb-2 flex flex-col gap-6 sm:px-6 sm:pt-6">
13166
- <Field label="Agent Busy Prompt">
13167
- <StyledTextarea
13168
- value={data.agentBusyPrompt ?? ""}
13169
- onChange={(v) => onChange({ agentBusyPrompt: v })}
13170
- placeholder="Executives are busy at the moment, we will connect you soon."
13171
- />
13172
- </Field>
13173
- <Field label="No Extension Found">
13174
- <StyledTextarea
13175
- value={data.noExtensionPrompt ?? ""}
13176
- onChange={(v) => onChange({ noExtensionPrompt: v })}
13177
- placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
13178
- />
13179
- </Field>
13180
- </div>
13181
- </AccordionContent>
13182
- </AccordionItem>
13183
- </Accordion>
13184
- </div>
13087
+ function useFakeProgress() {
13088
+ const intervalsRef = React.useRef<
13089
+ Record<string, ReturnType<typeof setInterval>>
13090
+ >({});
13091
+
13092
+ const start = React.useCallback(
13093
+ (
13094
+ id: string,
13095
+ setItems: React.Dispatch<React.SetStateAction<UploadItem[]>>
13096
+ ) => {
13097
+ const interval = setInterval(() => {
13098
+ setItems((prev) => {
13099
+ let done = false;
13100
+ const updated = prev.map((item) => {
13101
+ if (item.id !== id || item.status !== "uploading") return item;
13102
+ const next = Math.min(item.progress + 15, 100);
13103
+ if (next === 100) done = true;
13104
+ return {
13105
+ ...item,
13106
+ progress: next,
13107
+ status: (next === 100 ? "done" : "uploading") as UploadStatus,
13108
+ };
13109
+ });
13110
+ if (done) {
13111
+ clearInterval(interval);
13112
+ delete intervalsRef.current[id];
13113
+ }
13114
+ return updated;
13115
+ });
13116
+ }, 500);
13117
+ intervalsRef.current[id] = interval;
13118
+ },
13119
+ []
13185
13120
  );
13186
- }
13187
13121
 
13188
- // \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
13189
- const DEFAULT_DATA: IvrBotConfigData = {
13190
- botName: "",
13191
- primaryRole: "",
13192
- tone: [],
13193
- voice: "",
13194
- language: "",
13195
- systemPrompt: "",
13196
- agentBusyPrompt: "",
13197
- noExtensionPrompt: "",
13198
- knowledgeBaseFiles: [],
13199
- functions: [
13200
- { id: "fn-1", name: "transfer_to_extension (extension_number)", isBuiltIn: true },
13201
- { id: "fn-2", name: "end_call()", isBuiltIn: true },
13202
- ],
13203
- frustrationHandoverEnabled: false,
13204
- escalationDepartment: "",
13205
- silenceTimeout: 15,
13206
- callEndThreshold: 3,
13207
- interruptionHandling: true,
13208
- };
13122
+ const cancel = React.useCallback((id: string) => {
13123
+ clearInterval(intervalsRef.current[id]);
13124
+ delete intervalsRef.current[id];
13125
+ }, []);
13209
13126
 
13210
- // \u2500\u2500\u2500 Main IvrBotConfig \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13211
- export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13212
- (
13213
- {
13214
- botTitle = "IVR bot",
13215
- botType = "Voicebot",
13216
- lastUpdatedAt,
13217
- initialData,
13218
- onSaveAsDraft,
13219
- onPublish,
13220
- onSaveKnowledgeFiles,
13221
- onUploadKnowledgeFile,
13222
- onSampleFileDownload,
13223
- onDownloadKnowledgeFile,
13224
- onDeleteKnowledgeFile,
13225
- onCreateFunction,
13226
- onEditFunction,
13227
- onDeleteFunction,
13228
- onTestApi,
13229
- onBack,
13230
- onPlayVoice,
13231
- onPauseVoice,
13232
- playingVoice,
13233
- roleOptions,
13234
- toneOptions,
13235
- voiceOptions,
13236
- languageOptions,
13237
- sessionVariables,
13238
- escalationDepartmentOptions,
13239
- silenceTimeoutMin,
13240
- silenceTimeoutMax,
13241
- callEndThresholdMin,
13242
- callEndThresholdMax,
13243
- className,
13244
- },
13245
- ref
13246
- ) => {
13247
- const [data, setData] = React.useState<IvrBotConfigData>({
13248
- ...DEFAULT_DATA,
13249
- ...initialData,
13250
- });
13251
- const [createFnOpen, setCreateFnOpen] = React.useState(false);
13127
+ const cancelAll = React.useCallback(() => {
13128
+ Object.values(intervalsRef.current).forEach(clearInterval);
13129
+ intervalsRef.current = {};
13130
+ }, []);
13252
13131
 
13253
- const update = (patch: Partial<IvrBotConfigData>) =>
13254
- setData((prev) => ({ ...prev, ...patch }));
13132
+ return { start, cancel, cancelAll };
13133
+ }
13255
13134
 
13256
- const handleCreateFunction = (fnData: CreateFunctionData) => {
13257
- const newFn = { id: \`fn-\${Date.now()}\`, name: fnData.name };
13258
- update({ functions: [...data.functions, newFn] });
13259
- onCreateFunction?.(fnData);
13260
- };
13135
+ function getTimeRemaining(progress: number) {
13136
+ const steps = Math.ceil((100 - progress) / 15);
13137
+ const secs = steps * 3;
13138
+ return secs > 60
13139
+ ? \`\${Math.ceil(secs / 60)} minutes remaining\`
13140
+ : \`\${secs} seconds remaining\`;
13141
+ }
13261
13142
 
13262
- return (
13263
- <div ref={ref} className={cn("flex flex-col min-h-screen bg-semantic-bg-primary", className)}>
13264
- {/* Page header */}
13265
- <PageHeader
13266
- showBackButton
13267
- onBackClick={onBack}
13268
- title={botTitle}
13269
- badge={
13270
- <Badge variant="outline" className="text-xs font-normal">
13271
- {botType}
13272
- </Badge>
13273
- }
13274
- actions={
13275
- <>
13276
- {lastUpdatedAt && (
13277
- <span className="hidden sm:inline text-sm text-semantic-text-muted mr-1">
13278
- Last updated at: {lastUpdatedAt}
13279
- </span>
13280
- )}
13281
- <Button
13282
- variant="outline"
13283
- onClick={() => onSaveAsDraft?.(data)}
13284
- >
13285
- Save as Draft
13286
- </Button>
13287
- <Button
13288
- variant="default"
13289
- onClick={() => onPublish?.(data)}
13290
- >
13291
- Publish Bot
13292
- </Button>
13293
- </>
13294
- }
13295
- />
13143
+ const FileUploadModal = React.forwardRef<HTMLDivElement, FileUploadModalProps>(
13144
+ (
13145
+ {
13146
+ open,
13147
+ onOpenChange,
13148
+ onUpload,
13149
+ onSave,
13150
+ onCancel,
13151
+ onSampleDownload,
13152
+ sampleDownloadLabel = "Download sample file",
13153
+ showSampleDownload,
13154
+ acceptedFormats = DEFAULT_ACCEPTED,
13155
+ formatDescription = DEFAULT_FORMAT_DESC,
13156
+ maxFileSizeMB = 100,
13157
+ multiple = true,
13158
+ title = "File Upload",
13159
+ uploadButtonLabel = "Upload from device",
13160
+ dropDescription = "or drag and drop file here",
13161
+ saveLabel = "Save",
13162
+ cancelLabel = "Cancel",
13163
+ saving = false,
13164
+ className,
13165
+ ...props
13166
+ },
13167
+ ref
13168
+ ) => {
13169
+ const [items, setItems] = React.useState<UploadItem[]>([]);
13170
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
13171
+ const fakeProgress = useFakeProgress();
13296
13172
 
13297
- {/* Body \u2014 two-column layout: left white, right gray panel */}
13298
- <div className="flex flex-col lg:flex-row lg:flex-1 min-h-0">
13299
- {/* Left column \u2014 white background */}
13300
- <div className="flex flex-col gap-6 px-4 py-4 sm:px-6 sm:py-6 lg:flex-[3] min-w-0 lg:max-w-[720px]">
13301
- <BotIdentityCard
13302
- data={data}
13303
- onChange={update}
13304
- onPlayVoice={onPlayVoice}
13305
- onPauseVoice={onPauseVoice}
13306
- playingVoice={playingVoice}
13307
- roleOptions={roleOptions}
13308
- toneOptions={toneOptions}
13309
- voiceOptions={voiceOptions}
13310
- languageOptions={languageOptions}
13311
- />
13312
- <BotBehaviorCard
13313
- data={data}
13314
- onChange={update}
13315
- sessionVariables={sessionVariables}
13316
- />
13317
- <FallbackPromptsAccordion data={data} onChange={update} />
13318
- </div>
13173
+ const shouldShowSampleDownload =
13174
+ showSampleDownload ?? !!onSampleDownload;
13319
13175
 
13320
- {/* Right column \u2014 gray panel extending full height */}
13321
- <div className="flex flex-col gap-6 px-4 py-4 sm:px-6 sm:py-6 lg:flex-[2] min-w-0 bg-semantic-bg-ui border-l border-semantic-border-layout">
13322
- <KnowledgeBaseCard
13323
- files={data.knowledgeBaseFiles}
13324
- onSaveFiles={onSaveKnowledgeFiles}
13325
- onUploadFile={onUploadKnowledgeFile}
13326
- onSampleDownload={onSampleFileDownload}
13327
- onDownload={onDownloadKnowledgeFile}
13328
- onDelete={(id) => {
13329
- update({
13330
- knowledgeBaseFiles: data.knowledgeBaseFiles.filter(
13331
- (f) => f.id !== id
13332
- ),
13333
- });
13334
- onDeleteKnowledgeFile?.(id);
13335
- }}
13336
- />
13337
- <FunctionsCard
13338
- functions={data.functions}
13339
- onAddFunction={() => setCreateFnOpen(true)}
13340
- onEditFunction={onEditFunction}
13341
- onDeleteFunction={(id) => {
13342
- update({
13343
- functions: data.functions.filter((f) => f.id !== id),
13344
- });
13345
- onDeleteFunction?.(id);
13346
- }}
13347
- />
13348
- <FrustrationHandoverCard
13349
- data={data}
13350
- onChange={update}
13351
- departmentOptions={escalationDepartmentOptions}
13352
- />
13353
- <AdvancedSettingsCard
13354
- data={data}
13355
- onChange={update}
13356
- silenceTimeoutMin={silenceTimeoutMin}
13357
- silenceTimeoutMax={silenceTimeoutMax}
13358
- callEndThresholdMin={callEndThresholdMin}
13359
- callEndThresholdMax={callEndThresholdMax}
13360
- />
13361
- </div>
13362
- </div>
13176
+ const addFiles = React.useCallback(
13177
+ (fileList: FileList | null) => {
13178
+ if (!fileList) return;
13363
13179
 
13364
- {/* Create Function Modal */}
13365
- <CreateFunctionModal
13366
- open={createFnOpen}
13367
- onOpenChange={setCreateFnOpen}
13368
- onSubmit={handleCreateFunction}
13369
- onTestApi={onTestApi}
13370
- />
13371
- </div>
13372
- );
13373
- }
13374
- );
13180
+ Array.from(fileList).forEach((file) => {
13181
+ if (file.size > maxFileSizeMB * 1024 * 1024) {
13182
+ const id = generateId();
13183
+ setItems((prev) => [
13184
+ ...prev,
13185
+ {
13186
+ id,
13187
+ file,
13188
+ progress: 0,
13189
+ status: "error",
13190
+ errorMessage: \`File exceeds \${maxFileSizeMB} MB limit\`,
13191
+ },
13192
+ ]);
13193
+ return;
13194
+ }
13375
13195
 
13376
- IvrBotConfig.displayName = "IvrBotConfig";
13377
- `, prefix)
13378
- },
13379
- {
13380
- name: "create-function-modal.tsx",
13381
- content: prefixTailwindClasses(`import * as React from "react";
13382
- import { Trash2, ChevronDown, X, Plus } from "lucide-react";
13383
- import { cn } from "../../../lib/utils";
13384
- import {
13385
- Dialog,
13386
- DialogContent,
13387
- DialogTitle,
13388
- } from "../dialog";
13389
- import { Button } from "../button";
13390
- import type {
13391
- CreateFunctionModalProps,
13392
- CreateFunctionData,
13393
- CreateFunctionStep2Data,
13394
- FunctionTabType,
13395
- HttpMethod,
13396
- KeyValuePair,
13397
- } from "./types";
13196
+ const id = generateId();
13197
+ setItems((prev) => [
13198
+ ...prev,
13199
+ { id, file, progress: 0, status: "uploading" },
13200
+ ]);
13398
13201
 
13399
- const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "DELETE", "PATCH"];
13400
- const FUNCTION_NAME_MAX = 30;
13401
- const BODY_MAX = 4000;
13202
+ if (onUpload) {
13203
+ onUpload(file, {
13204
+ onProgress: (progress) => {
13205
+ setItems((prev) =>
13206
+ prev.map((item) =>
13207
+ item.id === id
13208
+ ? {
13209
+ ...item,
13210
+ progress: Math.min(progress, 100),
13211
+ status:
13212
+ progress >= 100
13213
+ ? ("done" as UploadStatus)
13214
+ : ("uploading" as UploadStatus),
13215
+ }
13216
+ : item
13217
+ )
13218
+ );
13219
+ },
13220
+ onError: (message) => {
13221
+ setItems((prev) =>
13222
+ prev.map((item) =>
13223
+ item.id === id
13224
+ ? { ...item, status: "error" as UploadStatus, errorMessage: message }
13225
+ : item
13226
+ )
13227
+ );
13228
+ },
13229
+ }).then(() => {
13230
+ setItems((prev) =>
13231
+ prev.map((item) =>
13232
+ item.id === id && item.status === "uploading"
13233
+ ? { ...item, progress: 100, status: "done" as UploadStatus }
13234
+ : item
13235
+ )
13236
+ );
13237
+ }).catch((err) => {
13238
+ setItems((prev) =>
13239
+ prev.map((item) =>
13240
+ item.id === id && item.status !== "error"
13241
+ ? {
13242
+ ...item,
13243
+ status: "error" as UploadStatus,
13244
+ errorMessage:
13245
+ err instanceof Error
13246
+ ? err.message
13247
+ : "Upload failed",
13248
+ }
13249
+ : item
13250
+ )
13251
+ );
13252
+ });
13253
+ } else {
13254
+ fakeProgress.start(id, setItems);
13255
+ }
13256
+ });
13257
+ },
13258
+ [onUpload, maxFileSizeMB, fakeProgress]
13259
+ );
13402
13260
 
13403
- function generateId() {
13404
- return Math.random().toString(36).slice(2, 9);
13405
- }
13261
+ const removeItem = (id: string) => {
13262
+ fakeProgress.cancel(id);
13263
+ setItems((prev) => prev.filter((i) => i.id !== id));
13264
+ };
13406
13265
 
13407
- // \u2500\u2500 Shared input/textarea styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13408
- const inputCls = cn(
13409
- "w-full h-[42px] px-4 text-base rounded border",
13410
- "border-semantic-border-input bg-semantic-bg-primary",
13411
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
13412
- "outline-none hover:border-semantic-border-input-focus",
13413
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
13414
- );
13415
-
13416
- const textareaCls = cn(
13417
- "w-full px-4 py-2.5 text-base rounded border resize-none",
13418
- "border-semantic-border-input bg-semantic-bg-primary",
13419
- "text-semantic-text-primary placeholder:text-semantic-text-muted",
13420
- "outline-none hover:border-semantic-border-input-focus",
13421
- "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
13422
- );
13423
-
13424
- // \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
13425
- function KeyValueTable({
13426
- rows,
13427
- onChange,
13428
- label,
13429
- }: {
13430
- rows: KeyValuePair[];
13431
- onChange: (rows: KeyValuePair[]) => void;
13432
- label: string;
13433
- }) {
13434
- const update = (id: string, patch: Partial<KeyValuePair>) =>
13435
- onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
13436
-
13437
- const remove = (id: string) => onChange(rows.filter((r) => r.id !== id));
13438
-
13439
- const add = () =>
13440
- onChange([...rows, { id: generateId(), key: "", value: "" }]);
13441
-
13442
- return (
13443
- <div className="flex flex-col gap-1.5">
13444
- <span className="text-xs text-semantic-text-muted">{label}</span>
13445
- <div className="border border-semantic-border-layout rounded overflow-hidden">
13446
- {/* Column headers \u2014 desktop only */}
13447
- <div className="hidden sm:flex bg-semantic-bg-ui border-b border-semantic-border-layout">
13448
- <div className="flex-1 px-3 py-2 text-xs font-semibold text-semantic-text-muted border-r border-semantic-border-layout">
13449
- Key
13450
- </div>
13451
- <div className="flex-[2] px-3 py-2 text-xs font-semibold text-semantic-text-muted">
13452
- Value
13453
- </div>
13454
- <div className="w-10 shrink-0" />
13455
- </div>
13456
-
13457
- {/* Filled rows */}
13458
- {rows.map((row) => (
13459
- <div
13460
- key={row.id}
13461
- className="border-b border-semantic-border-layout last:border-b-0"
13462
- >
13463
- {/* Mobile: label + input pairs stacked */}
13464
- <div className="flex sm:hidden flex-col">
13465
- <div className="flex flex-col px-3 pt-2.5 pb-1 gap-0.5">
13466
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
13467
- Key
13468
- </span>
13469
- <input
13470
- type="text"
13471
- value={row.key}
13472
- onChange={(e) => update(row.id, { key: e.target.value })}
13473
- placeholder="Key"
13474
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
13475
- />
13476
- </div>
13477
- <div className="h-px bg-semantic-border-layout mx-3" />
13478
- <div className="flex items-start gap-2 px-3 py-2.5">
13479
- <div className="flex flex-col flex-1 gap-0.5">
13480
- <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
13481
- Value
13482
- </span>
13483
- <input
13484
- type="text"
13485
- value={row.value}
13486
- onChange={(e) => update(row.id, { value: e.target.value })}
13487
- placeholder="Type {{ to add variables"
13488
- className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
13489
- />
13490
- </div>
13491
- <button
13492
- type="button"
13493
- onClick={() => remove(row.id)}
13494
- 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"
13495
- aria-label="Delete row"
13496
- >
13497
- <Trash2 className="size-3.5" />
13498
- </button>
13499
- </div>
13500
- </div>
13501
-
13502
- {/* Desktop: side-by-side */}
13503
- <div className="hidden sm:flex">
13504
- <input
13505
- type="text"
13506
- value={row.key}
13507
- onChange={(e) => update(row.id, { key: e.target.value })}
13508
- placeholder="Key"
13509
- 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"
13510
- />
13511
- <input
13512
- type="text"
13513
- value={row.value}
13514
- onChange={(e) => update(row.id, { value: e.target.value })}
13515
- placeholder="Type {{ to add variables"
13516
- 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"
13517
- />
13518
- <button
13519
- type="button"
13520
- onClick={() => remove(row.id)}
13521
- 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"
13522
- aria-label="Delete row"
13523
- >
13524
- <Trash2 className="size-3.5" />
13525
- </button>
13526
- </div>
13527
- </div>
13528
- ))}
13529
-
13530
- {/* Add row \u2014 always visible */}
13531
- <button
13532
- type="button"
13533
- onClick={add}
13534
- className="w-full flex items-center gap-2 px-3 py-2.5 text-sm text-semantic-text-muted hover:bg-semantic-bg-hover transition-colors"
13535
- >
13536
- <Plus className="size-3.5 shrink-0" />
13537
- <span>Add row</span>
13538
- </button>
13539
- </div>
13540
- </div>
13541
- );
13542
- }
13543
-
13544
- // \u2500\u2500 Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13545
- export const CreateFunctionModal = React.forwardRef<
13546
- HTMLDivElement,
13547
- CreateFunctionModalProps
13548
- >(
13549
- (
13550
- {
13551
- open,
13552
- onOpenChange,
13553
- onSubmit,
13554
- onTestApi,
13555
- initialStep = 1,
13556
- initialTab = "header",
13557
- className,
13558
- },
13559
- ref
13560
- ) => {
13561
- const [step, setStep] = React.useState<1 | 2>(initialStep);
13562
-
13563
- const [name, setName] = React.useState("");
13564
- const [prompt, setPrompt] = React.useState("");
13565
-
13566
- const [method, setMethod] = React.useState<HttpMethod>("GET");
13567
- const [url, setUrl] = React.useState("");
13568
- const [activeTab, setActiveTab] =
13569
- React.useState<FunctionTabType>(initialTab);
13570
- const [headers, setHeaders] = React.useState<KeyValuePair[]>([]);
13571
- const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>([]);
13572
- const [body, setBody] = React.useState("");
13573
- const [apiResponse, setApiResponse] = React.useState("");
13574
- const [isTesting, setIsTesting] = React.useState(false);
13575
-
13576
- const reset = React.useCallback(() => {
13577
- setStep(initialStep);
13578
- setName("");
13579
- setPrompt("");
13580
- setMethod("GET");
13581
- setUrl("");
13582
- setActiveTab(initialTab);
13583
- setHeaders([]);
13584
- setQueryParams([]);
13585
- setBody("");
13586
- setApiResponse("");
13587
- }, [initialStep, initialTab]);
13588
-
13589
- const handleClose = React.useCallback(() => {
13590
- reset();
13266
+ const handleClose = () => {
13267
+ fakeProgress.cancelAll();
13268
+ setItems([]);
13269
+ onCancel?.();
13591
13270
  onOpenChange(false);
13592
- }, [reset, onOpenChange]);
13593
-
13594
- const handleNext = () => {
13595
- if (name.trim() && prompt.trim()) setStep(2);
13596
- };
13597
-
13598
- const handleSubmit = () => {
13599
- const data: CreateFunctionData = {
13600
- name: name.trim(),
13601
- prompt: prompt.trim(),
13602
- method,
13603
- url: url.trim(),
13604
- headers,
13605
- queryParams,
13606
- body,
13607
- };
13608
- onSubmit?.(data);
13609
- handleClose();
13610
13271
  };
13611
13272
 
13612
- const handleTestApi = async () => {
13613
- if (!onTestApi) return;
13614
- setIsTesting(true);
13615
- try {
13616
- const step2: CreateFunctionStep2Data = {
13617
- method,
13618
- url,
13619
- headers,
13620
- queryParams,
13621
- body,
13622
- };
13623
- const response = await onTestApi(step2);
13624
- setApiResponse(response);
13625
- } finally {
13626
- setIsTesting(false);
13627
- }
13273
+ const handleSave = () => {
13274
+ const completedFiles = items
13275
+ .filter((i) => i.status === "done")
13276
+ .map((i) => i.file);
13277
+ onSave?.(completedFiles);
13278
+ fakeProgress.cancelAll();
13279
+ setItems([]);
13280
+ onOpenChange(false);
13628
13281
  };
13629
13282
 
13630
- const isStep1Valid =
13631
- name.trim().length > 0 && prompt.trim().length > 0;
13632
-
13633
- const tabLabels: Record<FunctionTabType, string> = {
13634
- header: \`Header (\${headers.length})\`,
13635
- queryParams: \`Query params (\${queryParams.length})\`,
13636
- body: "Body",
13637
- };
13283
+ const hasCompleted = items.some((i) => i.status === "done");
13284
+ const hasUploading = items.some((i) => i.status === "uploading");
13638
13285
 
13639
13286
  return (
13640
13287
  <Dialog open={open} onOpenChange={onOpenChange}>
13641
13288
  <DialogContent
13642
13289
  ref={ref}
13643
- size="lg"
13290
+ size="default"
13644
13291
  hideCloseButton
13645
13292
  className={cn(
13646
- "flex flex-col gap-0 p-0 w-[calc(100vw-2rem)] sm:w-full",
13647
- "max-h-[calc(100svh-2rem)] overflow-hidden",
13293
+ "max-w-[min(660px,calc(100vw-2rem))] rounded-xl p-4 gap-0 sm:p-6",
13648
13294
  className
13649
13295
  )}
13296
+ {...props}
13650
13297
  >
13651
- {/* \u2500\u2500 Header \u2500\u2500 */}
13652
- <div className="flex items-center justify-between px-4 py-4 border-b border-semantic-border-layout shrink-0 sm:px-6">
13653
- <DialogTitle className="text-base font-semibold text-semantic-text-primary">
13654
- Create Function
13298
+ {/* Header */}
13299
+ <div className="flex items-center justify-between mb-6">
13300
+ <DialogTitle className="m-0 text-base font-semibold text-semantic-text-primary">
13301
+ {title}
13655
13302
  </DialogTitle>
13303
+ <DialogDescription className="sr-only">
13304
+ Upload files by clicking the button or dragging and dropping.
13305
+ </DialogDescription>
13656
13306
  <button
13657
13307
  type="button"
13658
13308
  onClick={handleClose}
13659
- className="rounded p-1.5 text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors"
13660
- aria-label="Close"
13309
+ className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-semantic-text-primary"
13310
+ aria-label="Close dialog"
13661
13311
  >
13662
- <X className="size-4" />
13312
+ <X className="h-4 w-4" />
13663
13313
  </button>
13664
13314
  </div>
13665
13315
 
13666
- {/* \u2500\u2500 Scrollable body \u2500\u2500 */}
13667
- <div className="flex-1 overflow-y-auto min-h-0 px-4 py-5 sm:px-6">
13668
- {/* \u2500 Step 1 \u2500 */}
13669
- {step === 1 && (
13670
- <div className="flex flex-col gap-5">
13671
- <div className="flex flex-col gap-1.5">
13672
- <label
13673
- htmlFor="fn-name"
13674
- className="text-sm font-semibold text-semantic-text-primary"
13675
- >
13676
- Function Name{" "}
13677
- <span className="text-semantic-error-primary">*</span>
13678
- </label>
13679
- <div className={cn("relative")}>
13680
- <input
13681
- id="fn-name"
13682
- type="text"
13683
- value={name}
13684
- maxLength={FUNCTION_NAME_MAX}
13685
- onChange={(e) => setName(e.target.value)}
13686
- placeholder="Enter name of the function"
13687
- className={cn(inputCls, "pr-16")}
13688
- />
13689
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs italic text-semantic-text-muted pointer-events-none">
13690
- {name.length}/{FUNCTION_NAME_MAX}
13691
- </span>
13692
- </div>
13693
- </div>
13694
-
13695
- <div className="flex flex-col gap-1.5">
13696
- <label
13697
- htmlFor="fn-prompt"
13698
- className="text-sm font-semibold text-semantic-text-primary"
13699
- >
13700
- Prompt{" "}
13701
- <span className="text-semantic-error-primary">*</span>
13702
- </label>
13703
- <textarea
13704
- id="fn-prompt"
13705
- value={prompt}
13706
- onChange={(e) => setPrompt(e.target.value)}
13707
- placeholder="Enter the description of the function"
13708
- rows={5}
13709
- className={textareaCls}
13710
- />
13711
- </div>
13712
- </div>
13316
+ {/* Body */}
13317
+ <div className="flex flex-col gap-4 items-end w-full">
13318
+ {shouldShowSampleDownload && (
13319
+ <button
13320
+ type="button"
13321
+ onClick={onSampleDownload}
13322
+ className="flex items-center gap-1.5 text-sm font-semibold text-semantic-text-link hover:opacity-80 transition-opacity"
13323
+ >
13324
+ <Download className="size-3.5" />
13325
+ {sampleDownloadLabel}
13326
+ </button>
13713
13327
  )}
13714
13328
 
13715
- {/* \u2500 Step 2 \u2500 */}
13716
- {step === 2 && (
13717
- <div className="flex flex-col gap-5">
13718
- {/* API URL \u2014 always a single combined row */}
13719
- <div className="flex flex-col gap-1.5">
13720
- <span className="text-xs text-semantic-text-muted tracking-[0.048px]">
13721
- API URL
13722
- </span>
13723
- <div
13724
- className={cn(
13725
- "flex h-[42px] rounded border border-semantic-border-input overflow-hidden bg-semantic-bg-primary",
13726
- "hover:border-semantic-border-input-focus",
13727
- "focus-within:border-semantic-border-input-focus focus-within:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
13728
- "transition-shadow"
13729
- )}
13730
- >
13731
- {/* Method selector */}
13732
- <div className="relative shrink-0 border-r border-semantic-border-layout">
13733
- <select
13734
- value={method}
13735
- onChange={(e) =>
13736
- setMethod(e.target.value as HttpMethod)
13737
- }
13738
- className="h-full w-[80px] pl-3 pr-7 text-base text-semantic-text-primary bg-transparent outline-none cursor-pointer appearance-none sm:w-[100px]"
13739
- aria-label="HTTP method"
13740
- >
13741
- {HTTP_METHODS.map((m) => (
13742
- <option key={m} value={m}>
13743
- {m}
13744
- </option>
13745
- ))}
13746
- </select>
13747
- <ChevronDown
13748
- className="absolute right-2 top-1/2 -translate-y-1/2 size-3 text-semantic-text-muted pointer-events-none"
13749
- aria-hidden="true"
13750
- />
13751
- </div>
13752
- {/* URL input */}
13753
- <input
13754
- type="text"
13755
- value={url}
13756
- onChange={(e) => setUrl(e.target.value)}
13757
- placeholder="Enter URL or Type {{ to add variables"
13758
- className="flex-1 min-w-0 px-3 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
13759
- />
13760
- </div>
13329
+ {/* Drop zone */}
13330
+ <div
13331
+ className="w-full border border-dashed border-semantic-border-layout bg-semantic-bg-ui rounded p-4"
13332
+ onDrop={(e) => {
13333
+ e.preventDefault();
13334
+ addFiles(e.dataTransfer.files);
13335
+ }}
13336
+ onDragOver={(e) => e.preventDefault()}
13337
+ >
13338
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
13339
+ <button
13340
+ type="button"
13341
+ onClick={() => fileInputRef.current?.click()}
13342
+ className="h-[42px] px-4 rounded border border-semantic-border-layout bg-semantic-bg-primary text-base font-semibold text-semantic-text-secondary shrink-0 hover:bg-semantic-bg-hover transition-colors w-full sm:w-auto"
13343
+ >
13344
+ {uploadButtonLabel}
13345
+ </button>
13346
+ <div className="flex flex-col gap-1">
13347
+ <p className="m-0 text-sm text-semantic-text-secondary tracking-[0.035px]">
13348
+ {dropDescription}
13349
+ </p>
13350
+ <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
13351
+ {formatDescription}
13352
+ </p>
13761
13353
  </div>
13354
+ </div>
13355
+ <input
13356
+ ref={fileInputRef}
13357
+ type="file"
13358
+ multiple={multiple}
13359
+ accept={acceptedFormats}
13360
+ className="hidden"
13361
+ onChange={(e) => {
13362
+ addFiles(e.target.files);
13363
+ e.target.value = "";
13364
+ }}
13365
+ />
13366
+ </div>
13762
13367
 
13763
- {/* Tabs \u2014 scrollable, no visible scrollbar */}
13764
- <div className="flex flex-col gap-4">
13368
+ {/* Upload item list */}
13369
+ {items.length > 0 && (
13370
+ <div className="flex flex-col gap-2.5 w-full">
13371
+ {items.map((item) => (
13765
13372
  <div
13766
- className={cn(
13767
- "flex border-b border-semantic-border-layout",
13768
- "overflow-x-auto",
13769
- "[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
13770
- )}
13373
+ key={item.id}
13374
+ className="bg-semantic-bg-primary border border-semantic-border-layout rounded px-4 py-3 flex flex-col gap-2"
13771
13375
  >
13772
- {(
13773
- ["header", "queryParams", "body"] as FunctionTabType[]
13774
- ).map((tab) => (
13376
+ <div className="flex items-start gap-3">
13377
+ <div className="flex flex-col gap-0.5 flex-1 min-w-0">
13378
+ <p className="m-0 text-sm text-semantic-text-primary tracking-[0.035px] truncate">
13379
+ {item.status === "uploading"
13380
+ ? "Uploading..."
13381
+ : item.file.name}
13382
+ </p>
13383
+ {item.status === "uploading" && (
13384
+ <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
13385
+ {item.progress}%&nbsp;&bull;&nbsp;
13386
+ {getTimeRemaining(item.progress)}
13387
+ </p>
13388
+ )}
13389
+ {item.status === "error" && (
13390
+ <p className="m-0 text-xs text-semantic-error-primary tracking-[0.048px]">
13391
+ {item.errorMessage ??
13392
+ "Something went wrong, Upload Failed."}
13393
+ </p>
13394
+ )}
13395
+ </div>
13775
13396
  <button
13776
- key={tab}
13777
13397
  type="button"
13778
- onClick={() => setActiveTab(tab)}
13398
+ onClick={() => removeItem(item.id)}
13399
+ aria-label={
13400
+ item.status === "uploading"
13401
+ ? "Cancel upload"
13402
+ : "Remove file"
13403
+ }
13779
13404
  className={cn(
13780
- "px-3 py-2 text-sm font-semibold transition-colors whitespace-nowrap shrink-0",
13781
- activeTab === tab
13782
- ? "text-semantic-text-secondary border-b-2 border-semantic-text-secondary -mb-px"
13783
- : "text-semantic-text-muted hover:text-semantic-text-primary"
13405
+ "shrink-0 mt-0.5 transition-colors",
13406
+ item.status === "uploading"
13407
+ ? "text-semantic-error-primary"
13408
+ : "text-semantic-text-muted hover:text-semantic-error-primary"
13784
13409
  )}
13785
13410
  >
13786
- {tabLabels[tab]}
13411
+ {item.status === "uploading" ? (
13412
+ <XCircle className="size-5" />
13413
+ ) : (
13414
+ <Trash2 className="size-5" />
13415
+ )}
13787
13416
  </button>
13788
- ))}
13789
- </div>
13790
-
13791
- {activeTab === "header" && (
13792
- <KeyValueTable
13793
- rows={headers}
13794
- onChange={setHeaders}
13795
- label="Header"
13796
- />
13797
- )}
13798
- {activeTab === "queryParams" && (
13799
- <KeyValueTable
13800
- rows={queryParams}
13801
- onChange={setQueryParams}
13802
- label="Query parameter"
13803
- />
13804
- )}
13805
- {activeTab === "body" && (
13806
- <div className="flex flex-col gap-1.5">
13807
- <span className="text-xs text-semantic-text-muted">
13808
- Body
13809
- </span>
13810
- <div className={cn("relative")}>
13811
- <textarea
13812
- value={body}
13813
- maxLength={BODY_MAX}
13814
- onChange={(e) => setBody(e.target.value)}
13815
- placeholder="Enter request body (JSON, XML etc). Type {{ to add variables"
13816
- rows={6}
13817
- className={cn(textareaCls, "pb-7")}
13417
+ </div>
13418
+ {item.status === "uploading" && (
13419
+ <div className="h-2 bg-semantic-bg-ui rounded-full overflow-hidden">
13420
+ <div
13421
+ className="h-full bg-semantic-success-primary rounded-full transition-all duration-300"
13422
+ style={{ width: \`\${item.progress}%\` }}
13818
13423
  />
13819
- <span className="absolute bottom-2 right-3 text-xs italic text-semantic-text-muted pointer-events-none">
13820
- {body.length}/{BODY_MAX}
13821
- </span>
13822
13424
  </div>
13823
- </div>
13824
- )}
13825
- </div>
13425
+ )}
13426
+ </div>
13427
+ ))}
13428
+ </div>
13429
+ )}
13430
+ </div>
13431
+
13432
+ {/* Footer */}
13433
+ <div className="flex flex-col-reverse gap-3 mt-4 sm:mt-6 sm:flex-row sm:justify-end sm:gap-2">
13434
+ <Button
13435
+ variant="outline"
13436
+ className="w-full sm:w-auto"
13437
+ onClick={handleClose}
13438
+ >
13439
+ {cancelLabel}
13440
+ </Button>
13441
+ <Button
13442
+ className="w-full sm:w-auto"
13443
+ onClick={handleSave}
13444
+ disabled={!hasCompleted || hasUploading}
13445
+ loading={saving}
13446
+ >
13447
+ {saveLabel}
13448
+ </Button>
13449
+ </div>
13450
+ </DialogContent>
13451
+ </Dialog>
13452
+ );
13453
+ }
13454
+ );
13455
+
13456
+ FileUploadModal.displayName = "FileUploadModal";
13457
+
13458
+ export { FileUploadModal };
13459
+ `, prefix)
13460
+ },
13461
+ {
13462
+ name: "types.ts",
13463
+ content: prefixTailwindClasses(`export type UploadStatus = "pending" | "uploading" | "done" | "error";
13464
+
13465
+ export interface UploadItem {
13466
+ id: string;
13467
+ file: File;
13468
+ progress: number;
13469
+ status: UploadStatus;
13470
+ errorMessage?: string;
13471
+ }
13472
+
13473
+ export interface UploadProgressHandlers {
13474
+ onProgress: (progress: number) => void;
13475
+ onError: (message: string) => void;
13476
+ }
13477
+
13478
+ export interface FileUploadModalProps
13479
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSave"> {
13480
+ open: boolean;
13481
+ onOpenChange: (open: boolean) => void;
13482
+ /** Called for each file to handle the actual upload. If not provided, uses fake progress (demo mode). */
13483
+ onUpload?: (file: File, handlers: UploadProgressHandlers) => Promise<void>;
13484
+ onSave?: (files: File[]) => void;
13485
+ onCancel?: () => void;
13486
+ onSampleDownload?: () => void;
13487
+ sampleDownloadLabel?: string;
13488
+ showSampleDownload?: boolean;
13489
+ acceptedFormats?: string;
13490
+ formatDescription?: string;
13491
+ maxFileSizeMB?: number;
13492
+ multiple?: boolean;
13493
+ title?: string;
13494
+ uploadButtonLabel?: string;
13495
+ dropDescription?: string;
13496
+ saveLabel?: string;
13497
+ cancelLabel?: string;
13498
+ saving?: boolean;
13499
+ }
13500
+ `, prefix)
13501
+ },
13502
+ {
13503
+ name: "index.ts",
13504
+ content: prefixTailwindClasses(`export { FileUploadModal } from "./file-upload-modal";
13505
+ export type {
13506
+ FileUploadModalProps,
13507
+ UploadProgressHandlers,
13508
+ UploadItem,
13509
+ UploadStatus,
13510
+ } from "./types";
13511
+ `, prefix)
13512
+ }
13513
+ ]
13514
+ },
13515
+ "ivr-bot": {
13516
+ name: "ivr-bot",
13517
+ description: "IVR/Voicebot configuration page with Create Function modal (2-step wizard)",
13518
+ category: "custom",
13519
+ dependencies: [
13520
+ "clsx",
13521
+ "tailwind-merge",
13522
+ "lucide-react"
13523
+ ],
13524
+ internalDependencies: [
13525
+ "button",
13526
+ "badge",
13527
+ "switch",
13528
+ "accordion",
13529
+ "dialog",
13530
+ "select",
13531
+ "creatable-select",
13532
+ "creatable-multi-select",
13533
+ "page-header",
13534
+ "tag",
13535
+ "file-upload-modal"
13536
+ ],
13537
+ isMultiFile: true,
13538
+ directory: "ivr-bot",
13539
+ mainFile: "ivr-bot-config.tsx",
13540
+ files: [
13541
+ {
13542
+ name: "ivr-bot-config.tsx",
13543
+ content: prefixTailwindClasses(`import * as React from "react";
13544
+ import { Info } from "lucide-react";
13545
+ import { cn } from "../../../lib/utils";
13546
+ import { Button } from "../button";
13547
+ import { Badge } from "../badge";
13548
+ import { PageHeader } from "../page-header";
13549
+ import {
13550
+ Accordion,
13551
+ AccordionItem,
13552
+ AccordionTrigger,
13553
+ AccordionContent,
13554
+ } from "../accordion";
13555
+ import { BotIdentityCard } from "./bot-identity-card";
13556
+ import { BotBehaviorCard } from "./bot-behavior-card";
13557
+ import { KnowledgeBaseCard } from "./knowledge-base-card";
13558
+ import { FunctionsCard } from "./functions-card";
13559
+ import { FrustrationHandoverCard } from "./frustration-handover-card";
13560
+ import { AdvancedSettingsCard } from "./advanced-settings-card";
13561
+ import { CreateFunctionModal } from "./create-function-modal";
13562
+ import { FileUploadModal } from "../file-upload-modal";
13563
+ import type {
13564
+ IvrBotConfigProps,
13565
+ IvrBotConfigData,
13566
+ CreateFunctionData,
13567
+ } from "./types";
13568
+
13569
+ // \u2500\u2500\u2500 Styled Textarea (still used by FallbackPromptsAccordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13570
+ function StyledTextarea({
13571
+ placeholder,
13572
+ value,
13573
+ rows = 3,
13574
+ onChange,
13575
+ className,
13576
+ }: {
13577
+ placeholder?: string;
13578
+ value?: string;
13579
+ rows?: number;
13580
+ onChange?: (v: string) => void;
13581
+ className?: string;
13582
+ }) {
13583
+ return (
13584
+ <textarea
13585
+ value={value ?? ""}
13586
+ rows={rows}
13587
+ onChange={(e) => onChange?.(e.target.value)}
13588
+ placeholder={placeholder}
13589
+ className={cn(
13590
+ "w-full px-4 py-2.5 text-base rounded border resize-none",
13591
+ "border-semantic-border-input bg-semantic-bg-primary",
13592
+ "text-semantic-text-primary placeholder:text-semantic-text-muted",
13593
+ "outline-none hover:border-semantic-border-input-focus",
13594
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
13595
+ className
13596
+ )}
13597
+ />
13598
+ );
13599
+ }
13600
+
13601
+ // \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
13602
+ function Field({
13603
+ label,
13604
+ children,
13605
+ }: {
13606
+ label: string;
13607
+ children: React.ReactNode;
13608
+ }) {
13609
+ return (
13610
+ <div className="flex flex-col gap-1.5">
13611
+ <label className="text-sm font-semibold text-semantic-text-secondary tracking-[0.014px]">
13612
+ {label}
13613
+ </label>
13614
+ {children}
13615
+ </div>
13616
+ );
13617
+ }
13618
+
13619
+ // \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
13620
+ function FallbackPromptsAccordion({
13621
+ data,
13622
+ onChange,
13623
+ }: {
13624
+ data: Partial<IvrBotConfigData>;
13625
+ onChange: (patch: Partial<IvrBotConfigData>) => void;
13626
+ }) {
13627
+ return (
13628
+ <div className="bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden">
13629
+ <Accordion type="single">
13630
+ <AccordionItem value="fallback">
13631
+ <AccordionTrigger className="px-4 py-4 border-b border-semantic-border-layout hover:no-underline sm:px-6 sm:py-5">
13632
+ <span className="flex items-center gap-1.5 text-base font-semibold text-semantic-text-primary">
13633
+ Fallback Prompts
13634
+ <Info className="size-3.5 text-semantic-text-muted shrink-0" />
13635
+ </span>
13636
+ </AccordionTrigger>
13637
+ <AccordionContent>
13638
+ <div className="px-4 pt-4 pb-2 flex flex-col gap-6 sm:px-6 sm:pt-6">
13639
+ <Field label="Agent Busy Prompt">
13640
+ <StyledTextarea
13641
+ value={data.agentBusyPrompt ?? ""}
13642
+ onChange={(v) => onChange({ agentBusyPrompt: v })}
13643
+ placeholder="Executives are busy at the moment, we will connect you soon."
13644
+ />
13645
+ </Field>
13646
+ <Field label="No Extension Found">
13647
+ <StyledTextarea
13648
+ value={data.noExtensionPrompt ?? ""}
13649
+ onChange={(v) => onChange({ noExtensionPrompt: v })}
13650
+ placeholder="Sorry, the requested extension is currently unavailable. Let me help you directly."
13651
+ />
13652
+ </Field>
13653
+ </div>
13654
+ </AccordionContent>
13655
+ </AccordionItem>
13656
+ </Accordion>
13657
+ </div>
13658
+ );
13659
+ }
13660
+
13661
+ // \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
13662
+ const DEFAULT_DATA: IvrBotConfigData = {
13663
+ botName: "",
13664
+ primaryRole: "",
13665
+ tone: [],
13666
+ voice: "",
13667
+ language: "",
13668
+ systemPrompt: "",
13669
+ agentBusyPrompt: "",
13670
+ noExtensionPrompt: "",
13671
+ knowledgeBaseFiles: [],
13672
+ functions: [
13673
+ { id: "fn-1", name: "transfer_to_extension (extension_number)", isBuiltIn: true },
13674
+ { id: "fn-2", name: "end_call()", isBuiltIn: true },
13675
+ ],
13676
+ frustrationHandoverEnabled: false,
13677
+ escalationDepartment: "",
13678
+ silenceTimeout: 15,
13679
+ callEndThreshold: 3,
13680
+ interruptionHandling: true,
13681
+ };
13682
+
13683
+ // \u2500\u2500\u2500 Main IvrBotConfig \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13684
+ export const IvrBotConfig = React.forwardRef<HTMLDivElement, IvrBotConfigProps>(
13685
+ (
13686
+ {
13687
+ botTitle = "IVR bot",
13688
+ botType = "Voicebot",
13689
+ lastUpdatedAt,
13690
+ initialData,
13691
+ onSaveAsDraft,
13692
+ onPublish,
13693
+ onSaveKnowledgeFiles,
13694
+ onUploadKnowledgeFile,
13695
+ onSampleFileDownload,
13696
+ onDownloadKnowledgeFile,
13697
+ onDeleteKnowledgeFile,
13698
+ onCreateFunction,
13699
+ onEditFunction,
13700
+ onDeleteFunction,
13701
+ onTestApi,
13702
+ onBack,
13703
+ onPlayVoice,
13704
+ onPauseVoice,
13705
+ playingVoice,
13706
+ roleOptions,
13707
+ toneOptions,
13708
+ voiceOptions,
13709
+ languageOptions,
13710
+ sessionVariables,
13711
+ escalationDepartmentOptions,
13712
+ silenceTimeoutMin,
13713
+ silenceTimeoutMax,
13714
+ callEndThresholdMin,
13715
+ callEndThresholdMax,
13716
+ className,
13717
+ },
13718
+ ref
13719
+ ) => {
13720
+ const [data, setData] = React.useState<IvrBotConfigData>({
13721
+ ...DEFAULT_DATA,
13722
+ ...initialData,
13723
+ });
13724
+ const [createFnOpen, setCreateFnOpen] = React.useState(false);
13725
+ const [uploadOpen, setUploadOpen] = React.useState(false);
13826
13726
 
13827
- {/* Test Your API */}
13828
- <div className="flex flex-col gap-4">
13829
- <div className="flex flex-col gap-1.5">
13830
- <span className="text-xs font-semibold text-semantic-text-muted tracking-[0.048px]">
13831
- Test Your API
13832
- </span>
13833
- <div className="border-t border-semantic-border-layout" />
13834
- </div>
13727
+ const update = (patch: Partial<IvrBotConfigData>) =>
13728
+ setData((prev) => ({ ...prev, ...patch }));
13835
13729
 
13836
- <button
13837
- type="button"
13838
- onClick={handleTestApi}
13839
- disabled={isTesting || !url.trim()}
13840
- className="w-full h-[42px] rounded text-sm font-semibold text-semantic-text-secondary bg-semantic-primary-surface disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-semantic-primary-surface/80 sm:w-auto sm:px-6 sm:self-end sm:ml-auto flex items-center justify-center"
13841
- >
13842
- {isTesting ? "Testing..." : "Test API"}
13843
- </button>
13730
+ const handleCreateFunction = (fnData: CreateFunctionData) => {
13731
+ const newFn = { id: \`fn-\${Date.now()}\`, name: fnData.name };
13732
+ update({ functions: [...data.functions, newFn] });
13733
+ onCreateFunction?.(fnData);
13734
+ };
13844
13735
 
13845
- <div className="flex flex-col gap-1.5">
13846
- <span className="text-xs text-semantic-text-muted">
13847
- Response from API
13848
- </span>
13849
- <textarea
13850
- readOnly
13851
- value={apiResponse}
13852
- rows={4}
13853
- className="w-full px-3 py-2.5 text-base rounded border border-semantic-border-layout bg-semantic-bg-ui text-semantic-text-primary resize-none outline-none"
13854
- placeholder=""
13855
- />
13856
- </div>
13857
- </div>
13858
- </div>
13859
- )}
13736
+ return (
13737
+ <div ref={ref} className={cn("flex flex-col min-h-screen bg-semantic-bg-primary", className)}>
13738
+ {/* Page header */}
13739
+ <PageHeader
13740
+ showBackButton
13741
+ onBackClick={onBack}
13742
+ title={botTitle}
13743
+ badge={
13744
+ <Badge variant="outline" className="text-xs font-normal">
13745
+ {botType}
13746
+ </Badge>
13747
+ }
13748
+ actions={
13749
+ <>
13750
+ {lastUpdatedAt && (
13751
+ <span className="hidden sm:inline text-sm text-semantic-text-muted mr-1">
13752
+ Last updated at: {lastUpdatedAt}
13753
+ </span>
13754
+ )}
13755
+ <Button
13756
+ variant="outline"
13757
+ onClick={() => onSaveAsDraft?.(data)}
13758
+ >
13759
+ Save as Draft
13760
+ </Button>
13761
+ <Button
13762
+ variant="default"
13763
+ onClick={() => onPublish?.(data)}
13764
+ >
13765
+ Publish Bot
13766
+ </Button>
13767
+ </>
13768
+ }
13769
+ />
13770
+
13771
+ {/* Body \u2014 two-column layout: left white, right gray panel */}
13772
+ <div className="flex flex-col lg:flex-row lg:flex-1 min-h-0">
13773
+ {/* Left column \u2014 white background */}
13774
+ <div className="flex flex-col gap-6 px-4 py-4 sm:px-6 sm:py-6 lg:flex-[3] min-w-0 lg:max-w-[720px]">
13775
+ <BotIdentityCard
13776
+ data={data}
13777
+ onChange={update}
13778
+ onPlayVoice={onPlayVoice}
13779
+ onPauseVoice={onPauseVoice}
13780
+ playingVoice={playingVoice}
13781
+ roleOptions={roleOptions}
13782
+ toneOptions={toneOptions}
13783
+ voiceOptions={voiceOptions}
13784
+ languageOptions={languageOptions}
13785
+ />
13786
+ <BotBehaviorCard
13787
+ data={data}
13788
+ onChange={update}
13789
+ sessionVariables={sessionVariables}
13790
+ />
13791
+ <FallbackPromptsAccordion data={data} onChange={update} />
13860
13792
  </div>
13861
13793
 
13862
- {/* \u2500\u2500 Footer \u2500\u2500 */}
13863
- <div className="flex items-center justify-between gap-3 px-4 py-3 border-t border-semantic-border-layout shrink-0 sm:px-6 sm:py-4">
13864
- {step === 1 ? (
13865
- <>
13866
- <Button
13867
- variant="outline"
13868
- className="flex-1 sm:flex-none"
13869
- onClick={handleClose}
13870
- >
13871
- Cancel
13872
- </Button>
13873
- <Button
13874
- variant="default"
13875
- className="flex-1 sm:flex-none"
13876
- onClick={handleNext}
13877
- disabled={!isStep1Valid}
13878
- >
13879
- Next
13880
- </Button>
13881
- </>
13882
- ) : (
13883
- <>
13884
- <Button
13885
- variant="outline"
13886
- className="flex-1 sm:flex-none"
13887
- onClick={() => setStep(1)}
13888
- >
13889
- Back
13890
- </Button>
13891
- <Button
13892
- variant="default"
13893
- className="flex-1 sm:flex-none"
13894
- onClick={handleSubmit}
13895
- >
13896
- Submit
13897
- </Button>
13898
- </>
13899
- )}
13794
+ {/* Right column \u2014 gray panel extending full height */}
13795
+ <div className="flex flex-col gap-6 px-4 py-4 sm:px-6 sm:py-6 lg:flex-[2] min-w-0 bg-semantic-bg-ui border-l border-semantic-border-layout">
13796
+ <KnowledgeBaseCard
13797
+ files={data.knowledgeBaseFiles}
13798
+ onAdd={() => setUploadOpen(true)}
13799
+ onDownload={onDownloadKnowledgeFile}
13800
+ onDelete={(id) => {
13801
+ update({
13802
+ knowledgeBaseFiles: data.knowledgeBaseFiles.filter(
13803
+ (f) => f.id !== id
13804
+ ),
13805
+ });
13806
+ onDeleteKnowledgeFile?.(id);
13807
+ }}
13808
+ />
13809
+ <FunctionsCard
13810
+ functions={data.functions}
13811
+ onAddFunction={() => setCreateFnOpen(true)}
13812
+ onEditFunction={onEditFunction}
13813
+ onDeleteFunction={(id) => {
13814
+ update({
13815
+ functions: data.functions.filter((f) => f.id !== id),
13816
+ });
13817
+ onDeleteFunction?.(id);
13818
+ }}
13819
+ />
13820
+ <FrustrationHandoverCard
13821
+ data={data}
13822
+ onChange={update}
13823
+ departmentOptions={escalationDepartmentOptions}
13824
+ />
13825
+ <AdvancedSettingsCard
13826
+ data={data}
13827
+ onChange={update}
13828
+ silenceTimeoutMin={silenceTimeoutMin}
13829
+ silenceTimeoutMax={silenceTimeoutMax}
13830
+ callEndThresholdMin={callEndThresholdMin}
13831
+ callEndThresholdMax={callEndThresholdMax}
13832
+ />
13900
13833
  </div>
13901
- </DialogContent>
13902
- </Dialog>
13834
+ </div>
13835
+
13836
+ {/* Create Function Modal */}
13837
+ <CreateFunctionModal
13838
+ open={createFnOpen}
13839
+ onOpenChange={setCreateFnOpen}
13840
+ onSubmit={handleCreateFunction}
13841
+ onTestApi={onTestApi}
13842
+ />
13843
+
13844
+ {/* File Upload Modal */}
13845
+ <FileUploadModal
13846
+ open={uploadOpen}
13847
+ onOpenChange={setUploadOpen}
13848
+ onUpload={onUploadKnowledgeFile}
13849
+ onSampleDownload={onSampleFileDownload}
13850
+ onSave={onSaveKnowledgeFiles}
13851
+ />
13852
+ </div>
13903
13853
  );
13904
13854
  }
13905
13855
  );
13906
13856
 
13907
- CreateFunctionModal.displayName = "CreateFunctionModal";
13857
+ IvrBotConfig.displayName = "IvrBotConfig";
13908
13858
  `, prefix)
13909
13859
  },
13910
13860
  {
13911
- name: "file-upload-modal.tsx",
13861
+ name: "create-function-modal.tsx",
13912
13862
  content: prefixTailwindClasses(`import * as React from "react";
13913
- import { Download, Trash2, X, XCircle } from "lucide-react";
13863
+ import { Trash2, ChevronDown, X, Plus } from "lucide-react";
13914
13864
  import { cn } from "../../../lib/utils";
13915
- import { Button } from "../button";
13916
13865
  import {
13917
13866
  Dialog,
13918
13867
  DialogContent,
13919
13868
  DialogTitle,
13920
- DialogDescription,
13921
13869
  } from "../dialog";
13870
+ import { Button } from "../button";
13922
13871
  import type {
13923
- FileUploadModalProps,
13924
- UploadItem,
13925
- UploadStatus,
13872
+ CreateFunctionModalProps,
13873
+ CreateFunctionData,
13874
+ CreateFunctionStep2Data,
13875
+ FunctionTabType,
13876
+ HttpMethod,
13877
+ KeyValuePair,
13926
13878
  } from "./types";
13927
13879
 
13928
- const DEFAULT_ACCEPTED = ".doc,.docx,.pdf,.csv,.xls,.xlsx,.txt";
13929
- const DEFAULT_FORMAT_DESC =
13930
- "Max file size 100 MB (Supported Format: .docs, .pdf, .csv, .xls, .xlxs, .txt)";
13880
+ const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "DELETE", "PATCH"];
13881
+ const FUNCTION_NAME_MAX = 30;
13882
+ const BODY_MAX = 4000;
13931
13883
 
13932
13884
  function generateId() {
13933
13885
  return Math.random().toString(36).slice(2, 9);
13934
13886
  }
13935
13887
 
13936
- function useFakeProgress() {
13937
- const intervalsRef = React.useRef<
13938
- Record<string, ReturnType<typeof setInterval>>
13939
- >({});
13888
+ // \u2500\u2500 Shared input/textarea styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
13889
+ const inputCls = cn(
13890
+ "w-full h-[42px] px-4 text-base rounded border",
13891
+ "border-semantic-border-input bg-semantic-bg-primary",
13892
+ "text-semantic-text-primary placeholder:text-semantic-text-muted",
13893
+ "outline-none hover:border-semantic-border-input-focus",
13894
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
13895
+ );
13940
13896
 
13941
- const start = React.useCallback(
13942
- (
13943
- id: string,
13944
- setItems: React.Dispatch<React.SetStateAction<UploadItem[]>>
13945
- ) => {
13946
- const interval = setInterval(() => {
13947
- setItems((prev) => {
13948
- let done = false;
13949
- const updated = prev.map((item) => {
13950
- if (item.id !== id || item.status !== "uploading") return item;
13951
- const next = Math.min(item.progress + 15, 100);
13952
- if (next === 100) done = true;
13953
- return {
13954
- ...item,
13955
- progress: next,
13956
- status: (next === 100 ? "done" : "uploading") as UploadStatus,
13957
- };
13958
- });
13959
- if (done) {
13960
- clearInterval(interval);
13961
- delete intervalsRef.current[id];
13962
- }
13963
- return updated;
13964
- });
13965
- }, 500);
13966
- intervalsRef.current[id] = interval;
13967
- },
13968
- []
13969
- );
13897
+ const textareaCls = cn(
13898
+ "w-full px-4 py-2.5 text-base rounded border resize-none",
13899
+ "border-semantic-border-input bg-semantic-bg-primary",
13900
+ "text-semantic-text-primary placeholder:text-semantic-text-muted",
13901
+ "outline-none hover:border-semantic-border-input-focus",
13902
+ "focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
13903
+ );
13970
13904
 
13971
- const cancel = React.useCallback((id: string) => {
13972
- clearInterval(intervalsRef.current[id]);
13973
- delete intervalsRef.current[id];
13974
- }, []);
13905
+ // \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
13906
+ function KeyValueTable({
13907
+ rows,
13908
+ onChange,
13909
+ label,
13910
+ }: {
13911
+ rows: KeyValuePair[];
13912
+ onChange: (rows: KeyValuePair[]) => void;
13913
+ label: string;
13914
+ }) {
13915
+ const update = (id: string, patch: Partial<KeyValuePair>) =>
13916
+ onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
13975
13917
 
13976
- const cancelAll = React.useCallback(() => {
13977
- Object.values(intervalsRef.current).forEach(clearInterval);
13978
- intervalsRef.current = {};
13979
- }, []);
13918
+ const remove = (id: string) => onChange(rows.filter((r) => r.id !== id));
13980
13919
 
13981
- return { start, cancel, cancelAll };
13982
- }
13920
+ const add = () =>
13921
+ onChange([...rows, { id: generateId(), key: "", value: "" }]);
13983
13922
 
13984
- function getTimeRemaining(progress: number) {
13985
- const steps = Math.ceil((100 - progress) / 15);
13986
- const secs = steps * 3;
13987
- return secs > 60
13988
- ? \`\${Math.ceil(secs / 60)} minutes remaining\`
13989
- : \`\${secs} seconds remaining\`;
13923
+ return (
13924
+ <div className="flex flex-col gap-1.5">
13925
+ <span className="text-xs text-semantic-text-muted">{label}</span>
13926
+ <div className="border border-semantic-border-layout rounded overflow-hidden">
13927
+ {/* Column headers \u2014 desktop only */}
13928
+ <div className="hidden sm:flex bg-semantic-bg-ui border-b border-semantic-border-layout">
13929
+ <div className="flex-1 px-3 py-2 text-xs font-semibold text-semantic-text-muted border-r border-semantic-border-layout">
13930
+ Key
13931
+ </div>
13932
+ <div className="flex-[2] px-3 py-2 text-xs font-semibold text-semantic-text-muted">
13933
+ Value
13934
+ </div>
13935
+ <div className="w-10 shrink-0" />
13936
+ </div>
13937
+
13938
+ {/* Filled rows */}
13939
+ {rows.map((row) => (
13940
+ <div
13941
+ key={row.id}
13942
+ className="border-b border-semantic-border-layout last:border-b-0"
13943
+ >
13944
+ {/* Mobile: label + input pairs stacked */}
13945
+ <div className="flex sm:hidden flex-col">
13946
+ <div className="flex flex-col px-3 pt-2.5 pb-1 gap-0.5">
13947
+ <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
13948
+ Key
13949
+ </span>
13950
+ <input
13951
+ type="text"
13952
+ value={row.key}
13953
+ onChange={(e) => update(row.id, { key: e.target.value })}
13954
+ placeholder="Key"
13955
+ className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
13956
+ />
13957
+ </div>
13958
+ <div className="h-px bg-semantic-border-layout mx-3" />
13959
+ <div className="flex items-start gap-2 px-3 py-2.5">
13960
+ <div className="flex flex-col flex-1 gap-0.5">
13961
+ <span className="text-[10px] font-semibold text-semantic-text-muted uppercase tracking-wide">
13962
+ Value
13963
+ </span>
13964
+ <input
13965
+ type="text"
13966
+ value={row.value}
13967
+ onChange={(e) => update(row.id, { value: e.target.value })}
13968
+ placeholder="Type {{ to add variables"
13969
+ className="w-full text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
13970
+ />
13971
+ </div>
13972
+ <button
13973
+ type="button"
13974
+ onClick={() => remove(row.id)}
13975
+ 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"
13976
+ aria-label="Delete row"
13977
+ >
13978
+ <Trash2 className="size-3.5" />
13979
+ </button>
13980
+ </div>
13981
+ </div>
13982
+
13983
+ {/* Desktop: side-by-side */}
13984
+ <div className="hidden sm:flex">
13985
+ <input
13986
+ type="text"
13987
+ value={row.key}
13988
+ onChange={(e) => update(row.id, { key: e.target.value })}
13989
+ placeholder="Key"
13990
+ 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"
13991
+ />
13992
+ <input
13993
+ type="text"
13994
+ value={row.value}
13995
+ onChange={(e) => update(row.id, { value: e.target.value })}
13996
+ placeholder="Type {{ to add variables"
13997
+ 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"
13998
+ />
13999
+ <button
14000
+ type="button"
14001
+ onClick={() => remove(row.id)}
14002
+ 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"
14003
+ aria-label="Delete row"
14004
+ >
14005
+ <Trash2 className="size-3.5" />
14006
+ </button>
14007
+ </div>
14008
+ </div>
14009
+ ))}
14010
+
14011
+ {/* Add row \u2014 always visible */}
14012
+ <button
14013
+ type="button"
14014
+ onClick={add}
14015
+ className="w-full flex items-center gap-2 px-3 py-2.5 text-sm text-semantic-text-muted hover:bg-semantic-bg-hover transition-colors"
14016
+ >
14017
+ <Plus className="size-3.5 shrink-0" />
14018
+ <span>Add row</span>
14019
+ </button>
14020
+ </div>
14021
+ </div>
14022
+ );
13990
14023
  }
13991
14024
 
13992
- const FileUploadModal = React.forwardRef<HTMLDivElement, FileUploadModalProps>(
14025
+ // \u2500\u2500 Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14026
+ export const CreateFunctionModal = React.forwardRef<
14027
+ HTMLDivElement,
14028
+ CreateFunctionModalProps
14029
+ >(
13993
14030
  (
13994
14031
  {
13995
14032
  open,
13996
14033
  onOpenChange,
13997
- onUpload,
13998
- onSave,
13999
- onCancel,
14000
- onSampleDownload,
14001
- sampleDownloadLabel = "Download sample file",
14002
- showSampleDownload,
14003
- acceptedFormats = DEFAULT_ACCEPTED,
14004
- formatDescription = DEFAULT_FORMAT_DESC,
14005
- maxFileSizeMB = 100,
14006
- multiple = true,
14007
- title = "File Upload",
14008
- uploadButtonLabel = "Upload from device",
14009
- dropDescription = "or drag and drop file here",
14010
- saveLabel = "Save",
14011
- cancelLabel = "Cancel",
14012
- saving = false,
14034
+ onSubmit,
14035
+ onTestApi,
14036
+ initialStep = 1,
14037
+ initialTab = "header",
14013
14038
  className,
14014
- ...props
14015
- },
14016
- ref
14017
- ) => {
14018
- const [items, setItems] = React.useState<UploadItem[]>([]);
14019
- const fileInputRef = React.useRef<HTMLInputElement>(null);
14020
- const fakeProgress = useFakeProgress();
14021
-
14022
- const shouldShowSampleDownload =
14023
- showSampleDownload ?? !!onSampleDownload;
14024
-
14025
- const addFiles = React.useCallback(
14026
- (fileList: FileList | null) => {
14027
- if (!fileList) return;
14028
-
14029
- Array.from(fileList).forEach((file) => {
14030
- if (file.size > maxFileSizeMB * 1024 * 1024) {
14031
- const id = generateId();
14032
- setItems((prev) => [
14033
- ...prev,
14034
- {
14035
- id,
14036
- file,
14037
- progress: 0,
14038
- status: "error",
14039
- errorMessage: \`File exceeds \${maxFileSizeMB} MB limit\`,
14040
- },
14041
- ]);
14042
- return;
14043
- }
14044
-
14045
- const id = generateId();
14046
- setItems((prev) => [
14047
- ...prev,
14048
- { id, file, progress: 0, status: "uploading" },
14049
- ]);
14050
-
14051
- if (onUpload) {
14052
- onUpload(file, {
14053
- onProgress: (progress) => {
14054
- setItems((prev) =>
14055
- prev.map((item) =>
14056
- item.id === id
14057
- ? {
14058
- ...item,
14059
- progress: Math.min(progress, 100),
14060
- status:
14061
- progress >= 100
14062
- ? ("done" as UploadStatus)
14063
- : ("uploading" as UploadStatus),
14064
- }
14065
- : item
14066
- )
14067
- );
14068
- },
14069
- onError: (message) => {
14070
- setItems((prev) =>
14071
- prev.map((item) =>
14072
- item.id === id
14073
- ? { ...item, status: "error" as UploadStatus, errorMessage: message }
14074
- : item
14075
- )
14076
- );
14077
- },
14078
- }).then(() => {
14079
- setItems((prev) =>
14080
- prev.map((item) =>
14081
- item.id === id && item.status === "uploading"
14082
- ? { ...item, progress: 100, status: "done" as UploadStatus }
14083
- : item
14084
- )
14085
- );
14086
- }).catch((err) => {
14087
- setItems((prev) =>
14088
- prev.map((item) =>
14089
- item.id === id && item.status !== "error"
14090
- ? {
14091
- ...item,
14092
- status: "error" as UploadStatus,
14093
- errorMessage:
14094
- err instanceof Error
14095
- ? err.message
14096
- : "Upload failed",
14097
- }
14098
- : item
14099
- )
14100
- );
14101
- });
14102
- } else {
14103
- fakeProgress.start(id, setItems);
14104
- }
14105
- });
14106
- },
14107
- [onUpload, maxFileSizeMB, fakeProgress]
14108
- );
14039
+ },
14040
+ ref
14041
+ ) => {
14042
+ const [step, setStep] = React.useState<1 | 2>(initialStep);
14109
14043
 
14110
- const removeItem = (id: string) => {
14111
- fakeProgress.cancel(id);
14112
- setItems((prev) => prev.filter((i) => i.id !== id));
14113
- };
14044
+ const [name, setName] = React.useState("");
14045
+ const [prompt, setPrompt] = React.useState("");
14114
14046
 
14115
- const handleClose = () => {
14116
- fakeProgress.cancelAll();
14117
- setItems([]);
14118
- onCancel?.();
14047
+ const [method, setMethod] = React.useState<HttpMethod>("GET");
14048
+ const [url, setUrl] = React.useState("");
14049
+ const [activeTab, setActiveTab] =
14050
+ React.useState<FunctionTabType>(initialTab);
14051
+ const [headers, setHeaders] = React.useState<KeyValuePair[]>([]);
14052
+ const [queryParams, setQueryParams] = React.useState<KeyValuePair[]>([]);
14053
+ const [body, setBody] = React.useState("");
14054
+ const [apiResponse, setApiResponse] = React.useState("");
14055
+ const [isTesting, setIsTesting] = React.useState(false);
14056
+
14057
+ const reset = React.useCallback(() => {
14058
+ setStep(initialStep);
14059
+ setName("");
14060
+ setPrompt("");
14061
+ setMethod("GET");
14062
+ setUrl("");
14063
+ setActiveTab(initialTab);
14064
+ setHeaders([]);
14065
+ setQueryParams([]);
14066
+ setBody("");
14067
+ setApiResponse("");
14068
+ }, [initialStep, initialTab]);
14069
+
14070
+ const handleClose = React.useCallback(() => {
14071
+ reset();
14119
14072
  onOpenChange(false);
14073
+ }, [reset, onOpenChange]);
14074
+
14075
+ const handleNext = () => {
14076
+ if (name.trim() && prompt.trim()) setStep(2);
14120
14077
  };
14121
14078
 
14122
- const handleSave = () => {
14123
- const completedFiles = items
14124
- .filter((i) => i.status === "done")
14125
- .map((i) => i.file);
14126
- onSave?.(completedFiles);
14127
- fakeProgress.cancelAll();
14128
- setItems([]);
14129
- onOpenChange(false);
14079
+ const handleSubmit = () => {
14080
+ const data: CreateFunctionData = {
14081
+ name: name.trim(),
14082
+ prompt: prompt.trim(),
14083
+ method,
14084
+ url: url.trim(),
14085
+ headers,
14086
+ queryParams,
14087
+ body,
14088
+ };
14089
+ onSubmit?.(data);
14090
+ handleClose();
14130
14091
  };
14131
14092
 
14132
- const hasCompleted = items.some((i) => i.status === "done");
14133
- const hasUploading = items.some((i) => i.status === "uploading");
14093
+ const handleTestApi = async () => {
14094
+ if (!onTestApi) return;
14095
+ setIsTesting(true);
14096
+ try {
14097
+ const step2: CreateFunctionStep2Data = {
14098
+ method,
14099
+ url,
14100
+ headers,
14101
+ queryParams,
14102
+ body,
14103
+ };
14104
+ const response = await onTestApi(step2);
14105
+ setApiResponse(response);
14106
+ } finally {
14107
+ setIsTesting(false);
14108
+ }
14109
+ };
14110
+
14111
+ const isStep1Valid =
14112
+ name.trim().length > 0 && prompt.trim().length > 0;
14113
+
14114
+ const tabLabels: Record<FunctionTabType, string> = {
14115
+ header: \`Header (\${headers.length})\`,
14116
+ queryParams: \`Query params (\${queryParams.length})\`,
14117
+ body: "Body",
14118
+ };
14134
14119
 
14135
14120
  return (
14136
14121
  <Dialog open={open} onOpenChange={onOpenChange}>
14137
14122
  <DialogContent
14138
14123
  ref={ref}
14139
- size="default"
14124
+ size="lg"
14140
14125
  hideCloseButton
14141
14126
  className={cn(
14142
- "max-w-[min(660px,calc(100vw-2rem))] rounded-xl p-4 gap-0 sm:p-6",
14127
+ "flex flex-col gap-0 p-0 w-[calc(100vw-2rem)] sm:w-full",
14128
+ "max-h-[calc(100svh-2rem)] overflow-hidden",
14143
14129
  className
14144
14130
  )}
14145
- {...props}
14146
14131
  >
14147
- {/* Header */}
14148
- <div className="flex items-center justify-between mb-6">
14149
- <DialogTitle className="m-0 text-base font-semibold text-semantic-text-primary">
14150
- {title}
14132
+ {/* \u2500\u2500 Header \u2500\u2500 */}
14133
+ <div className="flex items-center justify-between px-4 py-4 border-b border-semantic-border-layout shrink-0 sm:px-6">
14134
+ <DialogTitle className="text-base font-semibold text-semantic-text-primary">
14135
+ Create Function
14151
14136
  </DialogTitle>
14152
- <DialogDescription className="sr-only">
14153
- Upload files by clicking the button or dragging and dropping.
14154
- </DialogDescription>
14155
14137
  <button
14156
14138
  type="button"
14157
14139
  onClick={handleClose}
14158
- className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-semantic-text-primary"
14159
- aria-label="Close dialog"
14140
+ className="rounded p-1.5 text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-hover transition-colors"
14141
+ aria-label="Close"
14160
14142
  >
14161
- <X className="h-4 w-4" />
14143
+ <X className="size-4" />
14162
14144
  </button>
14163
14145
  </div>
14164
14146
 
14165
- {/* Body */}
14166
- <div className="flex flex-col gap-4 items-end w-full">
14167
- {shouldShowSampleDownload && (
14168
- <button
14169
- type="button"
14170
- onClick={onSampleDownload}
14171
- className="flex items-center gap-1.5 text-sm font-semibold text-semantic-text-link hover:opacity-80 transition-opacity"
14172
- >
14173
- <Download className="size-3.5" />
14174
- {sampleDownloadLabel}
14175
- </button>
14176
- )}
14147
+ {/* \u2500\u2500 Scrollable body \u2500\u2500 */}
14148
+ <div className="flex-1 overflow-y-auto min-h-0 px-4 py-5 sm:px-6">
14149
+ {/* \u2500 Step 1 \u2500 */}
14150
+ {step === 1 && (
14151
+ <div className="flex flex-col gap-5">
14152
+ <div className="flex flex-col gap-1.5">
14153
+ <label
14154
+ htmlFor="fn-name"
14155
+ className="text-sm font-semibold text-semantic-text-primary"
14156
+ >
14157
+ Function Name{" "}
14158
+ <span className="text-semantic-error-primary">*</span>
14159
+ </label>
14160
+ <div className={cn("relative")}>
14161
+ <input
14162
+ id="fn-name"
14163
+ type="text"
14164
+ value={name}
14165
+ maxLength={FUNCTION_NAME_MAX}
14166
+ onChange={(e) => setName(e.target.value)}
14167
+ placeholder="Enter name of the function"
14168
+ className={cn(inputCls, "pr-16")}
14169
+ />
14170
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs italic text-semantic-text-muted pointer-events-none">
14171
+ {name.length}/{FUNCTION_NAME_MAX}
14172
+ </span>
14173
+ </div>
14174
+ </div>
14177
14175
 
14178
- {/* Drop zone */}
14179
- <div
14180
- className="w-full border border-dashed border-semantic-border-layout bg-semantic-bg-ui rounded p-4"
14181
- onDrop={(e) => {
14182
- e.preventDefault();
14183
- addFiles(e.dataTransfer.files);
14184
- }}
14185
- onDragOver={(e) => e.preventDefault()}
14186
- >
14187
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
14188
- <button
14189
- type="button"
14190
- onClick={() => fileInputRef.current?.click()}
14191
- className="h-[42px] px-4 rounded border border-semantic-border-layout bg-semantic-bg-primary text-base font-semibold text-semantic-text-secondary shrink-0 hover:bg-semantic-bg-hover transition-colors w-full sm:w-auto"
14192
- >
14193
- {uploadButtonLabel}
14194
- </button>
14195
- <div className="flex flex-col gap-1">
14196
- <p className="m-0 text-sm text-semantic-text-secondary tracking-[0.035px]">
14197
- {dropDescription}
14198
- </p>
14199
- <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
14200
- {formatDescription}
14201
- </p>
14176
+ <div className="flex flex-col gap-1.5">
14177
+ <label
14178
+ htmlFor="fn-prompt"
14179
+ className="text-sm font-semibold text-semantic-text-primary"
14180
+ >
14181
+ Prompt{" "}
14182
+ <span className="text-semantic-error-primary">*</span>
14183
+ </label>
14184
+ <textarea
14185
+ id="fn-prompt"
14186
+ value={prompt}
14187
+ onChange={(e) => setPrompt(e.target.value)}
14188
+ placeholder="Enter the description of the function"
14189
+ rows={5}
14190
+ className={textareaCls}
14191
+ />
14202
14192
  </div>
14203
14193
  </div>
14204
- <input
14205
- ref={fileInputRef}
14206
- type="file"
14207
- multiple={multiple}
14208
- accept={acceptedFormats}
14209
- className="hidden"
14210
- onChange={(e) => {
14211
- addFiles(e.target.files);
14212
- e.target.value = "";
14213
- }}
14214
- />
14215
- </div>
14194
+ )}
14216
14195
 
14217
- {/* Upload item list */}
14218
- {items.length > 0 && (
14219
- <div className="flex flex-col gap-2.5 w-full">
14220
- {items.map((item) => (
14196
+ {/* \u2500 Step 2 \u2500 */}
14197
+ {step === 2 && (
14198
+ <div className="flex flex-col gap-5">
14199
+ {/* API URL \u2014 always a single combined row */}
14200
+ <div className="flex flex-col gap-1.5">
14201
+ <span className="text-xs text-semantic-text-muted tracking-[0.048px]">
14202
+ API URL
14203
+ </span>
14204
+ <div
14205
+ className={cn(
14206
+ "flex h-[42px] rounded border border-semantic-border-input overflow-hidden bg-semantic-bg-primary",
14207
+ "hover:border-semantic-border-input-focus",
14208
+ "focus-within:border-semantic-border-input-focus focus-within:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
14209
+ "transition-shadow"
14210
+ )}
14211
+ >
14212
+ {/* Method selector */}
14213
+ <div className="relative shrink-0 border-r border-semantic-border-layout">
14214
+ <select
14215
+ value={method}
14216
+ onChange={(e) =>
14217
+ setMethod(e.target.value as HttpMethod)
14218
+ }
14219
+ className="h-full w-[80px] pl-3 pr-7 text-base text-semantic-text-primary bg-transparent outline-none cursor-pointer appearance-none sm:w-[100px]"
14220
+ aria-label="HTTP method"
14221
+ >
14222
+ {HTTP_METHODS.map((m) => (
14223
+ <option key={m} value={m}>
14224
+ {m}
14225
+ </option>
14226
+ ))}
14227
+ </select>
14228
+ <ChevronDown
14229
+ className="absolute right-2 top-1/2 -translate-y-1/2 size-3 text-semantic-text-muted pointer-events-none"
14230
+ aria-hidden="true"
14231
+ />
14232
+ </div>
14233
+ {/* URL input */}
14234
+ <input
14235
+ type="text"
14236
+ value={url}
14237
+ onChange={(e) => setUrl(e.target.value)}
14238
+ placeholder="Enter URL or Type {{ to add variables"
14239
+ className="flex-1 min-w-0 px-3 text-base text-semantic-text-primary placeholder:text-semantic-text-muted bg-transparent outline-none"
14240
+ />
14241
+ </div>
14242
+ </div>
14243
+
14244
+ {/* Tabs \u2014 scrollable, no visible scrollbar */}
14245
+ <div className="flex flex-col gap-4">
14221
14246
  <div
14222
- key={item.id}
14223
- className="bg-semantic-bg-primary border border-semantic-border-layout rounded px-4 py-3 flex flex-col gap-2"
14247
+ className={cn(
14248
+ "flex border-b border-semantic-border-layout",
14249
+ "overflow-x-auto",
14250
+ "[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
14251
+ )}
14224
14252
  >
14225
- <div className="flex items-start gap-3">
14226
- <div className="flex flex-col gap-0.5 flex-1 min-w-0">
14227
- <p className="m-0 text-sm text-semantic-text-primary tracking-[0.035px] truncate">
14228
- {item.status === "uploading"
14229
- ? "Uploading..."
14230
- : item.file.name}
14231
- </p>
14232
- {item.status === "uploading" && (
14233
- <p className="m-0 text-xs text-semantic-text-muted tracking-[0.048px]">
14234
- {item.progress}%&nbsp;&bull;&nbsp;
14235
- {getTimeRemaining(item.progress)}
14236
- </p>
14237
- )}
14238
- {item.status === "error" && (
14239
- <p className="m-0 text-xs text-semantic-error-primary tracking-[0.048px]">
14240
- {item.errorMessage ??
14241
- "Something went wrong, Upload Failed."}
14242
- </p>
14243
- )}
14244
- </div>
14253
+ {(
14254
+ ["header", "queryParams", "body"] as FunctionTabType[]
14255
+ ).map((tab) => (
14245
14256
  <button
14257
+ key={tab}
14246
14258
  type="button"
14247
- onClick={() => removeItem(item.id)}
14248
- aria-label={
14249
- item.status === "uploading"
14250
- ? "Cancel upload"
14251
- : "Remove file"
14252
- }
14259
+ onClick={() => setActiveTab(tab)}
14253
14260
  className={cn(
14254
- "shrink-0 mt-0.5 transition-colors",
14255
- item.status === "uploading"
14256
- ? "text-semantic-error-primary"
14257
- : "text-semantic-text-muted hover:text-semantic-error-primary"
14261
+ "px-3 py-2 text-sm font-semibold transition-colors whitespace-nowrap shrink-0",
14262
+ activeTab === tab
14263
+ ? "text-semantic-text-secondary border-b-2 border-semantic-text-secondary -mb-px"
14264
+ : "text-semantic-text-muted hover:text-semantic-text-primary"
14258
14265
  )}
14259
14266
  >
14260
- {item.status === "uploading" ? (
14261
- <XCircle className="size-5" />
14262
- ) : (
14263
- <Trash2 className="size-5" />
14264
- )}
14267
+ {tabLabels[tab]}
14265
14268
  </button>
14266
- </div>
14267
- {item.status === "uploading" && (
14268
- <div className="h-2 bg-semantic-bg-ui rounded-full overflow-hidden">
14269
- <div
14270
- className="h-full bg-semantic-success-primary rounded-full transition-all duration-300"
14271
- style={{ width: \`\${item.progress}%\` }}
14269
+ ))}
14270
+ </div>
14271
+
14272
+ {activeTab === "header" && (
14273
+ <KeyValueTable
14274
+ rows={headers}
14275
+ onChange={setHeaders}
14276
+ label="Header"
14277
+ />
14278
+ )}
14279
+ {activeTab === "queryParams" && (
14280
+ <KeyValueTable
14281
+ rows={queryParams}
14282
+ onChange={setQueryParams}
14283
+ label="Query parameter"
14284
+ />
14285
+ )}
14286
+ {activeTab === "body" && (
14287
+ <div className="flex flex-col gap-1.5">
14288
+ <span className="text-xs text-semantic-text-muted">
14289
+ Body
14290
+ </span>
14291
+ <div className={cn("relative")}>
14292
+ <textarea
14293
+ value={body}
14294
+ maxLength={BODY_MAX}
14295
+ onChange={(e) => setBody(e.target.value)}
14296
+ placeholder="Enter request body (JSON, XML etc). Type {{ to add variables"
14297
+ rows={6}
14298
+ className={cn(textareaCls, "pb-7")}
14272
14299
  />
14300
+ <span className="absolute bottom-2 right-3 text-xs italic text-semantic-text-muted pointer-events-none">
14301
+ {body.length}/{BODY_MAX}
14302
+ </span>
14273
14303
  </div>
14274
- )}
14304
+ </div>
14305
+ )}
14306
+ </div>
14307
+
14308
+ {/* Test Your API */}
14309
+ <div className="flex flex-col gap-4">
14310
+ <div className="flex flex-col gap-1.5">
14311
+ <span className="text-xs font-semibold text-semantic-text-muted tracking-[0.048px]">
14312
+ Test Your API
14313
+ </span>
14314
+ <div className="border-t border-semantic-border-layout" />
14275
14315
  </div>
14276
- ))}
14316
+
14317
+ <button
14318
+ type="button"
14319
+ onClick={handleTestApi}
14320
+ disabled={isTesting || !url.trim()}
14321
+ className="w-full h-[42px] rounded text-sm font-semibold text-semantic-text-secondary bg-semantic-primary-surface disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-semantic-primary-surface/80 sm:w-auto sm:px-6 sm:self-end sm:ml-auto flex items-center justify-center"
14322
+ >
14323
+ {isTesting ? "Testing..." : "Test API"}
14324
+ </button>
14325
+
14326
+ <div className="flex flex-col gap-1.5">
14327
+ <span className="text-xs text-semantic-text-muted">
14328
+ Response from API
14329
+ </span>
14330
+ <textarea
14331
+ readOnly
14332
+ value={apiResponse}
14333
+ rows={4}
14334
+ className="w-full px-3 py-2.5 text-base rounded border border-semantic-border-layout bg-semantic-bg-ui text-semantic-text-primary resize-none outline-none"
14335
+ placeholder=""
14336
+ />
14337
+ </div>
14338
+ </div>
14277
14339
  </div>
14278
14340
  )}
14279
14341
  </div>
14280
14342
 
14281
- {/* Footer */}
14282
- <div className="flex flex-col-reverse gap-3 mt-4 sm:mt-6 sm:flex-row sm:justify-end sm:gap-2">
14283
- <Button
14284
- variant="outline"
14285
- className="w-full sm:w-auto"
14286
- onClick={handleClose}
14287
- >
14288
- {cancelLabel}
14289
- </Button>
14290
- <Button
14291
- className="w-full sm:w-auto"
14292
- onClick={handleSave}
14293
- disabled={!hasCompleted || hasUploading}
14294
- loading={saving}
14295
- >
14296
- {saveLabel}
14297
- </Button>
14343
+ {/* \u2500\u2500 Footer \u2500\u2500 */}
14344
+ <div className="flex items-center justify-between gap-3 px-4 py-3 border-t border-semantic-border-layout shrink-0 sm:px-6 sm:py-4">
14345
+ {step === 1 ? (
14346
+ <>
14347
+ <Button
14348
+ variant="outline"
14349
+ className="flex-1 sm:flex-none"
14350
+ onClick={handleClose}
14351
+ >
14352
+ Cancel
14353
+ </Button>
14354
+ <Button
14355
+ variant="default"
14356
+ className="flex-1 sm:flex-none"
14357
+ onClick={handleNext}
14358
+ disabled={!isStep1Valid}
14359
+ >
14360
+ Next
14361
+ </Button>
14362
+ </>
14363
+ ) : (
14364
+ <>
14365
+ <Button
14366
+ variant="outline"
14367
+ className="flex-1 sm:flex-none"
14368
+ onClick={() => setStep(1)}
14369
+ >
14370
+ Back
14371
+ </Button>
14372
+ <Button
14373
+ variant="default"
14374
+ className="flex-1 sm:flex-none"
14375
+ onClick={handleSubmit}
14376
+ >
14377
+ Submit
14378
+ </Button>
14379
+ </>
14380
+ )}
14298
14381
  </div>
14299
14382
  </DialogContent>
14300
14383
  </Dialog>
@@ -14302,9 +14385,7 @@ const FileUploadModal = React.forwardRef<HTMLDivElement, FileUploadModalProps>(
14302
14385
  }
14303
14386
  );
14304
14387
 
14305
- FileUploadModal.displayName = "FileUploadModal";
14306
-
14307
- export { FileUploadModal };
14388
+ CreateFunctionModal.displayName = "CreateFunctionModal";
14308
14389
  `, prefix)
14309
14390
  },
14310
14391
  {
@@ -14799,8 +14880,6 @@ export { BotBehaviorCard };
14799
14880
  import { Download, Trash2, Plus, Info } from "lucide-react";
14800
14881
  import { cn } from "../../../lib/utils";
14801
14882
  import { Badge } from "../badge";
14802
- import { FileUploadModal } from "./file-upload-modal";
14803
- import type { UploadProgressHandlers } from "./types";
14804
14883
  import type { KnowledgeBaseFile } from "./types";
14805
14884
 
14806
14885
  // \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
@@ -14808,12 +14887,8 @@ import type { KnowledgeBaseFile } from "./types";
14808
14887
  export interface KnowledgeBaseCardProps {
14809
14888
  /** List of knowledge base files */
14810
14889
  files: KnowledgeBaseFile[];
14811
- /** Called when files are uploaded and saved */
14812
- onSaveFiles?: (uploadedFiles: File[]) => void;
14813
- /** Called for each file to handle the actual upload. If not provided, uses fake progress. */
14814
- onUploadFile?: (file: File, handlers: UploadProgressHandlers) => Promise<void>;
14815
- /** Called when user clicks "Download sample file" */
14816
- onSampleDownload?: () => void;
14890
+ /** Called when user clicks the "+ Files" button */
14891
+ onAdd?: () => void;
14817
14892
  /** Called when user clicks the download button on a file */
14818
14893
  onDownload?: (id: string) => void;
14819
14894
  /** Called when user clicks the delete button on a file */
@@ -14838,26 +14913,21 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
14838
14913
  (
14839
14914
  {
14840
14915
  files,
14841
- onSaveFiles,
14842
- onUploadFile,
14843
- onSampleDownload,
14916
+ onAdd,
14844
14917
  onDownload,
14845
14918
  onDelete,
14846
14919
  className,
14847
14920
  },
14848
14921
  ref
14849
14922
  ) => {
14850
- const [uploadOpen, setUploadOpen] = React.useState(false);
14851
-
14852
14923
  return (
14853
- <>
14854
- <div
14855
- ref={ref}
14856
- className={cn(
14857
- "bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden",
14858
- className
14859
- )}
14860
- >
14924
+ <div
14925
+ ref={ref}
14926
+ className={cn(
14927
+ "bg-semantic-bg-primary border border-semantic-border-layout rounded-lg overflow-hidden",
14928
+ className
14929
+ )}
14930
+ >
14861
14931
  {/* Header */}
14862
14932
  <div className="flex items-center justify-between px-4 py-4 border-b border-semantic-border-layout sm:px-6">
14863
14933
  <div className="flex items-center gap-1.5">
@@ -14868,7 +14938,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
14868
14938
  </div>
14869
14939
  <button
14870
14940
  type="button"
14871
- onClick={() => setUploadOpen(true)}
14941
+ onClick={() => onAdd?.()}
14872
14942
  className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded text-xs font-semibold text-semantic-text-secondary bg-semantic-primary-surface hover:bg-semantic-bg-hover transition-colors"
14873
14943
  >
14874
14944
  <Plus className="size-3.5" />
@@ -14926,15 +14996,7 @@ const KnowledgeBaseCard = React.forwardRef<HTMLDivElement, KnowledgeBaseCardProp
14926
14996
  </div>
14927
14997
  )}
14928
14998
  </div>
14929
- </div>
14930
- <FileUploadModal
14931
- open={uploadOpen}
14932
- onOpenChange={setUploadOpen}
14933
- onUpload={onUploadFile}
14934
- onSampleDownload={onSampleDownload}
14935
- onSave={onSaveFiles}
14936
- />
14937
- </>
14999
+ </div>
14938
15000
  );
14939
15001
  }
14940
15002
  );
@@ -15379,7 +15441,9 @@ export { AdvancedSettingsCard };
15379
15441
  },
15380
15442
  {
15381
15443
  name: "types.ts",
15382
- content: prefixTailwindClasses(`export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
15444
+ content: prefixTailwindClasses(`import type { UploadProgressHandlers } from "../file-upload-modal";
15445
+
15446
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
15383
15447
 
15384
15448
  export type FunctionTabType = "header" | "queryParams" | "body";
15385
15449
 
@@ -15503,45 +15567,14 @@ export interface IvrBotConfigProps {
15503
15567
  className?: string;
15504
15568
  }
15505
15569
 
15506
- // \u2500\u2500\u2500 File Upload Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15507
-
15508
- export type UploadStatus = "pending" | "uploading" | "done" | "error";
15509
-
15510
- export interface UploadItem {
15511
- id: string;
15512
- file: File;
15513
- progress: number;
15514
- status: UploadStatus;
15515
- errorMessage?: string;
15516
- }
15517
-
15518
- export interface UploadProgressHandlers {
15519
- onProgress: (progress: number) => void;
15520
- onError: (message: string) => void;
15521
- }
15570
+ // \u2500\u2500\u2500 File Upload Modal (re-exported from shared module) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15522
15571
 
15523
- export interface FileUploadModalProps
15524
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSave"> {
15525
- open: boolean;
15526
- onOpenChange: (open: boolean) => void;
15527
- /** Called for each file to handle the actual upload. If not provided, uses fake progress (demo mode). */
15528
- onUpload?: (file: File, handlers: UploadProgressHandlers) => Promise<void>;
15529
- onSave?: (files: File[]) => void;
15530
- onCancel?: () => void;
15531
- onSampleDownload?: () => void;
15532
- sampleDownloadLabel?: string;
15533
- showSampleDownload?: boolean;
15534
- acceptedFormats?: string;
15535
- formatDescription?: string;
15536
- maxFileSizeMB?: number;
15537
- multiple?: boolean;
15538
- title?: string;
15539
- uploadButtonLabel?: string;
15540
- dropDescription?: string;
15541
- saveLabel?: string;
15542
- cancelLabel?: string;
15543
- saving?: boolean;
15544
- }
15572
+ export type {
15573
+ UploadStatus,
15574
+ UploadItem,
15575
+ UploadProgressHandlers,
15576
+ FileUploadModalProps,
15577
+ } from "../file-upload-modal";
15545
15578
  `, prefix)
15546
15579
  },
15547
15580
  {
@@ -15553,7 +15586,7 @@ export { FunctionsCard } from "./functions-card";
15553
15586
  export { FrustrationHandoverCard } from "./frustration-handover-card";
15554
15587
  export { AdvancedSettingsCard } from "./advanced-settings-card";
15555
15588
  export { CreateFunctionModal } from "./create-function-modal";
15556
- export { FileUploadModal } from "./file-upload-modal";
15589
+ export { FileUploadModal } from "../file-upload-modal";
15557
15590
  export { IvrBotConfig } from "./ivr-bot-config";
15558
15591
 
15559
15592
  export type {