Spaces:
Sleeping
Sleeping
File size: 7,233 Bytes
2857363 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
import { useState, useCallback, useEffect } from 'react';
import { Send, Loader2, Trash2, Sparkles } from 'lucide-react';
import { useSentimentAnalysis } from '../hooks/useSentimentAnalysis';
import { validateText, MAX_TEXT_LENGTH } from '../utils/validators';
import ResultCard from './ResultCard';
const SAMPLE_TEXTS = {
positive: "I absolutely love this product! It exceeded all my expectations and the customer service was fantastic. Highly recommend to everyone!",
negative: "This is terrible. I'm extremely disappointed with the quality and service. Complete waste of money and time. Do not buy!",
};
interface SentimentAnalyzerProps {
onAnalysisComplete?: () => void;
}
const SentimentAnalyzer = ({ onAnalysisComplete }: SentimentAnalyzerProps) => {
const [text, setText] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const { result, isLoading, error, analyze, reset } = useSentimentAnalysis();
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newText = e.target.value;
setText(newText);
if (newText.trim()) {
const validation = validateText(newText);
setValidationError(validation.error);
} else {
setValidationError(null);
}
};
const handleSubmit = useCallback(async (e?: React.FormEvent) => {
e?.preventDefault();
const validation = validateText(text);
if (!validation.isValid) {
setValidationError(validation.error);
return;
}
await analyze(text.trim());
onAnalysisComplete?.();
}, [text, analyze, onAnalysisComplete]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
handleSubmit();
}
};
const handleClear = () => {
setText('');
setValidationError(null);
reset();
};
const handleSampleText = (type: 'positive' | 'negative') => {
setText(SAMPLE_TEXTS[type]);
setValidationError(null);
reset();
};
// Scroll to result when analysis completes
useEffect(() => {
if (result) {
document.getElementById('result-section')?.scrollIntoView({ behavior: 'smooth' });
}
}, [result]);
const isValid = text.trim().length > 0 && !validationError;
const charCount = text.length;
const charCountClass = charCount > MAX_TEXT_LENGTH
? 'text-red-500'
: charCount > MAX_TEXT_LENGTH * 0.9
? 'text-yellow-500'
: 'text-gray-400';
return (
<div className="space-y-6">
{/* Hero Section */}
<div className="text-center mb-8">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">
Analyze Text Sentiment in Real-Time
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Powered by DistilBERT transformer model with 99%+ accuracy.
Experience lightning-fast results with Redis caching.
</p>
</div>
{/* Input Section */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="relative">
<textarea
value={text}
onChange={handleTextChange}
onKeyDown={handleKeyDown}
placeholder="Type or paste your text here to analyze sentiment..."
className={`w-full min-h-[150px] max-h-[400px] p-4 text-gray-900 bg-white border-2 rounded-xl resize-y focus:outline-none focus:ring-2 transition-all ${
validationError
? 'border-red-300 focus:ring-red-200 focus:border-red-400'
: 'border-gray-200 focus:ring-blue-200 focus:border-blue-400'
}`}
disabled={isLoading}
aria-label="Text input for sentiment analysis"
aria-invalid={!!validationError}
aria-describedby={validationError ? 'validation-error' : undefined}
/>
<div className="absolute bottom-3 right-3 flex items-center gap-2">
<span className={`text-sm font-mono ${charCountClass}`}>
{charCount}/{MAX_TEXT_LENGTH}
</span>
</div>
</div>
{/* Validation Error */}
{validationError && (
<p id="validation-error" className="text-sm text-red-500 animate-fade-in">
{validationError}
</p>
)}
{/* API Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg animate-fade-in">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
{/* Sample Text Buttons */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-gray-500">Try examples:</span>
<button
type="button"
onClick={() => handleSampleText('positive')}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-green-700 bg-green-50 hover:bg-green-100 rounded-lg transition-colors"
disabled={isLoading}
>
<Sparkles className="w-3 h-3" />
Positive
</button>
<button
type="button"
onClick={() => handleSampleText('negative')}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-red-700 bg-red-50 hover:bg-red-100 rounded-lg transition-colors"
disabled={isLoading}
>
<Sparkles className="w-3 h-3" />
Negative
</button>
{text && (
<button
type="button"
onClick={handleClear}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
disabled={isLoading}
>
<Trash2 className="w-3 h-3" />
Clear
</button>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={!isValid || isLoading}
className={`w-full flex items-center justify-center gap-2 px-6 py-4 text-lg font-semibold rounded-xl transition-all ${
isValid && !isLoading
? 'bg-gradient-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Analyzing...
</>
) : (
<>
<Send className="w-5 h-5" />
Analyze Sentiment
</>
)}
</button>
<p className="text-center text-xs text-gray-400">
Press <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-600">Ctrl</kbd> + <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-600">Enter</kbd> to analyze
</p>
</form>
{/* Result Section */}
{result && (
<div id="result-section" className="pt-4">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Result</h3>
<ResultCard result={result} />
</div>
)}
</div>
);
};
export default SentimentAnalyzer;
|