344 lines · 12.0 KB · TypeScript
RawDownload
import { derivePendingRequests } from "@t3tools/client-runtime/pending-requests";import { useServerConfigs } from "./entities";import { Alert } from "react-native";import {  questionAttachmentDraftKey,  questionAttachmentDraftPrefix,  questionAttachmentPreparationAtom,} from "./question-attachments";import { composerDraftsAtom, clearComposerDraft } from "./use-composer-drafts";import {  composerAttachmentUploadsAtom,  composerAttachmentUploadBlockReason,  composerAttachmentsStillUploading,} from "./composer-attachment-uploads";import { useAtomValue } from "@effect/atom-react";import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import {  ApprovalRequestId,  type ProviderApprovalDecision,  type UserInputQuestion,} from "@t3tools/contracts";import { Atom } from "effect/unstable/reactivity"; import { threadEnvironment } from "../state/threads";import { scopedRequestKey } from "../lib/scopedEntities";import {  buildPendingUserInputAnswers,  setPendingUserInputCustomAnswer,  togglePendingUserInputOptionSelection,  type PendingUserInputDraftAnswer,} from "../lib/threadActivity";import { appAtomRegistry } from "./atom-registry";import { useSelectedThreadDetail } from "./use-thread-detail";import { useThreadSelection } from "./use-thread-selection";import { useAtomCommand } from "./use-atom-command"; const userInputDraftsByRequestKeyAtom = Atom.make<  Record<string, Record<string, PendingUserInputDraftAnswer>>>({}).pipe(Atom.keepAlive, Atom.withLabel("mobile:user-input-drafts")); function setUserInputDraftOption(  requestKey: string,  question: UserInputQuestion,  value: string,): void {  const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);  appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {    ...current,    [requestKey]: {      ...current[requestKey],      [question.id]: togglePendingUserInputOptionSelection(        question,        current[requestKey]?.[question.id],        value,      ),    },  });} function setUserInputDraftCustomAnswer(  requestKey: string,  question: UserInputQuestion,  customAnswer: string,): void {  const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);  appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {    ...current,    [requestKey]: {      ...current[requestKey],      [question.id]: setPendingUserInputCustomAnswer(        question,        current[requestKey]?.[question.id],        customAnswer,      ),    },  });} export function useSelectedThreadRequests() {  const respondToApproval = useAtomCommand(    threadEnvironment.respondToApproval,    "thread approval response",  );  const respondToUserInput = useAtomCommand(    threadEnvironment.respondToUserInput,    "thread user input response",  );  const dismissUserInput = useAtomCommand(    threadEnvironment.dismissUserInput,    "thread user input dismissal",  );  const { selectedThread: selectedThreadShell } = useThreadSelection();  const selectedThread = useSelectedThreadDetail();  const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom);  const [respondingApprovalId, setRespondingApprovalId] = useState<ApprovalRequestId | null>(null);  const userInputResponsesInFlight = useRef(new Set<string>());  const [respondingUserInputId, setRespondingUserInputId] = useState<ApprovalRequestId | null>(    null,  );   const { approvals: activePendingApprovals, userInputs: activePendingUserInputs } = useMemo(    () => derivePendingRequests(selectedThread?.activities ?? []),    [selectedThread?.activities],  );  const activePendingApproval = activePendingApprovals[0] ?? null;  const activePendingUserInput = activePendingUserInputs[0] ?? null;  const questionServerConfigs = useServerConfigs();  const attachmentDrafts = useAtomValue(composerDraftsAtom);  const preparationCounts = useAtomValue(questionAttachmentPreparationAtom);  const uploadStates = useAtomValue(composerAttachmentUploadsAtom);  useEffect(() => {    if (!selectedThreadShell || !selectedThread) return;    const prefix = questionAttachmentDraftPrefix(      selectedThreadShell.environmentId,      selectedThreadShell.id,    );    const retained = new Set(      activePendingUserInputs.flatMap((request) =>        request.questions.map((question) =>          questionAttachmentDraftKey(            selectedThreadShell.environmentId,            selectedThreadShell.id,            request.requestId,            question.id,          ),        ),      ),    );    const counts = { ...appAtomRegistry.get(questionAttachmentPreparationAtom) };    let changed = false;    for (const key of new Set([...Object.keys(attachmentDrafts), ...Object.keys(counts)])) {      if (!key.startsWith(prefix) || retained.has(key)) continue;      if (attachmentDrafts[key]) clearComposerDraft(key);      if (key in counts) {        delete counts[key];        changed = true;      }    }    if (changed) appAtomRegistry.set(questionAttachmentPreparationAtom, counts);  }, [activePendingUserInputs, attachmentDrafts, selectedThread, selectedThreadShell]);  const activePendingUserInputDrafts =    activePendingUserInput && selectedThreadShell      ? Object.fromEntries(          activePendingUserInput.questions.map((question) => {            const key = questionAttachmentDraftKey(              selectedThreadShell.environmentId,              selectedThreadShell.id,              activePendingUserInput.requestId,              question.id,            );            const attachments = attachmentDrafts[key]?.attachments ?? [];            const uploadInput = {              environmentId: selectedThreadShell.environmentId,              attachments,              serverConfig: questionServerConfigs.get(selectedThreadShell.environmentId) ?? null,              states: uploadStates,            };            return [              question.id,              {                ...userInputDraftsByRequestKey[                  scopedRequestKey(                    selectedThreadShell.environmentId,                    activePendingUserInput.requestId,                  )                ]?.[question.id],                attachmentCount: attachments.length,                attachmentsBlocked:                  (attachments.length > 0 &&                    questionServerConfigs.get(selectedThreadShell.environmentId)?.environment                      .capabilities.questionAttachments !== true) ||                  (preparationCounts[key] ?? 0) > 0 ||                  composerAttachmentsStillUploading(uploadInput) ||                  composerAttachmentUploadBlockReason({                    ...uploadInput,                    connected: true,                  }) !== null,              },            ];          }),        )      : {};  const activePendingUserInputAnswers = activePendingUserInput    ? buildPendingUserInputAnswers(activePendingUserInput.questions, activePendingUserInputDrafts)    : null;   const onSelectUserInputOption = useCallback(    (requestId: ApprovalRequestId, question: UserInputQuestion, value: string) => {      if (!selectedThreadShell) {        return;      }       const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);      setUserInputDraftOption(requestKey, question, value);    },    [selectedThreadShell],  );   const onChangeUserInputCustomAnswer = useCallback(    (requestId: ApprovalRequestId, questionId: string, customAnswer: string) => {      const question = activePendingUserInputs        .find((request) => request.requestId === requestId)        ?.questions.find((entry) => entry.id === questionId);      if (!selectedThreadShell || !question) {        return;      }       const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);      setUserInputDraftCustomAnswer(requestKey, question, customAnswer);    },    [activePendingUserInputs, selectedThreadShell],  );   const onRespondToApproval = useCallback(    async (requestId: ApprovalRequestId, decision: ProviderApprovalDecision) => {      if (!selectedThreadShell) {        return;      }       setRespondingApprovalId(requestId);      const result = await respondToApproval({        environmentId: selectedThreadShell.environmentId,        input: {          threadId: selectedThreadShell.id,          requestId,          decision,        },      });      setRespondingApprovalId((current) => (current === requestId ? null : current));      return result;    },    [respondToApproval, selectedThreadShell],  );   const onSubmitUserInput = useCallback(async () => {    if (!selectedThreadShell || !activePendingUserInput || !activePendingUserInputAnswers) {      return;    }     const responseKey = questionAttachmentDraftKey(      selectedThreadShell.environmentId,      selectedThreadShell.id,      activePendingUserInput.requestId,      "",    );    if (userInputResponsesInFlight.current.has(responseKey)) return;    const attachmentsByQuestionId = new Map<      string,      import("@t3tools/contracts").UserInputAttachments[string]    >();    for (const question of activePendingUserInput.questions) {      const key = questionAttachmentDraftKey(        selectedThreadShell.environmentId,        selectedThreadShell.id,        activePendingUserInput.requestId,        question.id,      );      if ((appAtomRegistry.get(questionAttachmentPreparationAtom)[key] ?? 0) > 0) return;      const attachments = appAtomRegistry.get(composerDraftsAtom)[key]?.attachments ?? [];      if (attachments.length === 0) continue;      if (        attachments.some(          (attachment) =>            !attachment.uploadedAttachmentId ||            attachment.uploadEnvironmentId !== selectedThreadShell.environmentId,        )      ) {        Alert.alert(          "Attachments are not ready",          "Wait for uploads to finish, or retry failed uploads.",        );        return;      }      attachmentsByQuestionId.set(        question.id,        attachments.map((attachment) => ({          type: attachment.type,          id: attachment.uploadedAttachmentId!,          name: attachment.name,          mimeType: attachment.mimeType,          sizeBytes: attachment.sizeBytes,        })),      );    }    userInputResponsesInFlight.current.add(responseKey);    setRespondingUserInputId(activePendingUserInput.requestId);    const result = await respondToUserInput({      environmentId: selectedThreadShell.environmentId,      input: {        threadId: selectedThreadShell.id,        requestId: activePendingUserInput.requestId,        answers: activePendingUserInputAnswers,        ...(attachmentsByQuestionId.size > 0          ? { attachmentsByQuestionId: Object.fromEntries(attachmentsByQuestionId) }          : {}),      },    });    userInputResponsesInFlight.current.delete(responseKey);    setRespondingUserInputId((current) =>      current === activePendingUserInput.requestId ? null : current,    );    return result;  }, [    activePendingUserInput,    activePendingUserInputAnswers,    respondToUserInput,    selectedThreadShell,  ]);   // Closes an async question without messaging the agent.  const onDismissUserInput = useCallback(async () => {    if (!selectedThreadShell || !activePendingUserInput) {      return;    }     setRespondingUserInputId(activePendingUserInput.requestId);    const result = await dismissUserInput({      environmentId: selectedThreadShell.environmentId,      input: {        threadId: selectedThreadShell.id,        requestId: activePendingUserInput.requestId,      },    });    setRespondingUserInputId((current) =>      current === activePendingUserInput.requestId ? null : current,    );    return result;  }, [activePendingUserInput, dismissUserInput, selectedThreadShell]);   return {    activePendingApproval,    activePendingUserInput,    activePendingUserInputDrafts,    activePendingUserInputAnswers,    respondingApprovalId,    respondingUserInputId,    onRespondToApproval,    onSelectUserInputOption,    onChangeUserInputCustomAnswer,    onSubmitUserInput,    onDismissUserInput,  };}