112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { api } from "@code/backend/convex/_generated/api";
|
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
|
import { useMutation, useQuery } from "convex/react";
|
|
import { useState } from "react";
|
|
|
|
import type { PersonalOrganizationState } from "@/hooks/use-personal-organization";
|
|
import { projectConversation } from "@/lib/chat/conversation";
|
|
import type { AgentStatus, ChatAgentState } from "@/lib/chat/types";
|
|
|
|
const requestId = (): string =>
|
|
globalThis.crypto?.randomUUID?.() ??
|
|
`${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
|
|
const uploadImage = async (
|
|
file: File,
|
|
generateUploadUrl: () => Promise<string>
|
|
) => {
|
|
const response = await fetch(await generateUploadUrl(), {
|
|
body: file,
|
|
headers: { "content-type": file.type },
|
|
method: "POST",
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Image upload failed (${response.status})`);
|
|
}
|
|
const payload: unknown = await response.json();
|
|
if (
|
|
typeof payload !== "object" ||
|
|
payload === null ||
|
|
!("storageId" in payload) ||
|
|
typeof payload.storageId !== "string"
|
|
) {
|
|
throw new Error("Image upload returned an invalid response");
|
|
}
|
|
return {
|
|
filename: file.name || undefined,
|
|
mimeType: file.type,
|
|
storageId: payload.storageId as Id<"_storage">,
|
|
};
|
|
};
|
|
|
|
export const useOrganizationChatAgent = (
|
|
organization: PersonalOrganizationState
|
|
): ChatAgentState => {
|
|
const { organizationId } = organization;
|
|
const rows = useQuery(
|
|
api.conversationMessages.listForCurrentOrganization,
|
|
organizationId ? { organizationId } : "skip"
|
|
);
|
|
const send = useMutation(api.conversationMessages.send);
|
|
const generateUploadUrl = useMutation(
|
|
api.conversationMessages.generateUploadUrl
|
|
);
|
|
const [sendError, setSendError] = useState<Error>();
|
|
|
|
const projected = projectConversation(rows ?? []);
|
|
|
|
let status: AgentStatus = "idle";
|
|
if (projected.streaming) {
|
|
status = "streaming";
|
|
} else if (projected.pending) {
|
|
status = "submitted";
|
|
}
|
|
if (!organizationId || rows === undefined) {
|
|
status = organization.error ? "error" : "connecting";
|
|
} else if (projected.failedError || sendError) {
|
|
status = "error";
|
|
}
|
|
|
|
const sendMessage = async (
|
|
message: string,
|
|
options?: { readonly images?: readonly File[] }
|
|
): Promise<void> => {
|
|
if (!organizationId) {
|
|
throw (
|
|
organization.error ??
|
|
new Error("Personal organization is still being prepared")
|
|
);
|
|
}
|
|
setSendError(undefined);
|
|
try {
|
|
const images = await Promise.all(
|
|
(options?.images ?? []).map((file) =>
|
|
uploadImage(file, generateUploadUrl)
|
|
)
|
|
);
|
|
await send({
|
|
clientRequestId: requestId(),
|
|
images,
|
|
organizationId,
|
|
rawText: message,
|
|
});
|
|
} catch (error) {
|
|
const normalized =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
setSendError(normalized);
|
|
throw normalized;
|
|
}
|
|
};
|
|
|
|
return {
|
|
error:
|
|
organization.error ??
|
|
sendError ??
|
|
(projected.failedError ? new Error(projected.failedError) : undefined),
|
|
historyReady: rows !== undefined,
|
|
messages: projected.messages,
|
|
sendMessage,
|
|
status,
|
|
};
|
|
};
|