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.
- package/dist/index.js +1205 -1172
- 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
|
-
"
|
|
13045
|
-
name: "
|
|
13046
|
-
description: "
|
|
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
|
-
"
|
|
13060
|
-
"creatable-select",
|
|
13061
|
-
"creatable-multi-select",
|
|
13062
|
-
"page-header",
|
|
13063
|
-
"tag"
|
|
13055
|
+
"button"
|
|
13064
13056
|
],
|
|
13065
13057
|
isMultiFile: true,
|
|
13066
|
-
directory: "
|
|
13067
|
-
mainFile: "
|
|
13058
|
+
directory: "file-upload-modal",
|
|
13059
|
+
mainFile: "file-upload-modal.tsx",
|
|
13068
13060
|
files: [
|
|
13069
13061
|
{
|
|
13070
|
-
name: "
|
|
13062
|
+
name: "file-upload-modal.tsx",
|
|
13071
13063
|
content: prefixTailwindClasses(`import * as React from "react";
|
|
13072
|
-
import {
|
|
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
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13082
|
-
} from "../
|
|
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
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13074
|
+
FileUploadModalProps,
|
|
13075
|
+
UploadItem,
|
|
13076
|
+
UploadStatus,
|
|
13094
13077
|
} from "./types";
|
|
13095
13078
|
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
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
|
-
|
|
13129
|
-
|
|
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
|
-
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
13155
|
-
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
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
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
|
|
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
|
-
|
|
13211
|
-
|
|
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
|
-
|
|
13254
|
-
|
|
13132
|
+
return { start, cancel, cancelAll };
|
|
13133
|
+
}
|
|
13255
13134
|
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
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
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
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
|
-
|
|
13298
|
-
|
|
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
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
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
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
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
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
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
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
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
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13261
|
+
const removeItem = (id: string) => {
|
|
13262
|
+
fakeProgress.cancel(id);
|
|
13263
|
+
setItems((prev) => prev.filter((i) => i.id !== id));
|
|
13264
|
+
};
|
|
13406
13265
|
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
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
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
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
|
|
13631
|
-
|
|
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="
|
|
13290
|
+
size="default"
|
|
13644
13291
|
hideCloseButton
|
|
13645
13292
|
className={cn(
|
|
13646
|
-
"
|
|
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
|
-
{/*
|
|
13652
|
-
<div className="flex items-center justify-between
|
|
13653
|
-
<DialogTitle className="text-base font-semibold text-semantic-text-primary">
|
|
13654
|
-
|
|
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
|
|
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="
|
|
13312
|
+
<X className="h-4 w-4" />
|
|
13663
13313
|
</button>
|
|
13664
13314
|
</div>
|
|
13665
13315
|
|
|
13666
|
-
{/*
|
|
13667
|
-
<div className="flex
|
|
13668
|
-
{
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
|
|
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
|
-
{/*
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
|
|
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
|
-
|
|
13764
|
-
|
|
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
|
-
|
|
13767
|
-
|
|
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
|
-
|
|
13774
|
-
|
|
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}% •
|
|
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={() =>
|
|
13398
|
+
onClick={() => removeItem(item.id)}
|
|
13399
|
+
aria-label={
|
|
13400
|
+
item.status === "uploading"
|
|
13401
|
+
? "Cancel upload"
|
|
13402
|
+
: "Remove file"
|
|
13403
|
+
}
|
|
13779
13404
|
className={cn(
|
|
13780
|
-
"
|
|
13781
|
-
|
|
13782
|
-
? "text-semantic-
|
|
13783
|
-
: "text-semantic-text-muted hover:text-semantic-
|
|
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
|
-
{
|
|
13411
|
+
{item.status === "uploading" ? (
|
|
13412
|
+
<XCircle className="size-5" />
|
|
13413
|
+
) : (
|
|
13414
|
+
<Trash2 className="size-5" />
|
|
13415
|
+
)}
|
|
13787
13416
|
</button>
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
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
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
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
|
-
|
|
13828
|
-
|
|
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
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
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
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
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
|
-
{/*
|
|
13863
|
-
<div className="flex
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13878
|
-
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
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
|
-
</
|
|
13902
|
-
|
|
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
|
-
|
|
13857
|
+
IvrBotConfig.displayName = "IvrBotConfig";
|
|
13908
13858
|
`, prefix)
|
|
13909
13859
|
},
|
|
13910
13860
|
{
|
|
13911
|
-
name: "
|
|
13861
|
+
name: "create-function-modal.tsx",
|
|
13912
13862
|
content: prefixTailwindClasses(`import * as React from "react";
|
|
13913
|
-
import {
|
|
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
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13872
|
+
CreateFunctionModalProps,
|
|
13873
|
+
CreateFunctionData,
|
|
13874
|
+
CreateFunctionStep2Data,
|
|
13875
|
+
FunctionTabType,
|
|
13876
|
+
HttpMethod,
|
|
13877
|
+
KeyValuePair,
|
|
13926
13878
|
} from "./types";
|
|
13927
13879
|
|
|
13928
|
-
const
|
|
13929
|
-
const
|
|
13930
|
-
|
|
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
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
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
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
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
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
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
|
|
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
|
-
|
|
13982
|
-
}
|
|
13920
|
+
const add = () =>
|
|
13921
|
+
onChange([...rows, { id: generateId(), key: "", value: "" }]);
|
|
13983
13922
|
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
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
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
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
|
|
14111
|
-
|
|
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
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
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
|
|
14123
|
-
const
|
|
14124
|
-
.
|
|
14125
|
-
.
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
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
|
|
14133
|
-
|
|
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="
|
|
14124
|
+
size="lg"
|
|
14140
14125
|
hideCloseButton
|
|
14141
14126
|
className={cn(
|
|
14142
|
-
"
|
|
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
|
|
14149
|
-
<DialogTitle className="
|
|
14150
|
-
|
|
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
|
|
14159
|
-
aria-label="Close
|
|
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="
|
|
14143
|
+
<X className="size-4" />
|
|
14162
14144
|
</button>
|
|
14163
14145
|
</div>
|
|
14164
14146
|
|
|
14165
|
-
{/*
|
|
14166
|
-
<div className="flex
|
|
14167
|
-
{
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
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
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
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
|
-
|
|
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
|
-
{/*
|
|
14218
|
-
{
|
|
14219
|
-
<div className="flex flex-col gap-
|
|
14220
|
-
{
|
|
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
|
-
|
|
14223
|
-
|
|
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
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
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}% •
|
|
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={() =>
|
|
14248
|
-
aria-label={
|
|
14249
|
-
item.status === "uploading"
|
|
14250
|
-
? "Cancel upload"
|
|
14251
|
-
: "Remove file"
|
|
14252
|
-
}
|
|
14259
|
+
onClick={() => setActiveTab(tab)}
|
|
14253
14260
|
className={cn(
|
|
14254
|
-
"
|
|
14255
|
-
|
|
14256
|
-
? "text-semantic-
|
|
14257
|
-
: "text-semantic-text-muted hover:text-semantic-
|
|
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
|
-
{
|
|
14261
|
-
<XCircle className="size-5" />
|
|
14262
|
-
) : (
|
|
14263
|
-
<Trash2 className="size-5" />
|
|
14264
|
-
)}
|
|
14267
|
+
{tabLabels[tab]}
|
|
14265
14268
|
</button>
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
14269
|
-
|
|
14270
|
-
|
|
14271
|
-
|
|
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
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
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
|
-
|
|
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
|
|
14812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
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={() =>
|
|
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
|
-
|
|
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(`
|
|
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
|
|
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
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
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 "
|
|
15589
|
+
export { FileUploadModal } from "../file-upload-modal";
|
|
15557
15590
|
export { IvrBotConfig } from "./ivr-bot-config";
|
|
15558
15591
|
|
|
15559
15592
|
export type {
|