| '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() { |
| 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 || (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('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 'Pozitif'; |
| case 'NEGATIVE': |
| return 'Negatif'; |
| default: |
| return '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">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"> |
| Haber Metni (Türkçe) |
| </label> |
| <textarea |
| id="sentiment-text" |
| value={text} |
| onChange={(e) => setText(e.target.value)} |
| placeholder="Ö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 ? 'Analiz Ediliyor...' : '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">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">Analiz Edilen Metin:</div> |
| <div className="mt-2 text-sm italic">"{result.text}"</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"> |
| 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">Duygu Analizi</p> |
| <p className="text-xs text-blue-700"> |
| 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> |
| ); |
| } |
|
|