aura-ai
Version:
AI-powered marketing strategist CLI tool for developers
438 lines (400 loc) โข 14.4 kB
JSX
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