Files
zk-data-agent/frontend/app/src/components/thread/agent-inbox/index.tsx
T
2026-05-06 12:10:22 +08:00

105 lines
3.3 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { Interrupt } from "@langchain/langgraph-sdk";
import { cn } from "@/lib/utils";
import { useStreamContext } from "@/providers/Stream";
import { HITLRequest } from "./types";
import { StateView } from "./components/state-view";
import { ThreadActionsView } from "./components/thread-actions-view";
interface ThreadViewProps {
interrupt: Interrupt<HITLRequest> | Interrupt<HITLRequest>[];
}
export function ThreadView({ interrupt }: ThreadViewProps) {
const thread = useStreamContext();
const interrupts = useMemo(
() =>
(Array.isArray(interrupt) ? interrupt : [interrupt]).filter(
(item): item is Interrupt<HITLRequest> => !!item,
),
[interrupt],
);
const [activeInterruptIndex, setActiveInterruptIndex] = useState(0);
const [showDescription, setShowDescription] = useState(false);
const [showState, setShowState] = useState(false);
const showSidePanel = showDescription || showState;
useEffect(() => {
setActiveInterruptIndex(0);
}, [interrupts.length]);
const activeInterrupt = interrupts[activeInterruptIndex];
const activeDescription =
activeInterrupt?.value?.action_requests?.[0]?.description ?? "";
const handleShowSidePanel = (
showStateFlag: boolean,
showDescriptionFlag: boolean,
) => {
if (showStateFlag && showDescriptionFlag) {
console.error("Cannot show both state and description");
return;
}
if (showStateFlag) {
setShowDescription(false);
setShowState(true);
} else if (showDescriptionFlag) {
setShowState(false);
setShowDescription(true);
} else {
setShowState(false);
setShowDescription(false);
}
};
if (!activeInterrupt) {
return null;
}
return (
<div className="flex h-full w-full flex-col rounded-2xl bg-gray-50 p-8 lg:flex-row">
{showSidePanel ? (
<StateView
handleShowSidePanel={handleShowSidePanel}
description={activeDescription}
values={thread.values}
view={showState ? "state" : "description"}
/>
) : (
<div className="flex w-full flex-col gap-6">
{interrupts.length > 1 && (
<div className="flex flex-wrap items-center gap-2">
{interrupts.map((it, idx) => {
const title =
it.value?.action_requests?.[0]?.name ??
`Interrupt ${idx + 1}`;
return (
<button
key={it.id ?? idx}
type="button"
onClick={() => setActiveInterruptIndex(idx)}
className={cn(
"rounded-full border px-3 py-1 text-sm transition-colors",
idx === activeInterruptIndex
? "border-primary bg-primary/10 text-primary"
: "hover:border-primary hover:text-primary border-gray-300 bg-white text-gray-600",
)}
>
{title}
</button>
);
})}
</div>
)}
<ThreadActionsView
interrupt={activeInterrupt}
handleShowSidePanel={handleShowSidePanel}
showState={showState}
showDescription={showDescription}
/>
</div>
)}
</div>
);
}