better-chatbot / src /components /tool-invocation /workflow-invocation.tsx
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
4.98 kB
import { useCopy } from "@/hooks/use-copy";
import { VercelAIWorkflowToolStreamingResult } from "app-types/workflow";
import equal from "lib/equal";
import { AlertTriangleIcon, Check, Copy, Loader2, XIcon } from "lucide-react";
import { memo, useEffect, useMemo, useRef } from "react";
import { Alert, AlertDescription, AlertTitle } from "ui/alert";
import { Button } from "ui/button";
import JsonView from "ui/json-view";
import { NodeResultPopup } from "../workflow/node-result-popup";
import { cn } from "lib/utils";
import { NodeIcon } from "../workflow/node-icon";
import { TextShimmer } from "ui/text-shimmer";
interface WorkflowInvocationProps {
result: VercelAIWorkflowToolStreamingResult;
}
function PureWorkflowInvocation({ result }: WorkflowInvocationProps) {
const { copied, copy } = useCopy();
const savedResult = useRef<VercelAIWorkflowToolStreamingResult>(result);
const output = useMemo(() => {
if (result.status == "running") return null;
if (result.status == "fail")
return (
<Alert variant={"destructive"} className="border-destructive">
<AlertTriangleIcon className="size-3" />
<AlertTitle>{result?.error?.name || "ERROR"}</AlertTitle>
<AlertDescription>{result.error?.message}</AlertDescription>
</Alert>
);
if (!result.result) return null;
return (
<div className="w-full bg-card p-4 border text-xs rounded-lg text-muted-foreground">
<div className="flex items-center">
<h5 className="text-muted-foreground font-medium select-none">
Response
</h5>
<div className="flex-1" />
{copied ? (
<Check className="size-3" />
) : (
<Button
variant="ghost"
size="icon"
className="size-3 text-muted-foreground"
onClick={() => copy(JSON.stringify(result.result))}
>
<Copy className="size-3" />
</Button>
)}
</div>
<div className="p-2 max-h-[300px] overflow-y-auto">
<JsonView data={result.result} />
</div>
</div>
);
}, [result.status, result.error, result.result, copied]);
useEffect(() => {
if (result.status == "running") {
savedResult.current = result;
}
}, [result]);
return (
<div className="w-full flex flex-col gap-1">
{result.history.map((item, i) => {
const result = item.result || savedResult.current.history[i]?.result;
return (
<NodeResultPopup
key={item.id}
disabled={!result}
history={{
name: item.name,
status: item.status,
startedAt: item.startedAt,
endedAt: item.endedAt,
error: item.error?.message,
result,
}}
>
<div
key={item.id}
className={cn(
"flex items-center gap-2 text-sm rounded-sm px-2 py-1.5 relative",
item.status == "fail" && "text-destructive",
!!result && "cursor-pointer hover:bg-secondary",
)}
>
<div className="border rounded overflow-hidden">
<NodeIcon
type={item.kind}
iconClassName="size-3"
className="rounded-none"
/>
</div>
{item.status == "running" ? (
<TextShimmer className="font-semibold">
{`${item.name} Running...`}
</TextShimmer>
) : (
<span className="font-semibold">{item.name}</span>
)}
<span
className={cn(
"ml-auto text-xs",
item.status != "fail" && "text-muted-foreground",
)}
>
{item.status != "running" &&
((item.endedAt! - item.startedAt!) / 1000).toFixed(2)}
</span>
{item.status == "success" ? (
<Check className="size-3" />
) : item.status == "fail" ? (
<XIcon className="size-3" />
) : (
<Loader2 className="size-3 animate-spin" />
)}
</div>
</NodeResultPopup>
);
})}
<div className="mt-2">{output}</div>
</div>
);
}
function areEqual(
prev: WorkflowInvocationProps,
next: WorkflowInvocationProps,
) {
if (prev.result.status != next.result.status) return false;
if (prev.result.error?.message != next.result.error?.message) return false;
if (prev.result.result != next.result.result) return false;
if (!equal(prev.result.history, next.result.history)) return false;
if (!equal(prev.result.result, next.result.result)) return false;
return true;
}
export const WorkflowInvocation = memo(PureWorkflowInvocation, areEqual);