2025-05-28 13:25:30 -07:00
|
|
|
import React, { createContext, useContext, ReactNode, useState } from "react";
|
2025-05-28 12:26:47 -07:00
|
|
|
import { useStream } from "@langchain/langgraph-sdk/react";
|
|
|
|
|
import { type Message } from "@langchain/langgraph-sdk";
|
|
|
|
|
import {
|
|
|
|
|
uiMessageReducer,
|
|
|
|
|
isUIMessage,
|
|
|
|
|
isRemoveUIMessage,
|
|
|
|
|
type UIMessage,
|
|
|
|
|
type RemoveUIMessage,
|
|
|
|
|
} from "@langchain/langgraph-sdk/react-ui";
|
|
|
|
|
import { useQueryState } from "nuqs";
|
|
|
|
|
import { LangGraphLogoSVG } from "@/components/icons/langgraph";
|
|
|
|
|
import { useThreads } from "./Thread";
|
2025-05-28 13:25:30 -07:00
|
|
|
import { TooltipIconButton } from "@/components/thread/tooltip-icon-button";
|
|
|
|
|
import { Copy, CopyCheck } from "lucide-react";
|
|
|
|
|
import { motion } from "framer-motion";
|
2025-05-28 12:26:47 -07:00
|
|
|
|
|
|
|
|
export type StateType = { messages: Message[]; ui?: UIMessage[] };
|
|
|
|
|
|
|
|
|
|
const useTypedStream = useStream<
|
|
|
|
|
StateType,
|
|
|
|
|
{
|
|
|
|
|
UpdateType: {
|
|
|
|
|
messages?: Message[] | Message | string;
|
|
|
|
|
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
|
|
|
|
context?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
CustomEventType: UIMessage | RemoveUIMessage;
|
|
|
|
|
}
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
type StreamContextType = ReturnType<typeof useTypedStream>;
|
|
|
|
|
const StreamContext = createContext<StreamContextType | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
async function sleep(ms = 4000) {
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const StreamSession = ({
|
|
|
|
|
children,
|
|
|
|
|
apiUrl,
|
|
|
|
|
assistantId,
|
|
|
|
|
}: {
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
apiUrl: string;
|
|
|
|
|
assistantId: string;
|
|
|
|
|
}) => {
|
|
|
|
|
const [threadId, setThreadId] = useQueryState("threadId");
|
|
|
|
|
const { getThreads, setThreads } = useThreads();
|
|
|
|
|
const streamValue = useTypedStream({
|
|
|
|
|
apiUrl,
|
|
|
|
|
assistantId,
|
|
|
|
|
threadId: threadId ?? null,
|
|
|
|
|
onCustomEvent: (event, options) => {
|
|
|
|
|
if (isUIMessage(event) || isRemoveUIMessage(event)) {
|
|
|
|
|
options.mutate((prev) => {
|
|
|
|
|
const ui = uiMessageReducer(prev.ui ?? [], event);
|
|
|
|
|
return { ...prev, ui };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onThreadId: (id) => {
|
|
|
|
|
setThreadId(id);
|
|
|
|
|
// Refetch threads list when thread ID changes.
|
|
|
|
|
// Wait for some seconds before fetching so we're able to get the new thread that was created.
|
|
|
|
|
sleep().then(() => getThreads().then(setThreads).catch(console.error));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StreamContext.Provider value={streamValue}>
|
|
|
|
|
{children}
|
|
|
|
|
</StreamContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|
|
|
|
children,
|
|
|
|
|
}) => {
|
2025-05-28 13:25:30 -07:00
|
|
|
const baseCopyTooltipText = "Copy environment variables";
|
|
|
|
|
const [copyTooltipText, setCopyTooltipText] = useState(baseCopyTooltipText);
|
2025-05-28 12:26:47 -07:00
|
|
|
|
2025-05-28 13:25:30 -07:00
|
|
|
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
|
|
|
|
const assistantId: string | undefined =
|
|
|
|
|
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
2025-05-28 12:26:47 -07:00
|
|
|
|
2025-05-28 13:25:30 -07:00
|
|
|
if (!apiUrl || !assistantId) {
|
2025-05-28 12:26:47 -07:00
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
2025-05-28 13:25:30 -07:00
|
|
|
<div className="animate-in fade-in-0 zoom-in-95 flex max-w-3xl flex-col rounded-lg border bg-red-50 shadow-lg">
|
|
|
|
|
<div className="flex flex-col gap-4 border-b p-6">
|
2025-05-28 12:26:47 -07:00
|
|
|
<div className="flex flex-col items-start gap-2">
|
|
|
|
|
<LangGraphLogoSVG className="h-7" />
|
|
|
|
|
<h1 className="text-xl font-semibold tracking-tight">
|
2025-05-28 13:25:30 -07:00
|
|
|
Environment Variables Missing
|
2025-05-28 12:26:47 -07:00
|
|
|
</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-muted-foreground">
|
2025-05-28 13:25:30 -07:00
|
|
|
Whoops, looks like you don't have an API URL or assistant ID
|
|
|
|
|
set in your environment variables. Please make sure you have both
|
|
|
|
|
of these set before continuing.
|
2025-05-28 12:26:47 -07:00
|
|
|
</p>
|
2025-05-28 13:25:30 -07:00
|
|
|
<div className="relative">
|
|
|
|
|
<TooltipIconButton
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const textToCopy = `NEXT_PUBLIC_API_URL=${apiUrl}\nNEXT_PUBLIC_ASSISTANT_ID=${assistantId}`;
|
|
|
|
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
|
|
|
setCopyTooltipText("Copied!");
|
|
|
|
|
setTimeout(
|
|
|
|
|
() => setCopyTooltipText(baseCopyTooltipText),
|
|
|
|
|
2000,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
className="absolute top-2 right-2 cursor-pointer"
|
|
|
|
|
tooltip={copyTooltipText}
|
2025-05-28 12:26:47 -07:00
|
|
|
>
|
2025-05-28 13:25:30 -07:00
|
|
|
{copyTooltipText === baseCopyTooltipText ? (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="check"
|
|
|
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
|
|
|
transition={{ duration: 0.15 }}
|
|
|
|
|
>
|
|
|
|
|
<Copy />
|
|
|
|
|
</motion.div>
|
|
|
|
|
) : (
|
|
|
|
|
<motion.div
|
|
|
|
|
key="copy"
|
|
|
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
|
|
|
transition={{ duration: 0.15 }}
|
|
|
|
|
>
|
|
|
|
|
<CopyCheck className="text-green-500" />
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</TooltipIconButton>
|
|
|
|
|
<code className="bg-muted flex flex-col gap-2 rounded-md border border-red-200 px-4 py-3 text-sm">
|
|
|
|
|
<span>NEXT_PUBLIC_API_URL={apiUrl}</span>
|
|
|
|
|
<span>NEXT_PUBLIC_ASSISTANT_ID={assistantId}</span>
|
|
|
|
|
</code>
|
2025-05-28 12:26:47 -07:00
|
|
|
</div>
|
2025-05-28 13:25:30 -07:00
|
|
|
</div>
|
2025-05-28 12:26:47 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StreamSession
|
|
|
|
|
apiUrl={apiUrl}
|
|
|
|
|
assistantId={assistantId}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</StreamSession>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create a custom hook to use the context
|
|
|
|
|
export const useStreamContext = (): StreamContextType => {
|
|
|
|
|
const context = useContext(StreamContext);
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
throw new Error("useStreamContext must be used within a StreamProvider");
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default StreamContext;
|