Spaces:
Runtime error
Runtime error
| 'use client'; | |
| import React from 'react'; | |
| import { X, Brain, TrendingUp, Database, Zap } from 'lucide-react'; | |
| interface Question { | |
| id: string; | |
| label: string; | |
| type: 'text' | 'select' | 'multiselect' | 'textarea'; | |
| options?: string[]; | |
| required?: boolean; | |
| placeholder?: string; | |
| tooltip?: string; | |
| agent_source?: string; | |
| relevance?: number; | |
| } | |
| interface QuestionnaireData { | |
| title?: string; | |
| subtitle?: string; | |
| questions?: Question[]; | |
| fields?: Question[]; | |
| explanation?: string; | |
| data_requirements?: { | |
| minimum_samples?: number; | |
| preferred_samples?: number; | |
| quality_factors?: Record<string, any>; | |
| }; | |
| confidence_score?: number; | |
| ai_decision_trace?: { | |
| agent_sources?: string[]; | |
| }; | |
| } | |
| interface QuestionnaireProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onSubmit: (answers: Record<string, any>) => void; | |
| isSubmitting?: boolean; | |
| workflowId?: string; | |
| questionnaireData?: QuestionnaireData | null; | |
| } | |
| const UnifiedQuestionnaire: React.FC<QuestionnaireProps> = ({ | |
| isOpen, | |
| onClose, | |
| onSubmit, | |
| isSubmitting = false, | |
| workflowId, | |
| questionnaireData | |
| }) => { | |
| // Default questions if no dynamic questionnaire data | |
| const defaultQuestions: Question[] = [ | |
| { | |
| id: "problem_domain", | |
| label: "What type of AI are you building?", | |
| type: "select", | |
| options: ["vision", "text", "audio", "data", "multimodal"], | |
| required: true, | |
| tooltip: "This helps us choose the right model architecture" | |
| }, | |
| { | |
| id: "primary_objective", | |
| label: "What do you want your AI to do?", | |
| type: "textarea", | |
| placeholder: "Describe in simple terms, like 'identify cats in photos' or 'predict tomorrow's weather'", | |
| required: true | |
| }, | |
| { | |
| id: "data_availability", | |
| label: "Do you have example data to train with?", | |
| type: "select", | |
| options: ["yes", "some", "no"], | |
| required: true | |
| }, | |
| { | |
| id: "data_sources", | |
| label: "Where should we look for training data?", | |
| type: "multiselect", | |
| options: ["files", "database", "apis", "events", "logs", "auto"], | |
| required: true | |
| } | |
| ]; | |
| // Use dynamic questions if available, otherwise fall back to defaults | |
| const questions = questionnaireData?.questions || questionnaireData?.fields || defaultQuestions; | |
| const title = questionnaireData?.title || "AI Model Personalization Questionnaire"; | |
| const subtitle = questionnaireData?.subtitle || `${questions.length} questions to optimize your AI model`; | |
| const explanation = questionnaireData?.explanation || "Answer these questions to help us create the perfect AI solution for you."; | |
| const confidenceScore = questionnaireData?.confidence_score || 0.5; | |
| const [answers, setAnswers] = React.useState<Record<string, any>>({}); | |
| const [isLoading, setIsLoading] = React.useState(false); | |
| // Initialize answers once on open or when the questions array identity changes meaningfully | |
| React.useEffect(() => { | |
| if (!isOpen) return; | |
| if (questions.length === 0) return; | |
| // Only initialize if answers are empty (avoid loops) | |
| if (Object.keys(answers).length === 0) { | |
| const initial: Record<string, any> = {}; | |
| for (const q of questions) { | |
| initial[q.id] = q.type === 'multiselect' ? [] : ''; | |
| } | |
| setAnswers(initial); | |
| } | |
| }, [isOpen, questions, answers]); | |
| // Reset when a new questionnaire arrives while modal is open | |
| React.useEffect(() => { | |
| if (!isOpen) return; | |
| const newQs = questionnaireData?.questions || questionnaireData?.fields; | |
| if (newQs && newQs.length > 0) { | |
| const newIds = new Set(newQs.map(q => q.id)); | |
| const currentIds = new Set(Object.keys(answers)); | |
| let same = newIds.size === currentIds.size && [...newIds].every(id => currentIds.has(id)); | |
| if (!same) { | |
| const initial: Record<string, any> = {}; | |
| for (const q of newQs) initial[q.id] = q.type === 'multiselect' ? [] : ''; | |
| setAnswers(initial); | |
| } | |
| } | |
| }, [isOpen, questionnaireData]); | |
| if (!isOpen) return null; | |
| const handleSubmit = () => { | |
| // Validate required fields | |
| const missingRequired = questions.filter(q => | |
| q.required && (!answers[q.id] || (Array.isArray(answers[q.id]) && answers[q.id].length === 0)) | |
| ); | |
| if (missingRequired.length > 0) { | |
| alert(`Please fill in all required fields: ${missingRequired.map(q => q.label).join(', ')}`); | |
| return; | |
| } | |
| console.log('📋 Submitting questionnaire with answers:', answers); | |
| onSubmit(answers); | |
| }; | |
| const handleClose = () => { | |
| console.log('❌ Questionnaire closed by user'); | |
| onClose(); | |
| }; | |
| const updateAnswer = (questionId: string, value: any) => { | |
| setAnswers(prev => ({ | |
| ...prev, | |
| [questionId]: value | |
| })); | |
| }; | |
| return ( | |
| <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4"> | |
| <div className="bg-gray-900 border border-gray-800 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden"> | |
| {/* Header */} | |
| <div className="bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-cyan-600/20 border-b border-gray-800 p-6"> | |
| <div className="flex justify-between items-start"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="p-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg"> | |
| <Brain className="w-6 h-6 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent"> | |
| {title} | |
| </h2> | |
| <p className="text-gray-400 text-sm"> | |
| {subtitle} | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleClose} | |
| className="text-gray-400 hover:text-white transition-colors p-1 hover:bg-gray-800 rounded" | |
| > | |
| <X className="w-6 h-6" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="p-6 space-y-6 overflow-y-auto max-h-[calc(90vh-200px)]"> | |
| {/* Explanation */} | |
| {explanation && ( | |
| <div className="bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-500/20 rounded-lg p-4"> | |
| <div className="flex items-start space-x-3"> | |
| <TrendingUp className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" /> | |
| <div> | |
| <p className="text-gray-300 text-sm leading-relaxed"> | |
| {explanation} | |
| </p> | |
| {questionnaireData?.data_requirements && ( | |
| <div className="mt-3 p-3 bg-gray-800/50 rounded border border-gray-700"> | |
| <p className="text-xs text-gray-400 mb-2">📊 Data Requirements:</p> | |
| <div className="grid grid-cols-2 gap-4 text-xs"> | |
| <div> | |
| <span className="text-gray-500">Minimum samples:</span> | |
| <span className="text-blue-400 ml-2 font-medium"> | |
| {questionnaireData.data_requirements.minimum_samples?.toLocaleString()} | |
| </span> | |
| </div> | |
| <div> | |
| <span className="text-gray-500">Preferred samples:</span> | |
| <span className="text-green-400 ml-2 font-medium"> | |
| {questionnaireData.data_requirements.preferred_samples?.toLocaleString()} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center py-12"> | |
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> | |
| <span className="ml-3 text-gray-400">Loading intelligent questionnaire...</span> | |
| </div> | |
| ) : ( | |
| /* Dynamic Questions */ | |
| questions.map((question, index) => ( | |
| <div key={question.id} className="space-y-3 p-4 bg-gray-800/30 rounded-lg border border-gray-700/50 hover:border-gray-600/50 transition-colors"> | |
| <div className="flex items-start justify-between"> | |
| <label className="block text-white font-medium flex items-center space-x-2"> | |
| <span>{question.label}</span> | |
| {question.required && <span className="text-red-400">*</span>} | |
| {question.agent_source && ( | |
| <span className={`text-xs px-2 py-1 rounded-full ${ | |
| question.agent_source === 'dataset_architect' ? 'bg-blue-500/20 text-blue-300' : | |
| question.agent_source === 'zpe_analyzer' ? 'bg-purple-500/20 text-purple-300' : | |
| 'bg-gray-500/20 text-gray-300' | |
| }`}> | |
| {question.agent_source.replace('_', ' ')} | |
| </span> | |
| )} | |
| </label> | |
| {question.relevance && ( | |
| <span className="text-xs text-gray-500"> | |
| {(question.relevance * 100).toFixed(0)}% relevant | |
| </span> | |
| )} | |
| </div> | |
| {question.tooltip && ( | |
| <p className="text-gray-400 text-sm">{question.tooltip}</p> | |
| )} | |
| {/* Question Input */} | |
| {question.type === 'text' && ( | |
| <input | |
| type="text" | |
| value={answers[question.id] || ''} | |
| onChange={(e) => updateAnswer(question.id, e.target.value)} | |
| placeholder={question.placeholder} | |
| className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" | |
| /> | |
| )} | |
| {question.type === 'textarea' && ( | |
| <textarea | |
| value={answers[question.id] || ''} | |
| onChange={(e) => updateAnswer(question.id, e.target.value)} | |
| placeholder={question.placeholder} | |
| rows={4} | |
| className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none transition-all" | |
| /> | |
| )} | |
| {question.type === 'select' && question.options && ( | |
| <select | |
| value={answers[question.id] || ''} | |
| onChange={(e) => updateAnswer(question.id, e.target.value)} | |
| className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" | |
| > | |
| <option value="">Select an option...</option> | |
| {question.options.map((option, i) => ( | |
| <option key={i} value={option}> | |
| {option.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} | |
| </option> | |
| ))} | |
| </select> | |
| )} | |
| {question.type === 'multiselect' && question.options && ( | |
| <div className="space-y-2"> | |
| {question.options.map((option, i) => ( | |
| <label key={i} className="flex items-center space-x-3 cursor-pointer p-2 hover:bg-gray-800/50 rounded transition-colors"> | |
| <input | |
| type="checkbox" | |
| value={option} | |
| checked={(answers[question.id] || []).includes(option)} | |
| onChange={(e) => { | |
| const current = answers[question.id] || []; | |
| const newValue = e.target.checked | |
| ? [...current, option] | |
| : current.filter((item: string) => item !== option); | |
| updateAnswer(question.id, newValue); | |
| }} | |
| className="w-4 h-4 text-blue-600 bg-gray-800 border-gray-600 rounded focus:ring-blue-500" | |
| /> | |
| <span className="text-white"> | |
| {option.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} | |
| </span> | |
| </label> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| {/* Footer */} | |
| <div className="bg-gradient-to-r from-gray-900 to-gray-800 border-t border-gray-800 px-6 py-4"> | |
| <div className="flex justify-between items-center"> | |
| <div className="flex items-center space-x-4"> | |
| <div className="flex items-center space-x-2"> | |
| <Zap className="w-4 h-4 text-purple-400" /> | |
| <p className="text-xs text-gray-400"> | |
| AI-powered questionnaire with {(confidenceScore * 100).toFixed(0)}% confidence | |
| </p> | |
| </div> | |
| {questionnaireData?.ai_decision_trace && ( | |
| <div className="text-xs text-gray-500"> | |
| Powered by {questionnaireData.ai_decision_trace.agent_sources?.length || 0} AI agents | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex space-x-3"> | |
| <button | |
| onClick={handleClose} | |
| className="px-4 py-2 text-gray-400 hover:text-white hover:bg-gray-700 transition-all rounded-lg" | |
| > | |
| Skip for now | |
| </button> | |
| <button | |
| onClick={handleSubmit} | |
| disabled={isSubmitting} | |
| className="px-6 py-2 bg-gradient-to-r from-blue-600 via-purple-600 to-cyan-600 text-white rounded-lg font-medium hover:from-blue-700 hover:via-purple-700 hover:to-cyan-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center space-x-2" | |
| > | |
| {isSubmitting ? ( | |
| <> | |
| <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> | |
| <span>Processing...</span> | |
| </> | |
| ) : ( | |
| <> | |
| <Database className="w-4 h-4" /> | |
| <span>Generate AI Solution</span> | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default UnifiedQuestionnaire; | |