better-chatbot / src /components /workflow /node-config /llm-node-config.tsx
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
10.4 kB
import { LLMNodeData, UINode } from "lib/ai/workflow/workflow.interface";
import { SelectModel } from "../../select-model";
import { Button } from "ui/button";
import {
InfoIcon,
MessageCirclePlusIcon,
TrashIcon,
VariableIcon,
} from "lucide-react";
import { Select, SelectTrigger, SelectContent, SelectItem } from "ui/select";
import { OutputSchemaMentionInput } from "../output-schema-mention-input";
import { Label } from "ui/label";
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { appStore } from "@/app/store";
import { Edge, useEdges, useNodes, useReactFlow } from "@xyflow/react";
import { useWorkflowStore } from "@/app/store/workflow.store";
import { useTranslations } from "next-intl";
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
import { Separator } from "ui/separator";
import { Switch } from "ui/switch";
import { OutputSchemaEditor } from "../output-schema-editor";
import { defaultLLMNodeOutputSchema } from "lib/ai/workflow/create-ui-node";
import { ObjectJsonSchema7 } from "app-types/util";
import { toAny } from "lib/utils";
import { notify } from "lib/notify";
export const LLMNodeDataConfig = memo(function ({
data,
}: {
data: LLMNodeData;
}) {
const { updateNodeData } = useReactFlow<UINode>();
const [structuredOutputOpen, setStructuredOutputOpen] = useState(false);
const t = useTranslations();
const editable = useWorkflowStore((state) => {
return (
state.processIds.length === 0 &&
state.hasEditAccess &&
!state.workflow?.isPublished
);
});
const nodes = useNodes() as UINode[];
const edges = useEdges() as Edge[];
const model = useMemo(() => {
return data.model || appStore.getState().chatModel!;
}, [data.model]);
const updateMessage = useCallback(
(index: number, message: Partial<LLMNodeData["messages"][number]>) => {
updateNodeData(data.id, (node) => {
const prev = node.data as LLMNodeData;
return {
messages: prev.messages.map((m, i) => {
if (i !== index) return m;
return { ...m, ...message };
}),
};
});
},
[data.id],
);
const removeMessage = useCallback(
(index: number) => {
updateNodeData(data.id, (node) => {
const prev = node.data as LLMNodeData;
return {
messages: prev.messages.filter((_, i) => i !== index),
};
});
},
[data.id],
);
const addMessage = useCallback(() => {
updateNodeData(data.id, (node) => {
const prev = node.data as LLMNodeData;
return {
messages: [...prev.messages, { role: "user" }],
};
});
}, [data.id]);
useEffect(() => {
if (!data.model) {
updateNodeData(data.id, {
model: appStore.getState().chatModel!,
});
}
}, []);
const isStructuredOutput = useMemo(() => {
return data.outputSchema.properties?.answer?.type != "string";
}, [data.outputSchema]);
return (
<div className="flex flex-col gap-2 text-sm h-full px-4 ">
<Label className="text-sm">Model</Label>
<SelectModel
currentModel={model}
onSelect={(model) => {
updateNodeData(data.id, {
model,
});
}}
/>
<div className="flex items-center justify-between">
<Label className="text-sm">LLM {t("Workflow.outputSchema")}</Label>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Label
className="text-xs font-normal text-muted-foreground"
htmlFor="structuredOutput"
>
{t("Workflow.structuredOutput")}
</Label>
<Switch
id="structuredOutput"
onClick={async () => {
if (isStructuredOutput) {
const ok = await notify.confirm({
description: t("Workflow.structuredOutputSwitchConfirm"),
okText: t("Workflow.structuredOutputSwitchConfirmOk"),
cancelText: t(
"Workflow.structuredOutputSwitchConfirmCancel",
),
});
if (!ok)
return updateNodeData(data.id, {
outputSchema: structuredClone(
defaultLLMNodeOutputSchema,
),
});
}
setStructuredOutputOpen(true);
}}
checked={isStructuredOutput}
/>
</div>
</TooltipTrigger>
<TooltipContent className="p-4 whitespace-pre-wrap">
{t("Workflow.structuredOutputDescription")}
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center flex-wrap gap-1">
{Object.keys(data.outputSchema.properties).flatMap((key) => {
if (
key === "answer" &&
data.outputSchema.properties[key].type === "object"
) {
return Object.keys(
data.outputSchema.properties[key].properties ?? {},
).map((property) => {
return (
<div
key={`${key}.${property}`}
className="flex items-center text-xs px-1.5 py-0.5 bg-secondary rounded-md"
>
<VariableIcon className="size-3.5 text-blue-500" />
<span className="font-semibold">{`${key}.${property}`}</span>
<span className="text-muted-foreground ml-2">
{
toAny(
data.outputSchema.properties[key].properties![property],
)?.type
}
</span>
</div>
);
});
}
return [
<div
key={key}
className="flex items-center text-xs px-1.5 py-0.5 bg-secondary rounded-md"
>
<VariableIcon className="size-3.5 text-blue-500" />
<span className="font-semibold">{key}</span>
<span className="text-muted-foreground ml-2">
{data.outputSchema.properties[key].type}
</span>
</div>,
];
})}
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<Label className="text-sm mt-1">Messages</Label>
<Tooltip>
<TooltipTrigger asChild>
<div className="p-1 hover:bg-secondary rounded cursor-pointer">
<InfoIcon className="size-3" />
</div>
</TooltipTrigger>
<TooltipContent className="p-4 whitespace-pre-wrap">
{t("Workflow.messagesDescription")}
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-col gap-2">
{data.messages.map((message, index) => {
return (
<div key={index} className="w-full bg-secondary rounded-md p-2">
<div className="flex items-center gap-2">
<Select
value={message.role}
onValueChange={(value) => {
updateMessage(index, {
role: value as "user" | "assistant" | "system",
});
}}
>
<SelectTrigger className="border-none" size={"sm"}>
{message.role.toUpperCase()}
</SelectTrigger>
<SelectContent>
<SelectItem value="user">USER</SelectItem>
<SelectItem value="assistant">ASSISTANT</SelectItem>
<SelectItem value="system">SYSTEM</SelectItem>
</SelectContent>
</Select>
<Button
variant={"ghost"}
size={"icon"}
className="ml-auto size-7 hover:bg-destructive/10! hover:text-destructive"
onClick={() => removeMessage(index)}
>
<TrashIcon className="size-3 hover:text-destructive" />
</Button>
</div>
<OutputSchemaMentionInput
currentNodeId={data.id}
nodes={nodes}
edges={edges}
content={message.content}
editable={editable}
onChange={(content) => {
updateMessage(index, {
content,
});
}}
/>
</div>
);
})}
<Button
variant={"ghost"}
size={"icon"}
className="w-full mt-1 border-dashed border text-muted-foreground"
onClick={addMessage}
>
<MessageCirclePlusIcon className="size-4" />{" "}
{t("Workflow.addMessage")}
</Button>
</div>
<OutputSchemaEditor
schema={data.outputSchema?.properties?.answer as ObjectJsonSchema7}
open={structuredOutputOpen}
onOpenChange={setStructuredOutputOpen}
onChange={(schema) => {
updateNodeData(data.id, {
outputSchema: {
...data.outputSchema,
properties: {
...data.outputSchema.properties,
answer: schema,
},
},
});
}}
>
<span className="sr-only"></span>
</OutputSchemaEditor>
</div>
);
});
LLMNodeDataConfig.displayName = "LLMNodeDataConfig";
export const LLMNodeDataStack = memo(function ({
data,
}: { data: LLMNodeData }) {
if (!data.model) return null;
const isTextResponse =
data.outputSchema.properties?.answer?.type === "string";
return (
<div className="flex flex-col gap-1 px-4 mt-4">
<div className="border bg-input text-[10px] rounded px-2 py-1 flex items-center gap-1">
<span className="font-semibold">{data.model.model}</span>
<VariableIcon className="size-3.5 text-blue-500 ml-auto" />
<span className="text-xs text-muted-foreground ">
{isTextResponse ? "text" : "object"}
</span>
</div>
</div>
);
});
LLMNodeDataStack.displayName = "LLMNodeDataStack";