Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions nepa-frontend/src/components/ClassificationResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useState, useEffect } from 'react';
import { announceToScreenReader } from '../utils/accessibility';

interface ClassificationResult {
id: string;
category: string;
confidence: number;
description: string;
timestamp: Date;
}

interface ClassificationResultProps {
results: ClassificationResult[];
loading: boolean;
error: string | null;
}

const ClassificationResult: React.FC<ClassificationResultProps> = ({
results,
loading,
error
}) => {
const [previousResultsCount, setPreviousResultsCount] = useState(0);

useEffect(() => {
if (results.length !== previousResultsCount) {
const newResultsCount = results.length - previousResultsCount;
if (newResultsCount > 0) {
announceToScreenReader(
`New classification results loaded. ${newResultsCount} new result${newResultsCount > 1 ? 's' : ''} available.`,
'polite'
);
}
setPreviousResultsCount(results.length);
}
}, [results.length, previousResultsCount]);

useEffect(() => {
if (error) {
announceToScreenReader(`Error loading classification results: ${error}`, 'assertive');
}
}, [error]);

useEffect(() => {
if (loading) {
announceToScreenReader('Loading classification results...', 'polite');
} else {
announceToScreenReader('Classification results loaded.', 'polite');
}
}, [loading]);

if (loading) {
return (
<div className="classification-results" aria-live="polite" aria-atomic="true">
<div className="loading" role="status" aria-label="Loading classification results">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-center mt-2">Loading classification results...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="classification-results" aria-live="assertive" aria-atomic="true">
<div className="error" role="alert" aria-label={`Error: ${error}`}>
<p className="text-red-500 text-center">Error: {error}</p>
</div>
</div>
);
}

return (
<div className="classification-results">
<h2 className="text-2xl font-bold mb-4">Classification Results</h2>

{/* Live region for dynamic updates */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{results.length > 0
? `${results.length} classification result${results.length > 1 ? 's' : ''} displayed.`
: 'No classification results available.'
}
</div>

{results.length === 0 ? (
<p className="text-center text-muted-foreground">No classification results available.</p>
) : (
<div className="space-y-4">
{results.map((result) => (
<article
key={result.id}
className="border border-border rounded-lg p-4 bg-card"
aria-labelledby={`result-${result.id}-category`}
aria-describedby={`result-${result.id}-description`}
>
<header className="flex justify-between items-start mb-2">
<h3
id={`result-${result.id}-category`}
className="text-lg font-semibold text-card-foreground"
>
{result.category}
</h3>
<span
className="text-sm text-muted-foreground"
aria-label={`Confidence: ${Math.round(result.confidence * 100)}%`}
>
{Math.round(result.confidence * 100)}% confidence
</span>
</header>

<p
id={`result-${result.id}-description`}
className="text-muted-foreground mb-2"
>
{result.description}
</p>

<time
className="text-xs text-muted-foreground"
dateTime={result.timestamp.toISOString()}
aria-label={`Classified on ${result.timestamp.toLocaleString()}`}
>
{result.timestamp.toLocaleString()}
</time>
</article>
))}
</div>
)}
</div>
);
};

export default ClassificationResult;
156 changes: 156 additions & 0 deletions nepa-frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react';
import ClassificationResult from '../components/ClassificationResult';
import { announceToScreenReader } from '../utils/accessibility';

interface ClassificationResultData {
id: string;
category: string;
confidence: number;
description: string;
timestamp: Date;
}

const IndexPage: React.FC = () => {
const [results, setResults] = useState<ClassificationResultData[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [inputText, setInputText] = useState('');

const handleClassify = async () => {
if (!inputText.trim()) {
setError('Please enter text to classify');
announceToScreenReader('Please enter text to classify', 'assertive');
return;
}

setLoading(true);
setError(null);

try {
// Simulate API call to classification service
const response = await fetch('/api/classify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: inputText }),
});

if (!response.ok) {
throw new Error('Failed to classify text');
}

const data = await response.json();

const newResult: ClassificationResultData = {
id: Date.now().toString(),
category: data.category || 'Unknown',
confidence: data.confidence || 0,
description: data.description || 'Classification completed',
timestamp: new Date(),
};

setResults(prev => [newResult, ...prev]);
setInputText('');

// Announce the new result
announceToScreenReader(
`New classification result: ${newResult.category} with ${Math.round(newResult.confidence * 100)}% confidence.`,
'polite'
);

} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
setError(errorMessage);
announceToScreenReader(`Classification failed: ${errorMessage}`, 'assertive');
} finally {
setLoading(false);
}
};

const handleClearResults = () => {
setResults([]);
announceToScreenReader('All classification results cleared', 'polite');
};

return (
<div className="min-h-screen bg-background text-foreground p-4">
<header className="mb-8">
<h1 className="text-3xl font-bold text-center mb-2">NEPA Classification Tool</h1>
<p className="text-center text-muted-foreground">
Enter text to get AI-powered classification results
</p>
</header>

<main className="max-w-4xl mx-auto">
<section
className="mb-8"
aria-labelledby="input-section"
>
<h2 id="input-section" className="sr-only">Text Input Section</h2>

<div className="bg-card border border-border rounded-lg p-6 shadow">
<label
htmlFor="classification-input"
className="block text-lg font-semibold mb-4"
>
Enter Text to Classify
</label>

<textarea
id="classification-input"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Type or paste text here for classification..."
className="w-full h-32 p-3 border border-border rounded-md bg-background text-foreground resize-none focus:ring-2 focus:ring-ring focus:border-transparent"
aria-describedby="input-help"
/>

<p id="input-help" className="text-sm text-muted-foreground mt-2">
Enter any text you want to classify. The AI will analyze it and provide a category with confidence score.
</p>

<div className="flex gap-4 mt-4">
<button
onClick={handleClassify}
disabled={loading}
className="px-6 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
aria-describedby="classify-button-help"
>
{loading ? 'Classifying...' : 'Classify Text'}
</button>

{results.length > 0 && (
<button
onClick={handleClearResults}
className="px-6 py-2 bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/90 transition-colors"
aria-describedby="clear-button-help"
>
Clear Results
</button>
)}
</div>

<div className="sr-only" id="classify-button-help">
Click to send the text for classification analysis
</div>
<div className="sr-only" id="clear-button-help">
Click to remove all classification results from the display
</div>
</div>
</section>

<section aria-labelledby="results-section">
<h2 id="results-section" className="sr-only">Classification Results</h2>
<ClassificationResult
results={results}
loading={loading}
error={error}
/>
</section>
</main>
</div>
);
};

export default IndexPage;