import { HTMLAttributes, ReactNode, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, } from "react"; import { createPortal } from "react-dom"; type Setter = (value: T | ((value: T) => T)) => void; const ArtifactSlotContext = createContext<{ open: [string | null, Setter]; mounted: [string | null, Setter]; title: [HTMLElement | null, Setter]; content: [HTMLElement | null, Setter]; context: [Record, Setter>]; }>(null!); /** * Headless component that will obtain the title and content of the artifact * and render them in place of the `ArtifactContent` and `ArtifactTitle` components via * React Portals. */ const ArtifactSlot = (props: { id: string; children?: ReactNode; title?: ReactNode; }) => { const context = useContext(ArtifactSlotContext); const [ctxMounted, ctxSetMounted] = context.mounted; const [content] = context.content; const [title] = context.title; const isMounted = ctxMounted === props.id; const isEmpty = props.children == null && props.title == null; useEffect(() => { if (isEmpty) { ctxSetMounted((open) => (open === props.id ? null : open)); } }, [isEmpty, ctxSetMounted, props.id]); if (!isMounted) return null; return ( <> {title != null ? createPortal(<>{props.title}, title) : null} {content != null ? createPortal(<>{props.children}, content) : null} ); }; export function ArtifactContent(props: HTMLAttributes) { const context = useContext(ArtifactSlotContext); const [mounted] = context.mounted; const ref = useRef(null); const [, setStateRef] = context.content; useLayoutEffect( () => setStateRef?.(mounted ? ref.current : null), [setStateRef, mounted], ); if (!mounted) return null; return (
); } export function ArtifactTitle(props: HTMLAttributes) { const context = useContext(ArtifactSlotContext); const ref = useRef(null); const [, setStateRef] = context.title; useLayoutEffect(() => setStateRef?.(ref.current), [setStateRef]); return (
); } 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} ); } /** * Provides a value to be passed into `meta.artifact` field * of the `LoadExternalComponent` component, to be consumed by the `useArtifact` hook * on the generative UI side. */ 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; const setOpen = useCallback( (value: boolean | ((value: boolean) => boolean)) => { if (typeof value === "boolean") { ctxSetOpen(value ? id : null); } else { ctxSetOpen((open) => (open === id ? null : id)); } ctxSetMounted(id); }, [ctxSetOpen, ctxSetMounted, id], ); const ArtifactContent = useCallback( (props: { title?: React.ReactNode; children: React.ReactNode }) => { return ( {props.children} ); }, [id], ); return [ ArtifactContent, { open, setOpen, context: ctxContext, setContext: ctxSetContext }, ] as [ typeof ArtifactContent, { open: typeof open; setOpen: typeof setOpen; context: typeof ctxContext; setContext: typeof ctxSetContext; }, ]; } /** * General hook for detecting if any artifact is open. */ export function useArtifactOpen() { const context = useContext(ArtifactSlotContext); const [ctxOpen, setCtxOpen] = context.open; const open = ctxOpen !== null; const onClose = useCallback(() => setCtxOpen(null), [setCtxOpen]); return [open, onClose] as const; } /** * Artifacts may at their discretion provide additional context * that will be used when creating a new run. */ export function useArtifactContext() { const context = useContext(ArtifactSlotContext); return context.context; }