better-chatbot / src /components /workflow /node-config /http-node-config.tsx
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
13.7 kB
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Link, Plus, TriangleAlertIcon, VariableIcon } from "lucide-react";
import {
HttpNodeData,
HttpMethod,
OutputSchemaSourceKey,
} from "lib/ai/workflow/workflow.interface";
import { UINode } from "lib/ai/workflow/workflow.interface";
import { HttpValueInput } from "../http-value-input";
import { useState } from "react";
import { useReactFlow } from "@xyflow/react";
import { capitalizeFirstLetter, cn } from "lib/utils";
import { Textarea } from "ui/textarea";
import { VariableSelect } from "../variable-select";
import { VariableMentionItem } from "../variable-mention-item";
import { findAvailableSchemaBySource } from "lib/ai/workflow/shared.workflow";
const HTTP_METHODS: HttpMethod[] = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
];
const tabs = ["basic", "headers", "query", "body"] as const;
const headerExample = [
{
key: "Content-Type",
value: "application/json",
},
{
key: "Authorization",
value: "Bearer <token>",
},
{
key: "X-API-Key",
value: "1234567890",
},
];
const queryExample = [
{
key: "page",
value: "1",
},
{
key: "limit",
value: "10",
},
{
key: "sort",
value: "created_at",
},
];
export function HttpNodeConfig({ node }: { node: UINode<any> }) {
const httpNode = node.data as HttpNodeData;
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>("basic");
const { updateNodeData, getNodes, getEdges } = useReactFlow<UINode>();
const handleUpdateNode = (updates: Partial<HttpNodeData>) => {
updateNodeData(node.id, updates);
};
const addHeader = () => {
const currentHeaders = httpNode.headers || [];
handleUpdateNode({
headers: [...currentHeaders, { key: "", value: undefined }],
});
};
const updateHeader = (index: number, key: string, value?: any) => {
const currentHeaders = httpNode.headers || [];
const newHeaders = [...currentHeaders];
newHeaders[index] = { key, value };
handleUpdateNode({ headers: newHeaders });
};
const removeHeader = (index: number) => {
const currentHeaders = httpNode.headers || [];
const newHeaders = currentHeaders.filter((_, i) => i !== index);
handleUpdateNode({ headers: newHeaders });
};
const addQueryParam = () => {
const currentQuery = httpNode.query || [];
handleUpdateNode({
query: [...currentQuery, { key: "", value: undefined }],
});
};
const updateQueryParam = (index: number, key: string, value?: any) => {
const currentQuery = httpNode.query || [];
const newQuery = [...currentQuery];
newQuery[index] = { key, value };
handleUpdateNode({ query: newQuery });
};
const removeQueryParam = (index: number) => {
const currentQuery = httpNode.query || [];
const newQuery = currentQuery.filter((_, i) => i !== index);
handleUpdateNode({ query: newQuery });
};
const isBodyVariable =
httpNode.body &&
typeof httpNode.body === "object" &&
"nodeId" in httpNode.body;
return (
<div className="space-y-4">
{/* Tab Navigation */}
<div className="flex relative">
<div className="absolute inset-0 w-full border-b pointer-events-none" />
{tabs.map((tab) => (
<Button
key={tab}
variant="ghost"
className={cn(
"rounded-none border-b",
tab === activeTab && "border-primary",
)}
onClick={() => setActiveTab(tab)}
>
{capitalizeFirstLetter(tab)}
</Button>
))}
</div>
<Card className="border-none">
<CardHeader className="sr-only">
<CardTitle>
{activeTab === "basic"
? "HTTP Request Configuration"
: activeTab === "headers"
? "Request Headers"
: activeTab === "query"
? "Query Parameters"
: "Request Body"}
</CardTitle>
</CardHeader>
{activeTab === "basic" && (
<CardContent className="space-y-4">
{/* HTTP Method */}
<div className="space-y-2">
<Label htmlFor="method">Method</Label>
<Select
value={httpNode.method}
onValueChange={(value) =>
handleUpdateNode({
method: value as HttpMethod,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select HTTP method" />
</SelectTrigger>
<SelectContent>
{HTTP_METHODS.map((method) => (
<SelectItem key={method} value={method}>
{method}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* URL */}
<div className="space-y-2">
<Label htmlFor="url">URL</Label>
<HttpValueInput
allowedTypes={["string"]}
currentNodeId={httpNode.id}
value={httpNode.url}
onChange={(value) => handleUpdateNode({ url: value })}
placeholder="https://api.example.com/endpoint"
/>
</div>
{/* Timeout */}
<div className="space-y-2">
<Label htmlFor="timeout">Timeout (ms)</Label>
<Input
id="timeout"
type="number"
value={httpNode.timeout || 30000}
onChange={(e) =>
handleUpdateNode({
timeout: parseInt(e.target.value) || 30000,
})
}
min={1000}
max={300000}
step={1000}
/>
<p className="text-xs text-gray-500">
Request timeout in milliseconds (1s - 5min)
</p>
</div>
</CardContent>
)}
{/* Headers Configuration */}
{activeTab === "headers" && (
<CardContent className="space-y-4">
{(httpNode.headers || []).map((header, index) => (
<div key={index} className="flex gap-2 items-end">
<div>
<Label className="text-xs mb-1 ml-1">Key</Label>
<Input
value={header.key}
className="w-24 placeholder:text-xs"
onChange={(e) =>
updateHeader(index, e.target.value, header.value)
}
placeholder={headerExample[index]?.key}
/>
</div>
<div className="flex-1">
<Label className="text-xs mb-1 ml-1">Value</Label>
<HttpValueInput
allowedTypes={["string", "number"]}
className="max-w-[230px]"
currentNodeId={httpNode.id}
value={header.value}
onChange={(value) => updateHeader(index, header.key, value)}
placeholder={headerExample[index]?.value}
onDelete={() => removeHeader(index)}
/>
</div>
</div>
))}
<Button onClick={addHeader} variant="outline" className="w-full">
<Plus className="size-3.5" />
Add Header
</Button>
</CardContent>
)}
{/* Query Parameters Configuration */}
{activeTab === "query" && (
<CardContent className="space-y-4">
{(httpNode.query || []).map((param, index) => (
<div key={index} className="flex gap-2 items-end">
<div>
<Label className="text-xs mb-1 ml-1 text-muted-foreground">
Key
</Label>
<Input
className="w-24 placeholder:text-xs"
value={param.key}
onChange={(e) =>
updateQueryParam(index, e.target.value, param.value)
}
placeholder={queryExample[index]?.key}
/>
</div>
<div className="flex-1">
<Label className="text-xs mb-1 ml-1 text-muted-foreground">
Value
</Label>
<HttpValueInput
allowedTypes={["string", "number"]}
currentNodeId={httpNode.id}
value={param.value}
onChange={(value) =>
updateQueryParam(index, param.key, value)
}
placeholder={queryExample[index]?.value}
onDelete={() => removeQueryParam(index)}
/>
</div>
</div>
))}
<Button
onClick={addQueryParam}
variant="outline"
className="w-full"
>
<Plus className="size-3.5" />
Add Query Parameter
</Button>
</CardContent>
)}
{/* Body Configuration */}
{activeTab === "body" && (
<CardContent className="space-y-4">
{["POST", "PUT", "PATCH"].includes(httpNode.method) ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Body</Label>
<VariableSelect
currentNodeId={httpNode.id}
onChange={(value) => handleUpdateNode({ body: value })}
>
<Button
variant={isBodyVariable ? "secondary" : "ghost"}
className="ml-auto data-[state=open]:bg-secondary"
size="sm"
>
<VariableIcon className="size-3" />
</Button>
</VariableSelect>
</div>
{!isBodyVariable ? (
<Textarea
className="resize-none h-48"
placeholder={`{
"name": "example",
"value": 123
}`}
value={httpNode.body?.toString() || ""}
onChange={(e) => handleUpdateNode({ body: e.target.value })}
/>
) : (
<VariableMentionItem
className="py-[7px] text-sm truncate"
{...findAvailableSchemaBySource({
nodeId: httpNode.id,
source: httpNode.body as OutputSchemaSourceKey,
nodes: getNodes().map((node) => node.data),
edges: getEdges(),
})}
onRemove={() => handleUpdateNode({ body: undefined })}
/>
)}
<p className="text-xs text-muted-foreground px-4 mt-4">
Request body content. JSON format will be auto-detected and
Content-Type header will be set automatically if not
specified.
</p>
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<p>
Request body is not available for {httpNode.method} requests.
</p>
<p className="text-xs mt-1">
Only POST, PUT, and PATCH requests can have a body.
</p>
</div>
)}
</CardContent>
)}
</Card>
</div>
);
}
export function HttpNodeDataStack({ data }: { data: HttpNodeData }) {
const { getNodes } = useReactFlow();
const urlDisplay = (() => {
if (!data.url) return "No URL";
if (typeof data.url === "string") {
const isUrl = data.url.startsWith("http");
return (
<div className="w-48 gap-1 flex items-center px-2 py-1 rounded-sm bg-background">
{isUrl ? (
<Link className="size-3 text-blue-500" />
) : (
<TriangleAlertIcon className="size-3 text-destructive" />
)}
<span className="text-foreground truncate flex-1">{data.url}</span>
</div>
);
}
if (typeof data.url === "object" && "nodeId" in data.url) {
const nodes = getNodes() as UINode[];
const urlAsSource = data.url as OutputSchemaSourceKey;
const sourceNode = nodes.find(
(node) => node.data.id === urlAsSource.nodeId,
);
return (
<VariableMentionItem
nodeName={sourceNode?.data.name || "ERROR"}
path={urlAsSource.path}
notFound={!sourceNode}
className="text-[10px] ring-0 w-full"
/>
);
}
return "Unknown source";
})();
return (
<div className="flex flex-col gap-1 px-4 mt-4">
<div className="text-[10px] px-2 py-1 flex items-center gap-2s">
<span className="px-1.5 py-0.5 rounded-lg font-semibold text-muted-foreground">
{data.method}
</span>
<div className="truncate flex-1 font-bold text-muted-foreground flex items-center">
{urlDisplay}
</div>
</div>
</div>
);
}