CHAIMA commited on
Commit
8d8f5f1
·
0 Parent(s):

Initial clean commit

Browse files
.gitignore ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ venv/
3
+ .venv/
4
+ env/
5
+ .env/
6
+ bin/
7
+ lib/
8
+ share/
9
+ include/
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ *.so
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+
30
+ # Node
31
+ node_modules/
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+ .pnpm-debug.log*
36
+ .pnpm-error.log*
37
+ frontend/dist/
38
+ frontend/node_modules/
39
+
40
+ # OS
41
+ .DS_Store
42
+ .DS_Store?
43
+ ._*
44
+ .Spotlight-V100
45
+ .Trashes
46
+ ehthumbs.db
47
+ Thumbs.db
48
+
49
+ # Databases
50
+ *.sqlite3
51
+ backend/database.db
52
+
53
+ # Uploads
54
+ backend/uploads/*
55
+ !backend/uploads/.gitkeep
56
+
57
+ # IDEs
58
+ .vscode/
59
+ .idea/
60
+ *.swp
61
+ *.swo
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build Frontend
2
+ FROM node:20-slim AS frontend-builder
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ # Stage 2: Backend & Final Image
10
+ FROM python:3.9-slim
11
+
12
+ # Install system dependencies (FFmpeg is required for Whisper)
13
+ RUN apt-get update && apt-get install -y \
14
+ ffmpeg \
15
+ build-essential \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Set working directory
19
+ WORKDIR /app
20
+
21
+ # Copy backend requirements first for caching
22
+ COPY backend/requirements.txt .
23
+ RUN pip install --no-cache-dir -r requirements.txt
24
+
25
+ # Copy backend code
26
+ COPY backend /app/backend
27
+
28
+ # Copy built frontend from Stage 1
29
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
30
+
31
+ # Set environment variables
32
+ ENV PYTHONUNBUFFERED=1
33
+ ENV PORT=7860
34
+
35
+ # Expose the port (Hugging Face uses 7860 by default)
36
+ EXPOSE 7860
37
+
38
+ # Command to run the application
39
+ # We run from the backend directory so that imports work correctly
40
+ WORKDIR /app/backend
41
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: HEAREM - SentiCall AT
3
+ emoji: 🎙️
4
+ colorFrom: blue
5
+ colorTo: orange
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ # HEAREM (SentiCall AT)
12
+ ### Algerian Call Center Sentiment Analysis Platform
13
+
14
+ HEAREM is an enterprise-grade AI platform designed for **Algérie Télécom** to analyze customer interactions. It leverages state-of-the-art NLP models to transcribe Algerian Arabic (Darija) and French, detect sentiment, and extract key business topics.
15
+
16
+ ## 🚀 Features
17
+ - **Multi-lingual Transcription**: Powered by OpenAI Whisper for accurate speech-to-text.
18
+ - **Sentiment Analysis**: Real-time detection of customer emotions.
19
+ - **Topic Classification**: Automated tagging of business-related keywords.
20
+ - **Enterprise Dashboard**: Interactive analytics for managers and agents.
21
+
22
+ ## 🛠 Deployment on Hugging Face Spaces
23
+ This app is containerized using Docker and optimized for Hugging Face Spaces.
24
+
25
+ 1. **Create a New Space** on Hugging Face.
26
+ 2. Select **Docker** as the SDK (Blank template).
27
+ 3. Push the repository contents to the Space.
28
+ 4. The build process will automatically:
29
+ - Build the React frontend.
30
+ - Set up the Python FastAPI backend.
31
+ - Serve the full-stack application on port 7860.
32
+
33
+ ---
34
+ *Developed for Algérie Télécom Sentiment Analysis MVP.*
backend/call_center.db ADDED
Binary file (32.8 kB). View file
 
backend/database.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
3
+ from sqlalchemy.orm import declarative_base
4
+ from sqlalchemy.orm import sessionmaker
5
+ from datetime import datetime
6
+
7
+ DATABASE_URL = "sqlite:///./call_center.db"
8
+
9
+ engine = create_engine(
10
+ DATABASE_URL, connect_args={"check_same_thread": False}
11
+ )
12
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
13
+
14
+ Base = declarative_base()
15
+
16
+ class CallRecord(Base):
17
+ __tablename__ = "call_records"
18
+
19
+ id = Column(Integer, primary_key=True, index=True)
20
+ filename = Column(String, index=True)
21
+ transcript = Column(String)
22
+ sentiment = Column(String) # Positive, Neutral, Negative
23
+ confidence = Column(Float)
24
+ keywords = Column(String) # comma-separated string
25
+ agent_name = Column(String, default="Agent Inconnu") # Traceability
26
+ created_at = Column(DateTime, default=datetime.utcnow)
27
+
28
+ Base.metadata.create_all(bind=engine)
29
+
30
+ def get_db():
31
+ db = SessionLocal()
32
+ try:
33
+ yield db
34
+ finally:
35
+ db.close()
backend/main.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ from fastapi import FastAPI, UploadFile, File, Form, Depends, HTTPException
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from sqlalchemy.orm import Session
6
+ from datetime import datetime
7
+ import uuid
8
+
9
+ from database import engine, Base, get_db, CallRecord
10
+ from services.transcription import extract_transcript
11
+ from services.sentiment import analyze_sentiment
12
+ from services.keywords import extract_keywords
13
+
14
+ app = FastAPI(title="Algerian Call Center Sentiment MVP")
15
+
16
+ # Allow CORS for local frontend development
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ UPLOAD_DIR = "uploads"
26
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
27
+
28
+ @app.post("/analyze-call")
29
+ async def analyze_call(
30
+ file: UploadFile = File(...),
31
+ agent_name: str = Form("user"),
32
+ db: Session = Depends(get_db)
33
+ ):
34
+ """
35
+ Upload an audio file, process it through the pipeline, and save to DB.
36
+ """
37
+ if not file.filename.endswith(('.wav', '.mp3', '.ogg', '.m4a')):
38
+ raise HTTPException(status_code=400, detail="Invalid audio file format.")
39
+
40
+ # Generate unique filename to avoid collisions
41
+ file_id = str(uuid.uuid4())
42
+ extension = os.path.splitext(file.filename)[1]
43
+ safe_filename = f"{file_id}{extension}"
44
+ file_path = os.path.join(UPLOAD_DIR, safe_filename)
45
+
46
+ try:
47
+ # Save uploaded file
48
+ with open(file_path, "wb") as buffer:
49
+ shutil.copyfileobj(file.file, buffer)
50
+
51
+ # 1. Transcribe Audio
52
+ transcript = extract_transcript(file_path)
53
+
54
+ # 2. Analyze Sentiment
55
+ sentiment_result = analyze_sentiment(transcript)
56
+ sentiment_label = sentiment_result['label']
57
+ confidence = sentiment_result['score']
58
+
59
+ # 3. Extract Keywords
60
+ keywords = extract_keywords(transcript)
61
+
62
+ # 4. Save to Database
63
+ new_record = CallRecord(
64
+ filename=file.filename,
65
+ transcript=transcript,
66
+ sentiment=sentiment_label,
67
+ confidence=confidence,
68
+ keywords=str(keywords),
69
+ agent_name=agent_name
70
+ )
71
+ db.add(new_record)
72
+ db.commit()
73
+ db.refresh(new_record)
74
+
75
+ return {
76
+ "success": True,
77
+ "data": {
78
+ "id": new_record.id,
79
+ "filename": new_record.filename,
80
+ "transcript": new_record.transcript,
81
+ "sentiment": new_record.sentiment,
82
+ "confidence": new_record.confidence,
83
+ "keywords": new_record.keywords,
84
+ "created_at": new_record.created_at
85
+ }
86
+ }
87
+
88
+ except Exception as e:
89
+ raise HTTPException(status_code=500, detail=str(e))
90
+ finally:
91
+ # Optionally clean up the file after processing,
92
+ # or keep it if playback is a desired feature later
93
+ pass
94
+
95
+ from fastapi.staticfiles import StaticFiles
96
+ from fastapi.responses import FileResponse
97
+
98
+ # ... existing code ...
99
+
100
+ @app.get("/history")
101
+ def get_history(db: Session = Depends(get_db)):
102
+ """
103
+ Return all analyzed calls from history.
104
+ """
105
+ records = db.query(CallRecord).order_by(CallRecord.created_at.desc()).all()
106
+ return records
107
+
108
+ @app.get("/call/{call_id}")
109
+ def get_call(call_id: int, db: Session = Depends(get_db)):
110
+ """
111
+ Return details of a specific call.
112
+ """
113
+ record = db.query(CallRecord).filter(CallRecord.id == call_id).first()
114
+ if not record:
115
+ raise HTTPException(status_code=404, detail="Call not found")
116
+ return record
117
+
118
+ # Mount static files (ensure frontend/dist exists)
119
+ # In production, we'll build the frontend into this folder.
120
+ FRONTEND_PATH = "../frontend/dist"
121
+ if os.path.exists(FRONTEND_PATH):
122
+ app.mount("/", StaticFiles(directory=FRONTEND_PATH, html=True), name="frontend")
123
+
124
+ # Catch-all route to serve index.html for React Router
125
+ @app.get("/{full_path:path}")
126
+ async def serve_react_app(full_path: str):
127
+ return FileResponse(os.path.join(FRONTEND_PATH, "index.html"))
backend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ python-multipart==0.0.6
4
+ openai-whisper
5
+ transformers==4.35.2
6
+ torch>=2.0.0
7
+ scikit-learn==1.3.2
8
+ SQLAlchemy==2.0.23
9
+ pydantic==2.5.2
backend/seed.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from datetime import datetime, timedelta
3
+ from sqlalchemy.orm import Session
4
+ from database import engine, Base, SessionLocal, CallRecord
5
+ import uuid
6
+
7
+ # Initialize DB
8
+ Base.metadata.create_all(bind=engine)
9
+ db = SessionLocal()
10
+
11
+ # Clear existing if needed (optional)
12
+ # db.query(CallRecord).delete()
13
+
14
+ issues = ["Coupure internet", "Lenteur ADSL", "Facture non payée", "Modem Fibre", "Demande 4G", "Changement de mot de passe", "Renseignement", "Réclamation technique"]
15
+ sentiments = ["Positive", "Neutral", "Negative"]
16
+ weights = [0.15, 0.25, 0.60] # Heavy on negative/neutral for realism in a support center
17
+ agents = ["Amine B.", "Sarah M.", "Karim D.", "Nadia Y.", "Walid T."]
18
+
19
+ def get_random_date(days_back=30):
20
+ start_date = datetime.now() - timedelta(days=days_back)
21
+ random_days = random.random() * days_back
22
+ return start_date + timedelta(days=random_days)
23
+
24
+ def generate_mock_data(num_records=50):
25
+ print(f"Generating {num_records} mock call records for Algérie Télécom context...")
26
+
27
+ for _ in range(num_records):
28
+ sentiment = random.choices(sentiments, weights=weights)[0]
29
+ issue = random.choice(issues)
30
+ agent = random.choice(agents)
31
+
32
+ # Adjust confidence
33
+ confidence = random.uniform(0.75, 0.99)
34
+
35
+ # Transcript mock
36
+ transcript = f"[Transcript simulé] Le client appelle concernant: {issue}. L'agent {agent} a répondu."
37
+
38
+ record = CallRecord(
39
+ filename=f"appel_at_{uuid.uuid4().hex[:6]}.wav",
40
+ transcript=transcript,
41
+ sentiment=sentiment,
42
+ confidence=confidence,
43
+ keywords=issue,
44
+ agent_name=agent,
45
+ created_at=get_random_date()
46
+ )
47
+ db.add(record)
48
+
49
+ db.commit()
50
+ print("Mock data generated successfully!")
51
+
52
+ if __name__ == "__main__":
53
+ generate_mock_data(75) # Generate 75 mock calls
backend/services/keywords.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline
2
+
3
+ # Use a powerful multilingual zero-shot classification model
4
+ # This model will download on first run (~500MB)
5
+ try:
6
+ print("Loading Zero-Shot Topic Classification model...")
7
+ topic_classifier = pipeline(
8
+ "zero-shot-classification",
9
+ model="MoritzLaurer/mDeBERTa-v3-base-mnli-xnli"
10
+ )
11
+ except Exception as e:
12
+ print(f"Error loading Topic model: {e}")
13
+ topic_classifier = None
14
+
15
+ # Predefined categories for Algérie Télécom context
16
+ AT_TOPICS = [
17
+ "Coupure Internet",
18
+ "Problème Modem",
19
+ "Facturation",
20
+ "Renseignement Technique",
21
+ "Fibre Optique",
22
+ "Réseau Mobile 4G",
23
+ "Réclamation Service Client",
24
+ "Demande de Ligne"
25
+ ]
26
+
27
+ def extract_keywords(text: str, top_k: int = 2) -> str:
28
+ """
29
+ Extracts the most relevant topics using Zero-Shot Classification.
30
+ Returns a comma-separated string of the most likely topics.
31
+ """
32
+ if not text.strip() or not topic_classifier:
33
+ return ""
34
+
35
+ try:
36
+ # Perform zero-shot classification
37
+ result = topic_classifier(
38
+ text,
39
+ candidate_labels=AT_TOPICS,
40
+ multi_label=True
41
+ )
42
+
43
+ # Get labels with high confidence (e.g., > 0.4)
44
+ top_labels = []
45
+ for label, score in zip(result['labels'], result['scores']):
46
+ if score > 0.4:
47
+ top_labels.append(label)
48
+ if len(top_labels) >= top_k:
49
+ break
50
+
51
+ # If none above threshold, take the top 1
52
+ if not top_labels and result['labels']:
53
+ top_labels = [result['labels'][0]]
54
+
55
+ return ", ".join(top_labels)
56
+ except Exception as e:
57
+ print(f"Error classifying topics: {e}")
58
+ return ""
backend/services/sentiment.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline
2
+
3
+ # We use alger-ia/dziribert_sentiment if it's available, otherwise fallback to a generic multilingual
4
+ # sentiment model that performs reasonably well for demonstration.
5
+ # alger-ia/dziribert is the base model.
6
+ try:
7
+ print("Loading DziriBERT sentiment model...")
8
+ sentiment_pipeline = pipeline(
9
+ "sentiment-analysis",
10
+ model="alger-ia/dziribert_sentiment"
11
+ )
12
+ except Exception as e:
13
+ print(f"Error loading DziriBERT model, using generic fallback: {e}")
14
+ sentiment_pipeline = pipeline("sentiment-analysis")
15
+
16
+ def analyze_sentiment(text: str) -> dict:
17
+ """
18
+ Analyzes the sentiment of the transcribed text.
19
+ Returns a dictionary with 'label' (Positive, Neutral, Negative) and 'score'.
20
+ """
21
+ if not text.strip():
22
+ return {"label": "Neutral", "score": 1.0}
23
+
24
+ # Truncate text if it's too long for the BERT model
25
+ max_length = 512
26
+ truncated_text = text[:max_length]
27
+
28
+ result = sentiment_pipeline(truncated_text)[0]
29
+
30
+ # Map the output to Positive/Neutral/Negative
31
+ # camelbert returns 'positive', 'neutral', 'negative'
32
+ label = result['label'].capitalize()
33
+
34
+ # Ensure it's one of the required labels
35
+ if label not in ["Positive", "Neutral", "Negative"]:
36
+ if "pos" in label.lower():
37
+ label = "Positive"
38
+ elif "neg" in label.lower():
39
+ label = "Negative"
40
+ else:
41
+ label = "Neutral"
42
+
43
+ score = float(result['score'])
44
+
45
+ return {
46
+ "label": label,
47
+ "score": round(score, 4)
48
+ }
backend/services/transcription.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import whisper
2
+ import os
3
+
4
+ # Upgraded to "large-v3" for maximum accuracy on code-switching (Arabic/French).
5
+ MODEL_SIZE = "large-v3"
6
+
7
+ print(f"Loading whisper model ({MODEL_SIZE})...")
8
+ # Load model (downloads on first run)
9
+ model = whisper.load_model(MODEL_SIZE)
10
+
11
+ def extract_transcript(audio_path: str) -> str:
12
+ """
13
+ Transcribes the audio file and returns the combined text.
14
+ """
15
+ print(f"Transcribing {audio_path}...")
16
+
17
+ # Optimized prompt for code-switching (Arabic and French scripts)
18
+ # This guides Whisper to use Latin for French and Arabic script for Darija.
19
+ darija_prompt = "Bonjour, l'appel d'aujourd'hui est pour une réclamation. كيفاش نقدر نعاونك؟ واش كاين مشكل؟ Merci beaucoup."
20
+
21
+ result = model.transcribe(
22
+ audio_path,
23
+ initial_prompt=darija_prompt
24
+ )
25
+
26
+ # Extract language info for logging if needed
27
+ print(f"Detected language: {result.get('language', 'unknown')}")
28
+
29
+ return result["text"].strip()
backend/test_dziri.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from transformers import pipeline
2
+ p = pipeline("sentiment-analysis", model="alger-ia/dziribert_sentiment")
3
+ print(p("Rani mlih hamdoullah"))
4
+ print(p("Connexion raïha fiha, connexion mita"))
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ 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).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ 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.
frontend/eslint.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ globals: globals.browser,
18
+ parserOptions: { ecmaFeatures: { jsx: true } },
19
+ },
20
+ },
21
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.16.1",
14
+ "clsx": "^2.1.1",
15
+ "lucide-react": "^1.16.0",
16
+ "react": "^19.2.6",
17
+ "react-dom": "^19.2.6",
18
+ "react-router-dom": "^7.15.1",
19
+ "recharts": "^3.8.1",
20
+ "tailwind-merge": "^3.6.0"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^10.0.1",
24
+ "@types/node": "^25.8.0",
25
+ "@types/react": "^19.2.14",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^6.0.1",
28
+ "autoprefixer": "^10.5.0",
29
+ "eslint": "^10.3.0",
30
+ "eslint-plugin-react-hooks": "^7.1.1",
31
+ "eslint-plugin-react-refresh": "^0.5.2",
32
+ "globals": "^17.6.0",
33
+ "postcss": "^8.5.14",
34
+ "tailwindcss": "^3.4.19",
35
+ "vite": "^8.0.12"
36
+ }
37
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
2
+
3
+ import Layout from "./components/layout/Layout"
4
+ import UploadPage from "./pages/UploadPage"
5
+ import ResultPage from "./pages/ResultPage"
6
+ import DashboardPage from "./pages/DashboardPage"
7
+ import HistoryPage from "./pages/HistoryPage"
8
+
9
+ function App() {
10
+ return (
11
+ <Router>
12
+ <Routes>
13
+ <Route path="/" element={<Layout />}>
14
+ <Route index element={<UploadPage />} />
15
+ <Route path="result/:id" element={<ResultPage />} />
16
+ <Route path="dashboard" element={<DashboardPage />} />
17
+ <Route path="history" element={<HistoryPage />} />
18
+ </Route>
19
+ </Routes>
20
+ </Router>
21
+ )
22
+ }
23
+
24
+ export default App
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/layout/Layout.jsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { Outlet } from "react-router-dom"
3
+ import Sidebar from "./Sidebar"
4
+ import { Bell, Search } from "lucide-react"
5
+
6
+ export default function Layout() {
7
+ return (
8
+ <div className="h-screen flex overflow-hidden bg-slate-50 font-sans">
9
+ <Sidebar />
10
+
11
+ <div className="flex flex-col w-0 flex-1 md:pl-64">
12
+ {/* Top Header */}
13
+ <div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow-sm border-b border-slate-200">
14
+ <div className="flex-1 px-8 flex justify-between items-center">
15
+ <div className="flex-1 flex">
16
+ <div className="w-full flex md:ml-0">
17
+ {/* Search bar removed per user request */}
18
+ </div>
19
+ </div>
20
+ <div className="ml-4 flex items-center md:ml-6">
21
+ <button className="bg-white p-1 rounded-full text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-at-blue">
22
+ <span className="sr-only">View notifications</span>
23
+ <Bell className="h-6 w-6" />
24
+ </button>
25
+
26
+ <div className="ml-3 flex items-center gap-2">
27
+ <div className="bg-emerald-100 text-emerald-800 text-xs font-bold px-2.5 py-1 rounded-full flex items-center gap-1.5">
28
+ <div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse"></div>
29
+ System Online
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ {/* Main Content Area */}
37
+ <main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
38
+ <div className="py-8 px-8">
39
+ <Outlet />
40
+ </div>
41
+ </main>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
frontend/src/components/layout/Sidebar.jsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { Link, useLocation } from "react-router-dom"
3
+ import { LayoutDashboard, PhoneCall, History, Mic } from "lucide-react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export default function Sidebar() {
7
+ const location = useLocation()
8
+
9
+ const navItems = [
10
+ { name: "Vue Globale", href: "/dashboard", icon: LayoutDashboard },
11
+ { name: "Analyse Audio", href: "/", icon: PhoneCall },
12
+ { name: "Data Explorer", href: "/history", icon: History },
13
+ ]
14
+
15
+ return (
16
+ <div className="hidden md:flex md:w-72 md:flex-col md:fixed md:inset-y-0 bg-gradient-to-br from-at-blue via-blue-800 to-slate-900 shadow-2xl z-20">
17
+ <div className="flex-1 flex flex-col min-h-0 relative overflow-hidden">
18
+
19
+ {/* Decorative background blur */}
20
+ <div className="absolute top-0 right-0 -mr-20 -mt-20 w-64 h-64 rounded-full bg-at-orange/10 blur-3xl pointer-events-none"></div>
21
+ <div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-64 h-64 rounded-full bg-blue-500/20 blur-3xl pointer-events-none"></div>
22
+
23
+ <div className="flex items-center h-24 flex-shrink-0 px-8 border-b border-white/5 relative z-10">
24
+ <div className="flex items-center gap-4">
25
+ <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-at-orange to-orange-600 flex items-center justify-center shadow-lg shadow-at-orange/30 animate-pulse-soft">
26
+ <Mic className="h-6 w-6 text-white" />
27
+ </div>
28
+ <div className="flex flex-col">
29
+ <span className="text-white text-2xl font-extrabold tracking-tight font-outfit uppercase">HEAREM</span>
30
+ <span className="text-at-orange text-[9px] font-bold tracking-tight uppercase mt-0.5">Hear the emotion behind the voice</span>
31
+ </div>
32
+ </div>
33
+ </div>
34
+
35
+ <div className="flex-1 flex flex-col overflow-y-auto relative z-10 mt-6">
36
+ <nav className="flex-1 px-4 space-y-3">
37
+ <div className="text-[11px] font-bold text-white/40 uppercase tracking-[0.2em] mb-4 px-4">Menu Principal</div>
38
+ {navItems.map((item) => {
39
+ const Icon = item.icon
40
+ const isActive = location.pathname === item.href || (item.href === "/" && location.pathname.startsWith("/result"))
41
+ return (
42
+ <Link
43
+ key={item.name}
44
+ to={item.href}
45
+ className={cn(
46
+ "group flex items-center px-4 py-3.5 text-sm font-semibold rounded-xl transition-all duration-300 relative overflow-hidden",
47
+ isActive
48
+ ? "bg-white/10 text-white shadow-[0_0_20px_rgba(255,121,0,0.1)] border border-white/10"
49
+ : "text-white/60 hover:bg-white/5 hover:text-white border border-transparent"
50
+ )}
51
+ >
52
+ {isActive && <div className="absolute left-0 top-0 bottom-0 w-1 bg-at-orange rounded-r-full shadow-[0_0_10px_#FF7900]"></div>}
53
+ <Icon className={cn("mr-4 h-5 w-5 transition-transform duration-300 group-hover:scale-110", isActive ? "text-at-orange drop-shadow-[0_0_8px_rgba(255,121,0,0.5)]" : "text-white/40 group-hover:text-white/70")} />
54
+ {item.name}
55
+ </Link>
56
+ )
57
+ })}
58
+ </nav>
59
+ </div>
60
+
61
+ <div className="flex-shrink-0 flex border-t border-white/5 p-6 relative z-10 bg-black/10 backdrop-blur-md">
62
+ <div className="flex items-center w-full group cursor-pointer">
63
+ <div className="h-10 w-10 rounded-full bg-gradient-to-r from-blue-400 to-at-blue p-[2px]">
64
+ <div className="h-full w-full rounded-full bg-slate-900 flex items-center justify-center">
65
+ <span className="text-white font-bold text-xs">AT</span>
66
+ </div>
67
+ </div>
68
+ <div className="ml-4 flex-1">
69
+ <p className="text-sm font-bold text-white group-hover:text-at-orange transition-colors">Admin Système</p>
70
+ <p className="text-xs font-medium text-white/50">admin@algerietelecom.dz</p>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
frontend/src/components/ui/badge.jsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const Badge = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
5
+ const variants = {
6
+ default: "border-transparent bg-slate-900 text-slate-50",
7
+ positive: "border-transparent bg-emerald-100 text-emerald-800",
8
+ neutral: "border-transparent bg-yellow-100 text-yellow-800",
9
+ negative: "border-transparent bg-red-100 text-red-800",
10
+ outline: "text-slate-950",
11
+ }
12
+ return (
13
+ <div
14
+ ref={ref}
15
+ className={cn(
16
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2",
17
+ variants[variant],
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ })
24
+ Badge.displayName = "Badge"
25
+
26
+ export { Badge }
frontend/src/components/ui/button.jsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const Button = React.forwardRef(({ className, variant = "default", size = "default", ...props }, ref) => {
5
+ const variants = {
6
+ default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90",
7
+ outline: "border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900",
8
+ ghost: "hover:bg-slate-100 hover:text-slate-900",
9
+ }
10
+ const sizes = {
11
+ default: "h-10 px-4 py-2",
12
+ sm: "h-9 rounded-md px-3",
13
+ lg: "h-11 rounded-md px-8",
14
+ }
15
+ return (
16
+ <button
17
+ ref={ref}
18
+ className={cn(
19
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
20
+ variants[variant],
21
+ sizes[size],
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ )
27
+ })
28
+ Button.displayName = "Button"
29
+
30
+ export { Button }
frontend/src/components/ui/card.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const Card = React.forwardRef(({ className, ...props }, ref) => (
5
+ <div ref={ref} className={cn("rounded-xl border bg-white text-slate-950 shadow-sm", className)} {...props} />
6
+ ))
7
+ Card.displayName = "Card"
8
+
9
+ const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
10
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
11
+ ))
12
+ CardHeader.displayName = "CardHeader"
13
+
14
+ const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
15
+ <h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
16
+ ))
17
+ CardTitle.displayName = "CardTitle"
18
+
19
+ const CardContent = React.forwardRef(({ className, ...props }, ref) => (
20
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
21
+ ))
22
+ CardContent.displayName = "CardContent"
23
+
24
+ export { Card, CardHeader, CardTitle, CardContent }
frontend/src/components/ui/progress.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
5
+ <div
6
+ ref={ref}
7
+ className={cn("relative h-4 w-full overflow-hidden rounded-full bg-slate-100", className)}
8
+ {...props}
9
+ >
10
+ <div
11
+ className="h-full w-full flex-1 bg-slate-900 transition-all"
12
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
13
+ />
14
+ </div>
15
+ ))
16
+ Progress.displayName = "Progress"
17
+
18
+ export { Progress }
frontend/src/index.css ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Inter:wght@400;500;600&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ @layer base {
8
+ body {
9
+ /* Subtle premium gradient background */
10
+ @apply bg-slate-50 text-slate-900 font-sans min-h-screen;
11
+ background-image: radial-gradient(circle at top right, rgba(0, 76, 151, 0.03) 0%, transparent 40%),
12
+ radial-gradient(circle at bottom left, rgba(255, 121, 0, 0.03) 0%, transparent 40%);
13
+ background-attachment: fixed;
14
+ }
15
+
16
+ ::selection {
17
+ background-color: rgba(255, 121, 0, 0.3); /* at-orange/30 */
18
+ color: #004C97; /* at-blue */
19
+ }
20
+
21
+ h1, h2, h3, h4, h5, h6 {
22
+ font-family: 'Outfit', sans-serif;
23
+ }
24
+ }
25
+
26
+ @layer utilities {
27
+ .glass-card {
28
+ @apply bg-white/80 backdrop-blur-md border border-white/40 shadow-[0_8px_30px_rgb(0,0,0,0.04)] transition-all duration-300;
29
+ }
30
+
31
+ .glass-card:hover {
32
+ @apply shadow-[0_8px_30px_rgb(0,0,0,0.08)] -translate-y-1 bg-white/90;
33
+ }
34
+ }
35
+
36
+ /* Custom Animations */
37
+ @keyframes float {
38
+ 0% { transform: translateY(0px); }
39
+ 50% { transform: translateY(-10px); }
40
+ 100% { transform: translateY(0px); }
41
+ }
42
+
43
+ .animate-float {
44
+ animation: float 6s ease-in-out infinite;
45
+ }
46
+
47
+ @keyframes pulse-soft {
48
+ 0%, 100% { opacity: 1; transform: scale(1); }
49
+ 50% { opacity: 0.8; transform: scale(1.02); }
50
+ }
51
+
52
+ .animate-pulse-soft {
53
+ animation: pulse-soft 3s ease-in-out infinite;
54
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/pages/DashboardPage.jsx ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from "react"
2
+ import axios from "axios"
3
+ import {
4
+ PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend,
5
+ LineChart, Line, XAxis, YAxis, CartesianGrid,
6
+ BarChart, Bar
7
+ } from "recharts"
8
+ import { TrendingUp, Users, AlertCircle, PhoneCall } from "lucide-react"
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
10
+
11
+ export default function DashboardPage() {
12
+ const [history, setHistory] = useState([])
13
+ const [loading, setLoading] = useState(true)
14
+
15
+ useEffect(() => {
16
+ const fetchHistory = async () => {
17
+ try {
18
+ const response = await axios.get("/history")
19
+ setHistory(response.data)
20
+ } catch (err) {
21
+ console.error(err)
22
+ } finally {
23
+ setLoading(false)
24
+ }
25
+ }
26
+ fetchHistory()
27
+ }, [])
28
+
29
+ if (loading) {
30
+ return <div className="flex justify-center items-center h-64"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-at-blue"></div></div>
31
+ }
32
+
33
+ // Calculate stats
34
+ const totalCalls = history.length
35
+ const positiveCalls = history.filter(h => h.sentiment === "Positive").length
36
+ const neutralCalls = history.filter(h => h.sentiment === "Neutral").length
37
+ const negativeCalls = history.filter(h => h.sentiment === "Negative").length
38
+
39
+ // Satisfaction Score calculation (Mock Formula)
40
+ const satisfactionScore = totalCalls ? Math.round(((positiveCalls * 100) + (neutralCalls * 50)) / totalCalls) : 0
41
+
42
+ // Sentiment Donut Data
43
+ const sentimentData = [
44
+ { name: 'Positive', value: positiveCalls, color: '#10b981' },
45
+ { name: 'Neutral', value: neutralCalls, color: '#eab308' },
46
+ { name: 'Negative', value: negativeCalls, color: '#ef4444' }
47
+ ].filter(d => d.value > 0)
48
+
49
+ // Top Issues Processing
50
+ const issueCounts = {}
51
+ history.forEach(call => {
52
+ if (call.keywords) {
53
+ const keys = call.keywords.split(',').map(k => k.trim())
54
+ keys.forEach(k => {
55
+ issueCounts[k] = (issueCounts[k] || 0) + 1
56
+ })
57
+ }
58
+ })
59
+ const topIssues = Object.entries(issueCounts)
60
+ .sort((a, b) => b[1] - a[1])
61
+ .slice(0, 5)
62
+ .map(([name, count]) => ({ name, count }))
63
+
64
+ // Mock Trend Data (Last 7 Days) for Line Chart
65
+ const trendData = [
66
+ { name: 'Mon', calls: 120, satisfaction: 75 },
67
+ { name: 'Tue', calls: 132, satisfaction: 72 },
68
+ { name: 'Wed', calls: 101, satisfaction: 80 },
69
+ { name: 'Thu', calls: 142, satisfaction: 78 },
70
+ { name: 'Fri', calls: 90, satisfaction: 85 },
71
+ { name: 'Sat', calls: 60, satisfaction: 88 },
72
+ { name: 'Sun', calls: Math.max(totalCalls, 50), satisfaction: satisfactionScore },
73
+ ]
74
+
75
+ return (
76
+ <div className="max-w-7xl mx-auto">
77
+ <div className="flex items-center justify-between mb-8">
78
+ <div>
79
+ <h1 className="text-3xl font-bold tracking-tight text-slate-900">Intelligence Dashboard</h1>
80
+ <p className="text-slate-500 mt-1">Real-time analysis of Algérie Télécom customer support interactions.</p>
81
+ </div>
82
+ <div className="flex items-center gap-4">
83
+ <div className="bg-white border rounded-lg px-4 py-2 text-sm font-medium text-slate-600 shadow-sm">
84
+ Last 7 Days
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ {/* KPI Cards */}
90
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
91
+ <Card className="glass-card border-t-4 border-t-at-blue border-l-0 overflow-hidden relative group">
92
+ <div className="absolute top-0 right-0 -mr-4 -mt-4 w-20 h-20 rounded-full bg-at-blue/5 group-hover:scale-150 transition-transform duration-500 ease-out"></div>
93
+ <CardContent className="p-6 relative z-10">
94
+ <div className="flex justify-between items-start">
95
+ <div>
96
+ <p className="text-sm font-semibold text-slate-500 mb-1">Total Appels Analysés</p>
97
+ <h3 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-slate-900 to-slate-600 font-outfit">{totalCalls}</h3>
98
+ </div>
99
+ <div className="p-3 bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl shadow-inner border border-white">
100
+ <PhoneCall className="h-6 w-6 text-at-blue" />
101
+ </div>
102
+ </div>
103
+ <div className="mt-5 flex items-center text-sm font-semibold">
104
+ <span className="flex items-center text-emerald-500 bg-emerald-50 px-2 py-0.5 rounded-md">
105
+ <TrendingUp className="h-3 w-3 mr-1" />+12.5%
106
+ </span>
107
+ <span className="text-slate-400 ml-2">vs sem. dernière</span>
108
+ </div>
109
+ </CardContent>
110
+ </Card>
111
+
112
+ <Card className="glass-card border-t-4 border-t-emerald-500 border-l-0 overflow-hidden relative group">
113
+ <div className="absolute top-0 right-0 -mr-4 -mt-4 w-20 h-20 rounded-full bg-emerald-500/5 group-hover:scale-150 transition-transform duration-500 ease-out"></div>
114
+ <CardContent className="p-6 relative z-10">
115
+ <div className="flex justify-between items-start">
116
+ <div>
117
+ <p className="text-sm font-semibold text-slate-500 mb-1">Satisfaction Client</p>
118
+ <h3 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-slate-900 to-slate-600 font-outfit">{satisfactionScore}<span className="text-xl text-slate-400">/100</span></h3>
119
+ </div>
120
+ <div className="p-3 bg-gradient-to-br from-emerald-50 to-emerald-100 rounded-xl shadow-inner border border-white">
121
+ <Users className="h-6 w-6 text-emerald-600" />
122
+ </div>
123
+ </div>
124
+ <div className="mt-5 flex items-center text-sm font-semibold">
125
+ <span className="flex items-center text-emerald-500 bg-emerald-50 px-2 py-0.5 rounded-md">
126
+ <TrendingUp className="h-3 w-3 mr-1" />+4.2 pts
127
+ </span>
128
+ <span className="text-slate-400 ml-2">vs sem. dernière</span>
129
+ </div>
130
+ </CardContent>
131
+ </Card>
132
+
133
+ <Card className="glass-card border-t-4 border-t-red-500 border-l-0 overflow-hidden relative group">
134
+ <div className="absolute top-0 right-0 -mr-4 -mt-4 w-20 h-20 rounded-full bg-red-500/5 group-hover:scale-150 transition-transform duration-500 ease-out"></div>
135
+ <CardContent className="p-6 relative z-10">
136
+ <div className="flex justify-between items-start">
137
+ <div>
138
+ <p className="text-sm font-semibold text-slate-500 mb-1">Appels Critiques (Risque)</p>
139
+ <h3 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-red-600 to-red-400 font-outfit">{negativeCalls}</h3>
140
+ </div>
141
+ <div className="p-3 bg-gradient-to-br from-red-50 to-red-100 rounded-xl shadow-inner border border-white">
142
+ <AlertCircle className="h-6 w-6 text-red-600" />
143
+ </div>
144
+ </div>
145
+ <div className="mt-5 flex items-center text-sm font-semibold">
146
+ <span className="flex items-center text-red-500 bg-red-50 px-2 py-0.5 rounded-md">
147
+ <TrendingUp className="h-3 w-3 mr-1" />+2.1%
148
+ </span>
149
+ <span className="text-slate-400 ml-2">alerte active</span>
150
+ </div>
151
+ </CardContent>
152
+ </Card>
153
+
154
+ <Card className="glass-card border-t-4 border-t-at-orange border-l-0 overflow-hidden relative group">
155
+ <div className="absolute top-0 right-0 -mr-4 -mt-4 w-20 h-20 rounded-full bg-at-orange/5 group-hover:scale-150 transition-transform duration-500 ease-out"></div>
156
+ <CardContent className="p-6 relative z-10">
157
+ <div className="flex justify-between items-start">
158
+ <div>
159
+ <p className="text-sm font-semibold text-slate-500 mb-1">Taux Résolution Auto</p>
160
+ <h3 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-at-orange to-orange-400 font-outfit">42%</h3>
161
+ </div>
162
+ <div className="p-3 bg-gradient-to-br from-orange-50 to-orange-100 rounded-xl shadow-inner border border-white">
163
+ <TrendingUp className="h-6 w-6 text-at-orange" />
164
+ </div>
165
+ </div>
166
+ <div className="mt-5 flex items-center text-sm font-semibold">
167
+ <span className="text-slate-400 bg-slate-100 px-2 py-0.5 rounded-md">Estimé par IA</span>
168
+ </div>
169
+ </CardContent>
170
+ </Card>
171
+ </div>
172
+
173
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
174
+ {/* Trend Chart */}
175
+ <Card className="lg:col-span-2">
176
+ <CardHeader>
177
+ <CardTitle>Call Volume & Satisfaction Trend</CardTitle>
178
+ </CardHeader>
179
+ <CardContent className="h-80">
180
+ <ResponsiveContainer width="100%" height="100%">
181
+ <LineChart data={trendData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
182
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
183
+ <XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fill: '#64748b'}} dy={10} />
184
+ <YAxis yAxisId="left" axisLine={false} tickLine={false} tick={{fill: '#64748b'}} />
185
+ <YAxis yAxisId="right" orientation="right" domain={[0, 100]} axisLine={false} tickLine={false} tick={{fill: '#64748b'}} />
186
+ <Tooltip
187
+ contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
188
+ />
189
+ <Legend wrapperStyle={{ paddingTop: '20px' }} />
190
+ <Line yAxisId="left" type="monotone" dataKey="calls" stroke="#004C97" strokeWidth={3} activeDot={{ r: 8 }} name="Call Volume" />
191
+ <Line yAxisId="right" type="monotone" dataKey="satisfaction" stroke="#FF7900" strokeWidth={3} name="Satisfaction Score" />
192
+ </LineChart>
193
+ </ResponsiveContainer>
194
+ </CardContent>
195
+ </Card>
196
+
197
+ {/* Sentiment Donut */}
198
+ <Card>
199
+ <CardHeader>
200
+ <CardTitle>Global Sentiment Distribution</CardTitle>
201
+ </CardHeader>
202
+ <CardContent className="h-80 flex flex-col items-center justify-center relative">
203
+ <ResponsiveContainer width="100%" height="100%">
204
+ <PieChart>
205
+ <Pie
206
+ data={sentimentData}
207
+ innerRadius={70}
208
+ outerRadius={100}
209
+ paddingAngle={5}
210
+ dataKey="value"
211
+ >
212
+ {sentimentData.map((entry, index) => (
213
+ <Cell key={`cell-${index}`} fill={entry.color} />
214
+ ))}
215
+ </Pie>
216
+ <Tooltip contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
217
+ <Legend verticalAlign="bottom" height={36} />
218
+ </PieChart>
219
+ </ResponsiveContainer>
220
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none pb-8">
221
+ <span className="text-3xl font-bold text-slate-900">{totalCalls}</span>
222
+ <span className="text-xs text-slate-500 font-medium">Total Calls</span>
223
+ </div>
224
+ </CardContent>
225
+ </Card>
226
+ </div>
227
+
228
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
229
+ {/* Top Issues Bar Chart */}
230
+ <Card>
231
+ <CardHeader>
232
+ <CardTitle>Top Extracted Issues (DziriBERT NLP)</CardTitle>
233
+ </CardHeader>
234
+ <CardContent className="h-80">
235
+ <ResponsiveContainer width="100%" height="100%">
236
+ <BarChart layout="vertical" data={topIssues} margin={{ top: 5, right: 30, left: 60, bottom: 5 }}>
237
+ <CartesianGrid strokeDasharray="3 3" horizontal={false} stroke="#e2e8f0" />
238
+ <XAxis type="number" axisLine={false} tickLine={false} tick={{fill: '#64748b'}} />
239
+ <YAxis dataKey="name" type="category" axisLine={false} tickLine={false} tick={{fill: '#475569', fontSize: 12}} width={120} />
240
+ <Tooltip cursor={{fill: '#f1f5f9'}} contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
241
+ <Bar dataKey="count" fill="#FF7900" radius={[0, 4, 4, 0]} barSize={24} name="Mentions" />
242
+ </BarChart>
243
+ </ResponsiveContainer>
244
+ </CardContent>
245
+ </Card>
246
+
247
+ {/* System Status Card */}
248
+ <Card>
249
+ <CardHeader>
250
+ <CardTitle>AI Pipeline Status</CardTitle>
251
+ </CardHeader>
252
+ <CardContent>
253
+ <div className="space-y-6">
254
+ <div className="flex items-center justify-between">
255
+ <div>
256
+ <p className="text-sm font-semibold text-slate-900">Speech-to-Text Engine</p>
257
+ <p className="text-xs text-slate-500">Whisper Medium (Code-Switching Support)</p>
258
+ </div>
259
+ <div className="bg-emerald-100 text-emerald-800 text-xs font-bold px-3 py-1 rounded-full">Operational</div>
260
+ </div>
261
+ <div className="flex items-center justify-between">
262
+ <div>
263
+ <p className="text-sm font-semibold text-slate-900">Sentiment Classifier</p>
264
+ <p className="text-xs text-slate-500">Alger-IA / DziriBERT Sentiment</p>
265
+ </div>
266
+ <div className="bg-emerald-100 text-emerald-800 text-xs font-bold px-3 py-1 rounded-full">Operational</div>
267
+ </div>
268
+ <div className="flex items-center justify-between">
269
+ <div>
270
+ <p className="text-sm font-semibold text-slate-900">Keyword Extractor</p>
271
+ <p className="text-xs text-slate-500">TF-IDF Local Engine</p>
272
+ </div>
273
+ <div className="bg-emerald-100 text-emerald-800 text-xs font-bold px-3 py-1 rounded-full">Operational</div>
274
+ </div>
275
+ <div className="flex items-center justify-between">
276
+ <div>
277
+ <p className="text-sm font-semibold text-slate-900">Database</p>
278
+ <p className="text-xs text-slate-500">SQLite Active Connection</p>
279
+ </div>
280
+ <div className="bg-emerald-100 text-emerald-800 text-xs font-bold px-3 py-1 rounded-full">Connected</div>
281
+ </div>
282
+ </div>
283
+ </CardContent>
284
+ </Card>
285
+ </div>
286
+
287
+ </div>
288
+ )
289
+ }
frontend/src/pages/HistoryPage.jsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from "react"
2
+ import { Link } from "react-router-dom"
3
+ import axios from "axios"
4
+ import { Search, Filter, PhoneCall, ChevronRight, Calendar, Activity } from "lucide-react"
5
+
6
+ export default function HistoryPage() {
7
+ const [history, setHistory] = useState([])
8
+ const [loading, setLoading] = useState(true)
9
+
10
+ useEffect(() => {
11
+ const fetchHistory = async () => {
12
+ try {
13
+ const response = await axios.get("/history")
14
+ setHistory(response.data)
15
+ } catch (err) {
16
+ console.error(err)
17
+ } finally {
18
+ setLoading(false)
19
+ }
20
+ }
21
+ fetchHistory()
22
+ }, [])
23
+
24
+ const getSentimentColor = (sentiment) => {
25
+ if (sentiment === "Positive") return "bg-emerald-100 text-emerald-800 border-emerald-200"
26
+ if (sentiment === "Negative") return "bg-red-100 text-red-800 border-red-200"
27
+ return "bg-yellow-100 text-yellow-800 border-yellow-200"
28
+ }
29
+
30
+ return (
31
+ <div className="max-w-7xl mx-auto">
32
+ <div className="flex items-center justify-between mb-8">
33
+ <div>
34
+ <h1 className="text-3xl font-bold tracking-tight text-slate-900">Call History</h1>
35
+ <p className="text-slate-500 mt-1">Detailed log of all processed customer interactions.</p>
36
+ </div>
37
+ <div className="flex gap-3">
38
+ <button className="flex items-center gap-2 bg-white border rounded-lg px-4 py-2 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50">
39
+ <Filter className="h-4 w-4" /> Filter
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ <div className="bg-white shadow-sm ring-1 ring-slate-200 rounded-xl overflow-hidden">
45
+ {loading ? (
46
+ <div className="flex justify-center items-center h-64"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-at-blue"></div></div>
47
+ ) : (
48
+ <div className="overflow-x-auto">
49
+ <table className="min-w-full divide-y divide-slate-200">
50
+ <thead className="bg-slate-50">
51
+ <tr>
52
+ <th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
53
+ Call ID / File
54
+ </th>
55
+ <th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
56
+ Date & Time
57
+ </th>
58
+ <th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
59
+ Agent
60
+ </th>
61
+ <th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
62
+ Sentiment
63
+ </th>
64
+ <th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
65
+ Extracted Topics
66
+ </th>
67
+ <th scope="col" className="relative px-6 py-4">
68
+ <span className="sr-only">View</span>
69
+ </th>
70
+ </tr>
71
+ </thead>
72
+ <tbody className="bg-white divide-y divide-slate-200">
73
+ {history.map((call) => (
74
+ <tr key={call.id} className="hover:bg-slate-50 transition-colors group">
75
+ <td className="px-6 py-4 whitespace-nowrap">
76
+ <div className="flex items-center">
77
+ <div className="flex-shrink-0 h-10 w-10 rounded-full bg-slate-100 flex items-center justify-center border border-slate-200">
78
+ <PhoneCall className="h-4 w-4 text-slate-500" />
79
+ </div>
80
+ <div className="ml-4">
81
+ <div className="text-sm font-medium text-slate-900 group-hover:text-at-blue transition-colors">
82
+ {call.filename}
83
+ </div>
84
+ <div className="text-xs text-slate-500">ID: AT-{10000 + call.id}</div>
85
+ </div>
86
+ </div>
87
+ </td>
88
+ <td className="px-6 py-4 whitespace-nowrap">
89
+ <div className="flex items-center text-sm text-slate-900">
90
+ <Calendar className="mr-1.5 h-4 w-4 text-slate-400" />
91
+ {new Date(call.created_at).toLocaleDateString()}
92
+ </div>
93
+ <div className="text-xs text-slate-500 mt-1">
94
+ {new Date(call.created_at).toLocaleTimeString()}
95
+ </div>
96
+ </td>
97
+ <td className="px-6 py-4 whitespace-nowrap">
98
+ <div className="text-sm font-bold text-slate-700">{call.agent_name || "Inconnu"}</div>
99
+ </td>
100
+ <td className="px-6 py-4 whitespace-nowrap">
101
+ <span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold border ${getSentimentColor(call.sentiment)}`}>
102
+ {call.sentiment}
103
+ </span>
104
+ <div className="text-xs text-slate-500 mt-1 flex items-center">
105
+ <Activity className="h-3 w-3 mr-1 text-slate-400" />
106
+ {(call.confidence * 100).toFixed(0)}% Conf.
107
+ </div>
108
+ </td>
109
+ <td className="px-6 py-4">
110
+ <div className="flex flex-wrap gap-1">
111
+ {call.keywords ? call.keywords.split(',').slice(0, 2).map((k, i) => (
112
+ <span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-700">
113
+ {k.trim()}
114
+ </span>
115
+ )) : <span className="text-xs text-slate-400">None</span>}
116
+ {call.keywords && call.keywords.split(',').length > 2 && (
117
+ <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-slate-50 text-slate-500 border border-slate-200">
118
+ +{call.keywords.split(',').length - 2}
119
+ </span>
120
+ )}
121
+ </div>
122
+ </td>
123
+ <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
124
+ <Link to={`/result/${call.id}`} className="inline-flex items-center text-at-blue hover:text-blue-900 font-semibold bg-blue-50 px-3 py-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity">
125
+ View Details <ChevronRight className="ml-1 h-4 w-4" />
126
+ </Link>
127
+ </td>
128
+ </tr>
129
+ ))}
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ )
137
+ }
frontend/src/pages/ResultPage.jsx ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from "react"
2
+ import { useParams, Link } from "react-router-dom"
3
+ import axios from "axios"
4
+ import { ArrowLeft, MessageSquare, Tags, BarChart2, Calendar, AlertTriangle, ShieldCheck, UserMinus, Clock } from "lucide-react"
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
+ import { Progress } from "@/components/ui/progress"
7
+
8
+ export default function ResultPage() {
9
+ const { id } = useParams()
10
+ const [data, setData] = useState(null)
11
+ const [loading, setLoading] = useState(true)
12
+ const [error, setError] = useState(null)
13
+
14
+ useEffect(() => {
15
+ const fetchResult = async () => {
16
+ try {
17
+ const response = await axios.get(`/call/${id}`)
18
+ setData(response.data)
19
+ } catch (err) {
20
+ console.error(err)
21
+ setError("Impossible de charger les détails de l'appel.")
22
+ } finally {
23
+ setLoading(false)
24
+ }
25
+ }
26
+ fetchResult()
27
+ }, [id])
28
+
29
+ if (loading) {
30
+ return <div className="flex justify-center items-center h-64"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-at-blue"></div></div>
31
+ }
32
+
33
+ if (error || !data) {
34
+ return <div className="text-center text-red-500 mt-10">{error || "Introuvable"}</div>
35
+ }
36
+
37
+ const getSentimentStyle = (sentiment) => {
38
+ if (sentiment === "Positive") return "bg-emerald-100 text-emerald-800 border-emerald-200"
39
+ if (sentiment === "Negative") return "bg-red-100 text-red-800 border-red-200"
40
+ return "bg-yellow-100 text-yellow-800 border-yellow-200"
41
+ }
42
+
43
+ const formatDate = (dateStr) => {
44
+ return new Date(dateStr).toLocaleString('fr-DZ')
45
+ }
46
+
47
+ // Churn Risk Logic
48
+ let churnRisk = "Faible"
49
+ let churnColor = "text-emerald-600 bg-emerald-50 border-emerald-200"
50
+ let churnIcon = ShieldCheck
51
+
52
+ if (data.sentiment === "Negative") {
53
+ if (data.confidence > 0.9) {
54
+ churnRisk = "Critique"
55
+ churnColor = "text-red-700 bg-red-50 border-red-200"
56
+ churnIcon = AlertTriangle
57
+ } else {
58
+ churnRisk = "Modéré"
59
+ churnColor = "text-orange-600 bg-orange-50 border-orange-200"
60
+ churnIcon = UserMinus
61
+ }
62
+ } else if (data.sentiment === "Neutral") {
63
+ churnRisk = "Modéré"
64
+ churnColor = "text-yellow-700 bg-yellow-50 border-yellow-200"
65
+ churnIcon = UserMinus
66
+ }
67
+
68
+ const RiskIcon = churnIcon
69
+
70
+ return (
71
+ <div className="max-w-6xl mx-auto">
72
+ <div className="mb-6">
73
+ <Link to="/history" className="inline-flex items-center text-sm font-semibold text-slate-500 hover:text-at-blue transition-colors bg-white px-4 py-2 rounded-lg border border-slate-200 shadow-sm">
74
+ <ArrowLeft className="mr-2 h-4 w-4" />
75
+ Retour à l'Historique
76
+ </Link>
77
+ </div>
78
+
79
+ <div className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8 mb-8">
80
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
81
+ <div>
82
+ <div className="flex items-center gap-3 mb-2">
83
+ <span className="text-xs font-bold text-slate-400 uppercase tracking-widest">ID Appel: AT-{10000 + data.id}</span>
84
+ <span className={`px-3 py-1 rounded-full text-xs font-bold border uppercase tracking-wider ${getSentimentStyle(data.sentiment)}`}>
85
+ Sentiment {data.sentiment}
86
+ </span>
87
+ </div>
88
+ <h1 className="text-3xl font-bold tracking-tight text-slate-900">{data.filename}</h1>
89
+ <div className="flex items-center text-slate-500 mt-3 text-sm gap-6 font-medium">
90
+ <span className="flex items-center"><Calendar className="mr-2 h-4 w-4 text-slate-400" /> {formatDate(data.created_at)}</span>
91
+ <span className="flex items-center"><UserMinus className="mr-2 h-4 w-4 text-slate-400" /> Agent: {data.agent_name || "Inconnu"}</span>
92
+ <span className="flex items-center"><Clock className="mr-2 h-4 w-4 text-slate-400" /> Durée: Analysée</span>
93
+ </div>
94
+ </div>
95
+
96
+ <div className={`flex items-center gap-4 px-6 py-4 rounded-xl border ${churnColor}`}>
97
+ <div className="p-3 bg-white/60 rounded-full">
98
+ <RiskIcon className="h-8 w-8" />
99
+ </div>
100
+ <div>
101
+ <p className="text-sm font-bold opacity-80 uppercase tracking-wider">Risque d'attrition (Churn)</p>
102
+ <p className="text-2xl font-black">{churnRisk}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
109
+ <div className="lg:col-span-2 space-y-8">
110
+ <Card className="shadow-sm border-slate-200 overflow-hidden">
111
+ <div className="bg-slate-50 border-b border-slate-100 px-6 py-4 flex items-center justify-between">
112
+ <h3 className="text-lg font-bold text-slate-900 flex items-center">
113
+ <MessageSquare className="mr-2 h-5 w-5 text-at-blue" />
114
+ Transcription Audio (DziriBERT / Whisper)
115
+ </h3>
116
+ </div>
117
+ <CardContent className="p-6">
118
+ <div className="prose prose-slate max-w-none">
119
+ <p className="text-slate-800 text-lg leading-relaxed whitespace-pre-wrap font-medium">
120
+ {data.transcript || "Aucune transcription disponible."}
121
+ </p>
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+ </div>
126
+
127
+ <div className="space-y-8">
128
+ <Card className="shadow-sm border-slate-200">
129
+ <div className="bg-slate-50 border-b border-slate-100 px-6 py-4">
130
+ <h3 className="text-lg font-bold text-slate-900 flex items-center">
131
+ <BarChart2 className="mr-2 h-5 w-5 text-at-blue" />
132
+ Fiabilité IA
133
+ </h3>
134
+ </div>
135
+ <CardContent className="p-6">
136
+ <div className="mb-2 flex justify-between items-end">
137
+ <span className="text-4xl font-black text-slate-900">{(data.confidence * 100).toFixed(1)}%</span>
138
+ <span className="text-sm text-slate-500 font-bold uppercase tracking-wider">Score de Confiance</span>
139
+ </div>
140
+ <Progress value={data.confidence * 100} className="h-3 mt-4" />
141
+ <p className="text-xs text-slate-400 mt-3 font-medium">
142
+ La probabilité que le modèle DziriBERT ait correctement identifié le sentiment de cet appel.
143
+ </p>
144
+ </CardContent>
145
+ </Card>
146
+
147
+ <Card className="shadow-sm border-slate-200">
148
+ <div className="bg-slate-50 border-b border-slate-100 px-6 py-4">
149
+ <h3 className="text-lg font-bold text-slate-900 flex items-center">
150
+ <Tags className="mr-2 h-5 w-5 text-at-blue" />
151
+ Mots-Clés Extraits
152
+ </h3>
153
+ </div>
154
+ <CardContent className="p-6">
155
+ <div className="flex flex-wrap gap-2">
156
+ {data.keywords ? (
157
+ data.keywords.split(',').map((keyword, idx) => (
158
+ <span key={idx} className="px-3 py-1.5 bg-blue-50 text-blue-700 border border-blue-100 font-bold text-sm rounded-lg shadow-sm">
159
+ {keyword.trim()}
160
+ </span>
161
+ ))
162
+ ) : (
163
+ <span className="text-sm text-slate-500 italic">Aucun mot-clé pertinent extrait.</span>
164
+ )}
165
+ </div>
166
+ </CardContent>
167
+ </Card>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
frontend/src/pages/UploadPage.jsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from "react"
2
+ import { useNavigate } from "react-router-dom"
3
+ import axios from "axios"
4
+ import { UploadCloud, FileAudio, Loader2, Server, ShieldCheck } from "lucide-react"
5
+
6
+ export default function UploadPage() {
7
+ const [file, setFile] = useState(null)
8
+ const [agentName, setAgentName] = useState("")
9
+ const [isDragging, setIsDragging] = useState(false)
10
+ const [isUploading, setIsUploading] = useState(false)
11
+ const [error, setError] = useState(null)
12
+
13
+ const fileInputRef = useRef(null)
14
+ const navigate = useNavigate()
15
+
16
+ const handleDragOver = (e) => {
17
+ e.preventDefault()
18
+ setIsDragging(true)
19
+ }
20
+
21
+ const handleDragLeave = () => {
22
+ setIsDragging(false)
23
+ }
24
+
25
+ const handleDrop = (e) => {
26
+ e.preventDefault()
27
+ setIsDragging(false)
28
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
29
+ const droppedFile = e.dataTransfer.files[0]
30
+ validateAndSetFile(droppedFile)
31
+ }
32
+ }
33
+
34
+ const handleFileChange = (e) => {
35
+ if (e.target.files && e.target.files.length > 0) {
36
+ validateAndSetFile(e.target.files[0])
37
+ }
38
+ }
39
+
40
+ const validateAndSetFile = (selectedFile) => {
41
+ const validTypes = ['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/x-m4a']
42
+ if (validTypes.includes(selectedFile.type) || selectedFile.name.match(/\.(wav|mp3|ogg|m4a)$/i)) {
43
+ setFile(selectedFile)
44
+ setError(null)
45
+ } else {
46
+ setError("Format non supporté. Veuillez utiliser .wav, .mp3, .ogg ou .m4a")
47
+ setFile(null)
48
+ }
49
+ }
50
+
51
+ const handleUpload = async () => {
52
+ if (!file) return
53
+ setIsUploading(true)
54
+ setError(null)
55
+
56
+ const formData = new FormData()
57
+ formData.append("file", file)
58
+ formData.append("agent_name", agentName || "user")
59
+
60
+ try {
61
+ const response = await axios.post("/analyze-call", formData, {
62
+ headers: { "Content-Type": "multipart/form-data" }
63
+ })
64
+ if (response.data.success) {
65
+ navigate(`/result/${response.data.data.id}`)
66
+ }
67
+ } catch (err) {
68
+ console.error(err)
69
+ setError(err.response?.data?.detail || "Erreur de connexion au serveur IA.")
70
+ } finally {
71
+ setIsUploading(false)
72
+ }
73
+ }
74
+
75
+ return (
76
+ <div className="max-w-4xl mx-auto h-full flex flex-col justify-center pb-20 relative">
77
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-gradient-to-br from-at-blue/5 to-at-orange/5 rounded-full blur-3xl -z-10 pointer-events-none animate-pulse-soft"></div>
78
+
79
+ <div className="mb-10 text-center relative z-10">
80
+ <h1 className="text-5xl font-black tracking-tight text-transparent bg-clip-text bg-gradient-to-r from-slate-900 to-slate-700 mb-4 font-outfit drop-shadow-sm">Module d'Ingestion Audio</h1>
81
+ <p className="text-lg text-slate-500 max-w-2xl mx-auto font-medium">
82
+ Déposez un enregistrement d'appel client (Dialecte Algérien / Français) pour une transcription sécurisée et une analyse sémantique par notre moteur IA.
83
+ </p>
84
+ </div>
85
+
86
+ <div className="glass-card rounded-3xl overflow-hidden relative z-10">
87
+ <div className="flex border-b border-slate-100 bg-white/50 backdrop-blur-xl px-8 py-5 items-center justify-between">
88
+ <div className="flex items-center gap-3 text-sm font-bold text-slate-700 uppercase tracking-widest">
89
+ <Server className="h-5 w-5 text-at-blue" /> Pipeline IA: Connecté
90
+ </div>
91
+ <div className="flex items-center gap-2 text-xs font-bold text-emerald-700 bg-emerald-50 px-3 py-1.5 rounded-full border border-emerald-200 shadow-sm">
92
+ <ShieldCheck className="h-4 w-4" /> Traitement Local Sécurisé
93
+ </div>
94
+ </div>
95
+
96
+ <div className="p-12">
97
+ <div
98
+ className={`relative border-2 border-dashed rounded-3xl p-16 text-center transition-all duration-500 ease-out cursor-pointer overflow-hidden ${
99
+ isDragging
100
+ ? "border-at-blue bg-blue-50/80 scale-[1.02] shadow-[0_0_40px_rgba(0,76,151,0.1)]"
101
+ : "border-slate-300 hover:border-at-blue/50 bg-slate-50/50 hover:bg-white"
102
+ }`}
103
+ onDragOver={handleDragOver}
104
+ onDragLeave={handleDragLeave}
105
+ onDrop={handleDrop}
106
+ onClick={() => !isUploading && fileInputRef.current?.click()}
107
+ >
108
+ {isDragging && <div className="absolute inset-0 bg-gradient-to-br from-at-blue/5 to-transparent pointer-events-none"></div>}
109
+
110
+ <input
111
+ type="file"
112
+ className="hidden"
113
+ ref={fileInputRef}
114
+ onChange={handleFileChange}
115
+ accept=".wav,.mp3,.ogg,.m4a"
116
+ />
117
+
118
+ {!file ? (
119
+ <div className="flex flex-col items-center justify-center relative z-10 animate-float">
120
+ <div className="h-24 w-24 bg-white rounded-2xl flex items-center justify-center shadow-xl shadow-slate-200/50 mb-8 border border-slate-100 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
121
+ <UploadCloud className="h-12 w-12 text-at-blue drop-shadow-md" />
122
+ </div>
123
+ <h3 className="text-2xl font-black text-slate-900 mb-3 font-outfit">Cliquez ou glissez un fichier audio ici</h3>
124
+ <p className="text-base text-slate-500 font-medium">Formats supportés: WAV, MP3, OGG, M4A (Max 50MB)</p>
125
+ </div>
126
+ ) : (
127
+ <div className="flex flex-col items-center justify-center relative z-10">
128
+ <div className="h-24 w-24 bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl flex items-center justify-center mb-6 shadow-lg shadow-blue-900/10 border border-blue-200 animate-pulse-soft">
129
+ <FileAudio className="h-12 w-12 text-at-blue" />
130
+ </div>
131
+ <h3 className="text-2xl font-black text-slate-900 mb-2 truncate max-w-md font-outfit">{file.name}</h3>
132
+ <p className="text-sm font-bold text-at-blue mb-8 bg-blue-50 px-4 py-1.5 rounded-full border border-blue-100">
133
+ {(file.size / 1024 / 1024).toFixed(2)} MB
134
+ </p>
135
+ <button
136
+ className="text-sm font-bold text-slate-400 hover:text-slate-800 transition-colors uppercase tracking-wider"
137
+ onClick={(e) => { e.stopPropagation(); setFile(null); }}
138
+ disabled={isUploading}
139
+ >
140
+ Changer de fichier
141
+ </button>
142
+ </div>
143
+ )}
144
+ </div>
145
+
146
+ <div className="mt-8 mb-4 max-w-sm mx-auto">
147
+ <label htmlFor="agent_name" className="block text-sm font-bold text-slate-700 mb-2">Traçabilité de l'Agent (Optionnel)</label>
148
+ <input
149
+ type="text"
150
+ id="agent_name"
151
+ placeholder="user"
152
+ value={agentName}
153
+ onChange={(e) => setAgentName(e.target.value)}
154
+ className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-at-blue focus:ring-2 focus:ring-at-blue/20 transition-all font-medium text-slate-700 outline-none"
155
+ />
156
+ </div>
157
+
158
+ {error && (
159
+ <div className="mt-6 p-4 bg-red-50 border border-red-100 text-red-700 rounded-xl text-sm flex items-center gap-3 font-semibold shadow-sm animate-pulse">
160
+ <ShieldCheck className="h-5 w-5" /> {error}
161
+ </div>
162
+ )}
163
+
164
+ <div className="mt-10 flex justify-center">
165
+ <button
166
+ onClick={handleUpload}
167
+ disabled={!file || isUploading}
168
+ className={`flex items-center justify-center h-16 px-10 rounded-2xl font-black text-lg text-white transition-all duration-300 font-outfit tracking-wide ${
169
+ !file || isUploading
170
+ ? "bg-slate-200 text-slate-400 cursor-not-allowed"
171
+ : "bg-gradient-to-r from-at-blue via-blue-600 to-at-blue hover:scale-105 shadow-[0_10px_40px_rgba(0,76,151,0.3)] hover:shadow-[0_15px_50px_rgba(0,76,151,0.4)] bg-[length:200%_auto] hover:bg-[position:right_center]"
172
+ } w-full sm:w-auto min-w-[300px]`}
173
+ >
174
+ {isUploading ? (
175
+ <>
176
+ <Loader2 className="mr-3 h-6 w-6 animate-spin" />
177
+ Analyse IA en cours...
178
+ </>
179
+ ) : (
180
+ "Lancer l'Analyse d'Appel"
181
+ )}
182
+ </button>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ )
188
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ at: {
11
+ blue: '#004C97',
12
+ orange: '#FF7900',
13
+ }
14
+ },
15
+ fontFamily: {
16
+ sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
17
+ outfit: ['Outfit', 'sans-serif'],
18
+ }
19
+ },
20
+ },
21
+ plugins: [],
22
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from "path"
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ })