"use client"; import { useChat, type UIMessage } from "@ai-sdk/react"; import type { AssistantCloud } from "assistant-cloud"; import { pickExternalStoreSharedOptions, type AssistantRuntime, type ExternalStoreSharedOptions, } from "@assistant-ui/core"; import { useCloudThreadListAdapter, useRemoteThreadListRuntime, } from "@assistant-ui/core/react"; import { useAui, useAuiState } from "@assistant-ui/store"; import { useAISDKRuntime, type AISDKRuntimeAdapter, type CustomToCreateMessageFunction, } from "./useAISDKRuntime"; import type { ChatInit, ChatTransport } from "ai"; import { AssistantChatTransport } from "./AssistantChatTransport"; import type { AssistantChatResumableOptions } from "../resumable"; import { useEffect, useMemo, useRef } from "react"; export type UseChatRuntimeOptions = ChatInit & ExternalStoreSharedOptions & { cloud?: AssistantCloud | undefined; adapters?: AISDKRuntimeAdapter["adapters"] | undefined; toCreateMessage?: CustomToCreateMessageFunction; onResume?: AISDKRuntimeAdapter["onResume"]; joinStrategy?: AISDKRuntimeAdapter["joinStrategy"]; onThreadIdChange?: ((threadId: string | undefined) => void) | undefined; }; const useDynamicChatTransport = ( transport: ChatTransport, ): ChatTransport => { const transportRef = useRef>(transport); useEffect(() => { transportRef.current = transport; }); const dynamicTransport = useMemo( () => new Proxy(transportRef.current, { get(_, prop) { const res = transportRef.current[prop as keyof ChatTransport]; return typeof res === "function" ? res.bind(transportRef.current) : res; }, }), [], ); return dynamicTransport; }; const getResumableAdapter = ( transport: ChatTransport, ): AssistantChatResumableOptions | undefined => { if (transport instanceof AssistantChatTransport) { return transport.getResumableAdapter(); } const candidate = (transport as { getResumableAdapter?: () => unknown }) .getResumableAdapter; if (typeof candidate !== "function") return undefined; return candidate.call(transport) as AssistantChatResumableOptions | undefined; }; const useChatThreadRuntime = ( options?: UseChatRuntimeOptions, ): AssistantRuntime => { const { adapters, transport: transportOptions, toCreateMessage, isDisabled: _isDisabled, isSendDisabled: _isSendDisabled, unstable_capabilities: _unstable_capabilities, suggestions: _suggestions, onResume, joinStrategy, ...chatOptions } = options ?? {}; // peel guard: any shared key left in `chatOptions` collapses this to `never` true satisfies keyof typeof chatOptions & keyof ExternalStoreSharedOptions extends never ? true : never; const transport = useDynamicChatTransport( transportOptions ?? new AssistantChatTransport(), ); const id = useAuiState((s) => s.threadListItem.id); const aui = useAui(); const chat = useChat({ ...chatOptions, id, transport, }); const runtime = useAISDKRuntime(chat, { adapters, ...pickExternalStoreSharedOptions(options ?? {}), ...(toCreateMessage && { toCreateMessage }), ...(onResume && { onResume }), ...(joinStrategy && { joinStrategy }), }); if (transport instanceof AssistantChatTransport) { transport.setRuntime(runtime); transport.__internal_setGetThreadListItem(() => aui.threadListItem.source ? aui.threadListItem() : undefined, ); } const resumeFiredRef = useRef(false); useEffect(() => { if (resumeFiredRef.current) return; const adapter = getResumableAdapter(transport); if (!adapter) return; const pending = adapter.storage.getStreamId(); if (!pending) return; resumeFiredRef.current = true; chat.resumeStream().catch((err: unknown) => { console.warn( "[assistant-ui] resumable: resume failed; clearing stored stream id", err, ); adapter.storage.clear(); }); }, [transport, chat]); return runtime; }; export const useChatRuntime = ({ cloud, onThreadIdChange, ...options }: UseChatRuntimeOptions = {}): AssistantRuntime => { const cloudAdapter = useCloudThreadListAdapter({ cloud }); return useRemoteThreadListRuntime({ runtimeHook: function RuntimeHook() { return useChatThreadRuntime(options); }, adapter: cloudAdapter, allowNesting: true, onThreadIdChange, }); };