From bc547a375c52616571c6ad5668503ee17b2593e0 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 1 May 2025 02:30:16 +0200 Subject: [PATCH] Add context-aware artifacts --- package.json | 2 +- pnpm-lock.yaml | 13 +- src/app/page.tsx | 5 +- src/components/thread/artifact.tsx | 29 +- src/components/thread/index.tsx | 481 +++++++++++++++-------------- src/providers/Stream.tsx | 1 + 6 files changed, 277 insertions(+), 254 deletions(-) diff --git a/package.json b/package.json index 8fd5055..98c34c6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@langchain/core": "^0.3.44", "@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-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4312a52..b60d076 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: 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)) '@langchain/langgraph-sdk': - specifier: ^0.0.66 - version: 0.0.66(@langchain/core@0.3.44(openai@4.85.4(zod@3.24.2)))(react@19.0.0) + specifier: ^0.0.73 + 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': 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) @@ -545,8 +545,8 @@ packages: peerDependencies: '@langchain/core': '>=0.2.31 <0.4.0' - '@langchain/langgraph-sdk@0.0.66': - resolution: {integrity: sha512-l0V4yfKXhHaTRK/1bKMfZ14k3wWZu27DWTlCUnbYJvdo7os5srhONgPCOqQgpazhi5EhXbW2EVgeu/wLW2zH6Q==} + '@langchain/langgraph-sdk@0.0.73': + resolution: {integrity: sha512-V3E7Bd1KNcTpnEglZqKpnQtth62WHf+Dxq5V+0CqMxbzPHnW9I4eynCO4c1/HSY/rHgEtvgj7fMvCAb6I5R+lQ==} peerDependencies: '@langchain/core': '>=0.2.31 <0.4.0' react: ^18 || ^19 @@ -2595,6 +2595,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.7.0: 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)) 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: '@types/json-schema': 7.0.15 p-queue: 6.6.2 @@ -3713,7 +3714,7 @@ snapshots: dependencies: '@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 zod: 3.24.2 optionalDependencies: diff --git a/src/app/page.tsx b/src/app/page.tsx index e84940a..19675de 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,6 +3,7 @@ import { Thread } from "@/components/thread"; import { StreamProvider } from "@/providers/Stream"; import { ThreadProvider } from "@/providers/Thread"; +import { ArtifactProvider } from "@/components/thread/artifact"; import { Toaster } from "@/components/ui/sonner"; import React from "react"; @@ -12,7 +13,9 @@ export default function DemoPage(): React.ReactNode { - + + + diff --git a/src/components/thread/artifact.tsx b/src/components/thread/artifact.tsx index 3a4426d..7dd360b 100644 --- a/src/components/thread/artifact.tsx +++ b/src/components/thread/artifact.tsx @@ -20,6 +20,8 @@ const ArtifactSlotContext = createContext<{ title: [HTMLElement | null, Setter]; content: [HTMLElement | null, Setter]; + + context: [Record, Setter>]; }>(null!); const ArtifactFill = (props: { @@ -88,15 +90,18 @@ export function ArtifactTitle(props: HTMLAttributes) { ); } -export function ArtifactContext(props: { children?: ReactNode }) { +export function ArtifactProvider(props: { children?: ReactNode }) { const content = useState(null); const title = useState(null); const open = useState(null); const mounted = useState(null); + const context = useState>({}); return ( - + {props.children} ); @@ -106,6 +111,7 @@ export function useArtifact() { const id = useId(); const context = useContext(ArtifactSlotContext); const [ctxOpen, ctxSetOpen] = context.open; + const [ctxContext, ctxSetContext] = context.context; const [, ctxSetMounted] = context.mounted; const open = ctxOpen === id; @@ -136,17 +142,26 @@ export function useArtifact() { [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 [ctxOpen, setCtxOpen] = context.open; const open = ctxOpen !== null; - const onClose = useCallback(() => { - setCtxOpen(null); - }, [setCtxOpen]); + const onClose = useCallback(() => setCtxOpen(null), [setCtxOpen]); return [open, onClose] as const; } + +export function useArtifactContext() { + const context = useContext(ArtifactSlotContext); + return context.context; +} diff --git a/src/components/thread/index.tsx b/src/components/thread/index.tsx index 34d70d1..632f2e8 100644 --- a/src/components/thread/index.tsx +++ b/src/components/thread/index.tsx @@ -37,10 +37,10 @@ import { TooltipTrigger, } from "../ui/tooltip"; import { - useAnyArtifactOpen, + useArtifactOpen, ArtifactContent, - ArtifactContext, ArtifactTitle, + useArtifactContext, } from "./artifact"; function StickyToBottomContent(props: { @@ -108,38 +108,11 @@ function OpenGitHubRepo() { ); } -function ArtifactLayout(props: { children: ReactNode }) { - const [open, onClose] = useAnyArtifactOpen(); - - return ( -
- {props.children} - -
-
-
- - -
- -
-
-
- ); -} - 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( "chatHistoryOpen", parseAsBoolean.withDefault(false), @@ -158,6 +131,14 @@ export function Thread() { const lastError = useRef(undefined); + const setThreadId = (id: string | null) => { + _setThreadId(id); + + // close artifact and reset artifact context + closeArtifact(); + setArtifactContext({}); + }; + useEffect(() => { if (!stream.error) { lastError.current = undefined; @@ -212,12 +193,17 @@ export function Thread() { }; const toolMessages = ensureToolCallsHaveResponses(stream.messages); + + const context = + Object.keys(artifactContext).length > 0 ? artifactContext : undefined; + stream.submit( - { messages: [...toolMessages, newHumanMessage] }, + { messages: [...toolMessages, newHumanMessage], context }, { streamMode: ["values"], optimisticValues: (prev) => ({ ...prev, + context, messages: [ ...(prev.messages ?? []), ...toolMessages, @@ -274,31 +260,58 @@ export function Thread() { - - - - {!chatStarted && ( -
-
+
+ + {!chatStarted && ( +
+
+ {(!chatHistoryOpen || !isLargeScreen) && ( + + )} +
+
+ +
+
+ )} + {chatStarted && ( +
+
+
{(!chatHistoryOpen || !isLargeScreen) && (
-
+ setThreadId(null)} + animate={{ + marginLeft: !chatHistoryOpen ? 48 : 0, + }} + transition={{ + type: "spring", + stiffness: 300, + damping: 30, + }} + > + + + Agent Chat + + +
+ +
+
+ setThreadId(null)} + > + +
- )} - {chatStarted && ( -
-
-
- {(!chatHistoryOpen || !isLargeScreen) && ( - - )} -
- setThreadId(null)} - animate={{ - marginLeft: !chatHistoryOpen ? 48 : 0, - }} - transition={{ - type: "spring", - stiffness: 300, - damping: 30, - }} - > - - - Agent Chat - - -
-
-
- -
- setThreadId(null)} - > - - -
+
+
+ )} -
-
- )} - - - - {messages - .filter((m) => !m.id?.startsWith(DO_NOT_RENDER_ID_PREFIX)) - .map((message, index) => - message.type === "human" ? ( - - ) : ( - - ), - )} - {/* 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 && ( - - )} - {isLoading && !firstTokenReceived && ( - - )} - - } - footer={ -
- {!chatStarted && ( -
- -

- Agent Chat -

-
- )} - - - -
-
-