UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

108 lines 4.8 kB
"use client"; import { useMemo, useState, useEffect } from "react"; import {} from "ai"; import {} from "../UIMessages.js"; import { applyUIMessageChunksIncremental, blankUIMessage, emptyIncrementalStreamState, getParts, statusFromStreamStatus, } from "../deltas.js"; import { useDeltaStreams } from "./useDeltaStreams.js"; // Polyfill structuredClone to support readUIMessageStream on ReactNative if (!("structuredClone" in globalThis)) { void import("@ungap/structured-clone").then(({ default: structuredClone }) => (globalThis.structuredClone = structuredClone)); } /** * A hook that fetches streaming messages from a thread and converts them to UIMessages * using AI SDK's readUIMessageStream. * This ONLY returns streaming UIMessages. To get both full and streaming messages, * use `useUIMessages`. * * @param query The query to use to fetch messages. * It must take as arguments `{ threadId, paginationOpts, streamArgs }` and * return a `streams` object returned from `agent.syncStreams`. * @param args The arguments to pass to the query other than `paginationOpts` * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that * you want to pass to the query. * @returns The streaming UIMessages. */ export function useStreamingUIMessages(query, args, options) { const [messageState, setMessageState] = useState({}); const streams = useDeltaStreams(query, args, options); const threadId = args === "skip" ? undefined : args.threadId; useEffect(() => { if (!streams) return; let noNewDeltas = true; for (const stream of streams) { const existingStreamState = messageState[stream.streamMessage.streamId]; const cursor = existingStreamState?.cursor; if (existingStreamState === undefined || cursor === undefined) { noNewDeltas = false; break; } if (stream.deltas.some((d) => d.parts.length > 0 && d.end > cursor)) { noNewDeltas = false; break; } if (existingStreamState && existingStreamState.uiMessage.status !== statusFromStreamStatus(stream.streamMessage.status)) { noNewDeltas = false; break; } } if (noNewDeltas) { return; } const abortController = new AbortController(); void (async () => { const newMessageState = Object.fromEntries(await Promise.all(streams.map(async ({ deltas, streamMessage }) => { const streamId = streamMessage.streamId; const existing = messageState[streamId]; const fromCursor = existing?.cursor ?? 0; const status = statusFromStreamStatus(streamMessage.status); const prevState = existing?.streamState ?? emptyIncrementalStreamState(); const { parts: newParts, cursor } = getParts(deltas, fromCursor); const base = existing?.uiMessage ?? blankUIMessage(streamMessage, threadId); if (newParts.length === 0) { if (existing && existing.uiMessage.status !== status) { return [ streamId, { uiMessage: { ...existing.uiMessage, status }, cursor: existing.cursor, streamState: prevState, }, ]; } return [ streamId, existing ?? { uiMessage: base, cursor: 0, streamState: prevState }, ]; } const { message, streamState } = applyUIMessageChunksIncremental(base, newParts, prevState); message.status = status; return [ streamId, { uiMessage: message, cursor, streamState, }, ]; }))); if (abortController.signal.aborted) return; setMessageState(newMessageState); })(); return () => { abortController.abort(); }; }, [messageState, streams, threadId]); return useMemo(() => { if (!streams) return undefined; return streams .map(({ streamMessage }) => messageState[streamMessage.streamId]?.uiMessage) .filter((uiMessage) => uiMessage !== undefined); }, [messageState, streams]); } //# sourceMappingURL=useStreamingUIMessages.js.map