"use client"; import { useStream } from "../react/index.js"; import type { UIMessage } from "./types.js"; import * as React from "react"; import * as ReactDOM from "react-dom"; import * as JsxRuntime from "react/jsx-runtime"; import type { UseStream } from "../react/stream.js"; const UseStreamContext = React.createContext<{ stream: ReturnType; meta: unknown; }>(null!); type BagTemplate = { ConfigurableType?: Record; InterruptType?: unknown; CustomEventType?: unknown; UpdateType?: unknown; MetaType?: unknown; }; type GetMetaType = Bag extends { MetaType: unknown } ? Bag["MetaType"] : unknown; interface UseStreamContext< StateType extends Record = Record, Bag extends BagTemplate = BagTemplate, > extends UseStream { meta?: GetMetaType; } export function useStreamContext< StateType extends Record = Record, Bag extends { ConfigurableType?: Record; InterruptType?: unknown; CustomEventType?: unknown; UpdateType?: unknown; MetaType?: unknown; } = BagTemplate, >(): UseStreamContext { const ctx = React.useContext(UseStreamContext); if (!ctx) { throw new Error( "useStreamContext must be used within a LoadExternalComponent", ); } return new Proxy(ctx, { get(target, prop: keyof UseStreamContext) { if (prop === "meta") return target.meta; return target.stream[prop]; }, }) as unknown as UseStreamContext; } interface ComponentTarget { comp: React.FunctionComponent | React.ComponentClass; target: HTMLElement; } class ComponentStore { private cache: Record = {}; private boundCache: Record< string, { subscribe: (onStoreChange: () => void) => () => void; getSnapshot: () => ComponentTarget | undefined; } > = {}; private callbacks: Record< string, (( comp: React.FunctionComponent | React.ComponentClass, el: HTMLElement, ) => void)[] > = {}; respond( shadowRootId: string, comp: React.FunctionComponent | React.ComponentClass, targetElement: HTMLElement, ) { this.cache[shadowRootId] = { comp, target: targetElement }; this.callbacks[shadowRootId]?.forEach((c) => c(comp, targetElement)); } getBoundStore(shadowRootId: string) { this.boundCache[shadowRootId] ??= { subscribe: (onStoreChange: () => void) => { this.callbacks[shadowRootId] ??= []; this.callbacks[shadowRootId].push(onStoreChange); return () => { this.callbacks[shadowRootId] = this.callbacks[shadowRootId].filter( (c) => c !== onStoreChange, ); }; }, getSnapshot: () => this.cache[shadowRootId], }; return this.boundCache[shadowRootId]; } } const COMPONENT_STORE = new ComponentStore(); const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE"); const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE"); interface LoadExternalComponentProps extends Pick, "style" | "className"> { /** Stream of the assistant */ stream: ReturnType; /** Namespace of UI components. Defaults to assistant ID. */ namespace?: string; /** UI message to be rendered */ message: UIMessage; /** Additional context to be passed to the child component */ meta?: unknown; /** Fallback to be rendered when the component is loading */ fallback?: React.ReactNode; /** * Map of components that can be rendered directly without fetching the UI code * from the server. */ components?: Record; } export function LoadExternalComponent({ stream, namespace, message, meta, fallback, components, ...props }: LoadExternalComponentProps) { const ref = React.useRef(null); const id = React.useId(); const shadowRootId = `child-shadow-${id}`; const store = React.useMemo( () => COMPONENT_STORE.getBoundStore(shadowRootId), [shadowRootId], ); const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot); const clientComponent = components?.[message.name]; const hasClientComponent = clientComponent != null; const uiNamespace = namespace ?? stream.assistantId; const uiClient = stream.client["~ui"]; React.useEffect(() => { if (hasClientComponent) return; uiClient.getComponent(uiNamespace, message.name).then((html) => { const dom = ref.current; if (!dom) return; const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" }); const fragment = document .createRange() .createContextualFragment( html.replace("{{shadowRootId}}", shadowRootId), ); root.appendChild(fragment); }); }, [uiClient, uiNamespace, message.name, shadowRootId, hasClientComponent]); if (hasClientComponent) { return React.createElement(clientComponent, message.props); } return ( <>
{state?.target != null ? ReactDOM.createPortal( React.createElement(state.comp, message.props), state.target, ) : fallback} ); } declare global { interface Window { [EXT_STORE_SYMBOL]: ComponentStore; [REQUIRE_SYMBOL]: (name: string) => unknown; } } export function bootstrapUiContext() { if (typeof window === "undefined") { console.warn( "Attempting to bootstrap UI context outside of browser environment. " + "Avoid importing from `@langchain/langgraph-sdk/react-ui` in server context.", ); return; } window[EXT_STORE_SYMBOL] = COMPONENT_STORE; window[REQUIRE_SYMBOL] = (name: string) => { if (name === "react") return React; if (name === "react-dom") return ReactDOM; if (name === "react/jsx-runtime") return JsxRuntime; if (name === "@langchain/langgraph-sdk/react") return { useStream }; if (name === "@langchain/langgraph-sdk/react-ui") { return { useStreamContext, LoadExternalComponent: () => { throw new Error("Nesting LoadExternalComponent is not supported"); }, }; } throw new Error(`Unknown module...: ${name}`); }; }