DATANESS commited on
Commit
b3dbee1
·
1 Parent(s): 5258d11
Files changed (6) hide show
  1. .gitignore +19 -0
  2. Procfile +1 -0
  3. README.md +134 -1
  4. app.py +185 -0
  5. requirements.txt +5 -0
  6. 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
- # sentiment_project
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>