Add context-aware artifacts
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/core": "^0.3.44",
|
"@langchain/core": "^0.3.44",
|
||||||
"@langchain/langgraph": "^0.2.63",
|
"@langchain/langgraph": "^0.2.63",
|
||||||
"@langchain/langgraph-sdk": "^0.0.66",
|
"@langchain/langgraph-sdk": "^0.0.73",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: ^0.2.63
|
specifier: ^0.2.63
|
||||||
version: 0.2.63(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)(zod-to-json-schema@3.24.3(zod@3.24.2))
|
version: 0.2.63(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)(zod-to-json-schema@3.24.3(zod@3.24.2))
|
||||||
'@langchain/langgraph-sdk':
|
'@langchain/langgraph-sdk':
|
||||||
specifier: ^0.0.66
|
specifier: ^0.0.73
|
||||||
version: 0.0.66(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
version: 0.0.73(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.3
|
specifier: ^1.1.3
|
||||||
version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@@ -545,8 +545,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@langchain/core': '>=0.2.31 <0.4.0'
|
'@langchain/core': '>=0.2.31 <0.4.0'
|
||||||
|
|
||||||
'@langchain/langgraph-sdk@0.0.66':
|
'@langchain/langgraph-sdk@0.0.73':
|
||||||
resolution: {integrity: sha512-l0V4yfKXhHaTRK/1bKMfZ14k3wWZu27DWTlCUnbYJvdo7os5srhONgPCOqQgpazhi5EhXbW2EVgeu/wLW2zH6Q==}
|
resolution: {integrity: sha512-V3E7Bd1KNcTpnEglZqKpnQtth62WHf+Dxq5V+0CqMxbzPHnW9I4eynCO4c1/HSY/rHgEtvgj7fMvCAb6I5R+lQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@langchain/core': '>=0.2.31 <0.4.0'
|
'@langchain/core': '>=0.2.31 <0.4.0'
|
||||||
react: ^18 || ^19
|
react: ^18 || ^19
|
||||||
@@ -2595,6 +2595,7 @@ packages:
|
|||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
@@ -3699,7 +3700,7 @@ snapshots:
|
|||||||
'@langchain/core': 0.3.44(openai@4.85.4(zod@3.24.2))
|
'@langchain/core': 0.3.44(openai@4.85.4(zod@3.24.2))
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
|
|
||||||
'@langchain/langgraph-sdk@0.0.66(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)':
|
'@langchain/langgraph-sdk@0.0.73(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
p-queue: 6.6.2
|
p-queue: 6.6.2
|
||||||
@@ -3713,7 +3714,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.44(openai@4.85.4(zod@3.24.2))
|
'@langchain/core': 0.3.44(openai@4.85.4(zod@3.24.2))
|
||||||
'@langchain/langgraph-checkpoint': 0.0.16(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))
|
'@langchain/langgraph-checkpoint': 0.0.16(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))
|
||||||
'@langchain/langgraph-sdk': 0.0.66(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
'@langchain/langgraph-sdk': 0.0.73(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
zod: 3.24.2
|
zod: 3.24.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Thread } from "@/components/thread";
|
import { Thread } from "@/components/thread";
|
||||||
import { StreamProvider } from "@/providers/Stream";
|
import { StreamProvider } from "@/providers/Stream";
|
||||||
import { ThreadProvider } from "@/providers/Thread";
|
import { ThreadProvider } from "@/providers/Thread";
|
||||||
|
import { ArtifactProvider } from "@/components/thread/artifact";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@@ -12,7 +13,9 @@ export default function DemoPage(): React.ReactNode {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<ThreadProvider>
|
<ThreadProvider>
|
||||||
<StreamProvider>
|
<StreamProvider>
|
||||||
<Thread />
|
<ArtifactProvider>
|
||||||
|
<Thread />
|
||||||
|
</ArtifactProvider>
|
||||||
</StreamProvider>
|
</StreamProvider>
|
||||||
</ThreadProvider>
|
</ThreadProvider>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const ArtifactSlotContext = createContext<{
|
|||||||
|
|
||||||
title: [HTMLElement | null, Setter<HTMLElement | null>];
|
title: [HTMLElement | null, Setter<HTMLElement | null>];
|
||||||
content: [HTMLElement | null, Setter<HTMLElement | null>];
|
content: [HTMLElement | null, Setter<HTMLElement | null>];
|
||||||
|
|
||||||
|
context: [Record<string, unknown>, Setter<Record<string, unknown>>];
|
||||||
}>(null!);
|
}>(null!);
|
||||||
|
|
||||||
const ArtifactFill = (props: {
|
const ArtifactFill = (props: {
|
||||||
@@ -88,15 +90,18 @@ export function ArtifactTitle(props: HTMLAttributes<HTMLDivElement>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ArtifactContext(props: { children?: ReactNode }) {
|
export function ArtifactProvider(props: { children?: ReactNode }) {
|
||||||
const content = useState<HTMLElement | null>(null);
|
const content = useState<HTMLElement | null>(null);
|
||||||
const title = useState<HTMLElement | null>(null);
|
const title = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const open = useState<string | null>(null);
|
const open = useState<string | null>(null);
|
||||||
const mounted = useState<string | null>(null);
|
const mounted = useState<string | null>(null);
|
||||||
|
const context = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArtifactSlotContext.Provider value={{ open, mounted, title, content }}>
|
<ArtifactSlotContext.Provider
|
||||||
|
value={{ open, mounted, title, content, context }}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ArtifactSlotContext.Provider>
|
</ArtifactSlotContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -106,6 +111,7 @@ export function useArtifact() {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
const context = useContext(ArtifactSlotContext);
|
const context = useContext(ArtifactSlotContext);
|
||||||
const [ctxOpen, ctxSetOpen] = context.open;
|
const [ctxOpen, ctxSetOpen] = context.open;
|
||||||
|
const [ctxContext, ctxSetContext] = context.context;
|
||||||
const [, ctxSetMounted] = context.mounted;
|
const [, ctxSetMounted] = context.mounted;
|
||||||
|
|
||||||
const open = ctxOpen === id;
|
const open = ctxOpen === id;
|
||||||
@@ -136,17 +142,26 @@ export function useArtifact() {
|
|||||||
[id],
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { open, setOpen, content: ArtifactContent };
|
return {
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
context: ctxContext,
|
||||||
|
setContext: ctxSetContext,
|
||||||
|
content: ArtifactContent,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAnyArtifactOpen() {
|
export function useArtifactOpen() {
|
||||||
const context = useContext(ArtifactSlotContext);
|
const context = useContext(ArtifactSlotContext);
|
||||||
const [ctxOpen, setCtxOpen] = context.open;
|
const [ctxOpen, setCtxOpen] = context.open;
|
||||||
|
|
||||||
const open = ctxOpen !== null;
|
const open = ctxOpen !== null;
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => setCtxOpen(null), [setCtxOpen]);
|
||||||
setCtxOpen(null);
|
|
||||||
}, [setCtxOpen]);
|
|
||||||
|
|
||||||
return [open, onClose] as const;
|
return [open, onClose] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useArtifactContext() {
|
||||||
|
const context = useContext(ArtifactSlotContext);
|
||||||
|
return context.context;
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../ui/tooltip";
|
} from "../ui/tooltip";
|
||||||
import {
|
import {
|
||||||
useAnyArtifactOpen,
|
useArtifactOpen,
|
||||||
ArtifactContent,
|
ArtifactContent,
|
||||||
ArtifactContext,
|
|
||||||
ArtifactTitle,
|
ArtifactTitle,
|
||||||
|
useArtifactContext,
|
||||||
} from "./artifact";
|
} from "./artifact";
|
||||||
|
|
||||||
function StickyToBottomContent(props: {
|
function StickyToBottomContent(props: {
|
||||||
@@ -108,38 +108,11 @@ function OpenGitHubRepo() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtifactLayout(props: { children: ReactNode }) {
|
|
||||||
const [open, onClose] = useAnyArtifactOpen();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"grid w-full grid-cols-[1fr_0fr] transition-all duration-500",
|
|
||||||
open && "grid-cols-[3fr_2fr]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
|
|
||||||
<div className="relative flex flex-col border-l">
|
|
||||||
<div className="absolute inset-0 flex min-w-[30vw] flex-col">
|
|
||||||
<div className="grid grid-cols-[1fr_auto] border-b p-4">
|
|
||||||
<ArtifactTitle className="truncate overflow-hidden" />
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<XIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ArtifactContent className="relative flex-grow" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Thread() {
|
export function Thread() {
|
||||||
const [threadId, setThreadId] = useQueryState("threadId");
|
const [artifactContext, setArtifactContext] = useArtifactContext();
|
||||||
|
const [artifactOpen, closeArtifact] = useArtifactOpen();
|
||||||
|
|
||||||
|
const [threadId, _setThreadId] = useQueryState("threadId");
|
||||||
const [chatHistoryOpen, setChatHistoryOpen] = useQueryState(
|
const [chatHistoryOpen, setChatHistoryOpen] = useQueryState(
|
||||||
"chatHistoryOpen",
|
"chatHistoryOpen",
|
||||||
parseAsBoolean.withDefault(false),
|
parseAsBoolean.withDefault(false),
|
||||||
@@ -158,6 +131,14 @@ export function Thread() {
|
|||||||
|
|
||||||
const lastError = useRef<string | undefined>(undefined);
|
const lastError = useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const setThreadId = (id: string | null) => {
|
||||||
|
_setThreadId(id);
|
||||||
|
|
||||||
|
// close artifact and reset artifact context
|
||||||
|
closeArtifact();
|
||||||
|
setArtifactContext({});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stream.error) {
|
if (!stream.error) {
|
||||||
lastError.current = undefined;
|
lastError.current = undefined;
|
||||||
@@ -212,12 +193,17 @@ export function Thread() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
|
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
|
||||||
|
|
||||||
|
const context =
|
||||||
|
Object.keys(artifactContext).length > 0 ? artifactContext : undefined;
|
||||||
|
|
||||||
stream.submit(
|
stream.submit(
|
||||||
{ messages: [...toolMessages, newHumanMessage] },
|
{ messages: [...toolMessages, newHumanMessage], context },
|
||||||
{
|
{
|
||||||
streamMode: ["values"],
|
streamMode: ["values"],
|
||||||
optimisticValues: (prev) => ({
|
optimisticValues: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
context,
|
||||||
messages: [
|
messages: [
|
||||||
...(prev.messages ?? []),
|
...(prev.messages ?? []),
|
||||||
...toolMessages,
|
...toolMessages,
|
||||||
@@ -274,31 +260,58 @@ export function Thread() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArtifactContext>
|
<div
|
||||||
<ArtifactLayout>
|
className={cn(
|
||||||
<motion.div
|
"grid w-full grid-cols-[1fr_0fr] transition-all duration-500",
|
||||||
className={cn(
|
artifactOpen && "grid-cols-[3fr_2fr]",
|
||||||
"relative flex min-w-0 flex-1 flex-col overflow-hidden",
|
)}
|
||||||
!chatStarted && "grid-rows-[1fr]",
|
>
|
||||||
)}
|
<motion.div
|
||||||
layout={isLargeScreen}
|
className={cn(
|
||||||
animate={{
|
"relative flex min-w-0 flex-1 flex-col overflow-hidden",
|
||||||
marginLeft: chatHistoryOpen ? (isLargeScreen ? 300 : 0) : 0,
|
!chatStarted && "grid-rows-[1fr]",
|
||||||
width: chatHistoryOpen
|
)}
|
||||||
? isLargeScreen
|
layout={isLargeScreen}
|
||||||
? "calc(100% - 300px)"
|
animate={{
|
||||||
: "100%"
|
marginLeft: chatHistoryOpen ? (isLargeScreen ? 300 : 0) : 0,
|
||||||
: "100%",
|
width: chatHistoryOpen
|
||||||
}}
|
? isLargeScreen
|
||||||
transition={
|
? "calc(100% - 300px)"
|
||||||
isLargeScreen
|
: "100%"
|
||||||
? { type: "spring", stiffness: 300, damping: 30 }
|
: "100%",
|
||||||
: { duration: 0 }
|
}}
|
||||||
}
|
transition={
|
||||||
>
|
isLargeScreen
|
||||||
{!chatStarted && (
|
? { type: "spring", stiffness: 300, damping: 30 }
|
||||||
<div className="absolute top-0 left-0 z-10 flex w-full items-center justify-between gap-3 p-2 pl-4">
|
: { duration: 0 }
|
||||||
<div>
|
}
|
||||||
|
>
|
||||||
|
{!chatStarted && (
|
||||||
|
<div className="absolute top-0 left-0 z-10 flex w-full items-center justify-between gap-3 p-2 pl-4">
|
||||||
|
<div>
|
||||||
|
{(!chatHistoryOpen || !isLargeScreen) && (
|
||||||
|
<Button
|
||||||
|
className="hover:bg-gray-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setChatHistoryOpen((p) => !p)}
|
||||||
|
>
|
||||||
|
{chatHistoryOpen ? (
|
||||||
|
<PanelRightOpen className="size-5" />
|
||||||
|
) : (
|
||||||
|
<PanelRightClose className="size-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-2 right-4 flex items-center">
|
||||||
|
<OpenGitHubRepo />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{chatStarted && (
|
||||||
|
<div className="relative z-10 flex items-center justify-between gap-3 p-2">
|
||||||
|
<div className="relative flex items-center justify-start gap-2">
|
||||||
|
<div className="absolute left-0 z-10">
|
||||||
{(!chatHistoryOpen || !isLargeScreen) && (
|
{(!chatHistoryOpen || !isLargeScreen) && (
|
||||||
<Button
|
<Button
|
||||||
className="hover:bg-gray-100"
|
className="hover:bg-gray-100"
|
||||||
@@ -313,194 +326,184 @@ export function Thread() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-2 right-4 flex items-center">
|
<motion.button
|
||||||
|
className="flex cursor-pointer items-center gap-2"
|
||||||
|
onClick={() => setThreadId(null)}
|
||||||
|
animate={{
|
||||||
|
marginLeft: !chatHistoryOpen ? 48 : 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LangGraphLogoSVG
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<span className="text-xl font-semibold tracking-tight">
|
||||||
|
Agent Chat
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center">
|
||||||
<OpenGitHubRepo />
|
<OpenGitHubRepo />
|
||||||
</div>
|
</div>
|
||||||
|
<TooltipIconButton
|
||||||
|
size="lg"
|
||||||
|
className="p-4"
|
||||||
|
tooltip="New thread"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setThreadId(null)}
|
||||||
|
>
|
||||||
|
<SquarePen className="size-5" />
|
||||||
|
</TooltipIconButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{chatStarted && (
|
|
||||||
<div className="relative z-10 flex items-center justify-between gap-3 p-2">
|
|
||||||
<div className="relative flex items-center justify-start gap-2">
|
|
||||||
<div className="absolute left-0 z-10">
|
|
||||||
{(!chatHistoryOpen || !isLargeScreen) && (
|
|
||||||
<Button
|
|
||||||
className="hover:bg-gray-100"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => setChatHistoryOpen((p) => !p)}
|
|
||||||
>
|
|
||||||
{chatHistoryOpen ? (
|
|
||||||
<PanelRightOpen className="size-5" />
|
|
||||||
) : (
|
|
||||||
<PanelRightClose className="size-5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<motion.button
|
|
||||||
className="flex cursor-pointer items-center gap-2"
|
|
||||||
onClick={() => setThreadId(null)}
|
|
||||||
animate={{
|
|
||||||
marginLeft: !chatHistoryOpen ? 48 : 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 30,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LangGraphLogoSVG
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
<span className="text-xl font-semibold tracking-tight">
|
|
||||||
Agent Chat
|
|
||||||
</span>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="from-background to-background/0 absolute inset-x-0 top-full h-5 bg-gradient-to-b" />
|
||||||
<div className="flex items-center">
|
</div>
|
||||||
<OpenGitHubRepo />
|
)}
|
||||||
</div>
|
|
||||||
<TooltipIconButton
|
|
||||||
size="lg"
|
|
||||||
className="p-4"
|
|
||||||
tooltip="New thread"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => setThreadId(null)}
|
|
||||||
>
|
|
||||||
<SquarePen className="size-5" />
|
|
||||||
</TooltipIconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="from-background to-background/0 absolute inset-x-0 top-full h-5 bg-gradient-to-b" />
|
<StickToBottom className="relative flex-1 overflow-hidden">
|
||||||
</div>
|
<StickyToBottomContent
|
||||||
)}
|
className={cn(
|
||||||
|
"absolute inset-0 overflow-y-scroll px-4 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent",
|
||||||
<StickToBottom className="relative flex-1 overflow-hidden">
|
!chatStarted && "mt-[25vh] flex flex-col items-stretch",
|
||||||
<StickyToBottomContent
|
chatStarted && "grid grid-rows-[1fr_auto]",
|
||||||
className={cn(
|
)}
|
||||||
"absolute inset-0 overflow-y-scroll px-4 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent",
|
contentClassName="pt-8 pb-16 max-w-3xl mx-auto flex flex-col gap-4 w-full"
|
||||||
!chatStarted && "mt-[25vh] flex flex-col items-stretch",
|
content={
|
||||||
chatStarted && "grid grid-rows-[1fr_auto]",
|
<>
|
||||||
)}
|
{messages
|
||||||
contentClassName="pt-8 pb-16 max-w-3xl mx-auto flex flex-col gap-4 w-full"
|
.filter((m) => !m.id?.startsWith(DO_NOT_RENDER_ID_PREFIX))
|
||||||
content={
|
.map((message, index) =>
|
||||||
<>
|
message.type === "human" ? (
|
||||||
{messages
|
<HumanMessage
|
||||||
.filter((m) => !m.id?.startsWith(DO_NOT_RENDER_ID_PREFIX))
|
key={message.id || `${message.type}-${index}`}
|
||||||
.map((message, index) =>
|
message={message}
|
||||||
message.type === "human" ? (
|
isLoading={isLoading}
|
||||||
<HumanMessage
|
|
||||||
key={message.id || `${message.type}-${index}`}
|
|
||||||
message={message}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AssistantMessage
|
|
||||||
key={message.id || `${message.type}-${index}`}
|
|
||||||
message={message}
|
|
||||||
isLoading={isLoading}
|
|
||||||
handleRegenerate={handleRegenerate}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
{/* Special rendering case where there are no AI/tool messages, but there is an interrupt.
|
|
||||||
We need to render it outside of the messages list, since there are no messages to render */}
|
|
||||||
{hasNoAIOrToolMessages && !!stream.interrupt && (
|
|
||||||
<AssistantMessage
|
|
||||||
key="interrupt-msg"
|
|
||||||
message={undefined}
|
|
||||||
isLoading={isLoading}
|
|
||||||
handleRegenerate={handleRegenerate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLoading && !firstTokenReceived && (
|
|
||||||
<AssistantMessageLoading />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
footer={
|
|
||||||
<div className="sticky bottom-0 flex flex-col items-center gap-8 bg-white">
|
|
||||||
{!chatStarted && (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<LangGraphLogoSVG className="h-8 flex-shrink-0" />
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
|
||||||
Agent Chat
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ScrollToBottom className="animate-in fade-in-0 zoom-in-95 absolute bottom-full left-1/2 mb-4 -translate-x-1/2" />
|
|
||||||
|
|
||||||
<div className="bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl border shadow-xs">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (
|
|
||||||
e.key === "Enter" &&
|
|
||||||
!e.shiftKey &&
|
|
||||||
!e.metaKey &&
|
|
||||||
!e.nativeEvent.isComposing
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
const el = e.target as HTMLElement | undefined;
|
|
||||||
const form = el?.closest("form");
|
|
||||||
form?.requestSubmit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Type your message..."
|
|
||||||
className="field-sizing-content resize-none border-none bg-transparent p-3.5 pb-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none"
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
<div className="flex items-center justify-between p-2 pt-4">
|
<AssistantMessage
|
||||||
<div>
|
key={message.id || `${message.type}-${index}`}
|
||||||
<div className="flex items-center space-x-2">
|
message={message}
|
||||||
<Switch
|
isLoading={isLoading}
|
||||||
id="render-tool-calls"
|
handleRegenerate={handleRegenerate}
|
||||||
checked={hideToolCalls ?? false}
|
/>
|
||||||
onCheckedChange={setHideToolCalls}
|
),
|
||||||
/>
|
)}
|
||||||
<Label
|
{/* Special rendering case where there are no AI/tool messages, but there is an interrupt.
|
||||||
htmlFor="render-tool-calls"
|
We need to render it outside of the messages list, since there are no messages to render */}
|
||||||
className="text-sm text-gray-600"
|
{hasNoAIOrToolMessages && !!stream.interrupt && (
|
||||||
>
|
<AssistantMessage
|
||||||
Hide Tool Calls
|
key="interrupt-msg"
|
||||||
</Label>
|
message={undefined}
|
||||||
</div>
|
isLoading={isLoading}
|
||||||
</div>
|
handleRegenerate={handleRegenerate}
|
||||||
{stream.isLoading ? (
|
/>
|
||||||
<Button
|
)}
|
||||||
key="stop"
|
{isLoading && !firstTokenReceived && (
|
||||||
onClick={() => stream.stop()}
|
<AssistantMessageLoading />
|
||||||
>
|
)}
|
||||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
</>
|
||||||
Cancel
|
}
|
||||||
</Button>
|
footer={
|
||||||
) : (
|
<div className="sticky bottom-0 flex flex-col items-center gap-8 bg-white">
|
||||||
<Button
|
{!chatStarted && (
|
||||||
type="submit"
|
<div className="flex items-center gap-3">
|
||||||
className="shadow-md transition-all"
|
<LangGraphLogoSVG className="h-8 flex-shrink-0" />
|
||||||
disabled={isLoading || !input.trim()}
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
>
|
Agent Chat
|
||||||
Send
|
</h1>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollToBottom className="animate-in fade-in-0 zoom-in-95 absolute bottom-full left-1/2 mb-4 -translate-x-1/2" />
|
||||||
|
|
||||||
|
<div className="bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl border shadow-xs">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.nativeEvent.isComposing
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
const el = e.target as HTMLElement | undefined;
|
||||||
|
const form = el?.closest("form");
|
||||||
|
form?.requestSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
className="field-sizing-content resize-none border-none bg-transparent p-3.5 pb-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-2 pt-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="render-tool-calls"
|
||||||
|
checked={hideToolCalls ?? false}
|
||||||
|
onCheckedChange={setHideToolCalls}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="render-tool-calls"
|
||||||
|
className="text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
Hide Tool Calls
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{stream.isLoading ? (
|
||||||
|
<Button
|
||||||
|
key="stop"
|
||||||
|
onClick={() => stream.stop()}
|
||||||
|
>
|
||||||
|
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="shadow-md transition-all"
|
||||||
|
disabled={isLoading || !input.trim()}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
/>
|
}
|
||||||
</StickToBottom>
|
/>
|
||||||
</motion.div>
|
</StickToBottom>
|
||||||
</ArtifactLayout>
|
</motion.div>
|
||||||
</ArtifactContext>
|
<div className="relative flex flex-col border-l">
|
||||||
|
<div className="absolute inset-0 flex min-w-[30vw] flex-col">
|
||||||
|
<div className="grid grid-cols-[1fr_auto] border-b p-4">
|
||||||
|
<ArtifactTitle className="truncate overflow-hidden" />
|
||||||
|
<button
|
||||||
|
onClick={closeArtifact}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<XIcon className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ArtifactContent className="relative flex-grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const useTypedStream = useStream<
|
|||||||
UpdateType: {
|
UpdateType: {
|
||||||
messages?: Message[] | Message | string;
|
messages?: Message[] | Message | string;
|
||||||
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
CustomEventType: UIMessage | RemoveUIMessage;
|
CustomEventType: UIMessage | RemoveUIMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user