diff --git a/.changeset/fn-7089-interview-other-option.md b/.changeset/fn-7089-interview-other-option.md new file mode 100644 index 0000000000..c9e7184760 --- /dev/null +++ b/.changeset/fn-7089-interview-other-option.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add an "Other" free-text answer to planning and mission interview questions. +category: feature +dev: single_select/multi_select questions now render a synthetic Other option backed by a reserved `_other` response key, threaded through formatResponseForAgent/history formatters in planning.ts, mission-interview.ts, and milestone-slice-interview.ts. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 58a695a389..ed7993d54d 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -736,11 +736,13 @@ Workflow behavior: - If no workflow is selected, or workflow columns are unavailable, mission-created tasks continue to use the project default workflow. + Plan Mission with AI modal behavior: - On desktop, the modal opens as a floating workspace that can be dragged by its title bar and resized from the window edges/corners. - On mobile, the mission interview keeps the fixed full-screen/sheet-style layout so touch users retain the original focused flow. - If the mission interview stream reports a terminal failure, the modal closes the failed stream, shows one normalized error, and offers retry without duplicating late error/complete events. +- Structured single-select and multi-select interview questions include **Other (write your own)** so users can decline all suggested options, submit a free-text answer, or combine that text with selected multi-select options. ## Roadmaps View diff --git a/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx b/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx index 3506fbfa0f..97ab3c38db 100644 --- a/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx +++ b/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx @@ -36,6 +36,8 @@ import { useViewportMode } from "../hooks/useViewportMode"; import { getSessionTabId } from "../utils/getSessionTabId"; const WARNING_ICON = "⚠️"; +const MILESTONE_SLICE_OTHER_RESPONSE_KEY = "_other"; +const MILESTONE_SLICE_OTHER_OPTION_ID = "__other__"; interface MilestoneSliceInterviewModalProps { isOpen: boolean; @@ -645,9 +647,12 @@ interface InterviewQuestionFormProps { function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: InterviewQuestionFormProps) { const { t } = useTranslation("app"); + const questionOptions = question.options ?? []; const [response, setResponse] = useState({}); const [textValue, setTextValue] = useState(""); const [commentValue, setCommentValue] = useState(""); + const [otherValue, setOtherValue] = useState(""); + const [isOtherSelected, setIsOtherSelected] = useState(false); const handleSubmit = useCallback(() => { let nextResponse: QuestionResponse; @@ -656,6 +661,24 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: nextResponse = { [question.id]: textValue }; } else if (question.type === "confirm") { nextResponse = { [question.id]: response[question.id] === true }; + } else if (question.type === "single_select") { + const trimmedOther = otherValue.trim(); + /* + FNXC:PlanningInterview 2026-06-26-00:00: + GitHub #1794 requires milestone and slice interviews to let users decline every AI-provided single-select option and submit their own answer through `_other`. + */ + nextResponse = isOtherSelected && trimmedOther.length > 0 + ? { [MILESTONE_SLICE_OTHER_RESPONSE_KEY]: trimmedOther } + : response; + } else if (question.type === "multi_select") { + const trimmedOther = otherValue.trim(); + /* + FNXC:PlanningInterview 2026-06-26-00:00: + Milestone and slice multi-select questions must support Other-only answers and Other-plus-option answers without forcing users into suggested choices they reject. + */ + nextResponse = isOtherSelected && trimmedOther.length > 0 + ? { ...response, [MILESTONE_SLICE_OTHER_RESPONSE_KEY]: trimmedOther } + : response; } else { nextResponse = response; } @@ -666,12 +689,14 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: } onSubmit(nextResponse); - }, [commentValue, question, response, textValue, onSubmit]); + }, [commentValue, isOtherSelected, otherValue, question, response, textValue, onSubmit]); useEffect(() => { setResponse({}); setTextValue(""); setCommentValue(""); + setOtherValue(""); + setIsOtherSelected(false); }, [question.id]); const isValid = () => { @@ -679,9 +704,17 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: case "text": return textValue.trim().length > 0; case "single_select": - return response[question.id] !== undefined; + /* + FNXC:PlanningInterview 2026-06-26-00:00: + Continue is valid for milestone/slice single-select questions when the user writes a non-empty Other answer, even with no provided option selected. + */ + return response[question.id] !== undefined || (isOtherSelected && otherValue.trim().length > 0); case "multi_select": - return Array.isArray(response[question.id] as unknown) && (response[question.id] as unknown[]).length > 0; + /* + FNXC:PlanningInterview 2026-06-26-00:00: + Continue is valid for milestone/slice multi-select questions when Other has non-whitespace text, including the Other-only case. + */ + return (Array.isArray(response[question.id] as unknown) && (response[question.id] as unknown[]).length > 0) || (isOtherSelected && otherValue.trim().length > 0); case "confirm": return response[question.id] !== undefined; default: @@ -735,16 +768,20 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: /> )} - {question.type === "single_select" && question.options && ( + {question.type === "single_select" && (
- {question.options.map((option) => ( + {questionOptions.map((option) => ( ))} + {/* FNXC:PlanningInterview 2026-06-26-00:00: The synthetic Other radio keeps milestone/slice interviews redirectable when every provided answer is wrong for the user's intent. */} + + {isOtherSelected && ( +
+