import React from 'react'; import { RiArrowRightSLine, RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import type { QuestionRequest } from '@/types/question'; import { useSessionStore } from '@/stores/useSessionStore'; interface QuestionCardProps { question: QuestionRequest; } type TabKey = string; const SUMMARY_TAB = 'summary'; export const QuestionCard: React.FC = ({ question }) => { const { respondToQuestion, rejectQuestion } = useSessionStore(); const isFromSubagent = useSessionStore( React.useCallback((state) => { const currentSessionId = state.currentSessionId; if (!currentSessionId || question.sessionID === currentSessionId) return false; const sourceSession = state.sessions.find((session) => session.id === question.sessionID); return Boolean(sourceSession?.parentID && sourceSession.parentID === currentSessionId); }, [question.sessionID]) ); const [activeTab, setActiveTab] = React.useState('0'); const [isResponding, setIsResponding] = React.useState(false); const [hasResponded, setHasResponded] = React.useState(false); const [selectedOptions, setSelectedOptions] = React.useState>({}); const [customMode, setCustomMode] = React.useState>({}); const [customText, setCustomText] = React.useState>({}); const questions = React.useMemo(() => question.questions ?? [], [question.questions]); const isSummaryTab = activeTab === SUMMARY_TAB; const activeIndex = isSummaryTab ? -1 : Math.max(0, Math.min(questions.length - 1, Number(activeTab) || 0)); const activeQuestion = isSummaryTab ? null : questions[activeIndex]; const activeHeader = React.useMemo(() => { if (isSummaryTab) return null; const header = activeQuestion?.header?.trim(); return header && header.length > 0 ? header : null; }, [activeQuestion?.header, isSummaryTab]); React.useEffect(() => { setActiveTab('0'); setSelectedOptions({}); setCustomMode({}); setCustomText({}); setHasResponded(false); }, [question.id]); const tabs = React.useMemo(() => { const questionTabs = questions.map((q, index) => ({ value: String(index), label: q.header?.trim() || `Q${index + 1}`, })); // Add summary tab when multiple questions if (questions.length > 1) { questionTabs.push({ value: SUMMARY_TAB, label: 'Summary' }); } return questionTabs; }, [questions]); // Helper to get answer display for a question index const getAnswerDisplay = React.useCallback((index: number): string => { const isCustom = Boolean(customMode[index]); if (isCustom) { const value = (customText[index] ?? '').trim(); return value || '(no answer)'; } const answers = selectedOptions[index] ?? []; return answers.length > 0 ? answers.join(', ') : '(no answer)'; }, [customMode, customText, selectedOptions]); const isMultiple = Boolean(activeQuestion?.multiple); const selectedForActive = selectedOptions[activeIndex] ?? []; const isCustomActive = Boolean(customMode[activeIndex]); const unansweredIndexes = React.useMemo(() => { const pending: number[] = []; for (let index = 0; index < questions.length; index += 1) { const isCustom = Boolean(customMode[index]); if (isCustom) { const value = (customText[index] ?? '').trim(); if (!value) pending.push(index); continue; } const answers = selectedOptions[index] ?? []; if (answers.length === 0) { pending.push(index); } } return pending; }, [customMode, customText, questions.length, selectedOptions]); const requiredSatisfied = React.useMemo(() => { if (questions.length === 0) return false; return unansweredIndexes.length === 0; }, [questions.length, unansweredIndexes.length]); const handleNextUnanswered = React.useCallback(() => { if (questions.length === 0 || unansweredIndexes.length === 0) return; const start = isSummaryTab ? -1 : activeIndex; for (let offset = 1; offset <= questions.length; offset += 1) { const candidate = (start + offset + questions.length) % questions.length; if (unansweredIndexes.includes(candidate)) { setActiveTab(String(candidate)); return; } } setActiveTab(String(unansweredIndexes[0])); }, [activeIndex, isSummaryTab, questions.length, unansweredIndexes]); const buildAnswersPayload = React.useCallback((): string[][] => { const answers: string[][] = []; for (let index = 0; index < questions.length; index += 1) { const isCustom = Boolean(customMode[index]); if (isCustom) { const value = (customText[index] ?? '').trim(); answers.push(value ? [value] : []); continue; } answers.push(selectedOptions[index] ?? []); } return answers; }, [customMode, customText, questions.length, selectedOptions]); const handleToggleOption = React.useCallback( (label: string) => { if (!activeQuestion) return; setCustomMode((prev) => ({ ...prev, [activeIndex]: false })); setSelectedOptions((prev) => { const current = prev[activeIndex] ?? []; if (isMultiple) { const exists = current.includes(label); const next = exists ? current.filter((item) => item !== label) : [...current, label]; return { ...prev, [activeIndex]: next }; } return { ...prev, [activeIndex]: [label] }; }); }, [activeIndex, activeQuestion, isMultiple] ); const handleSelectCustom = React.useCallback(() => { setCustomMode((prev) => ({ ...prev, [activeIndex]: true })); setSelectedOptions((prev) => ({ ...prev, [activeIndex]: [] })); }, [activeIndex]); const handleConfirm = React.useCallback(async () => { if (!requiredSatisfied) return; setIsResponding(true); try { const answers = buildAnswersPayload(); await respondToQuestion(question.sessionID, question.id, answers); setHasResponded(true); } catch { // ignored } finally { setIsResponding(false); } }, [buildAnswersPayload, question.id, question.sessionID, requiredSatisfied, respondToQuestion]); const handleDismiss = React.useCallback(async () => { setIsResponding(true); try { await rejectQuestion(question.sessionID, question.id); setHasResponded(true); } catch { // ignored } finally { setIsResponding(false); } }, [question.id, question.sessionID, rejectQuestion]); if (hasResponded || questions.length === 0) { return null; } return (
{/* Header */}
Input needed {isFromSubagent ? ( From subagent ) : null} {activeHeader ? ( {activeHeader} ) : null}
{/* Minimal inline tabs for multiple questions */} {tabs.length > 1 ? (
{tabs.map((tab) => { const isActive = activeTab === tab.value; const isSummary = tab.value === SUMMARY_TAB; const tabIndex = isSummary ? -1 : Number(tab.value); const isAnswered = !isSummary && Number.isFinite(tabIndex) && !unansweredIndexes.includes(tabIndex); return ( ); })}
) : null} {/* Summary view */} {isSummaryTab ? (
{questions.map((q, index) => { const answer = getAnswerDisplay(index); const hasAnswer = answer !== '(no answer)'; return ( ); })}
) : activeQuestion ? ( <>
{activeQuestion.question}
{isMultiple ? (
Select multiple
) : null}
{activeQuestion.options.map((option, index) => { const selected = selectedForActive.includes(option.label); const recommended = /\(recommended\)/i.test(option.label); return ( ); })} {/* Custom answer option */} {isCustomActive ? (