Add backend, frontend, and project files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
92022903ad
commit
2533f75f2c
15
.claude/settings.local.json
Executable file
15
.claude/settings.local.json
Executable file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir C:UsersviliaOneDrivePočítač*thesis*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(source venv/Scripts/activate)",
|
||||
"Bash(python -m pytest test_app.py -v)",
|
||||
"Bash(python -c \"import flask; import transformers; import torch; print\\(''Dependencies OK''\\)\")",
|
||||
"Bash(curl -s http://localhost:5000/api/stats)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(python -c \":*)",
|
||||
"Bash(python view_db.py)"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
.gitignore
vendored
Executable file
8
.gitignore
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
factchecker.db
|
||||
node_modules/
|
||||
dist/
|
||||
90
README.md
90
README.md
@ -0,0 +1,90 @@
|
||||
# README.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
AI Fact Checker - a full-stack web application that verifies claims using Natural Language Inference (NLI) models. The system searches the web via SerpAPI, analyzes snippets against claims, and returns verdicts (True/False/Ambiguous) with evidence.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Backend (Python/Flask):**
|
||||
- Flask REST API with CORS
|
||||
- ML: HuggingFace Transformers (RoBERTa, mDeBERTa-v3)
|
||||
- Translation: deep_translator (Google Translate)
|
||||
- Database: SQLite with caching layer
|
||||
- Search: SerpAPI for Google search results
|
||||
|
||||
**Frontend (React/Vite):**
|
||||
- React 19 with Vite 7
|
||||
- React Router for navigation
|
||||
- Axios for HTTP requests
|
||||
- CSS custom properties for theming
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
factchecker/
|
||||
├── backend/
|
||||
│ ├── app.py # Flask API, NLI logic, model switching
|
||||
│ ├── database.py # SQLite cache, verified facts, stats
|
||||
│ ├── .env # SERPAPI_API_KEY (required)
|
||||
│ └── venv/ # Python virtual environment
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── App.jsx # Router setup
|
||||
│ │ ├── main.jsx # React entry point
|
||||
│ │ ├── components/
|
||||
│ │ │ └── Layout.jsx
|
||||
│ │ └── pages/
|
||||
│ │ ├── Home.jsx # Main fact-check UI
|
||||
│ │ └── About.jsx
|
||||
│ └── package.json
|
||||
└── factchecker.db # SQLite database (auto-created)
|
||||
```
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Backend
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate # or: venv\Scripts\activate on Windows
|
||||
python app.py # Start Flask on port 5000
|
||||
python clear_cache.py # Clear cache
|
||||
python view_db.py # View database contents
|
||||
```
|
||||
|
||||
### Frontend
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Vite dev server
|
||||
npm run build # Production build
|
||||
npm run lint # ESLint
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `POST /api/check` - Verify a claim (body: claim, language, dateFrom, dateTo, selectedSource, model)
|
||||
- `GET /api/history` - Get check history (query: limit)
|
||||
- `GET /api/stats` - Get database statistics
|
||||
- `POST /api/admin/add-fact` - Add manually verified fact (admin)
|
||||
|
||||
## Models
|
||||
|
||||
Dva NLI modely dostupné s rôznymi компромismi:
|
||||
1. **RoBERTa** (`ynie/roberta-large-snli_mnli_fever_anli_R1_R2_R3-nli`) - Rýchly, vyžaduje preklad do angličtiny
|
||||
2. **mDeBERTa** (`MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7`) - Najlepší pre slovenčinu, multilingválny
|
||||
|
||||
## Database Schema
|
||||
|
||||
- `fact_checks` - Cached results with claim hash, verdict, NLI votes, evidence, sources
|
||||
- `verified_facts` - Manually verified facts (admin-added)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The `.env` file contains the SerpAPI key - do not commit
|
||||
- Models are loaded on-demand; switching triggers reload with cache clearing
|
||||
- Slovak claims are auto-translated for RoBERTa; mDeBERTa handles Slovak natively
|
||||
- Domain whitelist/blacklist filters search results for quality
|
||||
- Results are cached by claim hash to avoid redundant API calls
|
||||
563
backend/test_app.py
Executable file
563
backend/test_app.py
Executable file
@ -0,0 +1,563 @@
|
||||
"""
|
||||
Testy pre AI Fact Checker aplikáciu
|
||||
Spustenie: python -m pytest test_app.py -v
|
||||
Alebo: python test_app.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Nastavíme prostredie pre testovanie
|
||||
os.environ['TESTING'] = 'True'
|
||||
|
||||
# Importujeme aplikáciu
|
||||
from app import app, load_model, MODELS
|
||||
from database import (
|
||||
get_db_connection,
|
||||
get_cached_result,
|
||||
save_to_cache,
|
||||
get_history,
|
||||
get_stats,
|
||||
add_verified_fact,
|
||||
hash_claim,
|
||||
init_db
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FIXTURES - Pomocné funkcie pre testy
|
||||
# =============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Vytvorí testovacieho klienta pre Flask aplikáciu"""
|
||||
app.config['TESTING'] = True
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def init_test_db():
|
||||
"""Inicializuje čistú databázu pre testy"""
|
||||
init_db()
|
||||
yield
|
||||
# Cleanup - vymažeme testovacie dáta
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM fact_checks")
|
||||
cursor.execute("DELETE FROM verified_facts")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TESTY DATABÁZY
|
||||
# =============================================================================
|
||||
|
||||
class TestDatabase:
|
||||
"""Testy pre databázové operácie"""
|
||||
|
||||
def test_hash_claim_consistency(self):
|
||||
"""Test: Hash toho istého výroku je vždy rovnaký"""
|
||||
claim = "Bratislava je hlavné mesto Slovenska"
|
||||
hash1 = hash_claim(claim)
|
||||
hash2 = hash_claim(claim)
|
||||
assert hash1 == hash2, "Hash by mal byť konzistentný"
|
||||
|
||||
def test_hash_claim_case_insensitive(self):
|
||||
"""Test: Hash je case-insensitive"""
|
||||
claim1 = "Bratislava je hlavné mesto Slovenska"
|
||||
claim2 = "bratislava je hlavné mesto slovenska"
|
||||
hash1 = hash_claim(claim1)
|
||||
hash2 = hash_claim(claim2)
|
||||
assert hash1 == hash2, "Hash by mal byť case-insensitive"
|
||||
|
||||
def test_hash_claim_different_claims(self):
|
||||
"""Test: Rôzne výroky majú rôzny hash"""
|
||||
claim1 = "Bratislava je hlavné mesto Slovenska"
|
||||
claim2 = "Košice sú druhé najväčšie mesto"
|
||||
hash1 = hash_claim(claim1)
|
||||
hash2 = hash_claim(claim2)
|
||||
assert hash1 != hash2, "Rôzne výroky by mali mať rôzny hash"
|
||||
|
||||
def test_save_and_get_cached_result(self, init_test_db):
|
||||
"""Test: Uloženie a získanie cachovaného výsledku"""
|
||||
claim = "Testovací výrok pre cache"
|
||||
result = {
|
||||
"verdict": "✅ Pravda",
|
||||
"confidence": 0.85,
|
||||
"nli_votes": {"entailment": 3, "contradiction": 1},
|
||||
"evidence_for": ["Dôkaz 1"],
|
||||
"evidence_against": [],
|
||||
"sources": ["https://example.com"]
|
||||
}
|
||||
|
||||
# Uložíme do cache
|
||||
saved = save_to_cache(claim, result, model_name="roberta")
|
||||
assert saved == True, "Uloženie do cache zlyhalo"
|
||||
|
||||
# Získame z cache
|
||||
cached = get_cached_result(claim)
|
||||
assert cached is not None, "Cache by nemala byť prázdna"
|
||||
assert cached["verdict"] == "✅ Pravda"
|
||||
assert cached["cached"] == True
|
||||
assert cached["model_name"] == "roberta"
|
||||
|
||||
def test_cache_miss(self, init_test_db):
|
||||
"""Test: Získanie neexistujúceho cachovaného výsledku"""
|
||||
claim = "Tento výrok ešte nebol overený"
|
||||
cached = get_cached_result(claim)
|
||||
assert cached is None, "Cache by mala byť prázdna pre nový výrok"
|
||||
|
||||
def test_get_history(self, init_test_db):
|
||||
"""Test: Získanie histórie overení"""
|
||||
# Pridáme niekoľko záznamov
|
||||
for i in range(5):
|
||||
claim = f"Testovací výrok {i}"
|
||||
result = {
|
||||
"verdict": "✅ Pravda" if i % 2 == 0 else "❌ Nepravda",
|
||||
"sources": [f"https://example{i}.com"]
|
||||
}
|
||||
save_to_cache(claim, result, model_name="roberta")
|
||||
|
||||
history = get_history(limit=10)
|
||||
assert len(history) == 5, "História by mala obsahovať 5 záznamov"
|
||||
|
||||
def test_get_stats(self, init_test_db):
|
||||
"""Test: Získanie štatistík"""
|
||||
# Pridáme testovacie dáta
|
||||
for i in range(3):
|
||||
claim = f"Štatistický výrok {i}"
|
||||
result = {"verdict": "✅ Pravda", "sources": []}
|
||||
save_to_cache(claim, result, model_name="roberta")
|
||||
|
||||
# Pridáme manuálne overený fakt
|
||||
add_verified_fact("Manuálne overený fakt", "TRUE", "Vysvetlenie")
|
||||
|
||||
stats = get_stats()
|
||||
assert stats["unique_claims"] == 3
|
||||
assert stats["verified_facts"] == 1
|
||||
assert stats["total_checks"] >= 3
|
||||
|
||||
def test_add_verified_fact_duplicate(self, init_test_db):
|
||||
"""Test: Pridanie duplicitného manuálne overeného faktu"""
|
||||
claim = "Duplicitný fakt"
|
||||
result1 = add_verified_fact(claim, "TRUE", "Prvé vysvetlenie")
|
||||
result2 = add_verified_fact(claim, "FALSE", "Druhé vysvetlenie")
|
||||
|
||||
assert result1 == True, "Prvý fakt by sa mal pridať"
|
||||
assert result2 == False, "Duplicitný fakt by sa nemal pridať"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TESTY API ENDPOINTOV
|
||||
# =============================================================================
|
||||
|
||||
class TestAPIEndpoints:
|
||||
"""Testy pre REST API endpointy"""
|
||||
|
||||
def test_empty_claim_validation(self, client):
|
||||
"""Test: Validácia prázdneho výroku"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": ""}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert "error" in data
|
||||
|
||||
def test_whitespace_only_claim(self, client):
|
||||
"""Test: Validácia výroku s iba medzerami"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": " "}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_missing_claim_field(self, client):
|
||||
"""Test: Chýbajúce pole claim"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"language": "sk"}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_history_endpoint_empty(self, client, init_test_db):
|
||||
"""Test: História keď je prázdna"""
|
||||
response = client.get('/api/history')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert "history" in data
|
||||
assert "count" in data
|
||||
assert data["count"] == 0
|
||||
|
||||
def test_history_endpoint_with_data(self, client, init_test_db):
|
||||
"""Test: História s dátami"""
|
||||
# Pridáme dáta cez cache
|
||||
for i in range(3):
|
||||
claim = f"História test {i}"
|
||||
result = {"verdict": "✅ Pravda", "sources": []}
|
||||
save_to_cache(claim, result, model_name="roberta")
|
||||
|
||||
response = client.get('/api/history?limit=10')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data["count"] == 3
|
||||
|
||||
def test_stats_endpoint(self, client):
|
||||
"""Test: Štatistiky endpoint"""
|
||||
response = client.get('/api/stats')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert "unique_claims" in data
|
||||
assert "total_checks" in data
|
||||
assert "verified_facts" in data
|
||||
|
||||
def test_admin_add_fact_success(self, client, init_test_db):
|
||||
"""Test: Pridanie manuálne overeného faktu (admin)"""
|
||||
response = client.post('/api/admin/add-fact',
|
||||
data=json.dumps({
|
||||
"claim": "Manuálne pridaný fakt",
|
||||
"verdict": "TRUE",
|
||||
"explanation": "Toto je vysvetlenie",
|
||||
"source_url": "https://overenie.sk"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert "message" in data
|
||||
assert data["message"] == "Overený fakt pridaný"
|
||||
|
||||
def test_admin_add_fact_missing_fields(self, client):
|
||||
"""Test: Chýbajúce povinné polia pre admin endpoint"""
|
||||
response = client.post('/api/admin/add-fact',
|
||||
data=json.dumps({
|
||||
"claim": "Neúplný fakt"
|
||||
# Chýba verdict
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_admin_add_fact_duplicate(self, client, init_test_db):
|
||||
"""Test: Pridanie duplicitného faktu"""
|
||||
# Prvýkrát úspech
|
||||
response1 = client.post('/api/admin/add-fact',
|
||||
data=json.dumps({
|
||||
"claim": "Duplicitný admin fakt",
|
||||
"verdict": "TRUE"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response1.status_code == 200
|
||||
|
||||
# Druhýkrát chyba (duplicate)
|
||||
response2 = client.post('/api/admin/add-fact',
|
||||
data=json.dumps({
|
||||
"claim": "Duplicitný admin fakt",
|
||||
"verdict": "FALSE"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response2.status_code == 409
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TESTY VALIDÁCIE VSTUPU
|
||||
# =============================================================================
|
||||
|
||||
class TestInputValidation:
|
||||
"""Testy pre validáciu vstupov a filtre"""
|
||||
|
||||
def test_forbidden_word_politician(self, client):
|
||||
"""Test: Zakázané slová - politici"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "Fico je politik"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert "forbidden_words" in data
|
||||
|
||||
def test_forbidden_word_vulgar(self, client):
|
||||
"""Test: Zakázané slová - vulgárnosti"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "Toto je kokotina"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_forbidden_word_covid(self, client):
|
||||
"""Test: Zakázané slová - citlivé témy"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "Vakcína spôsobuje neplodnosť"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_forbidden_word_english(self, client):
|
||||
"""Test: Zakázané slová - anglické výrazy"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "This is bullshit"
|
||||
}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_clean_claim_passes(self, client):
|
||||
"""Test: Čistý výrok prejde"""
|
||||
# Tento test vyžaduje funkčný SerpAPI kľúč
|
||||
# Ak nie je nastavený, vráti 500
|
||||
if not os.getenv('SERPAPI_API_KEY'):
|
||||
pytest.skip("SERPAPI_API_KEY nie je nastavený")
|
||||
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "Bratislava je hlavné mesto Slovenska"
|
||||
}),
|
||||
content_type='application/json')
|
||||
# Môže byť 200 (úspech) alebo 429 (limit API)
|
||||
assert response.status_code in [200, 429]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TESTY MODELOV
|
||||
# =============================================================================
|
||||
|
||||
class TestModels:
|
||||
"""Testy pre AI modely"""
|
||||
|
||||
def test_model_config_exists(self):
|
||||
"""Test: Konfigurácia modelov existuje"""
|
||||
assert "roberta" in MODELS
|
||||
assert "mdeberta" in MODELS
|
||||
|
||||
def test_roberta_config(self):
|
||||
"""Test: RoBERTa konfigurácia"""
|
||||
config = MODELS["roberta"]
|
||||
assert config["needs_translation"] == True
|
||||
assert "roberta" in config["name"].lower()
|
||||
|
||||
def test_mdeberta_config(self):
|
||||
"""Test: mDeBERTa konfigurácia"""
|
||||
config = MODELS["mdeberta"]
|
||||
assert config["needs_translation"] == False
|
||||
assert "mdeberta" in config["name"].lower() or "DeBERTa" in config["name"]
|
||||
|
||||
def test_model_loading(self):
|
||||
"""Test: Načítanie modelu"""
|
||||
# Testujeme že funkcia load_model existuje a nespôsobí chybu
|
||||
try:
|
||||
load_model("roberta")
|
||||
assert True
|
||||
except Exception as e:
|
||||
pytest.fail(f"Načítanie modelu zlyhalo: {e}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TESTY CACHE LOGIKY
|
||||
# =============================================================================
|
||||
|
||||
class TestCacheLogic:
|
||||
"""Testy pre cachovaciu logiku"""
|
||||
|
||||
def test_cache_increments_count(self, init_test_db):
|
||||
"""Test: Cache inkrementuje počet overení"""
|
||||
claim = "Inkrementácia test"
|
||||
result = {"verdict": "✅ Pravda", "sources": []}
|
||||
|
||||
# Prvé uloženie
|
||||
save_to_cache(claim, result, model_name="roberta")
|
||||
cached1 = get_cached_result(claim)
|
||||
count1 = cached1["check_count"]
|
||||
|
||||
# Druhé získanie (inkrementuje)
|
||||
cached2 = get_cached_result(claim)
|
||||
count2 = cached2["check_count"]
|
||||
|
||||
assert count2 == count1 + 1, "Počet overení by sa mal inkrementovať"
|
||||
|
||||
def test_cache_updates_timestamp(self, init_test_db):
|
||||
"""Test: Cache aktualizuje časovú pečiatku"""
|
||||
claim = "Timestamp test"
|
||||
result = {"verdict": "✅ Pravda", "sources": []}
|
||||
|
||||
save_to_cache(claim, result, model_name="roberta")
|
||||
cached1 = get_cached_result(claim)
|
||||
timestamp1 = cached1["checked_at"]
|
||||
|
||||
import time
|
||||
time.sleep(1) # Počkáme sekundu
|
||||
|
||||
cached2 = get_cached_result(claim)
|
||||
timestamp2 = cached2["checked_at"]
|
||||
|
||||
# Timestamp by mal byť novší
|
||||
assert timestamp2 >= timestamp1
|
||||
|
||||
def test_cache_serialization(self, init_test_db):
|
||||
"""Test: Serializácia JSON polí v cache"""
|
||||
claim = "JSON serializácia test"
|
||||
result = {
|
||||
"verdict": "✅ Pravda",
|
||||
"nli_votes": {"entailment": 0.8, "contradiction": 0.2},
|
||||
"evidence_for": [{"text": "Dôkaz 1", "confidence": 0.9}],
|
||||
"evidence_against": [],
|
||||
"sources": [{"url": "https://example.com", "label": "entailment"}]
|
||||
}
|
||||
|
||||
save_to_cache(claim, result, model_name="mdeberta")
|
||||
cached = get_cached_result(claim)
|
||||
|
||||
assert isinstance(cached["nli_votes"], dict)
|
||||
assert isinstance(cached["evidence_for"], list)
|
||||
assert isinstance(cached["evidence_against"], list)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INTEGRÁCNE TESTY
|
||||
# =============================================================================
|
||||
|
||||
class TestIntegration:
|
||||
"""Integračné testy celého systému"""
|
||||
|
||||
def test_full_request_cycle(self, client, init_test_db):
|
||||
"""Test: Celý cyklus požiadavky (bez SerpAPI)"""
|
||||
# Testujeme validáciu a štruktúru odpovede
|
||||
# Bez SerpAPI kľúča očakávame chybu 500
|
||||
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "Testovací výrok",
|
||||
"language": "sk",
|
||||
"model": "roberta"
|
||||
}),
|
||||
content_type='application/json')
|
||||
|
||||
# Bez SerpAPI: 500, so SerpAPI: 200 alebo 429
|
||||
if os.getenv('SERPAPI_API_KEY'):
|
||||
assert response.status_code in [200, 429]
|
||||
else:
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_model_switching(self, client):
|
||||
"""Test: Prepínanie medzi modelmi"""
|
||||
# Overíme že endpoint akceptuje parameter model
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({
|
||||
"claim": "", # Prázdne pre rýchly test
|
||||
"model": "mdeberta"
|
||||
}),
|
||||
content_type='application/json')
|
||||
|
||||
# Očakávame 400 (validation error) nie 500 (model error)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_verified_fact_overrides_cache(self, client, init_test_db):
|
||||
"""Test: Manuálne overený fakt má prioritu pred cache"""
|
||||
claim = "Prioritný fakt"
|
||||
|
||||
# Najprv pridáme manuálne overený fakt
|
||||
add_verified_fact(claim, "TRUE", "Oficiálne overené", "https://overenie.sk")
|
||||
|
||||
# Potom pridáme do cache iný výsledok
|
||||
cache_result = {
|
||||
"verdict": "❌ Nepravda",
|
||||
"sources": ["https://ine-zdroj.sk"]
|
||||
}
|
||||
save_to_cache(claim, cache_result, model_name="roberta")
|
||||
|
||||
# Získanie by malo vrátiť manuálne overený fakt
|
||||
cached = get_cached_result(claim)
|
||||
assert cached is not None
|
||||
assert cached["verified"] == True
|
||||
assert "Overené" in cached["verdict"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HRANIČNÉ PRÍPADY
|
||||
# =============================================================================
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Testy hraničných prípadov"""
|
||||
|
||||
def test_very_long_claim(self, client):
|
||||
"""Test: Veľmi dlhý výrok"""
|
||||
long_claim = "A" * 10000 # 10k znakov
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": long_claim}),
|
||||
content_type='application/json')
|
||||
# Aplikácia by mala zvládnuť dlhý vstup (vráti "nedostatok zdrojov" alebo spracuje)
|
||||
assert response.status_code == 200 # Očakávame úspešné spracovanie
|
||||
data = json.loads(response.data)
|
||||
assert "verdict" in data # Mala by vrátiť verdikt
|
||||
|
||||
def test_unicode_claim(self, client):
|
||||
"""Test: Výrok s Unicode znakmi"""
|
||||
unicode_claim = "🌍 je guľatá 🌍"
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": unicode_claim}),
|
||||
content_type='application/json')
|
||||
# Aplikácia by mala správne spracovať Unicode (emoji, diakritika)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert "verdict" in data # Mala by vrátiť verdikt
|
||||
|
||||
def test_sql_injection_attempt(self, client):
|
||||
"""Test: Pokus o SQL injection"""
|
||||
malicious_claim = "'; DROP TABLE fact_checks; --"
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": malicious_claim}),
|
||||
content_type='application/json')
|
||||
# Aplikácia by mala bezpečne spracovať vstup (parametrované dotazy)
|
||||
assert response.status_code == 200 # Nemalo by to zhodiť server
|
||||
# Overíme že databáza stále existuje
|
||||
from database import get_stats
|
||||
stats = get_stats() # Ak toto prejde, SQL injection zlyhal
|
||||
assert stats is not None
|
||||
|
||||
def test_special_characters_claim(self, client):
|
||||
"""Test: Výrok so špeciálnymi znakmi"""
|
||||
special_claim = "Cena je 100€ (zľava 50%!) <script>alert('x')</script>"
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({"claim": special_claim}),
|
||||
content_type='application/json')
|
||||
assert response.status_code in [400, 500, 429]
|
||||
|
||||
def test_empty_json_body(self, client):
|
||||
"""Test: Prázdne JSON telo"""
|
||||
response = client.post('/api/check',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_invalid_json(self, client):
|
||||
"""Test: Neplatné JSON"""
|
||||
response = client.post('/api/check',
|
||||
data="nie je json",
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_history_limit_zero(self, client, init_test_db):
|
||||
"""Test: História s limitom 0"""
|
||||
# Pridáme dáta
|
||||
save_to_cache("Test", {"verdict": "✅", "sources": []}, "roberta")
|
||||
|
||||
response = client.get('/api/history?limit=0')
|
||||
data = json.loads(response.data)
|
||||
assert data["count"] == 0
|
||||
assert len(data["history"]) == 0
|
||||
|
||||
def test_history_large_limit(self, client, init_test_db):
|
||||
"""Test: História s veľkým limitom"""
|
||||
response = client.get('/api/history?limit=999999')
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SPUSTENIE TESTOV
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Spustenie priamym zavolaním: python test_app.py
|
||||
pytest.main([__file__, '-v', '--tb=short'])
|
||||
123
backend/view_db.py
Executable file
123
backend/view_db.py
Executable file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
Jednoduchý skript na prezeranie databázy factchecker.db
|
||||
"""
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
DB_NAME = "factchecker.db"
|
||||
|
||||
def print_separator():
|
||||
print("=" * 80)
|
||||
|
||||
def view_fact_checks():
|
||||
"""Zobrazí všetky fact-checky"""
|
||||
conn = sqlite3.connect(DB_NAME)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT * FROM fact_checks ORDER BY checked_at DESC')
|
||||
rows = cursor.fetchall()
|
||||
|
||||
print_separator()
|
||||
print(f"📊 FACT CHECKS (celkom: {len(rows)})")
|
||||
print_separator()
|
||||
|
||||
for row in rows:
|
||||
print(f"\n🔍 ID: {row['id']}")
|
||||
print(f" Tvrdenie: {row['claim']}")
|
||||
print(f" Verdikt: {row['verdict']}")
|
||||
|
||||
# Zobraziť model (ak existuje stĺpec model_name)
|
||||
try:
|
||||
model = row['model_name']
|
||||
if model:
|
||||
print(f" Model: {model}")
|
||||
else:
|
||||
print(f" Model: neznámy (starý záznam)")
|
||||
except IndexError:
|
||||
pass # Stĺpec neexistuje v starej databáze
|
||||
|
||||
print(f" Počet kontrol: {row['check_count']}")
|
||||
print(f" Posledná kontrola: {row['checked_at']}")
|
||||
|
||||
if row['nli_votes']:
|
||||
votes = json.loads(row['nli_votes'])
|
||||
print(f" NLI hlasy: {votes}")
|
||||
|
||||
if row['sources']:
|
||||
sources = json.loads(row['sources'])
|
||||
if sources:
|
||||
print(f" Zdroje: {sources[0] if sources else 'žiadne'}")
|
||||
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
def view_verified_facts():
|
||||
"""Zobrazí manuálne overené fakty"""
|
||||
conn = sqlite3.connect(DB_NAME)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT * FROM verified_facts ORDER BY added_at DESC')
|
||||
rows = cursor.fetchall()
|
||||
|
||||
print_separator()
|
||||
print(f"✅ OVERENÉ FAKTY (celkom: {len(rows)})")
|
||||
print_separator()
|
||||
|
||||
if not rows:
|
||||
print("\n⚠️ Žiadne manuálne overené fakty\n")
|
||||
else:
|
||||
for row in rows:
|
||||
print(f"\n✓ ID: {row['id']}")
|
||||
print(f" Tvrdenie: {row['claim']}")
|
||||
print(f" Verdikt: {row['verdict']}")
|
||||
if row['explanation']:
|
||||
print(f" Vysvetlenie: {row['explanation']}")
|
||||
if row['source_url']:
|
||||
print(f" Zdroj: {row['source_url']}")
|
||||
print(f" Pridané: {row['added_at']} ({row['added_by']})")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
def view_stats():
|
||||
"""Zobrazí štatistiky"""
|
||||
conn = sqlite3.connect(DB_NAME)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as total FROM fact_checks')
|
||||
total_claims = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT SUM(check_count) as total FROM fact_checks')
|
||||
total_checks = cursor.fetchone()[0] or 0
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as total FROM verified_facts')
|
||||
verified = cursor.fetchone()[0]
|
||||
|
||||
print_separator()
|
||||
print("📈 ŠTATISTIKY")
|
||||
print_separator()
|
||||
print(f" Unikátne tvrdenia: {total_claims}")
|
||||
print(f" Celkový počet kontrol: {total_checks}")
|
||||
print(f" Overené fakty: {verified}")
|
||||
print_separator()
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
print("\n" + "🔎 DATABÁZA FACT-CHECKERA".center(80))
|
||||
|
||||
try:
|
||||
view_stats()
|
||||
view_fact_checks()
|
||||
view_verified_facts()
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f"❌ Chyba: {e}")
|
||||
print(" Skontroluj či existuje factchecker.db")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
frontend/.gitignore
vendored
Executable file
24
frontend/.gitignore
vendored
Executable file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
frontend/README.md
Executable file
16
frontend/README.md
Executable file
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
frontend/eslint.config.js
Executable file
29
frontend/eslint.config.js
Executable file
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Executable file
13
frontend/index.html
Executable file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3156
frontend/package-lock.json
generated
Executable file
3156
frontend/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
29
frontend/package.json
Executable file
29
frontend/package.json
Executable file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^6.30.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/KEMT_logo.jpg
Executable file
BIN
frontend/public/KEMT_logo.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
1
frontend/public/vite.svg
Executable file
1
frontend/public/vite.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
903
frontend/src/App.css
Executable file
903
frontend/src/App.css
Executable file
@ -0,0 +1,903 @@
|
||||
:root{
|
||||
--kemt-orange:#ffa900;
|
||||
--kemt-blue:#0459b6;
|
||||
--text-dark:#05263d;
|
||||
--muted:#657786;
|
||||
--bg:#f6f8fb;
|
||||
--card:#ffffff;
|
||||
--max-w:1100px;
|
||||
}
|
||||
|
||||
.container{
|
||||
max-width:var(--max-w);
|
||||
margin:0 auto;
|
||||
padding:0 1rem;
|
||||
}
|
||||
|
||||
.site-header{
|
||||
background:white;
|
||||
border-bottom:1px solid #e6edf6;
|
||||
}
|
||||
|
||||
.header-inner{
|
||||
max-width:var(--max-w);
|
||||
margin:0 auto;
|
||||
padding:14px 0;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.brand{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
}
|
||||
|
||||
.logo{
|
||||
height:64px;
|
||||
}
|
||||
|
||||
.brand-text h1{
|
||||
margin:0;
|
||||
font-size:1.25rem;
|
||||
color:var(--text-dark);
|
||||
}
|
||||
.brand-text .muted{
|
||||
margin:0;
|
||||
font-size:0.9rem;
|
||||
color:#444;
|
||||
}
|
||||
|
||||
.nav-bar{
|
||||
background:var(--kemt-orange);
|
||||
}
|
||||
|
||||
.nav-inner{
|
||||
max-width:var(--max-w);
|
||||
margin:0 auto;
|
||||
padding:0.6rem 1rem;
|
||||
}
|
||||
|
||||
.main-nav{
|
||||
display:flex;
|
||||
gap:1.5rem;
|
||||
}
|
||||
|
||||
.main-nav a{
|
||||
color:white;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:0.3px;
|
||||
padding:6px 0;
|
||||
}
|
||||
|
||||
.main-nav .btn-outline{
|
||||
padding:6px 12px;
|
||||
background:transparent;
|
||||
border:1px solid rgba(255,255,255,0.7);
|
||||
border-radius:6px;
|
||||
}
|
||||
|
||||
.main-nav a:hover{opacity:0.9;}
|
||||
.main-nav .btn-outline:hover{background:rgba(255,255,255,0.15);}
|
||||
|
||||
.mobile-menu{
|
||||
display:none;
|
||||
font-size:28px;
|
||||
background:transparent;
|
||||
border:0;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 4rem 0;
|
||||
/* Gradient adjusted to match "About" page density */
|
||||
background: linear-gradient(180deg, var(--kemt-orange) 0%, #ffffff 35%);
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.hero-content.centered {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-content h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-dark);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.hero-content .lead {
|
||||
font-size: 1.2rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 2.5rem auto;
|
||||
}
|
||||
|
||||
/* ABOUT PAGE HEADER */
|
||||
.about-hero {
|
||||
padding: 4rem 0 2rem 0;
|
||||
background: linear-gradient(180deg, var(--kemt-orange) 0%, #ffffff 100%);
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.about-hero-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.about-hero h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.about-hero p {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
/* FORM STYLES REFRESH */
|
||||
.claim-form.shadow-depth {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.08); /* Deep shadow */
|
||||
border: 2px solid #ffa900; /* Visible border */
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
textarea#claim-input {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid var(--kemt-blue);
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
margin-bottom: 1rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
textarea#claim-input:focus {
|
||||
border-color: var(--kemt-blue);
|
||||
box-shadow: 0 0 0 4px rgba(4,89,182, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Advanced Filters & Buttons */
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.9rem;
|
||||
border: var(--kemt-blue) 1px solid;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
|
||||
.btn-text:hover {
|
||||
background-color: #e9ecef;
|
||||
color: var(--kemt-blue);
|
||||
}
|
||||
|
||||
.advanced-wrapper {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: max-height 0.4s ease-in-out, opacity 0.3s ease, transform 0.3s ease;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.advanced-wrapper.open {
|
||||
max-height: 500px; /* Adjust if content is very long */
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.advanced-panel {
|
||||
margin: 0 -2rem -2rem -2rem;
|
||||
padding: 1.5rem 2rem 2rem 2rem;
|
||||
background-color: #f1f5f9; /* Light contrast background */
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--kemt-blue);
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
background-color: #f8fafc;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.filter-group select:hover,
|
||||
.filter-group input:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.filter-group select:focus,
|
||||
.filter-group input:focus {
|
||||
border-color: var(--kemt-blue);
|
||||
background-color: white;
|
||||
box-shadow: 0 0 0 3px rgba(4, 89, 182, 0.1);
|
||||
}
|
||||
|
||||
/* Custom Calendar Icon fix for inputs */
|
||||
.filter-group input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.filter-group input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: white;
|
||||
border: 1px solid #e03131;
|
||||
padding: 8px 18px;
|
||||
color: #e03131;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.btn-primary-lg {
|
||||
background-color: var(--kemt-blue);
|
||||
color: white;
|
||||
padding: 12px 32px;
|
||||
font-size: 1.05rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(4, 89, 182, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary-lg:hover {
|
||||
background-color: #034691;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(4, 89, 182, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary-lg:disabled {
|
||||
background-color: #a0bfe0;
|
||||
cursor: wait;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* HOW IT WORKS STEPS NEW */
|
||||
.how-it-works-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: var(--kemt-orange) 2px solid;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
background: white;
|
||||
color: var(--kemt-blue);
|
||||
font-weight: 800;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
border: 2px solid var(--kemt-blue);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.step-card h4 {
|
||||
margin: 0.5rem 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text-dark);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step-card p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.step-arrow {
|
||||
color: var(--muted);
|
||||
font-size: 1.5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.step-arrow { display: none; }
|
||||
.how-it-works-row { flex-direction: column; gap: 2rem; }
|
||||
}
|
||||
|
||||
/* RESULTS REDESIGN */
|
||||
.result-card {
|
||||
margin-top: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
text-align: left;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
||||
border-left: 6px solid #ccc;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.verdict-success { border-left-color: #2e7d32; background: #f1f8f1; }
|
||||
.verdict-danger { border-left-color: #d32f2f; background: #fff5f5; }
|
||||
.verdict-warning { border-left-color: #ed6c02; background: #fff8e1; }
|
||||
|
||||
.verdict-main {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin: 1rem 0 1.5rem 0;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.verdict-success .verdict-main { color: #1b5e20; }
|
||||
.verdict-danger .verdict-main { color: #c62828; }
|
||||
.verdict-warning .verdict-main { color: #e65100; }
|
||||
|
||||
.sources-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
text-decoration: none;
|
||||
color: var(--text-dark);
|
||||
transition: hover 0.2s;
|
||||
}
|
||||
|
||||
.source-item:hover {
|
||||
border-color: var(--kemt-blue);
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.source-icon { font-size: 1.2rem; }
|
||||
.source-url { font-weight: 500; color: var(--kemt-blue); }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.card{
|
||||
background:white;
|
||||
padding:16px;
|
||||
border-radius:8px;
|
||||
border:1px solid #e6edf6;
|
||||
}
|
||||
|
||||
.features{
|
||||
margin-top:1.5rem;
|
||||
padding-bottom:2rem;
|
||||
}
|
||||
|
||||
.grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
|
||||
gap:1rem;
|
||||
}
|
||||
|
||||
.feature{
|
||||
background:white;
|
||||
padding:18px;
|
||||
border-radius:8px;
|
||||
border:1px solid #e6edf6;
|
||||
}
|
||||
|
||||
.site-footer{
|
||||
border-top:1px solid #e6edf6;
|
||||
margin-top:2rem;
|
||||
padding:14px 0;
|
||||
}
|
||||
|
||||
.footer-inner{
|
||||
max-width:var(--max-w);
|
||||
margin:0 auto;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
.tiny-nav a{
|
||||
margin-left:1rem;
|
||||
color:var(--kemt-blue);
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
@media (max-width:900px){
|
||||
.hero-inner{
|
||||
flex-direction:column;
|
||||
}
|
||||
.hero-aside{
|
||||
flex:none;
|
||||
max-width:100%;
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:700px){
|
||||
.mobile-menu{
|
||||
display:block;
|
||||
color:var(--kemt-blue);
|
||||
}
|
||||
|
||||
.main-nav{
|
||||
display:none;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
.main-nav.open{
|
||||
display:flex;
|
||||
background:var(--kemt-blue);
|
||||
position:absolute;
|
||||
left:0;
|
||||
right:0;
|
||||
top:110px;
|
||||
padding:1rem;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
.main-nav.open a{
|
||||
padding:10px 0;
|
||||
border-bottom:1px solid rgba(255,255,255,0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* About page explanation styles */
|
||||
.explanation-block, .features-section {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.about-context-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.context-card {
|
||||
background: transparent;
|
||||
border-left: 4px solid var(--kemt-orange);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.context-card h4 {
|
||||
color: var(--text-dark);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.context-card p {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #fcfdfe;
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #edf5fc;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.feature-card h4 {
|
||||
color: var(--text-dark);
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Steps bubbles */
|
||||
.steps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.steps-container {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.step.bubble-step {
|
||||
flex: 1;
|
||||
background: #f8fafc;
|
||||
padding: 2rem;
|
||||
border-radius: 20px; /* Bubble look */
|
||||
border: 2px solid #eef3f7;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background: var(--kemt-orange);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 4px 10px rgba(255, 169, 0, 0.3);
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
color: var(--kemt-blue);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding-bottom: 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.step.bubble-step:hover {
|
||||
border-color: var(--kemt-orange);
|
||||
background: white;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Accordion / Detailed Info Section */
|
||||
.detailed-info-section {
|
||||
margin-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.accordion-btn {
|
||||
background: white;
|
||||
border: 2px solid var(--kemt-blue);
|
||||
color: var(--kemt-blue);
|
||||
padding: 12px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-btn:hover, .accordion-btn.active {
|
||||
background: var(--kemt-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.detailed-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s ease-out, opacity 0.4s ease;
|
||||
opacity: 0;
|
||||
margin-top: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detailed-content.expanded {
|
||||
max-height: 2000px; /* arbitrary large value */
|
||||
opacity: 1;
|
||||
transition: max-height 0.8s ease-in, opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.detail-inner {
|
||||
background: #f1f8ff;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
border-left: 5px solid var(--kemt-blue);
|
||||
}
|
||||
|
||||
.detail-inner h3 {
|
||||
margin-top: 0;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.detail-inner h4 {
|
||||
color: var(--kemt-blue);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.detail-inner code {
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
.math-block {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
margin: 1rem 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.math-block p {
|
||||
margin: 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.limitations-list li {
|
||||
margin-bottom: 0.8rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.limitations-list strong {
|
||||
color: #d32f2f; /* Red standard for limitations/warnings */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Source Badges */
|
||||
.source-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.8em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #e6f4ea;
|
||||
color: #1e7e34;
|
||||
border: 1px solid #cce8d0;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #fce8e6;
|
||||
color: #d93025;
|
||||
border: 1px solid #fad2cf;
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
background-color: #f1f3f4;
|
||||
color: #5f6368;
|
||||
border: 1px solid #dcdcdc;
|
||||
}
|
||||
|
||||
|
||||
/* Model Selector Buttons */
|
||||
.model-selector-container {
|
||||
padding: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.model-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.model-option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
min-width: 100px; /* Ensure they don't shrink too much */
|
||||
}
|
||||
|
||||
.model-option:hover {
|
||||
background-color: #e2e6ea;
|
||||
border-color: #ced4da;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.model-option.active {
|
||||
background-color: #ffffff;
|
||||
border-color: #ffa900;
|
||||
box-shadow: 0 4px 10px rgba(255, 169, 0, 0.15);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.model-option .model-name {
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model-option .model-desc {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
letter-spacing: 0.02em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model-option.active .model-name {
|
||||
color: #ffa900;
|
||||
}
|
||||
|
||||
.model-option.active::after {
|
||||
display: none; /* No checkmark needed if highlighted well */
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.model-options {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.model-option {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
.model-option .model-name { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
21
frontend/src/App.jsx
Executable file
21
frontend/src/App.jsx
Executable file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Layout from "./components/Layout";
|
||||
import Home from "./pages/Home";
|
||||
import About from "./pages/About";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="about" element={<About />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
frontend/src/assets/react.svg
Executable file
1
frontend/src/assets/react.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
60
frontend/src/components/Layout.jsx
Executable file
60
frontend/src/components/Layout.jsx
Executable file
@ -0,0 +1,60 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import "../App.css";
|
||||
|
||||
const Layout = () => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="site-header">
|
||||
<div className="container header-inner">
|
||||
<div className="brand">
|
||||
<img src="/KEMT_logo.jpg" alt="KEMT logo" className="logo" />
|
||||
<div className="brand-text">
|
||||
<h1>AI Fact Checker</h1>
|
||||
<p className="muted">Demo KEMT</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="menu-toggle"
|
||||
className={`mobile-menu ${menuOpen ? 'open' : ''}`}
|
||||
aria-label="Otvoriť menu"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="nav-bar">
|
||||
<div className="nav-inner">
|
||||
<nav className={`main-nav ${menuOpen ? 'open' : ''}`} id="main-nav">
|
||||
<a href="https://kps.fei.tuke.sk/" target="_blank" rel="noopener noreferrer">
|
||||
Domov
|
||||
</a>
|
||||
<Link to="/">Fact-checker</Link>
|
||||
<Link to="/about">O projekte</Link>
|
||||
<a href="#contact" className="btn-outline">
|
||||
Kontakt
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Outlet />
|
||||
|
||||
<footer className="site-footer">
|
||||
<div className="container footer-inner">
|
||||
<p>© 2025 Fakulta elektrotechniky a informatiky — Demo</p>
|
||||
<nav className="tiny-nav">
|
||||
<a href="#">Súkromie</a>
|
||||
<a href="#">Kontakt</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
10
frontend/src/index.css
Executable file
10
frontend/src/index.css
Executable file
@ -0,0 +1,10 @@
|
||||
*{box-sizing:border-box;}
|
||||
|
||||
body{
|
||||
font-family:Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
margin:0;
|
||||
background:#f6f8fb;
|
||||
color:#000;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
10
frontend/src/main.jsx
Executable file
10
frontend/src/main.jsx
Executable file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
227
frontend/src/pages/About.jsx
Executable file
227
frontend/src/pages/About.jsx
Executable file
@ -0,0 +1,227 @@
|
||||
import React, { useState } from "react";
|
||||
import "../App.css";
|
||||
|
||||
const About = () => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="about-hero">
|
||||
<div className="about-hero-content">
|
||||
<h2>O projekte</h2>
|
||||
<p>
|
||||
Tento fact-checker využíva pokročilé metódy umelej inteligencie na
|
||||
overovanie pravdivosti tvrdení. Systém kombinuje vyhľadávanie
|
||||
informácií v reálnom čase s analytickými schopnosťami neurónových
|
||||
sietí.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="container">
|
||||
<div className="about-context-grid">
|
||||
<div className="context-card">
|
||||
<h4>O nástroji</h4>
|
||||
<p>
|
||||
Tento demo nástroj kombinuje model prirodzeného jazykového inferovania (NLI) s vyhľadávaním na webe. Zadáš tvrdenie, backend získa výsledky vyhľadávania, extrahuje relevantné úryvky a model vyhodnotí, či dôkazy tvrdenie podporujú, odporujú alebo nestačia.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="context-card">
|
||||
<h4>Prečo je to dôležité?</h4>
|
||||
<p>
|
||||
Fact-checking pomáha odhaľovať nepravdivé a zavádzajúce tvrdenia, ktoré sa šíria najmä na internete. Chráni ľudí pred manipuláciou, posilňuje dôveru v spoľahlivé zdroje a podporuje kritické myslenie.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="context-card">
|
||||
<h4>Open-source</h4>
|
||||
<p>
|
||||
Projekt využíva open-source technológie z oblasti NLP – Flask, Transformers, Sentence-Transformers a verejné fact-checking datasety. Kód je voľne dostupný a pripravený na ďalšie vylepšovanie.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="features-section">
|
||||
<h3>Kľúčové funkcie</h3>
|
||||
<div className="features-grid">
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">⚡</div>
|
||||
<h4>Rýchle overenie</h4>
|
||||
<p>
|
||||
Okamžitá analýza tvrdení pomocou moderných AI modelov a
|
||||
vyhľadávania v reálnom čase.
|
||||
</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">🌍</div>
|
||||
<h4>Podpora slovenčiny</h4>
|
||||
<p>
|
||||
Plne lokalizované rozhranie a schopnosť spracovať slovenské
|
||||
tvrdenia s prekladom na pozadí.
|
||||
</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">🔍</div>
|
||||
<h4>Transparentné zdroje</h4>
|
||||
<p>
|
||||
Každé overenie obsahuje odkazy na použité články a zdroje, z
|
||||
ktorých systém čerpal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="explanation-block">
|
||||
<h3>Ako to funguje?</h3>
|
||||
<p>Proces overovania sa skladá z troch hlavných krokov:</p>
|
||||
|
||||
<div className="steps-container">
|
||||
<article className="step bubble-step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<h4>Analýza a vyhľadávanie</h4>
|
||||
<p>
|
||||
Systém analyzuje text a vyhľadá relevantné články na internete.
|
||||
Používa preklad do angličtiny pre lepšie výsledky a získanie
|
||||
najnovších faktov.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="step bubble-step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<h4>Overovanie (NLI)</h4>
|
||||
<p>
|
||||
AI model (RoBERTa) porovnáva vaše tvrdenie s nájdenými textami.
|
||||
Určuje, či dôkazy potvrdzujú alebo vyvracajú hypotézu.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="step bubble-step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-content">
|
||||
<h4>Vyhodnotenie</h4>
|
||||
<p>
|
||||
Agregácia výsledkov z viacerých zdrojov. Systém vypočíta skóre
|
||||
dôveryhodnosti a zobrazí finálny verdikt s dôkazmi.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detailed-info-section">
|
||||
<button
|
||||
className={`accordion-btn ${isExpanded ? 'active' : ''}`}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? 'Skryť technické detaily' : 'Zobraziť technické detaily (Viac o technológii)'}
|
||||
<span className="accordion-icon">{isExpanded ? '−' : '+'}</span>
|
||||
</button>
|
||||
|
||||
<div className={`detailed-content ${isExpanded ? 'expanded' : ''}`}>
|
||||
<div className="detail-inner">
|
||||
<h3>Deep Dive: Architektúra systému</h3>
|
||||
|
||||
<h4>1. Získavanie informácií</h4>
|
||||
<p>
|
||||
Na rozdiel od generatívnych modelov (ako GPT), ktoré "halucinujú" odpovede zo svojej pamäte,
|
||||
náš systém funguje na princípe <strong>Retrieval-Augmented Verification</strong>.
|
||||
Najprv sa vykoná sémantický preklad dopytu do angličtiny (<code>GoogleTranslator</code>),
|
||||
čím sa sprístupní globálna báza vedomostí. Následne sa cez SerpAPI vykoná cielené
|
||||
vyhľadávanie relevantných úryvkov (snippetov) z dôveryhodných zdrojov.
|
||||
</p>
|
||||
|
||||
<h4>2. NLI Klasifikátor (Natural Language Inference)</h4>
|
||||
<p>
|
||||
Srdcom systému je model <code>unie/roberta-large-snli_mnli_fever_anli_R1_R2_R3-nli</code>.
|
||||
Ide o model architektúry Transformer (RoBERTa Large), ktorý bol jemne doladený (fine-tuned)
|
||||
na masívnych datasetoch pre úlohu NLI (SNLI, MNLI, FEVER, ANLI).
|
||||
</p>
|
||||
<p>
|
||||
Tento model nepredikuje ďalšie slovo, ale vykonáva <strong>troj-triednu klasifikáciu </strong>
|
||||
pre dvojicu textov (Tvrdenie vs. Dôkaz):
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Entailment (Vyplýva):</strong> Dôkaz potvrdzuje tvrdenie (Pravda).</li>
|
||||
<li><strong>Contradiction (Rozpor):</strong> Dôkaz je v priamom rozpore s tvrdením (Nepravda).</li>
|
||||
<li><strong>Neutral (Neutrálne):</strong> Dôkaz s tvrdením nesúvisí alebo ho nepotvrdzuje.</li>
|
||||
</ul>
|
||||
|
||||
<h4>3. Agregácia a Vyhodnotenie (Soft Scoring)</h4>
|
||||
<p>
|
||||
Webové vyhľadávanie vráti desiatky fragmentov, z ktorých niektoré môžu byť irelevantné.
|
||||
Preto systém nepoužíva jednoduchú väčšinu ("koľko článkov súhlasí"), ale <strong>váženú agregáciu (Soft Scoring)</strong>.
|
||||
Zohľadňuje sa miera istoty (Confidence Score) modelu – ak model s vysokou istotou (napr. 99%) nájde rozpor v dôveryhodnom zdroji,
|
||||
tento silný signál preváži nad množstvom neutrálnych výsledkov.
|
||||
</p>
|
||||
<p>
|
||||
Finálne skóre sa počíta sčítaním pravdepodobností:
|
||||
</p>
|
||||
<div className="math-block">
|
||||
<p><code>Skóre_Pravda = ∑ P(entailment)</code></p>
|
||||
<p><code>Skóre_Nepravda = ∑ P(contradiction)</code></p>
|
||||
<p><code>Celkové_Skóre = Skóre_Pravda + Skóre_Nepravda</code></p>
|
||||
</div>
|
||||
<p>
|
||||
Verdikt sa určí na základe pomeru:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Ak <code>Skóre_Pravda / Celkové_Skóre > 0.5</code> → <strong>✅ Pravdepodobne pravda</strong></li>
|
||||
<li>Ak <code>Skóre_Nepravda / Celkové_Skóre > 0.5</code> → <strong>❌ Pravdepodobne nepravda</strong></li>
|
||||
<li>Inak → <strong>⚠️ Nejednoznačné</strong> (ak sú dôkazy protichodné alebo slabé)</li>
|
||||
</ul>
|
||||
|
||||
<h4>4. Transparentnosť a vysvetliteľnosť </h4>
|
||||
<p>
|
||||
Kľúčovou vlastnosťou systému je možnosť overiť si prácu AI. Toto nie je "Black Box".
|
||||
Pri každom rozhodnutí systém zobrazí:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Presné zdroje:</strong> URL odkazy na články, ktoré boli použité pre analýzu.</li>
|
||||
<li><strong>Konkrétne úryvky:</strong> Text, ktorý AI našla a na základe ktorého rozhodla.</li>
|
||||
<li><strong>Skóre istoty:</strong> Ako veľmi si je model istý svojím tvrdením.</li>
|
||||
</ul>
|
||||
|
||||
<h4>5. Prečo je tento prístup spoľahlivejší než LLM (GPT)?</h4>
|
||||
<p>
|
||||
Bežné generatívne modely (LLMs) ako ChatGPT fungujú ako prediktor ďalšieho slova a často "halucinujú" (vymýšľajú si fakty).
|
||||
Tento <strong>Fact-Checking Pipeline</strong> je robustnejší pretože:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Nehalucinuje:</strong> Negeneruje text z pamäte, ale overuje ho voči reálnym zdrojom.</li>
|
||||
<li><strong>Aktuálnosť:</strong> Pracuje s informáciami z dnešného dňa (cez Google Search), nie s dátami spred rokov.</li>
|
||||
<li><strong>Objektivita:</strong> Model je trénovaný na logiku (Dôkaz vs. Tvrdenie), nie na kreatívne písanie.</li>
|
||||
</ul>
|
||||
|
||||
<h4>6. Obmedzenia systému </h4>
|
||||
<p>
|
||||
Napriek pokročilej technológii má systém svoje hranice a etické obmedzenia:
|
||||
</p>
|
||||
<ul className="limitations-list">
|
||||
<li>
|
||||
<strong>Zákaz politických tvrdení:</strong> Systém je navrhnutý ako nástroj na overovanie faktov, nie na politický boj.
|
||||
Aplikácia obsahuje filter, ktorý automaticky <strong>odmieta tvrdenia týkajúce sa aktívnych politikov, politických strán
|
||||
alebo citlivých spoločenských tém</strong> (napr. voľby), aby sa predišlo šíreniu zaujatosti alebo manipulácii.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Závislosť na zdrojoch:</strong> Ak Google nenájde k téme žiadne dôveryhodné články, systém nedokáže tvrdenie overiť
|
||||
(vráti status "Neoveriteľné").
|
||||
</li>
|
||||
<li>
|
||||
<strong>Jazyková bariéra:</strong> Hoci systém používa prekladač, pri veľmi špecifických slovenských slangových výrazoch
|
||||
alebo lokálnych kauzách môže byť presnosť nižšia než pri globálnych témach.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
274
frontend/src/pages/Home.jsx
Executable file
274
frontend/src/pages/Home.jsx
Executable file
@ -0,0 +1,274 @@
|
||||
import React, { useState } from "react";
|
||||
import "../App.css";
|
||||
|
||||
function Home() {
|
||||
const [claim, setClaim] = useState("");
|
||||
|
||||
// Advanced settings state
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
const [dateTo, setDateTo] = useState("");
|
||||
const [language, setLanguage] = useState("all");
|
||||
const [selectedSource, setSelectedSource] = useState("all");
|
||||
const [model, setModel] = useState("roberta");
|
||||
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Note: menuOpen state was moved to Layout.jsx
|
||||
|
||||
const handleCheck = async () => {
|
||||
if (!claim.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
setResult({ verdict: "Overujem…", sources: [] });
|
||||
|
||||
try {
|
||||
const res = await fetch("http://localhost:5000/api/check", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
claim,
|
||||
language,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedSource,
|
||||
model
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Spracuj chyby z backendu
|
||||
if (!res.ok) {
|
||||
setResult({
|
||||
verdict: data.message || data.error || "Chyba servera",
|
||||
sources: []
|
||||
});
|
||||
} else {
|
||||
setResult({
|
||||
verdict: data.verdict || "Neznámy výsledok",
|
||||
sources: data.sources || [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setResult({ verdict: "Backend nedostupný", sources: [] });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="hero">
|
||||
<div className="container hero-inner">
|
||||
|
||||
<div className="hero-content centered">
|
||||
<h2>Rýchle overenie tvrdení pomocou NLI</h2>
|
||||
<p className="lead">
|
||||
Zadaj výrok a systém prehľadá web, vyhodnotí dôkazy a vráti odhad - pravda, nepravda alebo neoveriteľné.
|
||||
</p>
|
||||
|
||||
<form className="claim-form shadow-depth" onSubmit={(e) => e.preventDefault()}>
|
||||
|
||||
<div className="model-selector-container">
|
||||
<div className="model-options">
|
||||
<button
|
||||
type="button"
|
||||
className={`model-option ${model === 'roberta' ? 'active' : ''}`}
|
||||
onClick={() => setModel('roberta')}
|
||||
title="Základný model "
|
||||
>
|
||||
<span className="model-name">RoBERTa</span>
|
||||
<span className="model-desc">Rýchly </span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`model-option ${model === 'mdeberta' ? 'active' : ''}`}
|
||||
onClick={() => setModel('mdeberta')}
|
||||
title="Najlepší pre slovenčinu "
|
||||
>
|
||||
<span className="model-name">mDeBERTa</span>
|
||||
<span className="model-desc">Najpresnejší </span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="claim-input"
|
||||
placeholder="Sem vložte výrok alebo tvrdenie, ktoré chcete overiť..."
|
||||
value={claim}
|
||||
onChange={(e) => setClaim(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
|
||||
<div className="form-footer">
|
||||
<div className="advanced-toggle-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-text toggle-advanced"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "▲ Skryť filtre" : "⚙ Rozšírené nastavenia"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="check-btn" type="button" onClick={handleCheck} disabled={loading} className="btn-primary-lg">
|
||||
{loading ? (
|
||||
<span className="spinner">⟳ Overujem...</span>
|
||||
) : (
|
||||
"Overiť tvrdenie"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`advanced-wrapper ${showAdvanced ? 'open' : ''}`}>
|
||||
<div className="advanced-panel">
|
||||
<div className="filter-row">
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Dátum od:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Dátum do:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<label>Jazyk:</label>
|
||||
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
|
||||
<option value="all">Všetky jazyky</option>
|
||||
<option value="en">Angličtina</option>
|
||||
<option value="sk">Slovenčina</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="filter-row">
|
||||
<div className="filter-group full-width">
|
||||
<label>Preferované zdroje:</label>
|
||||
<select value={selectedSource} onChange={(e) => setSelectedSource(e.target.value)}>
|
||||
<option value="all">Všetky zdroje</option>
|
||||
<option value="demagog">Demagog.sk</option>
|
||||
<option value="afp">AFP Fact Check</option>
|
||||
<option value="reuters">Reuters</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filter-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-link"
|
||||
onClick={() => {
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
setLanguage("all");
|
||||
setSelectedSource("all");
|
||||
}}
|
||||
>
|
||||
Resetovať filtre
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{result && (
|
||||
<div id="result" className={`result-card fade-in ${result.verdict.includes("Pravda") || result.verdict.includes("pravda") ? "verdict-success" : result.verdict.includes("Nepravda") || result.verdict.includes("nepravda") ? "verdict-danger" : "verdict-warning"}`}>
|
||||
<h3>Výsledok analýzy</h3>
|
||||
<div className="verdict-main">{result.verdict}</div>
|
||||
|
||||
<h4>Použité zdroje:</h4>
|
||||
<div className="sources-list">
|
||||
{result.sources.length > 0 ? result.sources.slice(0, 5).map((s, i) => {
|
||||
const url = typeof s === 'string' ? s : s.url;
|
||||
const isObject = typeof s === 'object';
|
||||
let badge = null;
|
||||
|
||||
if (isObject) {
|
||||
const entailment = s.entailment_prob || 0;
|
||||
const contradiction = s.contradiction_prob || 0;
|
||||
const neutral = s.neutral_prob || 0;
|
||||
|
||||
const entPct = Math.round(entailment * 100);
|
||||
const conPct = Math.round(contradiction * 100);
|
||||
const neuPct = Math.round(neutral * 100);
|
||||
|
||||
if (s.label === 'entailment') {
|
||||
badge = <span className="source-badge badge-success">{entPct}% Pravda</span>;
|
||||
} else if (s.label === 'contradiction') {
|
||||
badge = <span className="source-badge badge-danger">{conPct}% Nepravda</span>;
|
||||
} else {
|
||||
badge = <span className="source-badge badge-neutral">Neutrálne ({neuPct}%)</span>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="source-item"
|
||||
>
|
||||
<div className="source-content">
|
||||
<span className="source-icon"></span>
|
||||
<span className="source-url">{new URL(url).hostname.replace('www.', '')}</span>
|
||||
</div>
|
||||
{badge}
|
||||
</a>
|
||||
);
|
||||
}) : <p>Žiadne zdroje neboli nájdené.</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="how-it-works-section">
|
||||
<h3>Ako to funguje?</h3>
|
||||
<div className="how-it-works-row">
|
||||
<div className="step-card">
|
||||
<div className="step-num">1</div>
|
||||
<h4>Zadanie</h4>
|
||||
<p>Zadajte výrok, ktorý chcete overiť</p>
|
||||
</div>
|
||||
<div className="step-arrow">➝</div>
|
||||
<div className="step-card">
|
||||
<div className="step-num">2</div>
|
||||
<h4>Vyhľadávanie</h4>
|
||||
<p>AI vyhľadá dôveryhodné zdroje</p>
|
||||
</div>
|
||||
<div className="step-arrow">➝</div>
|
||||
<div className="step-card">
|
||||
<div className="step-num">3</div>
|
||||
<h4>Analýza</h4>
|
||||
<p>Model porovná fakty s tvrdením</p>
|
||||
</div>
|
||||
<div className="step-arrow">➝</div>
|
||||
<div className="step-card">
|
||||
<div className="step-num">4</div>
|
||||
<h4>Výsledok</h4>
|
||||
<p>Získate verdikt a zdroje</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
15
frontend/vite.config.js
Executable file
15
frontend/vite.config.js
Executable file
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user