import { useAtomValue } from "@effect/atom-react";import { useCallback, useMemo, 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, derivePendingApprovals, derivePendingUserInputs, setPendingUserInputCustomAnswer, sortThreadActivities, 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, label: string,): void { const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom); appAtomRegistry.set(userInputDraftsByRequestKeyAtom, { ...current, [requestKey]: { ...current[requestKey], [question.id]: togglePendingUserInputOptionSelection( question, current[requestKey]?.[question.id], label, ), }, });} function setUserInputDraftCustomAnswer( requestKey: string, questionId: string, customAnswer: string,): void { const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom); appAtomRegistry.set(userInputDraftsByRequestKeyAtom, { ...current, [requestKey]: { ...current[requestKey], [questionId]: setPendingUserInputCustomAnswer( current[requestKey]?.[questionId], customAnswer, ), }, });} export function useSelectedThreadRequests() { const respondToApproval = useAtomCommand( threadEnvironment.respondToApproval, "thread approval response", ); const respondToUserInput = useAtomCommand( threadEnvironment.respondToUserInput, "thread user input response", ); const { selectedThread: selectedThreadShell } = useThreadSelection(); const selectedThread = useSelectedThreadDetail(); const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom); const [respondingApprovalId, setRespondingApprovalId] = useState<ApprovalRequestId | null>(null); const [respondingUserInputId, setRespondingUserInputId] = useState<ApprovalRequestId | null>( null, ); // Sort once; both derivations expect the same lifecycle ordering. const sortedActivities = useMemo( () => (selectedThread ? sortThreadActivities(selectedThread.activities) : []), [selectedThread], ); const activePendingApprovals = useMemo( () => derivePendingApprovals(sortedActivities), [sortedActivities], ); const activePendingApproval = activePendingApprovals[0] ?? null; const activePendingUserInputs = useMemo( () => derivePendingUserInputs(sortedActivities), [sortedActivities], ); const activePendingUserInput = activePendingUserInputs[0] ?? null; const activePendingUserInputDrafts = activePendingUserInput && selectedThreadShell ? (userInputDraftsByRequestKey[ scopedRequestKey(selectedThreadShell.environmentId, activePendingUserInput.requestId) ] ?? {}) : {}; const activePendingUserInputAnswers = activePendingUserInput ? buildPendingUserInputAnswers(activePendingUserInput.questions, activePendingUserInputDrafts) : null; const onSelectUserInputOption = useCallback( (requestId: ApprovalRequestId, question: UserInputQuestion, label: string) => { if (!selectedThreadShell) { return; } const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId); setUserInputDraftOption(requestKey, question, label); }, [selectedThreadShell], ); const onChangeUserInputCustomAnswer = useCallback( (requestId: ApprovalRequestId, questionId: string, customAnswer: string) => { if (!selectedThreadShell) { return; } const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId); setUserInputDraftCustomAnswer(requestKey, questionId, customAnswer); }, [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; } setRespondingUserInputId(activePendingUserInput.requestId); const result = await respondToUserInput({ environmentId: selectedThreadShell.environmentId, input: { threadId: selectedThreadShell.id, requestId: activePendingUserInput.requestId, answers: activePendingUserInputAnswers, }, }); setRespondingUserInputId((current) => current === activePendingUserInput.requestId ? null : current, ); return result; }, [ activePendingUserInput, activePendingUserInputAnswers, respondToUserInput, selectedThreadShell, ]); return { activePendingApproval, activePendingUserInput, activePendingUserInputDrafts, activePendingUserInputAnswers, respondingApprovalId, respondingUserInputId, onRespondToApproval, onSelectUserInputOption, onChangeUserInputCustomAnswer, onSubmitUserInput, };}
import { useAtomValue } from "@effect/atom-react";
import { useCallback, useMemo, 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,
derivePendingApprovals,
derivePendingUserInputs,
setPendingUserInputCustomAnswer,
sortThreadActivities,
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,
label: string,
): void {
const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);
appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {
...current,
[requestKey]: {
...current[requestKey],
[question.id]: togglePendingUserInputOptionSelection(
question,
current[requestKey]?.[question.id],
label,
),
},
});
}
function setUserInputDraftCustomAnswer(
requestKey: string,
questionId: string,
customAnswer: string,
): void {
const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);
appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {
...current,
[requestKey]: {
...current[requestKey],
[questionId]: setPendingUserInputCustomAnswer(
current[requestKey]?.[questionId],
customAnswer,
),
},
});
}
export function useSelectedThreadRequests() {
const respondToApproval = useAtomCommand(
threadEnvironment.respondToApproval,
"thread approval response",
);
const respondToUserInput = useAtomCommand(
threadEnvironment.respondToUserInput,
"thread user input response",
);
const { selectedThread: selectedThreadShell } = useThreadSelection();
const selectedThread = useSelectedThreadDetail();
const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom);
const [respondingApprovalId, setRespondingApprovalId] = useState<ApprovalRequestId | null>(null);
const [respondingUserInputId, setRespondingUserInputId] = useState<ApprovalRequestId | null>(
null,
);
// Sort once; both derivations expect the same lifecycle ordering.
const sortedActivities = useMemo(
() => (selectedThread ? sortThreadActivities(selectedThread.activities) : []),
[selectedThread],
);
const activePendingApprovals = useMemo(
() => derivePendingApprovals(sortedActivities),
[sortedActivities],
);
const activePendingApproval = activePendingApprovals[0] ?? null;
const activePendingUserInputs = useMemo(
() => derivePendingUserInputs(sortedActivities),
[sortedActivities],
);
const activePendingUserInput = activePendingUserInputs[0] ?? null;
const activePendingUserInputDrafts =
activePendingUserInput && selectedThreadShell
? (userInputDraftsByRequestKey[
scopedRequestKey(selectedThreadShell.environmentId, activePendingUserInput.requestId)
] ?? {})
: {};
const activePendingUserInputAnswers = activePendingUserInput
? buildPendingUserInputAnswers(activePendingUserInput.questions, activePendingUserInputDrafts)
: null;
const onSelectUserInputOption = useCallback(
(requestId: ApprovalRequestId, question: UserInputQuestion, label: string) => {
if (!selectedThreadShell) {
return;
}
const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);
setUserInputDraftOption(requestKey, question, label);
},
[selectedThreadShell],
);
const onChangeUserInputCustomAnswer = useCallback(
(requestId: ApprovalRequestId, questionId: string, customAnswer: string) => {
if (!selectedThreadShell) {
return;
}
const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);
setUserInputDraftCustomAnswer(requestKey, questionId, customAnswer);
},
[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;
}
setRespondingUserInputId(activePendingUserInput.requestId);
const result = await respondToUserInput({
environmentId: selectedThreadShell.environmentId,
input: {
threadId: selectedThreadShell.id,
requestId: activePendingUserInput.requestId,
answers: activePendingUserInputAnswers,
},
});
setRespondingUserInputId((current) =>
current === activePendingUserInput.requestId ? null : current,
);
return result;
}, [
activePendingUserInput,
activePendingUserInputAnswers,
respondToUserInput,
selectedThreadShell,
]);
return {
activePendingApproval,
activePendingUserInput,
activePendingUserInputDrafts,
activePendingUserInputAnswers,
respondingApprovalId,
respondingUserInputId,
onRespondToApproval,
onSelectUserInputOption,
onChangeUserInputCustomAnswer,
onSubmitUserInput,
};
}