@convex-dev/agent
Version:
A agent component for Convex.
103 lines (100 loc) • 3.07 kB
text/typescript
import { insertAtTop } from "convex/react";
import type { MessageDoc, StreamArgs } from "../validators.js";
import type { OptimisticLocalStore } from "convex/browser";
import type { UIMessage } from "../UIMessages.js";
import type {
FunctionReference,
PaginationOptions,
PaginationResult,
} from "convex/server";
import type { SyncStreamsReturnValue } from "@convex-dev/agent";
/**
* Adds a sent message to the end of a list of messages, so it shows up until
* the message is saved on the server and arrives in the query.
* It generates a message with fields that match both MessageDoc and UIMessage,
* for convenience. It will not include any other fields you might have in your
* regular query, however.
*
* @param query The query used to fetch messages, typically with
* useThreadMessages or useUIMessages.
* @returns A function that can be used to optimistically send a message.
* If your mutation takes different arguments than { threadId, prompt }, you can
* use it as a helper function in your optimistic update:
* ```ts
* const sendMessage = useMutation(
* api.chatStreaming.streamStoryAsynchronously,
* ).withOptimisticUpdate(
* (store, args) => {
* optimisticallySendMessage(api.chatStreaming.listThreadMessages)(store, {
* threadId:
* prompt: whatever you would have passed to the mutation,
* })
* }
* );
* ```
*/
export function optimisticallySendMessage(
query: FunctionReference<
"query",
"public",
{
threadId: string;
paginationOpts: PaginationOptions;
streamArgs?: StreamArgs;
},
PaginationResult<MessageDoc | UIMessage> & {
streams?: SyncStreamsReturnValue;
}
>,
): (
store: OptimisticLocalStore,
args: { threadId: string; prompt: string },
) => void {
return (store, args) => {
const queries = store.getAllQueries(query);
let maxOrder = -1;
for (const q of queries) {
if (q.args?.threadId !== args.threadId) continue;
if (q.args.streamArgs) continue;
for (const m of q.value?.page ?? []) {
maxOrder = Math.max(maxOrder, m.order);
}
}
const order = maxOrder + 1;
const stepOrder = 0;
const id = randomUUID();
const { prompt, ...rest } = args;
insertAtTop({
paginatedQuery: query,
argsToMatch: { threadId: args.threadId, streamArgs: undefined },
item: {
...rest,
_creationTime: Date.now(),
_id: id,
id,
key: `${args.threadId}-${order}-${stepOrder}`,
order,
stepOrder,
status: "pending",
tool: false,
message: { role: "user", content: prompt },
parts: [{ type: "text", text: prompt }],
role: "user",
text: prompt,
},
localQueryStore: store,
});
};
}
export function randomUUID() {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}