Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import { useCallback, useMemo, useState } from "react";
|
|
import type { AgentAskUserQuestionVM } from "../contracts/agentAskUserQuestionItemVM";
|
|
import {
|
|
buildAskUserAnswerPayload,
|
|
readOwnAnswer,
|
|
writeOwnAnswer,
|
|
type InteractiveAnswerPayload
|
|
} from "../interactiveAnswerPayload";
|
|
|
|
export interface AskUserAnswerFlow {
|
|
allQuestionsAnswered: boolean;
|
|
answerPayload: InteractiveAnswerPayload;
|
|
currentIndex: number;
|
|
currentQuestion: AgentAskUserQuestionVM | null;
|
|
currentQuestionAnswered: boolean;
|
|
freeText: string;
|
|
isLastQuestion: boolean;
|
|
selectedOptions: string[];
|
|
goToNextQuestion: () => void;
|
|
goToPreviousQuestion: () => void;
|
|
setFreeText: (value: string) => void;
|
|
toggleOption: (optionLabel: string) => void;
|
|
}
|
|
|
|
export function useAskUserAnswerFlow({
|
|
isSubmitting,
|
|
questions
|
|
}: {
|
|
isSubmitting: boolean;
|
|
questions: AgentAskUserQuestionVM[];
|
|
}): AskUserAnswerFlow {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [selectedByQuestionId, setSelectedByQuestionId] = useState<
|
|
Record<string, string[]>
|
|
>({});
|
|
const [freeTextByQuestionId, setFreeTextByQuestionId] = useState<
|
|
Record<string, string>
|
|
>({});
|
|
|
|
const currentQuestion = questions[currentIndex] ?? null;
|
|
const selectedOptions = currentQuestion
|
|
? readOwnAnswer(selectedByQuestionId, currentQuestion.id, [])
|
|
: [];
|
|
const freeText = currentQuestion
|
|
? readOwnAnswer(freeTextByQuestionId, currentQuestion.id, "")
|
|
: "";
|
|
const currentQuestionAnswered =
|
|
currentQuestion !== null &&
|
|
(selectedOptions.length > 0 || freeText.trim() !== "");
|
|
const isLastQuestion = currentIndex >= questions.length - 1;
|
|
|
|
const answerPayload = useMemo(() => {
|
|
const answersByQuestionId: Record<string, string | string[]> = {};
|
|
for (const question of questions) {
|
|
const selected = readOwnAnswer(selectedByQuestionId, question.id, []);
|
|
const customAnswer = readOwnAnswer(
|
|
freeTextByQuestionId,
|
|
question.id,
|
|
""
|
|
).trim();
|
|
if (question.multiSelect) {
|
|
const answers = customAnswer ? [...selected, customAnswer] : selected;
|
|
if (answers.length > 0) {
|
|
writeOwnAnswer(answersByQuestionId, question.id, answers);
|
|
}
|
|
continue;
|
|
}
|
|
const answer = customAnswer || selected[0];
|
|
if (answer) {
|
|
writeOwnAnswer(answersByQuestionId, question.id, answer);
|
|
}
|
|
}
|
|
return buildAskUserAnswerPayload(answersByQuestionId);
|
|
}, [freeTextByQuestionId, questions, selectedByQuestionId]);
|
|
|
|
const allQuestionsAnswered =
|
|
questions.length > 0 &&
|
|
questions.every((question) =>
|
|
Object.prototype.hasOwnProperty.call(
|
|
answerPayload.answersByQuestionId,
|
|
question.id
|
|
)
|
|
);
|
|
|
|
const toggleOption = useCallback(
|
|
(optionLabel: string) => {
|
|
if (isSubmitting || !currentQuestion) return;
|
|
setSelectedByQuestionId((current) => {
|
|
const existing = readOwnAnswer(current, currentQuestion.id, []);
|
|
const next = currentQuestion.multiSelect
|
|
? existing.includes(optionLabel)
|
|
? existing.filter((value) => value !== optionLabel)
|
|
: [...existing, optionLabel]
|
|
: existing.includes(optionLabel)
|
|
? []
|
|
: [optionLabel];
|
|
const updated = { ...current };
|
|
writeOwnAnswer(updated, currentQuestion.id, next);
|
|
return updated;
|
|
});
|
|
},
|
|
[currentQuestion, isSubmitting]
|
|
);
|
|
|
|
const setFreeText = useCallback(
|
|
(value: string) => {
|
|
if (isSubmitting || !currentQuestion) return;
|
|
setFreeTextByQuestionId((current) => {
|
|
const updated = { ...current };
|
|
writeOwnAnswer(updated, currentQuestion.id, value);
|
|
return updated;
|
|
});
|
|
},
|
|
[currentQuestion, isSubmitting]
|
|
);
|
|
|
|
const goToPreviousQuestion = useCallback(() => {
|
|
if (isSubmitting) return;
|
|
setCurrentIndex((current) => Math.max(current - 1, 0));
|
|
}, [isSubmitting]);
|
|
|
|
const goToNextQuestion = useCallback(() => {
|
|
if (isSubmitting || !currentQuestionAnswered) return;
|
|
setCurrentIndex((current) => Math.min(current + 1, questions.length - 1));
|
|
}, [currentQuestionAnswered, isSubmitting, questions.length]);
|
|
|
|
return {
|
|
allQuestionsAnswered,
|
|
answerPayload,
|
|
currentIndex,
|
|
currentQuestion,
|
|
currentQuestionAnswered,
|
|
freeText,
|
|
isLastQuestion,
|
|
selectedOptions,
|
|
goToNextQuestion,
|
|
goToPreviousQuestion,
|
|
setFreeText,
|
|
toggleOption
|
|
};
|
|
}
|