UNPKG

aura-ai

Version:

AI-powered marketing strategist CLI tool for developers

438 lines (400 loc) โ€ข 14.4 kB
import React, { useState, useEffect } from 'react' import { Text, Box, Spacer } from 'ink' import { Spinner } from '@inkjs/ui' import TextInput from 'ink-text-input' import CompositeInput from './components/CompositeInput.jsx' import SimpleConfirm from './components/SimpleConfirm.jsx' import EmojiSpinner from './components/EmojiSpinner.jsx' import fs from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' import { dirname } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) /** * Init Page - Marketing Questionnaire */ const InitPage = ({ onBack }) => { const [questions, setQuestions] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [answers, setAnswers] = useState([]) const [showConfirmSubmit, setShowConfirmSubmit] = useState(false) const [showModifyInput, setShowModifyInput] = useState(false) const [modifyInputValue, setModifyInputValue] = useState('') const [editingQuestionIndex, setEditingQuestionIndex] = useState(-1) const [isProcessing, setIsProcessing] = useState(false) // Load questions on mount useEffect(() => { const loadQuestions = async () => { try { // Try different paths based on environment const possiblePaths = [ path.join(__dirname, '..', 'src', 'init', 'generated', 'questions.json'), // From dist to src/init path.join(path.dirname(__dirname), 'src', 'init', 'generated', 'questions.json'), // Alternative dist path path.resolve('./src/init/generated/questions.json'), // Absolute from project root '/Users/leo/Documents/GitHub/rina/src/init/generated/questions.json', // Fallback absolute path ] let data = null for (const jsonPath of possiblePaths) { try { data = await fs.readFile(jsonPath, 'utf-8') break } catch (err) { // Try next path - uncomment for debugging // console.log(`Failed to load from: ${jsonPath}`, err.message) } } if (!data) { throw new Error('Questions file not found in any expected location') } const parsed = JSON.parse(data) setQuestions(parsed) setLoading(false) } catch (err) { setError(`Failed to load questions: ${err.message}`) setLoading(false) } } loadQuestions() }, []) const handlePrevious = () => { // Go to previous question if not at the first question if (currentQuestionIndex > 0) { setCurrentQuestionIndex(prev => prev - 1) setEditingQuestionIndex(-1) // Clear editing state } } const handleCompositeSubmit = answer => { // Handle shortcuts like /1, /2, etc. if (answer.startsWith('/')) { const questionNum = parseInt(answer.substring(1)) if (questionNum >= 1 && questionNum <= questions.totalQuestions) { // Jump to specific question (convert to 0-based index) setCurrentQuestionIndex(questionNum - 1) setEditingQuestionIndex(questionNum - 1) return } } // Start loading animation setIsProcessing(true) // Store answer - update existing answer if re-answering const wasReAnswering = answers.some( ans => ans.question === questions.questions[currentQuestionIndex].question ) // Simulate processing delay setTimeout(() => { setAnswers(prev => { const newAnswers = [...prev] const existingIndex = newAnswers.findIndex( ans => ans.question === questions.questions[currentQuestionIndex].question ) if (existingIndex >= 0) { // Update existing answer newAnswers[existingIndex] = { question: questions.questions[currentQuestionIndex].question, answer: answer, } } else { // Add new answer newAnswers.push({ question: questions.questions[currentQuestionIndex].question, answer: answer, }) } return newAnswers }) // Clear editing state setEditingQuestionIndex(-1) // Stop loading animation setIsProcessing(false) // If user was re-answering a question, find next unanswered question if (wasReAnswering) { // Find the first unanswered question const answeredQuestions = new Set(answers.map(ans => ans.question)) // Add current question to the set since we just answered it answeredQuestions.add(questions.questions[currentQuestionIndex].question) for (let i = 0; i < questions.questions.length; i++) { if (!answeredQuestions.has(questions.questions[i].question)) { setCurrentQuestionIndex(i) return } } // All questions answered - show confirmation setShowConfirmSubmit(true) } else { // Normal flow: move to next question or finish if (currentQuestionIndex < questions.questions.length - 1) { setCurrentQuestionIndex(prev => prev + 1) } else { // All questions answered - show confirmation setShowConfirmSubmit(true) } } }, 2000) // 2 second delay } const handleConfirmSubmit = confirmed => { if (confirmed) { // User confirmed - return to main menu if (onBack) onBack() } else { // User wants to modify - show modify input setShowConfirmSubmit(false) setShowModifyInput(true) setModifyInputValue('') } } const handleModifyInput = value => { if (value.trim()) { // Handle shortcuts like /1, /2, etc. for modification if (value.startsWith('/')) { const questionNum = parseInt(value.substring(1)) if (questionNum >= 1 && questionNum <= questions.totalQuestions) { // Jump to specific question for modification setCurrentQuestionIndex(questionNum - 1) setEditingQuestionIndex(questionNum - 1) setShowModifyInput(false) return } } // If not a valid shortcut, try to parse as question number const questionNum = parseInt(value) if (questionNum >= 1 && questionNum <= questions.totalQuestions) { // Jump to specific question for modification setCurrentQuestionIndex(questionNum - 1) setEditingQuestionIndex(questionNum - 1) setShowModifyInput(false) } } } if (loading) { return ( <Box padding={1}> <Text color='yellow'>Loading questions...</Text> </Box> ) } if (error) { return ( <Box padding={1}> <Text color='red'>{error}</Text> </Box> ) } // Show confirmation dialog if (showConfirmSubmit) { return ( <Box flexDirection='column' padding={1}> <Box marginBottom={2}> <Text color='green' bold> ๐ŸŽ‰ All questions completed! </Text> </Box> <Box paddingX={2} paddingY={1} borderColor='white' borderStyle='round' marginBottom={2} flexDirection='column' > <Text color='yellow'>Your answers:</Text> {answers.map((ans, idx) => { const questionIndex = questions.questions.findIndex(q => q.question === ans.question) const isEditing = editingQuestionIndex === questionIndex return ( <Box key={idx} marginTop={1} flexDirection='column'> <Text color='white'> Q{idx + 1}: {ans.question} </Text> <Text color='gray'> A{idx + 1}: {isEditing ? '[editing]' : ans.answer} </Text> </Box> ) })} </Box> {/* Loading indicator for submission */} {isProcessing && ( <Box marginBottom={1}> <EmojiSpinner label='Aura is thinking...' /> </Box> )} {/* Question for submission */} <Box columnGap={1} marginBottom={2}> <Text>๐ŸŒ€</Text> <Text bold>Ready to submit?</Text> </Box> <CompositeInput options={[ { label: 'Yes,submit for deep analysis', value: 'submit' }, { label: 'No, modify answers...', value: 'modify' }, ]} onSubmit={value => { setIsProcessing(true) setTimeout(() => { if (value === 'submit') { handleConfirmSubmit(true) } else if (value === 'modify') { // ็›ดๆŽฅ้€ฒๅ…ฅไฟฎๆ”นๆจกๅผ setShowConfirmSubmit(false) setShowModifyInput(true) setModifyInputValue('') } setIsProcessing(false) }, 2000) }} onBack={onBack} isReview={true} showTypeYourAnswer={false} directSubmitValues={['submit', 'modify']} /> </Box> ) } // Show modify input if (showModifyInput) { return ( <Box flexDirection='column' padding={1}> {/* Show previous answers */} {answers.length > 0 && ( <Box marginBottom={2} flexDirection='column'> <Text color='yellow' bold> Your answers: </Text> {answers.map((ans, idx) => { const questionIndex = questions.questions.findIndex(q => q.question === ans.question) const isEditing = editingQuestionIndex === questionIndex return ( <Box key={idx} marginTop={1} flexDirection='column'> <Text color='white'> Q{idx + 1}: {ans.question} </Text> <Text color='gray'> A{idx + 1}: {isEditing ? '[editing]' : ans.answer} </Text> </Box> ) })} </Box> )} {/* Question for modification */} <Box columnGap={1} marginBottom={2}> <Text>๐ŸŒ€</Text> <Text bold>Which question do you want to modify?</Text> </Box> <CompositeInput options={[]} onSubmit={handleModifyInput} placeholder='/1, /2, etc. to jump to specific questions' initialValue={modifyInputValue} onBack={onBack} isReview={true} showTypeYourAnswer={false} /> </Box> ) } // Show composite input mode if (questions && questions.questions[currentQuestionIndex]) { const currentQuestion = questions.questions[currentQuestionIndex] return ( <Box flexDirection='column' padding={1}> <Box marginBottom={1} flexDirection='row'> <Box> <Text bold> ๐Ÿ“ Question {currentQuestionIndex + 1} of {questions.totalQuestions} </Text> </Box> <Spacer /> {/* Progress Bar */} <Box> <Box marginBottom={1} marginRight={2}> {(() => { const isExact = true if (isExact) { const completed = currentQuestionIndex + 1 const remaining = questions.totalQuestions - completed return ( <Text> <Text color='white'>{'๐Ÿค'.repeat(completed)}</Text> <Text color='white'>{'๐Ÿฅš'.repeat(remaining)}</Text> </Text> ) } else { const progress = (currentQuestionIndex + 1) / questions.totalQuestions const width = 30 const filled = Math.round(progress * width) const empty = width - filled return ( <Text> <Text color='white'>{'๐Ÿค'.repeat(filled)}</Text> <Text color='white'>{'๐Ÿฅš'.repeat(empty)}</Text> </Text> ) } })()} </Box> </Box> </Box> {/* Show previous answers only when all questions are completed */} {answers.length > 0 && answers.length === questions.totalQuestions && ( <Box marginBottom={2} flexDirection='column' width={'100%'}> {answers.map((ans, idx) => { const questionIndex = questions.questions.findIndex(q => q.question === ans.question) const isEditing = editingQuestionIndex === questionIndex return ( <Box key={idx} marginTop={1} flexDirection='column'> <Text color='white'> Q{idx + 1}: {ans.question} </Text> <Text color='gray'> A{idx + 1}: {isEditing ? '[editing]' : ans.answer} </Text> </Box> ) })} </Box> )} {/* Question display */} <Box columnGap={1} marginBottom={2}> <Text>๐ŸŒ€</Text> <Box flexDirection='column' rowGap={1}> <Text bold>{currentQuestion.question}</Text> {currentQuestion.explainer && ( <Text marginLeft={2} dimColor fontSize={12}> {currentQuestion.explainer} </Text> )} </Box> </Box> {/* Loading indicator */} {isProcessing && ( <Box marginBottom={1}> <EmojiSpinner label='Processing your answer...' /> </Box> )} <CompositeInput options={currentQuestion.options || []} onSubmit={handleCompositeSubmit} placeholder='Type your answer...' initialValue={ editingQuestionIndex === currentQuestionIndex ? answers.find(ans => ans.question === currentQuestion.question)?.answer || '' : '' } onBack={onBack} onPrevious={handlePrevious} isReview={answers.length === questions.totalQuestions} /> </Box> ) } return ( <Box padding={1}> <Text color='red'>No questions available</Text> </Box> ) } export default InitPage