@convex-dev/agent
Version:
A agent component for Convex.
108 lines • 4.8 kB
JavaScript
"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