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