golem-flask-backend / src /components /UnifiedQuestionnaire.tsx
mememechez's picture
Deploy final cleaned source code
ca28016
raw
history blame
15.1 kB
'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;