import { useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; import { aiQuestionSuggestionsOptions, type AIQuestionSuggestionsResponse, } from '../../queries/user-ai-session'; import type { AllowedFormat } from './ContentGenerator'; import { Loader2Icon, RefreshCcwIcon, SendIcon, Trash2 } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { cn } from '../../lib/classname'; import { flushSync } from 'react-dom'; import { CheckIcon } from '../ReactIcons/CheckIcon'; import { Tooltip } from '../Tooltip'; export type QuestionAnswerChatMessage = | { role: 'user'; answer: string } | { role: 'assistant'; question: string; possibleAnswers: string[]; }; type QuestionAnswerChatProps = { term: string; format: AllowedFormat | (string & {}); questionAnswerChatMessages: QuestionAnswerChatMessage[]; setQuestionAnswerChatMessages: ( messages: QuestionAnswerChatMessage[], ) => void; defaultQuestions?: AIQuestionSuggestionsResponse['questions']; type?: 'create' | 'update'; from?: 'content' | 'quiz'; className?: string; }; export function QuestionAnswerChat(props: QuestionAnswerChatProps) { const { term, format, defaultQuestions, questionAnswerChatMessages, setQuestionAnswerChatMessages, type = 'create', className = '', from = 'content', } = props; const [activeMessageIndex, setActiveMessageIndex] = useState( defaultQuestions?.length ?? 0, ); const [message, setMessage] = useState(''); const [status, setStatus] = useState<'answering' | 'done'>('answering'); const scrollAreaRef = useRef(null); const inputRef = useRef(null); const { data: aiQuestionSuggestions, isLoading: isLoadingAiQuestionSuggestions, } = useQuery( aiQuestionSuggestionsOptions({ term, format, from }, defaultQuestions), queryClient, ); const activeMessage = aiQuestionSuggestions?.questions[activeMessageIndex]; const scrollToBottom = () => { if (!scrollAreaRef.current) { return; } scrollAreaRef.current.scrollTo({ top: scrollAreaRef.current.scrollHeight, behavior: 'instant', }); }; const handleAnswerSelect = (answer: string) => { const trimmedAnswer = answer.trim(); if (!activeMessage || !trimmedAnswer) { return; } const newMessages: QuestionAnswerChatMessage[] = [ ...questionAnswerChatMessages, { role: 'assistant', ...activeMessage, }, { role: 'user', answer: trimmedAnswer, }, ]; setQuestionAnswerChatMessages(newMessages); setMessage(''); const hasMoreMessages = activeMessageIndex < aiQuestionSuggestions.questions.length - 1; if (!hasMoreMessages) { setStatus('done'); return; } flushSync(() => { setActiveMessageIndex(activeMessageIndex + 1); setStatus('answering'); // focus the input inputRef.current?.focus(); }); scrollToBottom(); }; const handleReset = () => { setQuestionAnswerChatMessages([]); setActiveMessageIndex(0); setStatus('answering'); }; const handleEditMessage = (messageIndex: number) => { // Remove the assistant question and user answer pair // Since user messages are at odd indices, we want to remove both // the assistant message (at messageIndex - 1) and the user message (at messageIndex) const assistantMessageIndex = messageIndex - 1; const newMessages = questionAnswerChatMessages.slice( 0, assistantMessageIndex, ); setQuestionAnswerChatMessages(newMessages); // Calculate which question should be active // Since we removed both assistant and user messages, the question index // is simply assistantMessageIndex / 2 const questionIndex = Math.floor(assistantMessageIndex / 2); setActiveMessageIndex(questionIndex); setStatus('answering'); setMessage(''); setTimeout(() => { inputRef.current?.focus(); }, 0); }; useEffect(() => { scrollToBottom(); }, [defaultQuestions, type]); return ( <>
{isLoadingAiQuestionSuggestions && (
Generating personalized questions...
)} {!isLoadingAiQuestionSuggestions && status === 'done' && (

Preferences saved

You can now start generating {format}

)} {!isLoadingAiQuestionSuggestions && status === 'answering' && ( <>
{questionAnswerChatMessages.map((message, index) => ( handleEditMessage(index) : undefined } /> ))} {activeMessage && ( )}
{!activeMessage && type === 'update' && (
)} {activeMessage && (
setMessage(e.target.value)} className="w-full bg-transparent text-sm focus:outline-none" placeholder={ activeMessage.possibleAnswers ? 'Type your answer...' : 'Or type your own answer...' } data-clarity-unmask="true" autoFocus onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAnswerSelect(message); setMessage(''); } }} />
)}
)}
); } type QuestionAnswerChatMessageProps = { role: 'user' | 'assistant'; question?: string; answer?: string; possibleAnswers?: string[]; onAnswerSelect?: (answer: string) => void; onEdit?: () => void; }; function QuestionAnswerChatMessage(props: QuestionAnswerChatMessageProps) { const { role, question, answer, possibleAnswers, onAnswerSelect, onEdit } = props; const hasAnswers = possibleAnswers && possibleAnswers.length > 0; return (
{role === 'assistant' && (
{question}
{hasAnswers && onAnswerSelect && (
{possibleAnswers.map((answer) => ( ))}
)}
)} {role === 'user' && (
{answer}
{onEdit && (
Reanswer after this point
)}
)}
); }