Add context-aware artifacts

This commit is contained in:
Tat Dat Duong
2025-05-01 02:30:16 +02:00
parent 58dd69d1a2
commit bc547a375c
6 changed files with 277 additions and 254 deletions

View File

@@ -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",

13
pnpm-lock.yaml generated
View File

@@ -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:

View File

@@ -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 {
<Toaster />
<ThreadProvider>
<StreamProvider>
<ArtifactProvider>
<Thread />
</ArtifactProvider>
</StreamProvider>
</ThreadProvider>
</React.Suspense>

View File

@@ -20,6 +20,8 @@ const ArtifactSlotContext = createContext<{
title: [HTMLElement | null, Setter<HTMLElement | null>];
content: [HTMLElement | null, Setter<HTMLElement | null>];
context: [Record<string, unknown>, Setter<Record<string, unknown>>];
}>(null!);
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 title = useState<HTMLElement | null>(null);
const open = useState<string | null>(null);
const mounted = useState<string | null>(null);
const context = useState<Record<string, unknown>>({});
return (
<ArtifactSlotContext.Provider value={{ open, mounted, title, content }}>
<ArtifactSlotContext.Provider
value={{ open, mounted, title, content, context }}
>
{props.children}
</ArtifactSlotContext.Provider>
);
@@ -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;
}

View File

@@ -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 (
<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() {
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<string | undefined>(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,8 +260,12 @@ export function Thread() {
</motion.div>
</div>
<ArtifactContext>
<ArtifactLayout>
<div
className={cn(
"grid w-full grid-cols-[1fr_0fr] transition-all duration-500",
artifactOpen && "grid-cols-[3fr_2fr]",
)}
>
<motion.div
className={cn(
"relative flex min-w-0 flex-1 flex-col overflow-hidden",
@@ -499,8 +489,21 @@ export function Thread() {
/>
</StickToBottom>
</motion.div>
</ArtifactLayout>
</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>
);
}

View File

@@ -33,6 +33,7 @@ const useTypedStream = useStream<
UpdateType: {
messages?: Message[] | Message | string;
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
context?: Record<string, unknown>;
};
CustomEventType: UIMessage | RemoveUIMessage;
}