My app
Browse files- .gitignore +19 -0
- Procfile +1 -0
- README.md +134 -1
- app.py +185 -0
- requirements.txt +5 -0
- templates/index.html +256 -0
.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔒 لا ترفع هذا الملف أبداً على GitHub!
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*.pyo
|
| 8 |
+
venv/
|
| 9 |
+
.venv/
|
| 10 |
+
env/
|
| 11 |
+
|
| 12 |
+
# IDE
|
| 13 |
+
.vscode/
|
| 14 |
+
.idea/
|
| 15 |
+
*.swp
|
| 16 |
+
|
| 17 |
+
# OS
|
| 18 |
+
.DS_Store
|
| 19 |
+
Thumbs.db
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: gunicorn app:app
|
README.md
CHANGED
|
@@ -1 +1,134 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧠 محلل المشاعر | Sentiment Analyzer
|
| 2 |
+
|
| 3 |
+
تطبيق ويب لتحليل مشاعر النصوص بأكثر من 20 لغة باستخدام Claude AI و Flask.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📁 هيكل المشروع
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
sentiment_project/
|
| 11 |
+
├── app.py ← السيرفر الخلفي (Flask)
|
| 12 |
+
├── requirements.txt ← المكتبات المطلوبة
|
| 13 |
+
├── Procfile ← للنشر على Render/Railway
|
| 14 |
+
├── .env.example ← نموذج إعدادات البيئة
|
| 15 |
+
├── .gitignore ← ملفات يجب استثناؤها من Git
|
| 16 |
+
├── README.md ← هذا الملف
|
| 17 |
+
└── templates/
|
| 18 |
+
└── index.html ← الواجهة الأمامية
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 🚀 التشغيل المحلي
|
| 24 |
+
|
| 25 |
+
### 1. استنسخ المشروع
|
| 26 |
+
```bash
|
| 27 |
+
git clone https://github.com/username/sentiment-analyzer.git
|
| 28 |
+
cd sentiment-analyzer
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### 2. أنشئ بيئة افتراضية
|
| 32 |
+
```bash
|
| 33 |
+
python -m venv venv
|
| 34 |
+
source venv/bin/activate # Mac/Linux
|
| 35 |
+
venv\Scripts\activate # Windows
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### 3. ثبّت المكتبات
|
| 39 |
+
```bash
|
| 40 |
+
pip install -r requirements.txt
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### 4. أعدّ مفتاح API
|
| 44 |
+
```bash
|
| 45 |
+
cp .env.example .env
|
| 46 |
+
```
|
| 47 |
+
افتح ملف `.env` وأضف مفتاحك من https://console.anthropic.com/settings/keys:
|
| 48 |
+
```
|
| 49 |
+
ANTHROPIC_API_KEY=sk-ant-api03-XXXXXXXXXXXXXXXXXXXXXXXX
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 5. شغّل السيرفر
|
| 53 |
+
```bash
|
| 54 |
+
python app.py
|
| 55 |
+
```
|
| 56 |
+
افتح المتصفح على: **http://localhost:5000**
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## ☁️ النشر على Render (مجاني)
|
| 61 |
+
|
| 62 |
+
### 1. ارفع المشروع على GitHub
|
| 63 |
+
```bash
|
| 64 |
+
git init
|
| 65 |
+
git add .
|
| 66 |
+
git commit -m "Initial commit"
|
| 67 |
+
git remote add origin https://github.com/username/sentiment-analyzer.git
|
| 68 |
+
git push -u origin main
|
| 69 |
+
```
|
| 70 |
+
> ⚠️ تأكد أن `.env` موجود في `.gitignore` ولم يُرفع!
|
| 71 |
+
|
| 72 |
+
### 2. أنشئ حساباً على Render
|
| 73 |
+
- اذهب إلى: https://render.com
|
| 74 |
+
- سجّل دخول بحساب GitHub
|
| 75 |
+
|
| 76 |
+
### 3. أنشئ Web Service جديد
|
| 77 |
+
- New → Web Service
|
| 78 |
+
- اختر repository المشروع
|
| 79 |
+
- الإعدادات:
|
| 80 |
+
- **Build Command:** `pip install -r requirements.txt`
|
| 81 |
+
- **Start Command:** `gunicorn app:app`
|
| 82 |
+
- **Environment:** Python 3
|
| 83 |
+
|
| 84 |
+
### 4. أضف متغيرات البيئة
|
| 85 |
+
في لوحة Render → Environment → Add:
|
| 86 |
+
```
|
| 87 |
+
ANTHROPIC_API_KEY = sk-ant-api03-XXXXXXXXXXXXXXXXXXXXXXXX
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### 5. انشر! 🎉
|
| 91 |
+
سيعطيك Render رابطاً مثل: `https://sentiment-analyzer.onrender.com`
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## 🔒 أمان API Key
|
| 96 |
+
|
| 97 |
+
| ✅ آمن | ❌ خطر |
|
| 98 |
+
|--------|--------|
|
| 99 |
+
| متغير بيئة `.env` | في الكود مباشرة |
|
| 100 |
+
| Environment Variables في Render | في ملف HTML |
|
| 101 |
+
| Backend فقط يرى المفتاح | في Git/GitHub |
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 🛠️ التقنيات المستخدمة
|
| 106 |
+
|
| 107 |
+
- **Backend:** Python, Flask, Anthropic SDK
|
| 108 |
+
- **Frontend:** HTML, CSS, JavaScript
|
| 109 |
+
- **AI Model:** Claude claude-sonnet-4-20250514
|
| 110 |
+
- **Deployment:** Render / Railway / Heroku
|
| 111 |
+
- **NLP Task:** Multilingual Sentiment Analysis
|
| 112 |
+
|
| 113 |
+
---
|
| 114 |
+
|
| 115 |
+
## 📊 API Endpoints
|
| 116 |
+
|
| 117 |
+
| Method | Route | Description |
|
| 118 |
+
|--------|-------|-------------|
|
| 119 |
+
| GET | `/` | الصفحة الرئيسية |
|
| 120 |
+
| POST | `/api/analyze` | تحليل النص |
|
| 121 |
+
| GET | `/api/health` | فحص حالة السيرفر |
|
| 122 |
+
|
| 123 |
+
### مثال على طلب API:
|
| 124 |
+
```bash
|
| 125 |
+
curl -X POST http://localhost:5000/api/analyze \
|
| 126 |
+
-H "Content-Type: application/json" \
|
| 127 |
+
-d '{"text": "المنتج رائع!", "text_lang": "ar", "response_lang": "Arabic"}'
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
## 👨💻 المطوّر
|
| 133 |
+
|
| 134 |
+
مشروع تعليمي في إطار تعلم **Data Science & NLP**
|
app.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API d'Analyse de Sentiment - Serveur Backend Flask
|
| 3 |
+
Utilise Hugging Face Transformers (100% gratuit, fonctionne hors ligne)
|
| 4 |
+
- Modèle anglais : distilbert-base-uncased-finetuned-sst-2-english
|
| 5 |
+
- Modèle multilingue : tabularisai/multilingual-sentiment-analysis
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from flask import Flask, request, jsonify, render_template
|
| 9 |
+
from flask_cors import CORS
|
| 10 |
+
from transformers import pipeline
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# Initialisation de l'application Flask
|
| 14 |
+
app = Flask(__name__)
|
| 15 |
+
|
| 16 |
+
# Autoriser les requêtes cross-origin (depuis le frontend)
|
| 17 |
+
CORS(app)
|
| 18 |
+
|
| 19 |
+
# ─────────────────────────────────────────
|
| 20 |
+
# Chargement des modèles Hugging Face
|
| 21 |
+
# Les modèles sont téléchargés automatiquement au premier lancement
|
| 22 |
+
# puis mis en cache sur le disque pour les prochaines fois
|
| 23 |
+
# ─────────────────────────────────────────
|
| 24 |
+
print("⏳ Chargement des modèles... (première fois = quelques minutes)")
|
| 25 |
+
|
| 26 |
+
# Modèle 1 : Anglais uniquement - rapide et précis
|
| 27 |
+
model_en = pipeline(
|
| 28 |
+
"sentiment-analysis",
|
| 29 |
+
model="distilbert-base-uncased-finetuned-sst-2-english"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Modèle 2 : Multilingue - supporte arabe, français, anglais, espagnol...
|
| 33 |
+
model_multi = pipeline(
|
| 34 |
+
"sentiment-analysis",
|
| 35 |
+
model="tabularisai/multilingual-sentiment-analysis"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
print("✅ Modèles chargés avec succès !")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ─────────────────────────────────────────
|
| 42 |
+
# Fonction utilitaire : normaliser le résultat Hugging Face
|
| 43 |
+
# Les modèles retournent des labels différents, on les unifie ici
|
| 44 |
+
# ─────────────────────────────────────────
|
| 45 |
+
def normalize_result(hf_result, text_lang):
|
| 46 |
+
"""
|
| 47 |
+
Convertit le résultat brut de Hugging Face en format unifié pour le frontend.
|
| 48 |
+
hf_result : liste retournée par le pipeline, ex: [{'label': 'POSITIVE', 'score': 0.99}]
|
| 49 |
+
text_lang : langue du texte pour choisir les labels d'affichage
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
label_raw = hf_result[0]["label"].upper()
|
| 53 |
+
score = round(hf_result[0]["score"] * 100)
|
| 54 |
+
|
| 55 |
+
# Mapping des labels vers notre format unifié
|
| 56 |
+
if any(p in label_raw for p in ["POSITIVE", "POS", "5 STARS", "4 STARS"]):
|
| 57 |
+
sentiment = "positive"
|
| 58 |
+
emoji = "😊"
|
| 59 |
+
pos_score = score
|
| 60 |
+
neg_score = 100 - score
|
| 61 |
+
neu_score = 0
|
| 62 |
+
elif any(p in label_raw for p in ["NEGATIVE", "NEG", "1 STAR", "2 STARS"]):
|
| 63 |
+
sentiment = "negative"
|
| 64 |
+
emoji = "😠"
|
| 65 |
+
pos_score = 100 - score
|
| 66 |
+
neg_score = score
|
| 67 |
+
neu_score = 0
|
| 68 |
+
else:
|
| 69 |
+
sentiment = "neutral"
|
| 70 |
+
emoji = "😐"
|
| 71 |
+
pos_score = 30
|
| 72 |
+
neg_score = 30
|
| 73 |
+
neu_score = score
|
| 74 |
+
|
| 75 |
+
# Labels multilingues selon la langue de l'interface
|
| 76 |
+
labels = {
|
| 77 |
+
"ar": {"positive": "إيجابي", "negative": "سلبي", "neutral": "محايد", "mixed": "مختلط"},
|
| 78 |
+
"fr": {"positive": "Positif", "negative": "Négatif", "neutral": "Neutre", "mixed": "Mixte"},
|
| 79 |
+
"en": {"positive": "Positive", "negative": "Negative", "neutral": "Neutral", "mixed": "Mixed"},
|
| 80 |
+
"es": {"positive": "Positivo", "negative": "Negativo", "neutral": "Neutral", "mixed": "Mixto"},
|
| 81 |
+
"de": {"positive": "Positiv", "negative": "Negativ", "neutral": "Neutral", "mixed": "Gemischt"},
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# Langue d'affichage (par défaut français)
|
| 85 |
+
ui_lang = text_lang if text_lang in labels else "fr"
|
| 86 |
+
lang_map = labels[ui_lang]
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"sentiment" : sentiment,
|
| 90 |
+
"emoji" : emoji,
|
| 91 |
+
"label" : lang_map[sentiment],
|
| 92 |
+
"sub" : f"Score de confiance : {score}%",
|
| 93 |
+
"scores" : {
|
| 94 |
+
"positive" : pos_score,
|
| 95 |
+
"negative" : neg_score,
|
| 96 |
+
"neutral" : neu_score
|
| 97 |
+
},
|
| 98 |
+
"analysis" : f"Le modèle a détecté un sentiment {lang_map[sentiment].lower()} "
|
| 99 |
+
f"avec un score de confiance de {score}%. "
|
| 100 |
+
f"Ce résultat est basé sur l'analyse locale via Hugging Face Transformers.",
|
| 101 |
+
"keywords" : [], # Hugging Face ne retourne pas de mots-clés
|
| 102 |
+
"business" : f"Un sentiment {lang_map[sentiment].lower()} peut être utilisé "
|
| 103 |
+
f"pour prioriser les retours clients et adapter la stratégie commerciale.",
|
| 104 |
+
"detected_lang" : text_lang if text_lang != "auto" else "Détection automatique"
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ─────────────────────────────────────────
|
| 109 |
+
# Route principale : affiche la page HTML
|
| 110 |
+
# ─────────────────────────────────────────
|
| 111 |
+
@app.route("/")
|
| 112 |
+
def index():
|
| 113 |
+
return render_template("index.html")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ───────────────���─────────────────────────
|
| 117 |
+
# Route API : analyse du sentiment
|
| 118 |
+
# Méthode : POST
|
| 119 |
+
# Corps attendu : { "text": "...", "text_lang": "...", "model": "en"|"multi" }
|
| 120 |
+
# ─────────────────────────────────────────
|
| 121 |
+
@app.route("/api/analyze", methods=["POST"])
|
| 122 |
+
def analyze():
|
| 123 |
+
# Récupération des données JSON envoyées par le frontend
|
| 124 |
+
data = request.get_json()
|
| 125 |
+
|
| 126 |
+
# Vérification que le texte est bien présent
|
| 127 |
+
if not data or "text" not in data:
|
| 128 |
+
return jsonify({"error": "Aucun texte fourni"}), 400
|
| 129 |
+
|
| 130 |
+
# Extraction des paramètres
|
| 131 |
+
text = data.get("text", "").strip()
|
| 132 |
+
text_lang = data.get("text_lang", "auto")
|
| 133 |
+
model_type = data.get("model", "multi") # "en" ou "multi"
|
| 134 |
+
|
| 135 |
+
# Vérification que le texte n'est pas vide
|
| 136 |
+
if not text:
|
| 137 |
+
return jsonify({"error": "Le texte est vide"}), 400
|
| 138 |
+
|
| 139 |
+
# Limitation de la taille du texte (les modèles ont une limite de tokens)
|
| 140 |
+
if len(text) > 512:
|
| 141 |
+
text = text[:512] # On tronque silencieusement
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
# Choix du modèle selon le paramètre reçu
|
| 145 |
+
if model_type == "en":
|
| 146 |
+
# Modèle anglais : rapide, très précis pour l'anglais
|
| 147 |
+
hf_result = model_en(text)
|
| 148 |
+
else:
|
| 149 |
+
# Modèle multilingue : supporte arabe, français, espagnol, etc.
|
| 150 |
+
hf_result = model_multi(text)
|
| 151 |
+
|
| 152 |
+
# Normalisation et formatage du résultat
|
| 153 |
+
result = normalize_result(hf_result, text_lang)
|
| 154 |
+
|
| 155 |
+
# Ajout du modèle utilisé dans la réponse (utile pour le debug)
|
| 156 |
+
result["model_used"] = "English (DistilBERT)" if model_type == "en" else "Multilingue"
|
| 157 |
+
|
| 158 |
+
return jsonify(result)
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return jsonify({"error": str(e)}), 500
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ─────────────────────────────────────────
|
| 165 |
+
# Route API : vérification de l'état du serveur
|
| 166 |
+
# ─────────────────────────────────────────
|
| 167 |
+
@app.route("/api/health", methods=["GET"])
|
| 168 |
+
def health():
|
| 169 |
+
return jsonify({
|
| 170 |
+
"status" : "ok",
|
| 171 |
+
"models" : {
|
| 172 |
+
"english" : "distilbert-base-uncased-finetuned-sst-2-english",
|
| 173 |
+
"multilingual": "tabularisai/multilingual-sentiment-analysis"
|
| 174 |
+
}
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# ─────────────────────────────────────────
|
| 179 |
+
# Point d'entrée : démarrage du serveur
|
| 180 |
+
# ─────────────────────────────────────────
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
port = int(os.environ.get("PORT", 5000))
|
| 183 |
+
debug = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
|
| 184 |
+
print(f"🚀 Serveur démarré sur http://localhost:{port}")
|
| 185 |
+
app.run(host="0.0.0.0", port=port, debug=debug)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.0.3
|
| 2 |
+
flask-cors==4.0.1
|
| 3 |
+
anthropic==0.34.0
|
| 4 |
+
python-dotenv==1.0.1
|
| 5 |
+
gunicorn==22.0.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>محلل المشاعر | Sentiment Analyzer</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root{--bg:#0a0a0f;--surface:#12121a;--surface2:#1a1a26;--border:#2a2a3d;--accent:#7c6af7;--accent2:#f76a8a;--accent3:#6af7c0;--text:#e8e8f0;--text-dim:#7a7a9a;--pos:#4ade80;--neg:#f87171;--neu:#facc15;}
|
| 10 |
+
*{margin:0;padding:0;box-sizing:border-box;}
|
| 11 |
+
body{background:var(--bg);color:var(--text);font-family:'Cairo',sans-serif;min-height:100vh;overflow-x:hidden;}
|
| 12 |
+
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 60% 40% at 20% 20%,rgba(124,106,247,.12) 0%,transparent 60%),radial-gradient(ellipse 50% 50% at 80% 80%,rgba(247,106,138,.08) 0%,transparent 60%);pointer-events:none;z-index:0;}
|
| 13 |
+
.container{max-width:900px;margin:0 auto;padding:36px 20px;position:relative;z-index:1;}
|
| 14 |
+
.ui-lang-bar{display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap;margin-bottom:26px;}
|
| 15 |
+
.ui-lang-bar button{background:var(--surface2);border:1px solid var(--border);color:var(--text-dim);padding:5px 12px;border-radius:8px;cursor:pointer;font-size:12px;font-family:'Cairo',sans-serif;transition:all .2s;}
|
| 16 |
+
.ui-lang-bar button.active{background:rgba(124,106,247,.15);border-color:var(--accent);color:var(--accent);}
|
| 17 |
+
.ui-lang-bar button:hover{border-color:var(--accent);color:var(--accent);}
|
| 18 |
+
header{text-align:center;margin-bottom:42px;}
|
| 19 |
+
.badge{display:inline-block;font-family:'Space Mono',monospace;font-size:11px;color:var(--accent);border:1px solid var(--accent);padding:4px 14px;border-radius:20px;letter-spacing:2px;margin-bottom:16px;}
|
| 20 |
+
h1{font-size:clamp(2rem,5vw,3.4rem);font-weight:900;line-height:1.1;}
|
| 21 |
+
h1 span{background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
|
| 22 |
+
.subtitle{color:var(--text-dim);margin-top:12px;font-size:1rem;}
|
| 23 |
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:28px 32px;margin-bottom:22px;}
|
| 24 |
+
.card-label{font-size:12px;font-family:'Space Mono',monospace;color:var(--text-dim);letter-spacing:2px;text-transform:uppercase;margin-bottom:12px;display:flex;align-items:center;gap:8px;}
|
| 25 |
+
.card-label::before{content:'';width:6px;height:6px;background:var(--accent);border-radius:50%;}
|
| 26 |
+
.text-lang-grid{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:16px;}
|
| 27 |
+
.tlang-btn{background:var(--surface2);border:1px solid var(--border);color:var(--text-dim);padding:5px 11px;border-radius:8px;cursor:pointer;font-family:'Cairo',sans-serif;font-size:12px;transition:all .2s;}
|
| 28 |
+
.tlang-btn.active{background:rgba(124,106,247,.15);border-color:var(--accent);color:var(--accent);}
|
| 29 |
+
.tlang-btn:hover{border-color:var(--accent);color:var(--accent);}
|
| 30 |
+
textarea{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-family:'Cairo',sans-serif;font-size:1rem;padding:16px;resize:vertical;min-height:140px;transition:border-color .3s;outline:none;direction:auto;}
|
| 31 |
+
textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,106,247,.15);}
|
| 32 |
+
textarea::placeholder{color:var(--text-dim);}
|
| 33 |
+
.examples{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}
|
| 34 |
+
.example-chip{background:var(--surface2);border:1px solid var(--border);color:var(--text-dim);padding:5px 12px;border-radius:20px;font-size:12px;cursor:pointer;transition:all .2s;font-family:'Cairo',sans-serif;}
|
| 35 |
+
.example-chip:hover{border-color:var(--accent3);color:var(--accent3);}
|
| 36 |
+
.analyze-btn{width:100%;padding:15px;background:linear-gradient(135deg,var(--accent),#9c6af7);border:none;border-radius:12px;color:white;font-family:'Cairo',sans-serif;font-size:1.1rem;font-weight:700;cursor:pointer;transition:all .3s;margin-top:20px;}
|
| 37 |
+
.analyze-btn:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 30px rgba(124,106,247,.4);}
|
| 38 |
+
.analyze-btn:disabled{opacity:.6;cursor:not-allowed;}
|
| 39 |
+
.loader{display:none;align-items:center;justify-content:center;gap:6px;padding:20px;}
|
| 40 |
+
.loader.active{display:flex;}
|
| 41 |
+
.dot{width:8px;height:8px;background:var(--accent);border-radius:50%;animation:bounce .8s infinite;}
|
| 42 |
+
.dot:nth-child(2){animation-delay:.15s;background:var(--accent2);}
|
| 43 |
+
.dot:nth-child(3){animation-delay:.3s;background:var(--accent3);}
|
| 44 |
+
.result-card{display:none;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:32px;}
|
| 45 |
+
.result-card.visible{display:block;}
|
| 46 |
+
.sentiment-main{display:flex;align-items:center;gap:20px;margin-bottom:26px;flex-wrap:wrap;}
|
| 47 |
+
.sentiment-emoji{font-size:4rem;}
|
| 48 |
+
.sentiment-info{flex:1;}
|
| 49 |
+
.sentiment-label{font-size:1.8rem;font-weight:900;margin-bottom:4px;}
|
| 50 |
+
.sentiment-label.positive{color:var(--pos);}
|
| 51 |
+
.sentiment-label.negative{color:var(--neg);}
|
| 52 |
+
.sentiment-label.neutral{color:var(--neu);}
|
| 53 |
+
.sentiment-label.mixed{color:var(--accent);}
|
| 54 |
+
.sentiment-sub{color:var(--text-dim);font-size:.9rem;}
|
| 55 |
+
.bar-wrap{background:var(--surface2);border-radius:4px;height:6px;margin:4px 0 12px;overflow:hidden;}
|
| 56 |
+
.bar-fill{height:100%;border-radius:4px;transition:width 1s cubic-bezier(.4,0,.2,1);}
|
| 57 |
+
.analysis-section{background:var(--surface2);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:14px;}
|
| 58 |
+
.analysis-section h3{font-size:13px;font-family:'Space Mono',monospace;color:var(--accent);letter-spacing:1px;margin-bottom:10px;}
|
| 59 |
+
.analysis-section p{color:var(--text-dim);font-size:.95rem;line-height:1.7;}
|
| 60 |
+
.keywords{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;}
|
| 61 |
+
.keyword{padding:4px 12px;border-radius:20px;font-size:13px;}
|
| 62 |
+
.keyword.pos{background:rgba(74,222,128,.1);color:var(--pos);border:1px solid rgba(74,222,128,.3);}
|
| 63 |
+
.keyword.neg{background:rgba(248,113,113,.1);color:var(--neg);border:1px solid rgba(248,113,113,.3);}
|
| 64 |
+
.keyword.neu{background:rgba(250,204,21,.1);color:var(--neu);border:1px solid rgba(250,204,21,.3);}
|
| 65 |
+
.history-section{margin-top:32px;}
|
| 66 |
+
.history-title{font-size:13px;font-family:'Space Mono',monospace;color:var(--text-dim);letter-spacing:2px;margin-bottom:14px;}
|
| 67 |
+
.history-list{display:flex;flex-direction:column;gap:10px;}
|
| 68 |
+
.history-item{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:13px 18px;display:flex;align-items:center;gap:14px;cursor:pointer;transition:border-color .2s;}
|
| 69 |
+
.history-item:hover{border-color:var(--accent);}
|
| 70 |
+
.history-emoji{font-size:1.4rem;}
|
| 71 |
+
.history-text{flex:1;font-size:.88rem;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 72 |
+
.history-sentiment{font-size:12px;font-family:'Space Mono',monospace;white-space:nowrap;}
|
| 73 |
+
.history-sentiment.positive{color:var(--pos);}
|
| 74 |
+
.history-sentiment.negative{color:var(--neg);}
|
| 75 |
+
.history-sentiment.neutral{color:var(--neu);}
|
| 76 |
+
.history-sentiment.mixed{color:var(--accent);}
|
| 77 |
+
.error-msg{display:none;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.3);color:var(--neg);padding:13px 18px;border-radius:12px;margin-top:14px;font-size:.9rem;}
|
| 78 |
+
.error-msg.visible{display:block;}
|
| 79 |
+
footer{text-align:center;margin-top:50px;color:var(--text-dim);font-size:12px;font-family:'Space Mono',monospace;}
|
| 80 |
+
footer span{color:var(--accent);}
|
| 81 |
+
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
| 82 |
+
</style>
|
| 83 |
+
</head>
|
| 84 |
+
<body>
|
| 85 |
+
<div class="container">
|
| 86 |
+
<div class="ui-lang-bar" id="uiLangBar">
|
| 87 |
+
<button onclick="setUiLang('ar',this)" class="active">🇩🇿 العربية</button>
|
| 88 |
+
<button onclick="setUiLang('en',this)">🇬🇧 English</button>
|
| 89 |
+
<button onclick="setUiLang('fr',this)">🇫🇷 Français</button>
|
| 90 |
+
<button onclick="setUiLang('es',this)">🇪🇸 Español</button>
|
| 91 |
+
<button onclick="setUiLang('de',this)">🇩🇪 Deutsch</button>
|
| 92 |
+
<button onclick="setUiLang('it',this)">🇮🇹 Italiano</button>
|
| 93 |
+
<button onclick="setUiLang('pt',this)">🇧🇷 Português</button>
|
| 94 |
+
<button onclick="setUiLang('ru',this)">🇷🇺 Русский</button>
|
| 95 |
+
<button onclick="setUiLang('zh',this)">🇨🇳 中文</button>
|
| 96 |
+
<button onclick="setUiLang('ja',this)">🇯🇵 日本語</button>
|
| 97 |
+
<button onclick="setUiLang('tr',this)">🇹🇷 Türkçe</button>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<header>
|
| 101 |
+
<div class="badge" id="badge">NLP · تحليل المشاعر</div>
|
| 102 |
+
<h1 id="mainTitle">محلل <span id="titleSpan">المشاعر</span></h1>
|
| 103 |
+
<p class="subtitle" id="subtitle">حلل مشاعر أي نص بأكثر من 20 لغة باستخدام الذكاء الاصطناعي</p>
|
| 104 |
+
</header>
|
| 105 |
+
|
| 106 |
+
<div class="card">
|
| 107 |
+
<div class="card-label" id="lblTextLang">لغة النص المُدخل</div>
|
| 108 |
+
<div class="text-lang-grid">
|
| 109 |
+
<button class="tlang-btn active" onclick="setTextLang('auto',this)">🌐 Auto</button>
|
| 110 |
+
<button class="tlang-btn" onclick="setTextLang('ar',this)">🇸🇦 العربية</button>
|
| 111 |
+
<button class="tlang-btn" onclick="setTextLang('en',this)">🇬🇧 English</button>
|
| 112 |
+
<button class="tlang-btn" onclick="setTextLang('fr',this)">🇫🇷 Français</button>
|
| 113 |
+
<button class="tlang-btn" onclick="setTextLang('es',this)">🇪🇸 Español</button>
|
| 114 |
+
<button class="tlang-btn" onclick="setTextLang('de',this)">🇩🇪 Deutsch</button>
|
| 115 |
+
<button class="tlang-btn" onclick="setTextLang('it',this)">🇮🇹 Italiano</button>
|
| 116 |
+
<button class="tlang-btn" onclick="setTextLang('pt',this)">🇧🇷 Português</button>
|
| 117 |
+
<button class="tlang-btn" onclick="setTextLang('ru',this)">🇷🇺 Русский</button>
|
| 118 |
+
<button class="tlang-btn" onclick="setTextLang('zh',this)">🇨🇳 中文</button>
|
| 119 |
+
<button class="tlang-btn" onclick="setTextLang('ja',this)">🇯🇵 日本語</button>
|
| 120 |
+
<button class="tlang-btn" onclick="setTextLang('ko',this)">🇰🇷 한국어</button>
|
| 121 |
+
<button class="tlang-btn" onclick="setTextLang('tr',this)">🇹🇷 Türkçe</button>
|
| 122 |
+
<button class="tlang-btn" onclick="setTextLang('nl',this)">🇳🇱 Nederlands</button>
|
| 123 |
+
<button class="tlang-btn" onclick="setTextLang('fa',this)">🇮🇷 فارسی</button>
|
| 124 |
+
<button class="tlang-btn" onclick="setTextLang('hi',this)">🇮🇳 हिंदी</button>
|
| 125 |
+
<button class="tlang-btn" onclick="setTextLang('he',this)">🇮🇱 עברית</button>
|
| 126 |
+
<button class="tlang-btn" onclick="setTextLang('uk',this)">🇺🇦 Українська</button>
|
| 127 |
+
<button class="tlang-btn" onclick="setTextLang('id',this)">🇮🇩 Indonesia</button>
|
| 128 |
+
<button class="tlang-btn" onclick="setTextLang('th',this)">🇹🇭 ภาษาไทย</button>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<div class="card-label" id="lblInputText">النص المراد تحليله</div>
|
| 132 |
+
<textarea id="inputText" rows="5" placeholder="اكتب أو الصق نصك هنا..."></textarea>
|
| 133 |
+
|
| 134 |
+
<div class="card-label" style="margin-top:16px;" id="lblExamples">أمثلة سريعة</div>
|
| 135 |
+
<div class="examples" id="examplesContainer"></div>
|
| 136 |
+
|
| 137 |
+
<button class="analyze-btn" id="analyzeBtn" onclick="analyze()">
|
| 138 |
+
<span id="btnText">🔍 تحليل المشاعر</span>
|
| 139 |
+
</button>
|
| 140 |
+
<div class="error-msg" id="errorMsg"></div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div class="loader" id="loader">
|
| 144 |
+
<div class="dot"></div><div class="dot"></div><div class="dot"></div>
|
| 145 |
+
<span id="loadingText" style="color:var(--text-dim);font-size:14px;margin-right:8px;">جاري التحليل...</span>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<div class="result-card" id="resultCard">
|
| 149 |
+
<div class="card-label" id="lblResult">نتيجة التحليل</div>
|
| 150 |
+
<div class="sentiment-main">
|
| 151 |
+
<div class="sentiment-emoji" id="resEmoji">😊</div>
|
| 152 |
+
<div class="sentiment-info">
|
| 153 |
+
<div class="sentiment-label" id="resLabel"></div>
|
| 154 |
+
<div class="sentiment-sub" id="resSub"></div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
<div style="margin-bottom:24px;">
|
| 158 |
+
<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span id="lblPos" style="font-size:13px;color:var(--pos)">إيجابي</span><span id="posVal" style="font-family:'Space Mono',monospace;font-size:13px;color:var(--pos)">0%</span></div>
|
| 159 |
+
<div class="bar-wrap"><div class="bar-fill" id="posBar" style="background:var(--pos);width:0%"></div></div>
|
| 160 |
+
<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span id="lblNeg" style="font-size:13px;color:var(--neg)">سلبي</span><span id="negVal" style="font-family:'Space Mono',monospace;font-size:13px;color:var(--neg)">0%</span></div>
|
| 161 |
+
<div class="bar-wrap"><div class="bar-fill" id="negBar" style="background:var(--neg);width:0%"></div></div>
|
| 162 |
+
<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span id="lblNeu" style="font-size:13px;color:var(--neu)">محايد</span><span id="neuVal" style="font-family:'Space Mono',monospace;font-size:13px;color:var(--neu)">0%</span></div>
|
| 163 |
+
<div class="bar-wrap"><div class="bar-fill" id="neuBar" style="background:var(--neu);width:0%"></div></div>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="analysis-section"><h3 id="lblAnalysis">📊 التحليل التفصيلي</h3><p id="resAnalysis"></p></div>
|
| 166 |
+
<div class="analysis-section"><h3 id="lblKeywords">🔑 الكلمات المفتاحية</h3><div class="keywords" id="resKeywords"></div></div>
|
| 167 |
+
<div class="analysis-section"><h3 id="lblBusiness">💡 الاستخدام التجاري</h3><p id="resBusiness"></p></div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<div class="history-section" id="historySection" style="display:none;">
|
| 171 |
+
<div class="history-title" id="historyTitle">// سجل التحليلات</div>
|
| 172 |
+
<div class="history-list" id="historyList"></div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<footer><span id="footerBuilt">مبني بـ</span> <span>Claude AI + Flask</span> · <span id="footerSub">مشروع NLP</span></footer>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<script>
|
| 179 |
+
const UI={ar:{dir:'rtl',lang:'ar',badge:'NLP · تحليل المشاعر',title:'محلل',titleSpan:'المشاعر',subtitle:'حلل مشاعر أي نص بأكثر من 20 لغة باستخدام الذكاء الاصطناعي',lblTextLang:'لغة النص المُدخل',lblInputText:'النص المراد تحليله',lblExamples:'أمثلة سريعة',placeholder:'اكتب أو الصق نصك هنا...',btnText:'🔍 تحليل المشاعر',loadingText:'جاري التحليل...',lblResult:'نتيجة التحليل',lblPos:'إيجابي',lblNeg:'سلبي',lblNeu:'محايد',lblAnalysis:'📊 التحليل التفصيلي',lblKeywords:'🔑 الكلمات المفتاحية',lblBusiness:'💡 الاستخدام التجاري',historyTitle:'// سجل التحليلات',footerBuilt:'مبني بـ',footerSub:'مشروع NLP',errEmpty:'⚠️ الرجاء إدخال نص للتحليل',errFail:'❌ حدث خطأ في السيرفر. تأكد من تشغيله.',responseLang:'Arabic',examples:[{text:'المنتج ممتاز والتوصيل سريع!',label:'إيجابي 😊'},{text:'خدمة سيئة لم تحل مشكلتي!',label:'سلبي 😠'},{text:'The product is okay, nothing special.',label:'محايد EN 😐'},{text:'Je suis très satisfait!',label:'إيجابي FR 🌟'},{text:'بعض الجوانب جيدة وأخرى سيئة',label:'مختلط 🤔'}]},en:{dir:'ltr',lang:'en',badge:'NLP · SENTIMENT ANALYSIS',title:'Sentiment',titleSpan:'Analyzer',subtitle:'Analyze the sentiment of any text in 20+ languages using AI',lblTextLang:'Input text language',lblInputText:'Text to analyze',lblExamples:'Quick examples',placeholder:'Type or paste your text here...',btnText:'🔍 Analyze Sentiment',loadingText:'Analyzing...',lblResult:'Analysis Result',lblPos:'Positive',lblNeg:'Negative',lblNeu:'Neutral',lblAnalysis:'📊 Detailed Analysis',lblKeywords:'🔑 Keywords',lblBusiness:'💡 Business Insight',historyTitle:'// ANALYSIS HISTORY',footerBuilt:'Built with',footerSub:'NLP Project',errEmpty:'⚠️ Please enter text to analyze.',errFail:'❌ Server error. Make sure the Flask server is running.',responseLang:'English',examples:[{text:'The product is amazing, fast delivery!',label:'Positive 😊'},{text:'Terrible service, never solved my issue!',label:'Negative 😠'},{text:'المنتج جيد لكن التوصيل بطيء',label:'Mixed AR 🤔'},{text:'Je suis très satisfait!',label:'Positive FR 🌟'},{text:'Das Produkt ist in Ordnung.',label:'Neutral DE 😐'}]},fr:{dir:'ltr',lang:'fr',badge:'NLP · ANALYSE DE SENTIMENT',title:'Analyseur de',titleSpan:'Sentiments',subtitle:"Analysez le sentiment de n'importe quel texte en 20+ langues",lblTextLang:'Langue du texte',lblInputText:'Texte à analyser',lblExamples:'Exemples',placeholder:'Tapez votre texte ici...',btnText:'🔍 Analyser',loadingText:'Analyse...',lblResult:'Résultat',lblPos:'Positif',lblNeg:'Négatif',lblNeu:'Neutre',lblAnalysis:'📊 Analyse',lblKeywords:'🔑 Mots-clés',lblBusiness:'💡 Usage commercial',historyTitle:'// HISTORIQUE',footerBuilt:'Créé avec',footerSub:'Projet NLP',errEmpty:'⚠️ Veuillez saisir un texte.',errFail:'❌ Erreur serveur. Vérifiez que Flask tourne.',responseLang:'French',examples:[{text:'Produit excellent, livraison rapide!',label:'Positif 😊'},{text:'Service horrible, problème non résolu!',label:'Négatif 😠'},{text:'The product is okay.',label:'Neutre EN 😐'},{text:'المنتج ممتاز!',label:'Positif AR 🌟'},{text:'Certains aspects bons, autres mauvais.',label:'Mixte 🤔'}]}};
|
| 180 |
+
|
| 181 |
+
let currentUiLang='ar',selectedTextLang='auto';
|
| 182 |
+
const analysisHistory=[];
|
| 183 |
+
|
| 184 |
+
function setUiLang(lang,btn){
|
| 185 |
+
currentUiLang=lang;const t=UI[lang];
|
| 186 |
+
document.documentElement.lang=t.lang;document.documentElement.dir=t.dir;
|
| 187 |
+
document.querySelectorAll('#uiLangBar button').forEach(b=>b.classList.remove('active'));btn.classList.add('active');
|
| 188 |
+
document.getElementById('badge').textContent=t.badge;
|
| 189 |
+
document.getElementById('mainTitle').childNodes[0].nodeValue=t.title+' ';
|
| 190 |
+
document.getElementById('titleSpan').textContent=t.titleSpan;
|
| 191 |
+
document.getElementById('subtitle').textContent=t.subtitle;
|
| 192 |
+
document.getElementById('inputText').placeholder=t.placeholder;
|
| 193 |
+
document.getElementById('btnText').textContent=t.btnText;
|
| 194 |
+
document.getElementById('loadingText').textContent=t.loadingText;
|
| 195 |
+
document.getElementById('lblPos').textContent=t.lblPos;document.getElementById('lblNeg').textContent=t.lblNeg;document.getElementById('lblNeu').textContent=t.lblNeu;
|
| 196 |
+
document.getElementById('lblAnalysis').textContent=t.lblAnalysis;document.getElementById('lblKeywords').textContent=t.lblKeywords;document.getElementById('lblBusiness').textContent=t.lblBusiness;
|
| 197 |
+
document.getElementById('historyTitle').textContent=t.historyTitle;document.getElementById('footerBuilt').textContent=t.footerBuilt;document.getElementById('footerSub').textContent=t.footerSub;
|
| 198 |
+
['lblTextLang','lblInputText','lblExamples','lblResult'].forEach(id=>{
|
| 199 |
+
const labels={lblTextLang:t.lblTextLang,lblInputText:t.lblInputText,lblExamples:t.lblExamples,lblResult:t.lblResult};
|
| 200 |
+
document.getElementById(id).innerHTML=`<span style="width:6px;height:6px;background:var(--accent);border-radius:50%;display:inline-block"></span> ${labels[id]}`;
|
| 201 |
+
});
|
| 202 |
+
renderExamples();
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function renderExamples(){
|
| 206 |
+
const c=document.getElementById('examplesContainer');c.innerHTML='';
|
| 207 |
+
UI[currentUiLang].examples.forEach(ex=>{const s=document.createElement('span');s.className='example-chip';s.textContent=ex.label;s.onclick=()=>{document.getElementById('inputText').value=ex.text;};c.appendChild(s);});
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function setTextLang(lang,btn){selectedTextLang=lang;document.querySelectorAll('.tlang-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');}
|
| 211 |
+
|
| 212 |
+
async function analyze(){
|
| 213 |
+
const t=UI[currentUiLang];
|
| 214 |
+
const text=document.getElementById('inputText').value.trim();
|
| 215 |
+
if(!text){showError(t.errEmpty);return;}
|
| 216 |
+
const btn=document.getElementById('analyzeBtn');const loader=document.getElementById('loader');
|
| 217 |
+
btn.disabled=true;loader.classList.add('active');
|
| 218 |
+
document.getElementById('resultCard').classList.remove('visible');document.getElementById('errorMsg').classList.remove('visible');
|
| 219 |
+
try{
|
| 220 |
+
// 🔑 الطلب يذهب إلى Backend (Flask) - المفتاح آمن على السيرفر
|
| 221 |
+
const res=await fetch('/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 222 |
+
body:JSON.stringify({text,text_lang:selectedTextLang,response_lang:t.responseLang})});
|
| 223 |
+
const result=await res.json();
|
| 224 |
+
if(!res.ok)throw new Error(result.error||'Server error');
|
| 225 |
+
displayResult(result,text);addToHistory(result,text);
|
| 226 |
+
}catch(err){showError(t.errFail+' ('+err.message+')');}
|
| 227 |
+
finally{btn.disabled=false;loader.classList.remove('active');}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function displayResult(r,text){
|
| 231 |
+
document.getElementById('resEmoji').textContent=r.emoji;
|
| 232 |
+
document.getElementById('resLabel').textContent=r.label;document.getElementById('resLabel').className=`sentiment-label ${r.sentiment}`;
|
| 233 |
+
document.getElementById('resSub').textContent=(r.detected_lang?`🌐 ${r.detected_lang} · `:'')+r.sub;
|
| 234 |
+
document.getElementById('resAnalysis').textContent=r.analysis;document.getElementById('resBusiness').textContent=r.business;
|
| 235 |
+
setTimeout(()=>{
|
| 236 |
+
document.getElementById('posBar').style.width=r.scores.positive+'%';document.getElementById('negBar').style.width=r.scores.negative+'%';document.getElementById('neuBar').style.width=r.scores.neutral+'%';
|
| 237 |
+
document.getElementById('posVal').textContent=r.scores.positive+'%';document.getElementById('negVal').textContent=r.scores.negative+'%';document.getElementById('neuVal').textContent=r.scores.neutral+'%';
|
| 238 |
+
},80);
|
| 239 |
+
const kw=document.getElementById('resKeywords');kw.innerHTML='';
|
| 240 |
+
(r.keywords||[]).forEach(k=>{const s=document.createElement('span');s.className=`keyword ${k.type}`;s.textContent=k.word;kw.appendChild(s);});
|
| 241 |
+
document.getElementById('resultCard').classList.add('visible');
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function addToHistory(r,text){
|
| 245 |
+
analysisHistory.unshift({result:r,text});if(analysisHistory.length>6)analysisHistory.pop();
|
| 246 |
+
document.getElementById('historySection').style.display='block';
|
| 247 |
+
const list=document.getElementById('historyList');list.innerHTML='';
|
| 248 |
+
analysisHistory.forEach(item=>{const div=document.createElement('div');div.className='history-item';div.innerHTML=`<span class="history-emoji">${item.result.emoji}</span><span class="history-text">${item.text}</span><span class="history-sentiment ${item.result.sentiment}">${item.result.label}</span>`;div.onclick=()=>{document.getElementById('inputText').value=item.text;displayResult(item.result,item.text);};list.appendChild(div);});
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
function showError(msg){const el=document.getElementById('errorMsg');el.textContent=msg;el.classList.add('visible');}
|
| 252 |
+
renderExamples();
|
| 253 |
+
document.addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')analyze();});
|
| 254 |
+
</script>
|
| 255 |
+
</body>
|
| 256 |
+
</html>
|