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": { "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
View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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>
); );
} }

View File

@@ -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;
} }