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;