Repository navigation
import React, { useState, useEffect, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
// --- Icon SVGs ---
const ChevronDownIcon = () => (
);
const CheckCircleIcon = () => (
);
// --- Enhanced Initial Survey Data ---
const initialSurveyData = [
{
id: "cat0_overview", // New ID prefix for clarity
categoryName: "Project Overview & Context",
guidingQuestions: "Provide a general description of the AI project or system being assessed. What is its primary purpose and scope?",
qualitativeAnswer: "",
isOpen: true, // First category is open by default
kpis: [
// This section might be primarily for qualitative input, but example KPIs could be added if relevant to context setting
{ id: "kpi0_1", text: "Clarity of project goals and objectives.", score: 0, maxScore: 5 },
{ id: "kpi0_2", text: "Definition of target users and use cases.", score: 0, maxScore: 5 },
]
},
{
id: "cat_problem",
categoryName: "Problem Statement",
guidingQuestions: "What specific issue does the AI solve?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpiprob_1", text: "Problem alignment rating (internal/external validation).", score: 0, maxScore: 5 },
{ id: "kpiprob_2", text: "Quantifiable impact of solving the problem.", score: 0, maxScore: 5 }
]
},
{
id: "cat_whyai",
categoryName: "Why AI?",
guidingQuestions: "Why is AI chosen for this solution, and what type of AI is optimal (e.g., machine learning, rule-based, hybrid)?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpiwhy_1", text: "Fit assessment score for AI vs. alternative solutions.", score: 0, maxScore: 5 },
{ id: "kpiwhy_2", text: "Performance-to-objective alignment percentage for chosen AI type.", score: 0, maxScore: 5 }
]
},
{
id: "cat1_accountability", // Original cat1, updated
categoryName: "Accountability",
guidingQuestions: "Who is responsible for the AI system's decisions, data processes, development, deployment, and outcomes? Are roles and responsibilities clearly defined, documented, and communicated? Is there a mechanism for redress if things go wrong?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpi1_1", text: "Existence of a clear governance structure for AI, with defined roles and responsibilities.", score: 0, maxScore: 5 },
{ id: "kpi1_2", text: "Documented processes for AI system approval, monitoring, and decommissioning.", score: 0, maxScore: 5 },
{ id: "kpi1_3", text: "Availability of clear channels for stakeholders to raise concerns or seek redress regarding AI system behavior.", score: 0, maxScore: 5 },
{ id: "kpi1_4", text: "Regular training for relevant personnel on AI ethics, risks, and accountability.", score: 0, maxScore: 5 },
{ id: "kpiacc_1", text: "# of accountable roles mapped to specific AI lifecycle stages.", score: 0, maxScore: 5 },
{ id: "kpiacc_2", text: "Average response time to incidents or identified issues (hours/days).", score: 0, maxScore: 5 } // Note: for # KPIs, consider how to score. Here, lower is better, so scoring might be inverted or based on targets. For simplicity, we'll keep 1-5 scale representing adherence to target response times.
]
},
{
id: "cat_stakeholders",
categoryName: "Stakeholders",
guidingQuestions: "Who are the internal and external stakeholders and impact groups (including vulnerable groups)? How are they identified and engaged?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpistake_1", text: "Stakeholder satisfaction index (e.g., from surveys).", score: 0, maxScore: 5 },
{ id: "kpistake_2", text: "# of stakeholder engagement sessions conducted per development phase.", score: 0, maxScore: 5 },
{ id: "kpistake_3", text: "Representation of diverse stakeholder groups in consultations.", score: 0, maxScore: 5 }
]
},
{
id: "cat2_fairness_diversity", // Original cat2, updated
categoryName: "Fairness, Non-Discrimination & Diversity",
guidingQuestions: "How does the AI system avoid unfair bias and discrimination? What measures are in place to identify, mitigate, and monitor biases in data, algorithms, and outcomes? How are accessibility and diverse perspectives integrated?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpi2_1", text: "Bias detection and mitigation techniques are implemented throughout the AI lifecycle.", score: 0, maxScore: 5 },
{ id: "kpi2_2", text: "Regular testing for fairness across different demographic groups and sensitive attributes.", score: 0, maxScore: 5 },
{ id: "kpi2_3", text: "Data sources are assessed for representativeness and potential biases.", score: 0, maxScore: 5 },
{ id: "kpifair_1", text: "% bias detected/mitigated in pre-deployment testing.", score: 0, maxScore: 5 },
{ id: "kpifair_2", text: "Inclusivity score in UX audits (e.g., based on heuristics).", score: 0, maxScore: 5 },
{ id: "kpifair_3", text: "Accessibility conformance level (e.g., WCAG AA).", score: 0, maxScore: 5 },
{ id: "kpifair_4", text: "# of diversity inputs incorporated into design/development.", score: 0, maxScore: 5 },
{ id: "kpifair_5", text: "Stakeholder diversity index for feedback collection.", score: 0, maxScore: 5 }
]
},
{
id: "cat3_transparency_explainability", // Original cat3, updated
categoryName: "Transparency & Explainability",
guidingQuestions: "How transparent is the AI system's decision-making process? Can its operations, logic, and outcomes be understood and explained to relevant stakeholders? How is it documented?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpi3_1", text: "Comprehensive documentation on AI model design, data, and limitations is maintained and accessible.", score: 0, maxScore: 5 },
{ id: "kpi3_2", text: "Methods for explaining AI decisions (e.g., XAI tools, LIME, SHAP) are used and validated.", score: 0, maxScore: 5 },
{ id: "kpi3_3", text: "Users and affected individuals are clearly informed when interacting with or being assessed by an AI system.", score: 0, maxScore: 5 },
{ id: "kpitrans_1", text: "Explainability rating from user surveys.", score: 0, maxScore: 5 },
{ id: "kpitrans_2", text: "# of explanations delivered/accessed for significant decisions.", score: 0, maxScore: 5 }
]
},
{
id: "cat_communication",
categoryName: "Communication",
guidingQuestions: "How are the AI system's functions, capabilities, limitations, potential risks, and user rights communicated to stakeholders?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpicomm_1", text: "Average # of user queries on system function resolved per week/month.", score: 0, maxScore: 5 },
{ id: "kpicomm_2", text: "Clarity score of communication materials from user surveys.", score: 0, maxScore: 5 },
{ id: "kpicomm_3", text: "Accessibility of information about system risks and rights.", score: 0, maxScore: 5 }
]
},
{
id: "cat4_robustness_safety", // Original cat4, updated
categoryName: "Technical Robustness, Security & Safety",
guidingQuestions: "How is the AI system protected from security threats and designed to operate safely and reliably? What testing, fallback, and cybersecurity plans exist?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpi4_1", text: "Robust security measures (e.g., encryption, access control) are in place for AI models, data, and infrastructure.", score: 0, maxScore: 5 },
{ id: "kpi4_2", text: "Regular vulnerability assessments and penetration testing are conducted.", score: 0, maxScore: 5 },
{ id: "kpi4_3", text: "Fail-safe mechanisms, redundancy, and human oversight are implemented to prevent critical failures.", score: 0, maxScore: 5 },
{ id: "kpi4_4", text: "AI systems are tested for robustness against adversarial attacks and unexpected inputs.", score: 0, maxScore: 5 },
{ id: "kpirobust_1", text: "% system uptime / availability.", score: 0, maxScore: 5 },
{ id: "kpirobust_2", text: "% drop in performance during stress/edge case testing.", score: 0, maxScore: 5 }, // Lower is better, score reflects adherence to tolerance
{ id: "kpirobust_3", text: "# of successful fallbacks triggered in test/production environments.", score: 0, maxScore: 5 }
]
},
{
id: "cat5_privacy_governance", // Original cat5, updated
categoryName: "Privacy & Data Governance",
guidingQuestions: "How does the AI system respect and protect individual privacy? How is data governed, collected, used, shared, accessed, and managed for interoperability, in compliance with regulations and ethical standards?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpi5_1", text: "Data minimization principles are applied; only necessary data is collected and processed.", score: 0, maxScore: 5 },
{ id: "kpi5_2", text: "Privacy-enhancing technologies (e.g., anonymization, differential privacy, federated learning) are used where appropriate.", score: 0, maxScore: 5 },
{ id: "kpi5_3", text: "Clear policies and procedures for data consent, access, storage, retention, and deletion are established and followed.", score: 0, maxScore: 5 },
{ id: "kpi5_4", text: "Regular privacy impact assessments (PIAs) or DPIAs are conducted for AI systems.", score: 0, maxScore: 5 },
{ id: "kpipriv_1", text: "% of data subject access requests (DSARs) processed within regulatory timelines.", score: 0, maxScore: 5 },
{ id: "kpipriv_2", text: "# of confirmed data breaches or privacy incidents in the last 12 months.", score: 0, maxScore: 5 }, // Lower is better
{ id: "kpipriv_3", text: "Data sharing agreement compliance rate.", score: 0, maxScore: 5 }
]
},
{
id: "cat_human_oversight",
categoryName: "Human Agency and Oversight",
guidingQuestions: "Where is meaningful human oversight integrated into the AI system's lifecycle? How is human feedback collected, processed, and used for system improvement or intervention?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpihuman_1", text: "# of critical decision points with mandatory human review/override capability.", score: 0, maxScore: 5 },
{ id: "kpihuman_2", text: "% of automated decisions reviewed or audited by humans.", score: 0, maxScore: 5 },
{ id: "kpihuman_3", text: "Effectiveness of feedback mechanisms for human operators/users.", score: 0, maxScore: 5 }
]
},
{
id: "cat_risk_mitigation",
categoryName: "Risk Assessment and Mitigation",
guidingQuestions: "What are the identified risks (ethical, legal, societal, technical) associated with the AI system? What mitigation plans are in place, including redress mechanisms and audit procedures?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpirisk_1", text: "% of identified high-impact risks with documented mitigation plans.", score: 0, maxScore: 5 },
{ id: "kpirisk_2", text: "# of unresolved critical incidents past a defined resolution time.", score: 0, maxScore: 5 }, // Lower is better
{ id: "kpirisk_3", text: "Audit completion rate for planned internal/external audits.", score: 0, maxScore: 5 }
]
},
{
id: "cat_societal_environmental",
categoryName: "Societal and Environmental Well-being",
guidingQuestions: "What are the potential societal and environmental impacts (positive and negative) of the AI system? What measures are taken to promote positive impacts and mitigate negative ones?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpisocenv_1", text: "Estimated CO₂ emissions per model training run or inference query (awareness/reduction efforts).", score: 0, maxScore: 5 },
{ id: "kpisocenv_2", text: "Digital inclusion index or similar metric for accessibility to affected populations.", score: 0, maxScore: 5 },
{ id: "kpisocenv_3", text: "# of sustainability or positive societal impact actions implemented.", score: 0, maxScore: 5 }
]
},
{
id: "cat_legal_compliance",
categoryName: "Legal Compliance and Oversight",
guidingQuestions: "Which national and international laws, regulations, and standards apply to the AI system? How is compliance ensured, monitored, and documented?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpilegal_1", text: "# of internal compliance checks/audits performed annually.", score: 0, maxScore: 5 },
{ id: "kpilegal_2", text: "% conformity with applicable sectoral laws and regulations.", score: 0, maxScore: 5 },
{ id: "kpilegal_3", text: "Process for tracking and adapting to new legal requirements.", score: 0, maxScore: 5 }
]
},
{
id: "cat_eu_ai_act",
categoryName: "EU AI Act Considerations (if applicable)",
guidingQuestions: "If subject to the EU AI Act: What is the determined risk category of the AI system (unacceptable, high, limited, minimal)? How are the specific obligations for this category being met (e.g., conformity assessment, documentation, transparency)?",
qualitativeAnswer: "",
isOpen: false,
kpis: [
{ id: "kpieu_1", text: "# of technical documentation updates aligned with EU AI Act requirements.", score: 0, maxScore: 5 },
{ id: "kpieu_2", text: "Frequency of risk reclassification reviews.", score: 0, maxScore: 5 },
{ id: "kpieu_3", text: "Conformity check completion rate for high-risk systems (if applicable).", score: 0, maxScore: 5 }
]
}
];
// --- Helper Functions ---
const calculateCategoryScore = (kpis) => {
const scoredKpis = kpis.filter(kpi => kpi.score > 0);
if (scoredKpis.length === 0) return { score: 0, scoredKpisCount: 0, totalKpis: kpis.length };
const totalScore = scoredKpis.reduce((sum, kpi) => sum + kpi.score, 0);
const maxPossibleScore = scoredKpis.reduce((sum, kpi) => sum + kpi.maxScore, 0);
return {
score: maxPossibleScore > 0 ? (totalScore / maxPossibleScore) * 100 : 0,
scoredKpisCount: scoredKpis.length,
totalKpis: kpis.length
};
};
// --- Components ---
const ScoreInput = ({ kpiId, currentScore, onChange, maxScore }) => {
const scoreOptions = [{value: 0, label: "N/A"}];
for (let i = 1; i <= maxScore; i++) {
scoreOptions.push({value: i, label: i.toString()});
}
return (
Rate (1-5):
{scoreOptions.map(option => (
<input
type="radio"
name={
kpi-score-${kpiId}}value={option.value}
checked={currentScore === option.value}
onChange={(e) => onChange(parseInt(e.target.value))}
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
<span className={
text-sm ${currentScore === option.value ? 'font-bold text-blue-700' : 'text-gray-700'}}>{option.label}
))}
);
};
const KpiItem = ({ kpi, categoryId, onKpiScoreChange }) => {
return (
{kpi.text}
<ScoreInput
kpiId={kpi.id}
currentScore={kpi.score}
maxScore={kpi.maxScore}
onChange={(newScore) => onKpiScoreChange(categoryId, kpi.id, newScore)}
/>
);
};
const CategorySection = ({ category, onKpiScoreChange, onQualitativeAnswerChange, onToggleCategory }) => {
const categoryMetrics = useMemo(() => calculateCategoryScore(category.kpis), [category.kpis]);
const progress = categoryMetrics.totalKpis > 0 ? (categoryMetrics.scoredKpisCount / categoryMetrics.totalKpis) * 100 : 0;
return (
// Added 'category-section-print' for print styling
<button
onClick={() => onToggleCategory(category.id)}
className="w-full flex justify-between items-center p-4 bg-slate-100 hover:bg-slate-200 transition-colors duration-150 no-print" // Hide button in print
>
{category.categoryName}
{categoryMetrics.scoredKpisCount}/{categoryMetrics.totalKpis} KPIs scored
<ChevronDownIcon className={
w-5 h-5 text-slate-600 transform transition-transform duration-200 ${category.isOpen ? 'rotate-180' : ''}} />{/* Added 'category-content-print' for print styling to ensure it's displayed /}
<div className={
p-4 border-t border-gray-200 category-content-print ${category.isOpen ? '' : 'hidden-screen'}}> {/ hidden-screen to hide on screen if not open, print CSS will override */}Guiding Questions:
{category.guidingQuestions}
<div className="mb-6">
<label htmlFor={`qualitative-answer-${category.id}`} className="block text-sm font-medium text-gray-700 mb-1">
Your response to the guiding questions:
</label>
<textarea
id={`qualitative-answer-${category.id}`}
rows="4"
className="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
placeholder="Type your detailed answer here..."
value={category.qualitativeAnswer || ''}
onChange={(e) => onQualitativeAnswerChange(category.id, e.target.value)}
></textarea>
</div>
<h4 className="text-md font-semibold text-slate-700 mb-2">Key Performance Indicators (KPIs):</h4>
<div className="space-y-2">
{category.kpis.map(kpi => (
<KpiItem
key={kpi.id}
kpi={kpi}
categoryId={category.id}
onKpiScoreChange={onKpiScoreChange}
/>
))}
</div>
<div className="mt-4 no-print"> {/* Hide progress bar in print */}
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
></div>
</div>
<p className="text-xs text-right text-gray-500 mt-1">{Math.round(progress)}% complete (KPIs)</p>
</div>
</div>
</div>
);
};
const ResultsDashboard = ({ surveyData }) => {
const categoryScores = useMemo(() => {
return surveyData.map(cat => {
const { score, scoredKpisCount, totalKpis } = calculateCategoryScore(cat.kpis);
return {
name: cat.categoryName,
score: parseFloat(score.toFixed(2)),
completed: ${scoredKpisCount}/${totalKpis}
};
});
}, [surveyData]);
const overallScoreData = useMemo(() => {
const allKpis = surveyData.flatMap(cat => cat.kpis);
const scoredKpis = allKpis.filter(kpi => kpi.score > 0);
if (scoredKpis.length === 0) return [{ name: 'Overall', value: 0 }];
const totalScoreSum = scoredKpis.reduce((sum, kpi) => sum + kpi.score, 0);
const maxPossibleScoreSum = scoredKpis.reduce((sum, kpi) => sum + kpi.maxScore, 0);
const overallPercentage = maxPossibleScoreSum > 0 ? (totalScoreSum / maxPossibleScoreSum) * 100 : 0;
return [
{ name: 'Achieved', value: parseFloat(overallPercentage.toFixed(2)) },
{ name: 'Remaining', value: parseFloat((100 - overallPercentage).toFixed(2)) }
];
}, [surveyData]);
const overallScoreValue = overallScoreData[0].value;
const totalScoredKpis = surveyData.reduce((sum, cat) => sum + cat.kpis.filter(kpi => kpi.score > 0).length, 0);
const totalKpisOverall = surveyData.reduce((sum, cat) => sum + cat.kpis.length, 0);
// Extended COLORS array for more categories
const COLORS = [
'#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#A4DE6C',
'#D0ED57', '#FFC658', '#82CAFF', '#FF69B4', '#20B2AA', '#FFD700',
'#7B68EE', '#FF4500', '#ADFF2F', '#4682B4'
];
const PIE_COLORS = ['#0088FE', '#E0E0E0'];
if (!surveyData || surveyData.length === 0) {
return
}
return (
// Added 'no-print' to hide the entire dashboard when printing
Survey Results (KPIs)
<div className="mb-8 p-6 bg-white rounded-lg shadow-md text-center">
<h3 className="text-lg font-semibold text-slate-700 mb-2">Overall KPI Progress</h3>
<ResponsiveContainer width="100%" height={150}>
<PieChart>
<Pie
data={overallScoreData}
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={60}
fill="#8884d8"
paddingAngle={2}
dataKey="value"
>
{overallScoreData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value) => `${value}%`} />
<text x="50%" y="50%" textAnchor="middle" dominantBaseline="middle" className="text-2xl font-bold fill-slate-700">
{`${overallScoreValue}%`}
</text>
</PieChart>
</ResponsiveContainer>
<p className="text-sm text-slate-600 mt-2">{totalScoredKpis} of {totalKpisOverall} KPIs scored.</p>
{overallScoreValue === 100 && totalScoredKpis === totalKpisOverall && (
<div className="mt-3 flex items-center justify-center text-green-600">
<CheckCircleIcon />
<span className="ml-2 font-semibold">KPI Assessment Complete!</span>
</div>
)}
</div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-slate-700 mb-4">KPI Scores by Category (%)</h3>
{categoryScores.filter(cat => cat.score > 0 || cat.completed !== `0/${initialSurveyData.find(d => d.categoryName === cat.name)?.kpis.length || 0}`).length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={categoryScores} margin={{ top: 5, right: 0, left: -20, bottom: 90 }}> {/* Increased bottom margin for long labels */}
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="name"
angle={-45} // Adjusted angle for better readability
textAnchor="end"
interval={0}
tick={{ fontSize: 10, fill: '#4A5568' }} // Slightly smaller font for XAxis
dy={5} // Adjust vertical position of ticks
/>
<YAxis domain={[0, 100]} tick={{ fontSize: 12, fill: '#4A5568' }} label={{ value: 'Score (%)', angle: -90, position: 'insideLeft', offset:-10, fontSize: 12, fill: '#4A5568' }} />
<Tooltip formatter={(value) => `${value}%`} />
<Legend wrapperStyle={{ fontSize: '12px', paddingTop: '20px' }} />
<Bar dataKey="score" name="Category Score" fill="#3B82F6" barSize={20} radius={[4, 4, 0, 0]}> {/* Adjusted barSize */}
{categoryScores.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<p className="text-sm text-slate-500 text-center py-8">Start scoring KPIs to see category results here.</p>
)}
</div>
<button
onClick={() => window.print()}
className="w-full mt-6 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
>
Print / Save as PDF
</button>
</div>
);
};
// Main App Component
function App() {
const [surveyData, setSurveyData] = useState(() => {
const savedData = localStorage.getItem('responsibleAiSurveyData');
if (savedData) {
const parsedData = JSON.parse(savedData);
// Ensure all fields are present, especially for data saved before new categories were added
return initialSurveyData.map(initialCat => {
const savedCat = parsedData.find(sc => sc.id === initialCat.id);
if (savedCat) {
return {
...initialCat, // Start with the latest structure
...savedCat, // Override with saved values
qualitativeAnswer: savedCat.qualitativeAnswer || "",
isOpen: savedCat.isOpen !== undefined ? savedCat.isOpen : (initialCat.id === initialSurveyData[0].id), // Maintain saved open state or default
kpis: initialCat.kpis.map(initialKpi => { // Ensure KPIs also merge correctly
const savedKpi = savedCat.kpis?.find(sk => sk.id === initialKpi.id);
return savedKpi ? {...initialKpi, ...savedKpi} : initialKpi;
})
};
}
return {...initialCat, isOpen: initialCat.id === initialSurveyData[0].id }; // For new categories not in saved data
});
}
return initialSurveyData.map((cat) => ({...cat})); // Ensure a deep copy and correct isOpen initialization
});
useEffect(() => {
localStorage.setItem('responsibleAiSurveyData', JSON.stringify(surveyData));
}, [surveyData]);
const handleKpiScoreChange = (categoryId, kpiId, newScore) => {
setSurveyData(prevData =>
prevData.map(category =>
category.id === categoryId
? {
...category,
kpis: category.kpis.map(kpi =>
kpi.id === kpiId ? { ...kpi, score: newScore } : kpi
),
}
: category
)
);
};
const handleQualitativeAnswerChange = (categoryId, newAnswer) => {
setSurveyData(prevData =>
prevData.map(category =>
category.id === categoryId
? { ...category, qualitativeAnswer: newAnswer }
: category
)
);
};
const handleToggleCategory = (categoryId) => {
setSurveyData(prevData =>
prevData.map(category =>
category.id === categoryId
? { ...category, isOpen: !category.isOpen }
: category
)
);
};
const handleResetSurvey = () => {
if (window.confirm("Are you sure you want to reset all scores and qualitative answers? This action cannot be undone.")) {
// Reset to a fresh copy of initialSurveyData, ensuring isOpen is correctly set for the first item
const resetData = initialSurveyData.map((cat, index) => ({
...cat,
kpis: cat.kpis.map(kpi => ({...kpi, score: 0})),
qualitativeAnswer: "",
isOpen: index === 0
}));
setSurveyData(resetData);
localStorage.removeItem('responsibleAiSurveyData');
}
};
return (
{/* Print-specific styles */}
<style jsx global>{
@media print { body { font-size: 10pt; /* Adjust base font size for print */ } .no-print { display: none !important; } .printable-area { /* This class should wrap the content you want to print */ display: block !important; width: 100% !important; position: static !important; /* Override sticky positioning if any */ } .category-section-print { page-break-inside: avoid; border: 1px solid #ccc !important; margin-bottom: 15px !important; box-shadow: none !important; } .category-section-print .category-content-print { display: block !important; /* Force display of category content */ height: auto !important; overflow: visible !important; border-top: 1px solid #eee !important; } .category-section-print .hidden-screen { /* Ensure content hidden on screen is visible for print */ display: block !important; } .category-section-print h3 { font-size: 12pt !important; } .category-section-print h4 { font-size: 11pt !important; } .category-section-print p, .category-section-print label, .category-section-print span, .category-section-print div { font-size: 9pt !important; } textarea { border: 1px solid #aaa !important; background-color: #fff !important; -webkit-print-color-adjust: exact !important; color-adjust: exact !important; min-height: 80px !important; /* Ensure textareas have some height */ width: 98% !important; } /* Attempt to make radio buttons more visible, though browser support for styling these in print is limited */ input[type="radio"] { border: 1px solid #666 !important; margin-right: 5px !important; vertical-align: middle !important; } input[type="radio"] + span { vertical-align: middle !important; } input[type="radio"]:checked { /* Basic visual cue for checked state if possible */ background-color: #ccc !important; /* May not render in all browsers */ outline: 1px solid black !important; } /* Hide Tailwind responsive prefixes in print if they cause issues, though generally they shouldn't */ .lg\\:w-3\\/5, .lg\\:w-2\\/5 { width: 100% !important; /* Make survey content take full width in print */ } } /* Class to hide content on screen if not open, but allow print CSS to override */ .hidden-screen { display: none; }}</style>
<header className="bg-slate-800 text-white p-6 shadow-md no-print"> {/* no-print class added */}
<div className="container mx-auto max-w-7xl">
<h1 className="text-3xl font-bold">Responsible AI Self-Assessment Tool</h1>
<p className="text-sm text-slate-300 mt-1">Evaluate your AI systems against key principles of responsible AI, providing both qualitative responses and KPI scores.</p>
</div>
</header>
{/* Added 'printable-area' to main content for better print control */}
<main className="container mx-auto max-w-7xl p-4 md:p-6 printable-area">
<div className="flex justify-end mb-6 no-print"> {/* no-print class added */}
<button
onClick={handleResetSurvey}
className="bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-md transition duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
>
Reset Survey
</button>
</div>
<div className="flex flex-col lg:flex-row gap-6">
{/* Survey Section */}
<div className="lg:w-full space-y-4"> {/* Changed from lg:w-3/5 to lg:w-full for print layout */}
{surveyData.map(category => (
<CategorySection
key={category.id}
category={category}
onKpiScoreChange={handleKpiScoreChange}
onQualitativeAnswerChange={handleQualitativeAnswerChange}
onToggleCategory={handleToggleCategory}
/>
))}
</div>
{/* Results Dashboard Section - already has no-print via its own component */}
<div className="lg:w-2/5"> {/* This will be hidden by no-print on ResultsDashboard itself */}
<ResultsDashboard surveyData={surveyData} />
</div>
</div>
</main>
<footer className="text-center p-6 text-sm text-slate-600 bg-slate-200 mt-12 no-print"> {/* no-print class added */}
<p className="mb-1">REV 1.0</p>
<p className="mb-1">Author: Fabrizio Degni</p>
<p className="mb-2 italic">Disclaimer: Do not add personal or sensitive information. Use this tool for preliminary scoring and note that it is a work-in-progress application.</p>
<p>© {new Date().getFullYear()} Responsible AI Assessment. For illustrative purposes.</p>
</footer>
</div>
);
}
export default App;