Download client/src/components/ScenarioComponent.tsx from Soha85/RAI: direct link, hf CLI and curl.
- Browser
- Download file 9.21 kB
-
https://huggingface.co/spaces/Soha85/RAI/resolve/main/client/src/components/ScenarioComponent.tsx
- Command line
-
hf download hf://spaces/Soha85/RAI/client/src/components/ScenarioComponent.tsx
-
curl -L -o ScenarioComponent.tsx https://huggingface.co/spaces/Soha85/RAI/resolve/main/client/src/components/ScenarioComponent.tsx
9.21 kB
| import { useState } from 'react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Card } from '@/components/ui/card'; | |
| import { Progress } from '@/components/ui/progress'; | |
| import { CheckCircle, XCircle, AlertCircle, ChevronRight, BookOpen } from 'lucide-react'; | |
| import { Scenario, DecisionPoint, ScenarioOption } from '@/data/scenariosData'; | |
| interface ScenarioComponentProps { | |
| scenario: Scenario; | |
| onComplete: (score: number) => void; | |
| } | |
| export function ScenarioComponent({ scenario, onComplete }: ScenarioComponentProps) { | |
| const [currentDecisionIndex, setCurrentDecisionIndex] = useState(0); | |
| const [selectedOption, setSelectedOption] = useState<string | null>(null); | |
| const [answered, setAnswered] = useState(false); | |
| const [score, setScore] = useState(0); | |
| const [decisions, setDecisions] = useState<Record<string, string>>({}); | |
| const currentDecision = scenario.decisionPoints[currentDecisionIndex]; | |
| const progress = ((currentDecisionIndex + 1) / scenario.decisionPoints.length) * 100; | |
| const handleSelectOption = (optionId: string) => { | |
| if (!answered) { | |
| setSelectedOption(optionId); | |
| } | |
| }; | |
| const handleSubmitDecision = () => { | |
| if (!selectedOption) return; | |
| const option = currentDecision.options.find(o => o.id === selectedOption); | |
| if (option?.isCorrect) { | |
| setScore(score + 1); | |
| } | |
| setDecisions({ | |
| ...decisions, | |
| [currentDecision.id]: selectedOption | |
| }); | |
| setAnswered(true); | |
| }; | |
| const handleNext = () => { | |
| if (currentDecisionIndex < scenario.decisionPoints.length - 1) { | |
| setCurrentDecisionIndex(currentDecisionIndex + 1); | |
| setSelectedOption(null); | |
| setAnswered(false); | |
| } else { | |
| // Count the last decision correctly | |
| const lastDecisionCorrect = selectedOptionData?.isCorrect ? 1 : 0; | |
| const finalScore = Math.round(((score + lastDecisionCorrect) / scenario.decisionPoints.length) * 100); | |
| onComplete(finalScore); | |
| } | |
| }; | |
| const selectedOptionData = currentDecision.options.find(o => o.id === selectedOption); | |
| const correctOption = currentDecision.options.find(o => o.isCorrect); | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Progress */} | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-sm font-medium text-slate-600"> | |
| Decision {currentDecisionIndex + 1} of {scenario.decisionPoints.length} | |
| </span> | |
| <span className="text-sm font-medium text-blue-600"> | |
| {Math.round(progress)}% | |
| </span> | |
| </div> | |
| <Progress value={progress} className="h-2" /> | |
| </div> | |
| {/* Scenario Context */} | |
| <Card className="p-6 bg-gradient-to-br from-blue-50 to-teal-50 border-blue-200 space-y-3"> | |
| <div className="flex items-start gap-3"> | |
| <BookOpen className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" /> | |
| <div> | |
| <h3 className="font-semibold text-slate-900 mb-1">{scenario.title}</h3> | |
| <p className="text-sm text-slate-700">{currentDecision.context}</p> | |
| </div> | |
| </div> | |
| </Card> | |
| {/* Decision Point */} | |
| <Card className="p-8 space-y-6 border-slate-200"> | |
| {/* Decision Header */} | |
| <div className="space-y-3"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {currentDecision.relatedPillars.map(pillar => ( | |
| <span key={pillar} className="px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs font-semibold"> | |
| {pillar} | |
| </span> | |
| ))} | |
| </div> | |
| <h2 className="text-2xl font-bold text-slate-900"> | |
| {currentDecision.question} | |
| </h2> | |
| </div> | |
| {/* Options */} | |
| <div className="space-y-3"> | |
| {currentDecision.options.map((option) => { | |
| const isSelected = selectedOption === option.id; | |
| const isCorrect = option.isCorrect; | |
| const showResult = answered && (isSelected || isCorrect); | |
| return ( | |
| <button | |
| key={option.id} | |
| onClick={() => handleSelectOption(option.id)} | |
| disabled={answered} | |
| className={`w-full text-left p-4 rounded-lg border-2 transition-all ${ | |
| isSelected && !answered | |
| ? 'border-blue-500 bg-blue-50' | |
| : answered && isCorrect | |
| ? 'border-green-500 bg-green-50' | |
| : answered && isSelected && !isCorrect | |
| ? 'border-red-500 bg-red-50' | |
| : 'border-slate-200 hover:border-slate-300 hover:bg-slate-50' | |
| } ${answered ? 'cursor-default' : 'cursor-pointer'}`} | |
| > | |
| <div className="flex items-start gap-3"> | |
| <div className={`flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center mt-0.5 ${ | |
| isSelected && !answered | |
| ? 'border-blue-500 bg-blue-500' | |
| : answered && isCorrect | |
| ? 'border-green-500 bg-green-500' | |
| : answered && isSelected && !isCorrect | |
| ? 'border-red-500 bg-red-500' | |
| : 'border-slate-300' | |
| }`}> | |
| {showResult && ( | |
| isCorrect ? ( | |
| <CheckCircle className="w-5 h-5 text-white" /> | |
| ) : ( | |
| <XCircle className="w-5 h-5 text-white" /> | |
| ) | |
| )} | |
| </div> | |
| <div className="flex-1"> | |
| <p className={`font-medium ${ | |
| isSelected && !answered | |
| ? 'text-blue-900' | |
| : answered && isCorrect | |
| ? 'text-green-900' | |
| : answered && isSelected && !isCorrect | |
| ? 'text-red-900' | |
| : 'text-slate-900' | |
| }`}> | |
| {option.text} | |
| </p> | |
| {option.riskLevel && answered && ( | |
| <div className="flex items-center gap-2 mt-2"> | |
| <AlertCircle className="w-4 h-4 text-amber-600" /> | |
| <span className={`text-xs font-semibold capitalize ${ | |
| option.riskLevel === 'critical' ? 'text-red-700' : | |
| option.riskLevel === 'high' ? 'text-orange-700' : | |
| option.riskLevel === 'medium' ? 'text-amber-700' : | |
| 'text-green-700' | |
| }`}> | |
| Risk: {option.riskLevel} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {/* Explanation */} | |
| {answered && selectedOptionData && ( | |
| <div className={`p-4 rounded-lg ${ | |
| selectedOptionData.isCorrect | |
| ? 'bg-green-50 border border-green-200' | |
| : 'bg-amber-50 border border-amber-200' | |
| }`}> | |
| <p className={`text-sm font-semibold mb-2 ${ | |
| selectedOptionData.isCorrect | |
| ? 'text-green-900' | |
| : 'text-amber-900' | |
| }`}> | |
| {selectedOptionData.isCorrect ? '✓ Optimal Decision' : 'Why This Isn\'t Optimal:'} | |
| </p> | |
| <p className={`text-sm mb-3 ${ | |
| selectedOptionData.isCorrect | |
| ? 'text-green-800' | |
| : 'text-amber-800' | |
| }`}> | |
| {selectedOptionData.explanation} | |
| </p> | |
| {!selectedOptionData.isCorrect && correctOption && ( | |
| <div className="mt-3 pt-3 border-t border-amber-200"> | |
| <p className="text-sm font-semibold text-green-900 mb-1">Better Approach:</p> | |
| <p className="text-sm text-green-800">{correctOption.explanation}</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Action Buttons */} | |
| <div className="flex gap-3 pt-4"> | |
| {!answered ? ( | |
| <Button | |
| onClick={handleSubmitDecision} | |
| disabled={selectedOption === null} | |
| className="flex-1" | |
| > | |
| Submit Decision | |
| </Button> | |
| ) : ( | |
| <Button | |
| onClick={handleNext} | |
| className="flex-1 gap-2" | |
| > | |
| {currentDecisionIndex < scenario.decisionPoints.length - 1 ? 'Next Decision' : 'Complete Scenario'} | |
| <ChevronRight size={18} /> | |
| </Button> | |
| )} | |
| </div> | |
| </Card> | |
| {/* Score Summary */} | |
| {answered && ( | |
| <div className="bg-gradient-to-r from-blue-50 to-teal-50 rounded-lg p-4 border border-blue-200"> | |
| <p className="text-sm text-slate-600"> | |
| Decisions Made: <span className="font-bold text-blue-600">{currentDecisionIndex + 1} / {scenario.decisionPoints.length}</span> | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |