File size: 6,766 Bytes
991ee9f
 
 
 
 
0e0410b
f79ab06
991ee9f
 
 
 
 
 
 
 
 
 
 
20f3b43
 
991ee9f
 
 
f1e1a93
991ee9f
 
 
396c1dd
991ee9f
f1e1a93
396c1dd
991ee9f
656ac31
f1e1a93
0e0410b
 
 
f1e1a93
ce7f322
 
 
 
 
396c1dd
 
 
 
 
 
f1e1a93
 
 
656ac31
396c1dd
 
f1e1a93
20f3b43
991ee9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20f3b43
991ee9f
20f3b43
991ee9f
20f3b43
991ee9f
 
 
 
 
 
 
20f3b43
991ee9f
 
 
 
656ac31
20f3b43
991ee9f
 
656ac31
991ee9f
 
20f3b43
991ee9f
 
 
 
 
 
 
 
 
 
 
20f3b43
991ee9f
 
 
 
 
 
 
 
 
 
 
 
20f3b43
991ee9f
 
 
 
 
 
 
20f3b43
f1e1a93
991ee9f
 
 
 
396c1dd
 
20f3b43
396c1dd
 
 
f1e1a93
 
 
 
 
 
991ee9f
 
 
 
20f3b43
991ee9f
20f3b43
 
 
991ee9f
 
 
 
 
 
 
 
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
'use client';

import { useState } from 'react';
import { MessageSquare, TrendingUp, TrendingDown, Minus, Sparkles } from 'lucide-react';

import { fetchJson } from '@/lib/http';

interface SentimentResult {
  label: 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
  score: number;
}

interface SentimentData {
  text: string;
  sentiment: SentimentResult;
  timestamp: string;
}

export default function SentimentAnalyzer({ market = 'bist' }: { market?: 'bist' | 'us' }) {
  const isUS = market === 'us';
  const [text, setText] = useState('');
  const [result, setResult] = useState<SentimentData | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const analyzeSentiment = async () => {
    if (!text.trim()) return;

    setLoading(true);
    setError(null);

    try {
      const data = await fetchJson<Record<string, unknown>>(
        `/api/sentiment`,
        { method: 'POST' },
        { timeoutMs: 20000, retries: 0, jsonBody: { text } }
      );

      if (data?.ok === false) {
        const message = [data.error, data.hint].filter((value) => typeof value === 'string' && value).join(' ');
        throw new Error(message || 'Invalid sentiment payload');
      }

      // No-toy: if backend returns no sentiment, we show no label.
      if (!data || (typeof data !== 'object')) {
        throw new Error('Invalid sentiment payload');
      }

      if (!('sentiment' in data)) {
        throw new Error('Invalid sentiment payload');
      }

      setResult(data as unknown as SentimentData);
    } catch (err) {
      console.error('Sentiment analysis failed:', err);
      setResult(null);
      setError(isUS ? 'Sentiment analysis service unreachable or model not ready. No result produced.' : 'Duygu analizi servisine ulaşılamadı veya model hazır değil. Bu durumda sonuç üretilmez.');
    } finally {
      setLoading(false);
    }
  };

  const getSentimentColor = (label: string) => {
    switch (label) {
      case 'POSITIVE':
        return 'text-green-600 bg-green-50 border-green-200';
      case 'NEGATIVE':
        return 'text-red-600 bg-red-50 border-red-200';
      default:
        return 'text-gray-600 bg-gray-50 border-gray-200';
    }
  };

  const getSentimentIcon = (label: string) => {
    switch (label) {
      case 'POSITIVE':
        return <TrendingUp className="w-5 h-5" />;
      case 'NEGATIVE':
        return <TrendingDown className="w-5 h-5" />;
      default:
        return <Minus className="w-5 h-5" />;
    }
  };

  const getSentimentLabel = (label: string) => {
    switch (label) {
      case 'POSITIVE':
        return isUS ? 'Positive' : 'Pozitif';
      case 'NEGATIVE':
        return isUS ? 'Negative' : 'Negatif';
      default:
        return isUS ? 'Neutral' : 'Nötr';
    }
  };

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex items-center gap-2 mb-6">
        <Sparkles className="w-5 h-5 text-indigo-600" />
        <h2 className="text-lg font-semibold">{isUS ? 'News Sentiment Analysis' : 'Haber Duygu Analizi'}</h2>
      </div>

      <div className="space-y-4">
        <div>
          <label htmlFor="sentiment-text" className="block text-sm font-medium text-gray-700 mb-2">
            {isUS ? 'News Text (English)' : 'Haber Metni (Türkçe)'}
          </label>
          <textarea
            id="sentiment-text"
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder={isUS ? 'Example: AAPL shares surged 5% today and hit a new all-time high...' : 'Örnek: THYAO hisseleri bugün %5 yükseliş gösterdi ve yeni rekor kırdı...'}
            className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none"
            rows={4}
          />
        </div>

        <button
          onClick={analyzeSentiment}
          disabled={loading || !text.trim()}
          className="w-full bg-indigo-600 text-white py-3 rounded-lg hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
        >
          <MessageSquare className="w-5 h-5" />
          {loading ? (isUS ? 'Analyzing...' : 'Analiz Ediliyor...') : (isUS ? 'Analyze Sentiment' : 'Duygu Analizi Yap')}
        </button>

        {result && (
          <div className={`mt-6 p-4 border-2 rounded-lg ${getSentimentColor(result.sentiment.label)}`}>
            <div className="flex items-center justify-between mb-4">
              <div className="flex items-center gap-2">
                {getSentimentIcon(result.sentiment.label)}
                <span className="text-xl font-bold">
                  {getSentimentLabel(result.sentiment.label)}
                </span>
              </div>
              <div className="text-right">
                <div className="text-sm opacity-75">{isUS ? 'Confidence' : 'Güven'}</div>
                <div className="text-2xl font-bold">
                  {Math.round(result.sentiment.score * 100)}%
                </div>
              </div>
            </div>

            <div className="mt-4 pt-4 border-t border-current border-opacity-20">
              <div className="text-sm opacity-75">{isUS ? 'Analyzed Text:' : 'Analiz Edilen Metin:'}</div>
              <div className="mt-2 text-sm italic">&quot;{result.text}&quot;</div>
            </div>
          </div>
        )}

        {result && !result.sentiment ? (
          <div className="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-900">
            {isUS ? 'No sentiment result could be produced for this text.' : 'Bu metin için sentiment sonucu üretilemedi.'}
          </div>
        ) : null}

        {error && (
          <div className="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-900">
            {error}
          </div>
        )}

        <div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
          <div className="flex items-start gap-2 text-sm text-blue-800">
            <MessageSquare className="w-4 h-4 flex-shrink-0 mt-0.5" />
            <div>
              <p className="font-semibold mb-1">{isUS ? 'Sentiment Analysis' : 'Duygu Analizi'}</p>
              <p className="text-xs text-blue-700">
                {isUS
                  ? 'This feature works best-effort with a real model. If the service is unreachable or the model cannot load, no result is returned.'
                  : 'Bu özellik gerçek bir model ile best-effort çalışır. Servis erişilemezse veya model yüklenemezse sonuç dönmez (toy sonuç üretilmez).'}
              </p>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}