Deploy AIM Learning Companion to HF Spaces
Browse files* Rewrite app: strict Socratic questioning engine with FastAPI backend
- Remove standalone client-side index.html (violated spec)
- Use openai Python library with configurable base_url (no hardcoded provider)
- System prompts verbatim from spec: one question per response, never direct answers
- Stateless sessions: no localStorage, no cookies, no persistence
- No dashboard/usage statistics — only end-of-session analysis report
- RAG: ChromaDB + sentence-transformers, 3 chunks injected per turn
- 5-phase protocol: Ciblage/Clarification/Mecanisme/Verification/Stress-test
- Two modes: TUTOR (accompaniment) and CRITIC (devil's advocate)
- JSON export of full session (messages + scores + timestamps)
- Docker + docker-compose with volume-mounted /corpus
- README: dual config (OpenRouter test / Ollama production)
- No hardcoded references to any LLM provider in code
https://claude.ai/code/session_015z3yZxNNfXF63JuQDuPbEG
* Add Render deployment config for online demo
- render.yaml Blueprint: auto-configures service, env vars, persistent disk
- Dockerfile: install PyTorch CPU-only to reduce image size (~800MB vs ~2.5GB)
- README: deployment instructions for Render
https://claude.ai/code/session_015z3yZxNNfXF63JuQDuPbEG
* Switch deployment to Hugging Face Spaces (free, no credit card)
- Remove render.yaml (requires paid plan)
- Dockerfile: use port 7860 (HF Spaces default)
- README: HF Spaces deployment instructions replace Render
https://claude.ai/code/session_015z3yZxNNfXF63JuQDuPbEG
* Add Hugging Face Spaces metadata to README.md
https://claude.ai/code/session_015z3yZxNNfXF63JuQDuPbEG
---------
Co-authored-by: Claude <noreply@anthropic.com>
- .env.example +7 -0
- .gitignore +1 -0
- Dockerfile +5 -2
- README.md +91 -30
- app/llm.py +57 -70
- app/main.py +5 -18
- app/rag.py +4 -4
- corpus/sample.txt +2 -2
- docker-compose.yml +2 -3
- index.html +0 -845
- requirements.txt +2 -1
- static/app.js +281 -247
- static/index.html +75 -98
- static/style.css +275 -308
|
@@ -1,2 +1,9 @@
|
|
|
|
|
| 1 |
OPENROUTER_API_KEY=sk-or-v1-your-key-here
|
|
|
|
| 2 |
LLM_MODEL=mistralai/mistral-7b-instruct
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Option A : Test (OpenRouter) ---
|
| 2 |
OPENROUTER_API_KEY=sk-or-v1-your-key-here
|
| 3 |
+
LLM_BASE_URL=https://openrouter.ai/api/v1
|
| 4 |
LLM_MODEL=mistralai/mistral-7b-instruct
|
| 5 |
+
|
| 6 |
+
# --- Option B : Production (Ollama local) ---
|
| 7 |
+
# OPENROUTER_API_KEY=ollama
|
| 8 |
+
# LLM_BASE_URL=http://localhost:11434/v1
|
| 9 |
+
# LLM_MODEL=mistral:instruct
|
|
@@ -2,3 +2,4 @@
|
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
| 4 |
chroma_data/
|
|
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
| 4 |
chroma_data/
|
| 5 |
+
*.egg-info/
|
|
@@ -7,10 +7,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 7 |
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
|
| 9 |
COPY requirements.txt .
|
|
|
|
|
|
|
|
|
|
| 10 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
|
| 12 |
COPY . .
|
| 13 |
|
| 14 |
-
EXPOSE
|
| 15 |
|
| 16 |
-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "
|
|
|
|
| 7 |
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
|
| 9 |
COPY requirements.txt .
|
| 10 |
+
|
| 11 |
+
# Install PyTorch CPU-only first (much smaller than default CUDA build)
|
| 12 |
+
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
|
| 13 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
|
| 15 |
COPY . .
|
| 16 |
|
| 17 |
+
EXPOSE 7860
|
| 18 |
|
| 19 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
@@ -1,53 +1,114 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Architecture
|
| 6 |
|
| 7 |
-
- **Backend**: FastAPI (Python)
|
| 8 |
-
- **LLM**:
|
| 9 |
-
- **Vector Store**: ChromaDB (local)
|
| 10 |
-
- **Embeddings**: sentence-transformers (`all-MiniLM-L6-v2`
|
| 11 |
-
- **Frontend**: Vanilla HTML/CSS/JS
|
| 12 |
|
| 13 |
-
##
|
| 14 |
|
| 15 |
-
1.
|
| 16 |
-
2. **Pull the model**:
|
| 17 |
```bash
|
| 18 |
-
|
| 19 |
```
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
| 21 |
```bash
|
| 22 |
-
|
| 23 |
```
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
|
|
|
| 27 |
```bash
|
| 28 |
-
|
|
|
|
| 29 |
```
|
| 30 |
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
## Corpus
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
##
|
| 40 |
|
| 41 |
-
- **
|
| 42 |
-
- **
|
| 43 |
-
- **
|
| 44 |
-
- **
|
| 45 |
-
- **
|
| 46 |
-
- **
|
| 47 |
|
| 48 |
-
##
|
| 49 |
|
| 50 |
-
-
|
| 51 |
-
-
|
| 52 |
-
-
|
| 53 |
-
-
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AIM Learning Companion
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
|
| 11 |
+
# AIM - Compagnon socratique d'apprentissage
|
| 12 |
+
|
| 13 |
+
Moteur de questionnement socratique pour apprenants adultes en formation professionnelle. Applique un protocole strict en 5 phases pour guider la reflexion critique, ancre dans les documents de cours via RAG.
|
| 14 |
|
| 15 |
## Architecture
|
| 16 |
|
| 17 |
+
- **Backend** : FastAPI (Python)
|
| 18 |
+
- **LLM** : OpenRouter ou Ollama (configurable via `.env`)
|
| 19 |
+
- **Vector Store** : ChromaDB (local, persistant)
|
| 20 |
+
- **Embeddings** : sentence-transformers (`all-MiniLM-L6-v2`)
|
| 21 |
+
- **Frontend** : Vanilla HTML/CSS/JS (servi par FastAPI)
|
| 22 |
|
| 23 |
+
## Demarrage rapide
|
| 24 |
|
| 25 |
+
1. Copier le fichier d'environnement :
|
|
|
|
| 26 |
```bash
|
| 27 |
+
cp .env.example .env
|
| 28 |
```
|
| 29 |
+
|
| 30 |
+
2. Configurer la cle API dans `.env` (voir section "Configuration LLM" ci-dessous)
|
| 31 |
+
|
| 32 |
+
3. Lancer :
|
| 33 |
```bash
|
| 34 |
+
docker-compose up --build
|
| 35 |
```
|
| 36 |
|
| 37 |
+
4. Ouvrir http://localhost:8000
|
| 38 |
+
|
| 39 |
+
## Configuration LLM
|
| 40 |
+
|
| 41 |
+
### Option A : Test (OpenRouter)
|
| 42 |
+
|
| 43 |
+
```env
|
| 44 |
+
OPENROUTER_API_KEY=sk-or-v1-votre-cle-ici
|
| 45 |
+
LLM_BASE_URL=https://openrouter.ai/api/v1
|
| 46 |
+
LLM_MODEL=mistralai/mistral-7b-instruct
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### Option B : Production (Ollama local)
|
| 50 |
+
|
| 51 |
+
```env
|
| 52 |
+
OPENROUTER_API_KEY=ollama
|
| 53 |
+
LLM_BASE_URL=http://localhost:11434/v1
|
| 54 |
+
LLM_MODEL=mistral:instruct
|
| 55 |
+
```
|
| 56 |
|
| 57 |
+
Prerequis pour Ollama :
|
| 58 |
```bash
|
| 59 |
+
ollama pull mistral:instruct
|
| 60 |
+
ollama serve
|
| 61 |
```
|
| 62 |
|
| 63 |
+
## Deploiement en ligne (Hugging Face Spaces)
|
| 64 |
+
|
| 65 |
+
Pour une demo accessible via URL publique, gratuite, sans carte bancaire :
|
| 66 |
+
|
| 67 |
+
1. Creer un compte sur [huggingface.co](https://huggingface.co)
|
| 68 |
+
2. **New Space** > choisir **Docker** comme SDK
|
| 69 |
+
3. Connecter le repo GitHub ou pousser le code directement
|
| 70 |
+
4. Dans **Settings > Variables and secrets**, ajouter comme **secrets** :
|
| 71 |
+
- `OPENROUTER_API_KEY` = votre cle OpenRouter
|
| 72 |
+
- `LLM_BASE_URL` = `https://openrouter.ai/api/v1`
|
| 73 |
+
- `LLM_MODEL` = `mistralai/mistral-7b-instruct`
|
| 74 |
+
5. Le Space se build et deploie automatiquement
|
| 75 |
+
6. URL publique : `https://votre-nom-aim.hf.space`
|
| 76 |
+
|
| 77 |
+
Free tier : 2 Go RAM, 16 Go disque — suffisant pour sentence-transformers + ChromaDB.
|
| 78 |
+
|
| 79 |
+
## Migration vers deploiement local
|
| 80 |
+
|
| 81 |
+
L'application est concue pour rendre la migration triviale :
|
| 82 |
+
|
| 83 |
+
1. **LLM** : Changer uniquement `LLM_BASE_URL` et `LLM_MODEL` dans `.env`
|
| 84 |
+
- Remplacer `https://openrouter.ai/api/v1` par `http://localhost:11434/v1`
|
| 85 |
+
- Remplacer `mistralai/mistral-7b-instruct` par `mistral:instruct`
|
| 86 |
+
- Mettre `OPENROUTER_API_KEY=ollama` (Ollama n'exige pas de cle mais le champ doit etre non-vide)
|
| 87 |
+
|
| 88 |
+
2. **Aucune modification de code requise** : le `base_url` et l'`api_key` sont charges exclusivement depuis `.env`
|
| 89 |
+
|
| 90 |
+
3. **Resultat** : inference 100% locale, aucune donnee ne quitte le reseau client
|
| 91 |
|
| 92 |
## Corpus
|
| 93 |
|
| 94 |
+
Placer des fichiers `.pdf` et `.txt` dans le dossier `/corpus`. Le pipeline RAG les charge et les indexe au demarrage.
|
| 95 |
+
|
| 96 |
+
Le dossier `/corpus` est monte en volume — ajout ou remplacement de documents sans reconstruction du conteneur.
|
| 97 |
|
| 98 |
+
Un fichier `sample.txt` est inclus pour tester le RAG immediatement.
|
| 99 |
|
| 100 |
+
## Fonctionnalites
|
| 101 |
|
| 102 |
+
- **Deux modes** : TUTEUR (accompagnement) et CRITIQUE (audit logique)
|
| 103 |
+
- **Protocole socratique en 5 phases** : Ciblage → Clarification → Mecanisme → Verification → Stress-test
|
| 104 |
+
- **Une seule question par reponse** — jamais de reponse directe
|
| 105 |
+
- **RAG** : questions ancrees dans les documents de cours
|
| 106 |
+
- **Analyse de fin de session** : 6 dimensions notees (0-100) + bilan + export JSON
|
| 107 |
+
- **Stateless** : aucune donnee persistee apres fermeture de l'onglet
|
| 108 |
|
| 109 |
+
## Confidentialite
|
| 110 |
|
| 111 |
+
- Sessions sans etat : aucune donnee persistee apres fermeture de l'onglet
|
| 112 |
+
- Pas de localStorage pour l'historique
|
| 113 |
+
- Pas de comptes utilisateurs, pas d'authentification
|
| 114 |
+
- Pas de cookies, pas de tracking
|
|
@@ -1,46 +1,67 @@
|
|
| 1 |
-
"""LLM interaction via
|
| 2 |
|
|
|
|
| 3 |
import os
|
| 4 |
-
import httpx
|
| 5 |
|
| 6 |
-
|
| 7 |
-
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
| 8 |
-
MODEL = os.environ.get("LLM_MODEL", "mistralai/mistral-7b-instruct")
|
| 9 |
-
TIMEOUT = 120.0
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
SYSTEM_TUTOR = """Tu es un mentor socratique bienveillant, empathique et complice.
|
| 13 |
Tu utilises systématiquement le "TU" pour t'adresser à l'apprenant·e.
|
|
|
|
| 14 |
Ton but est de faire accoucher l'esprit (maïeutique) en guidant la réflexion pas à pas.
|
| 15 |
Règles :
|
| 16 |
1. Ne dépasse jamais 3 à 4 phrases par message.
|
| 17 |
-
2. Valide l'effort avant de rediriger.
|
| 18 |
-
3. Si
|
| 19 |
-
4.
|
| 20 |
-
5.
|
|
|
|
| 21 |
À la fin de chaque message, ajoute obligatoirement :
|
| 22 |
---
|
| 23 |
Phase: [Numéro]
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
| 27 |
|
| 28 |
SYSTEM_CRITIC = """Tu es un mentor socratique bienveillant, empathique et complice.
|
| 29 |
Tu utilises systématiquement le "TU" pour t'adresser à l'apprenant·e.
|
|
|
|
| 30 |
Ton but est de faire accoucher l'esprit (maïeutique) en guidant la réflexion pas à pas.
|
| 31 |
Règles :
|
| 32 |
1. Ne dépasse jamais 3 à 4 phrases par message.
|
| 33 |
-
2. Valide l'effort avant de rediriger.
|
| 34 |
-
3. Si
|
| 35 |
-
4.
|
| 36 |
-
5.
|
|
|
|
| 37 |
À la fin de chaque message, ajoute obligatoirement :
|
| 38 |
---
|
| 39 |
Phase: [Numéro]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
ANALYSIS_SYSTEM = """Tu es un évaluateur pédagogique. Analyse la conversation suivante entre un mentor socratique et un apprenant.
|
| 46 |
Produis un JSON strict avec cette structure :
|
|
@@ -51,70 +72,38 @@ Produis un JSON strict avec cette structure :
|
|
| 51 |
"processScore": <0-100>,
|
| 52 |
"reflectionScore": <0-100>,
|
| 53 |
"integrityScore": <0-100>,
|
| 54 |
-
"summary": "<
|
| 55 |
"keyStrengths": ["...", "..."],
|
| 56 |
"weaknesses": ["...", "..."]
|
| 57 |
}
|
| 58 |
Réponds UNIQUEMENT avec le JSON, sans texte autour."""
|
| 59 |
|
| 60 |
|
| 61 |
-
|
| 62 |
-
0: "Phase actuelle: 0 (Ciblage). Identifie l'objet exact de l'interrogation et l'intention de l'apprenant·e.",
|
| 63 |
-
1: "Phase actuelle: 1 (Clarification). Fais émerger les ambiguïtés conceptuelles, définis les termes rigoureusement.",
|
| 64 |
-
2: "Phase actuelle: 2 (Mécanisme). Explore les relations cause-effet. 'Comment ça marche ?'",
|
| 65 |
-
3: "Phase actuelle: 3 (Vérification). Pousse vers des preuves, critères testables, protocoles de preuve.",
|
| 66 |
-
4: "Phase actuelle: 4 (Stress-test). Confronte le raisonnement avec des contre-exemples et des limites.",
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
STRATEGIES = [
|
| 70 |
-
"clarification", "test_necessite", "contre_exemple", "prediction",
|
| 71 |
-
"falsifiabilite", "mecanisme_causal", "changement_cadre",
|
| 72 |
-
"compression", "concession_controlee",
|
| 73 |
-
]
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def build_system_prompt(mode: str, topic: str, phase: int, rag_context: list[str]) -> str:
|
| 77 |
"""Build the full system prompt with mode, phase guidance, and RAG context."""
|
| 78 |
template = SYSTEM_TUTOR if mode == "TUTOR" else SYSTEM_CRITIC
|
| 79 |
-
system = template.replace("{topic}", topic)
|
| 80 |
-
|
| 81 |
-
system += f"\n\n{PHASE_GUIDANCE.get(phase, PHASE_GUIDANCE[0])}"
|
| 82 |
|
| 83 |
-
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
context_text = "\n---\n".join(rag_context)
|
| 87 |
-
system += f"\n\nContexte documentaire (utilise-le pour ancrer tes questions, ne le montre PAS directement à l'apprenant·e) :\n{context_text}"
|
| 88 |
|
| 89 |
-
return
|
| 90 |
|
| 91 |
|
| 92 |
async def chat(system_prompt: str, messages: list[dict]) -> str:
|
| 93 |
-
"""Send chat
|
| 94 |
-
api_messages = [{"role": "system", "content": system_prompt}]
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
| 102 |
-
"Content-Type": "application/json",
|
| 103 |
-
},
|
| 104 |
-
json={
|
| 105 |
-
"model": MODEL,
|
| 106 |
-
"messages": api_messages,
|
| 107 |
-
},
|
| 108 |
-
)
|
| 109 |
-
response.raise_for_status()
|
| 110 |
-
data = response.json()
|
| 111 |
-
return data["choices"][0]["message"]["content"]
|
| 112 |
|
| 113 |
|
| 114 |
async def analyze_session(messages: list[dict]) -> dict:
|
| 115 |
"""Generate end-of-session analysis via a second LLM call."""
|
| 116 |
-
import json as json_mod
|
| 117 |
-
|
| 118 |
conversation_text = "\n".join(
|
| 119 |
f"{'Apprenant' if m['role'] == 'user' else 'Companion'}: {m['content']}"
|
| 120 |
for m in messages
|
|
@@ -126,14 +115,12 @@ async def analyze_session(messages: list[dict]) -> dict:
|
|
| 126 |
|
| 127 |
raw = await chat(ANALYSIS_SYSTEM, analysis_messages)
|
| 128 |
|
| 129 |
-
# Try to parse JSON from response
|
| 130 |
try:
|
| 131 |
-
# Find JSON in the response
|
| 132 |
start = raw.find("{")
|
| 133 |
end = raw.rfind("}") + 1
|
| 134 |
if start >= 0 and end > start:
|
| 135 |
-
return
|
| 136 |
-
except (
|
| 137 |
pass
|
| 138 |
|
| 139 |
return {
|
|
|
|
| 1 |
+
"""LLM interaction via OpenAI-compatible API."""
|
| 2 |
|
| 3 |
+
import json
|
| 4 |
import os
|
|
|
|
| 5 |
|
| 6 |
+
from openai import AsyncOpenAI
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
_api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
| 9 |
+
_base_url = os.environ.get("LLM_BASE_URL", "")
|
| 10 |
+
_model = os.environ.get("LLM_MODEL", "mistralai/mistral-7b-instruct")
|
| 11 |
+
|
| 12 |
+
_client = AsyncOpenAI(api_key=_api_key, base_url=_base_url)
|
| 13 |
+
|
| 14 |
+
# ---------------------------------------------------------------------------
|
| 15 |
+
# System prompts (verbatim from spec)
|
| 16 |
+
# ---------------------------------------------------------------------------
|
| 17 |
|
| 18 |
SYSTEM_TUTOR = """Tu es un mentor socratique bienveillant, empathique et complice.
|
| 19 |
Tu utilises systématiquement le "TU" pour t'adresser à l'apprenant·e.
|
| 20 |
+
Tu ne donnes jamais de réponse directe. Tu poses une seule question par message.
|
| 21 |
Ton but est de faire accoucher l'esprit (maïeutique) en guidant la réflexion pas à pas.
|
| 22 |
Règles :
|
| 23 |
1. Ne dépasse jamais 3 à 4 phrases par message.
|
| 24 |
+
2. Valide l'effort avant de rediriger.
|
| 25 |
+
3. Si l'apprenant·e bloque, propose une analogie ou un indice progressif.
|
| 26 |
+
4. Si une définition est demandée, explique en max 2 phrases puis pose immédiatement une question de vérification.
|
| 27 |
+
5. Dès qu'une base est posée en Phase 1, avance vers Phase 2.
|
| 28 |
+
6. Préfère l'invitation au reproche : "Ce point semble complexe, essayons un autre angle..."
|
| 29 |
À la fin de chaque message, ajoute obligatoirement :
|
| 30 |
---
|
| 31 |
Phase: [Numéro]
|
| 32 |
+
Mode : Tuteur
|
| 33 |
+
Sujet d'exploration : "{topic}"
|
| 34 |
+
Contexte du cours (extrait RAG) :
|
| 35 |
+
{rag_context}"""
|
| 36 |
|
| 37 |
SYSTEM_CRITIC = """Tu es un mentor socratique bienveillant, empathique et complice.
|
| 38 |
Tu utilises systématiquement le "TU" pour t'adresser à l'apprenant·e.
|
| 39 |
+
Tu ne donnes jamais de réponse directe. Tu poses une seule question par message.
|
| 40 |
Ton but est de faire accoucher l'esprit (maïeutique) en guidant la réflexion pas à pas.
|
| 41 |
Règles :
|
| 42 |
1. Ne dépasse jamais 3 à 4 phrases par message.
|
| 43 |
+
2. Valide l'effort avant de rediriger.
|
| 44 |
+
3. Si l'apprenant·e bloque, propose une analogie ou un indice progressif.
|
| 45 |
+
4. Si une définition est demandée, explique en max 2 phrases puis pose immédiatement une question de vérification.
|
| 46 |
+
5. Dès qu'une base est posée en Phase 1, avance vers Phase 2.
|
| 47 |
+
6. Préfère l'invitation au reproche : "Ce point semble complexe, essayons un autre angle..."
|
| 48 |
À la fin de chaque message, ajoute obligatoirement :
|
| 49 |
---
|
| 50 |
Phase: [Numéro]
|
| 51 |
+
Mode : Critique
|
| 52 |
+
Ta mission : proposer des raisonnements fallacieux pour tester la vigilance.
|
| 53 |
+
Reste un partenaire de jeu élégant, jamais méprisant.
|
| 54 |
+
Sujet d'exploration : "{topic}"
|
| 55 |
+
Contexte du cours (extrait RAG) :
|
| 56 |
+
{rag_context}"""
|
| 57 |
|
| 58 |
+
PHASE_GUIDANCE = {
|
| 59 |
+
0: "Phase actuelle : 0 (Ciblage). Reformule l'input de l'apprenant·e pour identifier l'objet exact de l'interrogation.",
|
| 60 |
+
1: "Phase actuelle : 1 (Clarification). Fais émerger les ambiguïtés conceptuelles, demande des définitions de termes.",
|
| 61 |
+
2: "Phase actuelle : 2 (Mécanisme). Demande à l'apprenant·e d'expliquer les relations cause-effet.",
|
| 62 |
+
3: "Phase actuelle : 3 (Vérification). Demande à l'apprenant·e d'identifier des preuves ou des critères testables.",
|
| 63 |
+
4: "Phase actuelle : 4 (Stress-test). Confronte le raisonnement avec ses propres limites ou des contre-exemples.",
|
| 64 |
+
}
|
| 65 |
|
| 66 |
ANALYSIS_SYSTEM = """Tu es un évaluateur pédagogique. Analyse la conversation suivante entre un mentor socratique et un apprenant.
|
| 67 |
Produis un JSON strict avec cette structure :
|
|
|
|
| 72 |
"processScore": <0-100>,
|
| 73 |
"reflectionScore": <0-100>,
|
| 74 |
"integrityScore": <0-100>,
|
| 75 |
+
"summary": "<évaluation de la progression cognitive, 150 mots max>",
|
| 76 |
"keyStrengths": ["...", "..."],
|
| 77 |
"weaknesses": ["...", "..."]
|
| 78 |
}
|
| 79 |
Réponds UNIQUEMENT avec le JSON, sans texte autour."""
|
| 80 |
|
| 81 |
|
| 82 |
+
def build_system_prompt(mode: str, topic: str, phase: int, rag_chunks: list[str]) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
"""Build the full system prompt with mode, phase guidance, and RAG context."""
|
| 84 |
template = SYSTEM_TUTOR if mode == "TUTOR" else SYSTEM_CRITIC
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
rag_text = "\n---\n".join(rag_chunks) if rag_chunks else "(aucun document chargé)"
|
| 87 |
+
prompt = template.replace("{topic}", topic).replace("{rag_context}", rag_text)
|
| 88 |
|
| 89 |
+
prompt += f"\n\n{PHASE_GUIDANCE.get(phase, PHASE_GUIDANCE[0])}"
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
return prompt
|
| 92 |
|
| 93 |
|
| 94 |
async def chat(system_prompt: str, messages: list[dict]) -> str:
|
| 95 |
+
"""Send chat completion request and return assistant message."""
|
| 96 |
+
api_messages = [{"role": "system", "content": system_prompt}] + messages
|
| 97 |
+
|
| 98 |
+
response = await _client.chat.completions.create(
|
| 99 |
+
model=_model,
|
| 100 |
+
messages=api_messages,
|
| 101 |
+
)
|
| 102 |
+
return response.choices[0].message.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
async def analyze_session(messages: list[dict]) -> dict:
|
| 106 |
"""Generate end-of-session analysis via a second LLM call."""
|
|
|
|
|
|
|
| 107 |
conversation_text = "\n".join(
|
| 108 |
f"{'Apprenant' if m['role'] == 'user' else 'Companion'}: {m['content']}"
|
| 109 |
for m in messages
|
|
|
|
| 115 |
|
| 116 |
raw = await chat(ANALYSIS_SYSTEM, analysis_messages)
|
| 117 |
|
|
|
|
| 118 |
try:
|
|
|
|
| 119 |
start = raw.find("{")
|
| 120 |
end = raw.rfind("}") + 1
|
| 121 |
if start >= 0 and end > start:
|
| 122 |
+
return json.loads(raw[start:end])
|
| 123 |
+
except (json.JSONDecodeError, ValueError):
|
| 124 |
pass
|
| 125 |
|
| 126 |
return {
|
|
@@ -1,12 +1,11 @@
|
|
| 1 |
"""FastAPI application for AIM Learning Companion."""
|
| 2 |
|
| 3 |
-
import
|
| 4 |
-
import time
|
| 5 |
from contextlib import asynccontextmanager
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from fastapi import FastAPI
|
| 9 |
-
from fastapi.responses import
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from pydantic import BaseModel
|
| 12 |
|
|
@@ -33,7 +32,6 @@ class ChatRequest(BaseModel):
|
|
| 33 |
topic: str = ""
|
| 34 |
phase: int = 0
|
| 35 |
history: list[dict] = []
|
| 36 |
-
timestamp: float = 0.0
|
| 37 |
|
| 38 |
|
| 39 |
class ChatResponse(BaseModel):
|
|
@@ -59,14 +57,13 @@ class AnalysisResponse(BaseModel):
|
|
| 59 |
rhythmBreakCount: int = 0
|
| 60 |
|
| 61 |
|
| 62 |
-
@app.get("/"
|
| 63 |
async def index():
|
| 64 |
return FileResponse(str(STATIC_DIR / "index.html"))
|
| 65 |
|
| 66 |
|
| 67 |
def _detect_phase(reply: str, current_phase: int) -> int:
|
| 68 |
"""Extract phase number from the companion's reply."""
|
| 69 |
-
import re
|
| 70 |
match = re.search(r"Phase:\s*(\d)", reply)
|
| 71 |
if match:
|
| 72 |
return int(match.group(1))
|
|
@@ -75,22 +72,13 @@ def _detect_phase(reply: str, current_phase: int) -> int:
|
|
| 75 |
|
| 76 |
@app.post("/api/chat", response_model=ChatResponse)
|
| 77 |
async def api_chat(req: ChatRequest):
|
| 78 |
-
# Retrieve relevant context from corpus
|
| 79 |
rag_chunks = retrieve(req.message)
|
| 80 |
-
|
| 81 |
-
# Build system prompt
|
| 82 |
system_prompt = build_system_prompt(req.mode, req.topic, req.phase, rag_chunks)
|
| 83 |
|
| 84 |
-
|
| 85 |
-
messages = []
|
| 86 |
-
for msg in req.history:
|
| 87 |
-
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 88 |
messages.append({"role": "user", "content": req.message})
|
| 89 |
|
| 90 |
-
# Get LLM response
|
| 91 |
reply = await chat(system_prompt, messages)
|
| 92 |
-
|
| 93 |
-
# Detect phase from reply
|
| 94 |
detected_phase = _detect_phase(reply, req.phase)
|
| 95 |
|
| 96 |
return ChatResponse(reply=reply, phase=detected_phase)
|
|
@@ -98,10 +86,9 @@ async def api_chat(req: ChatRequest):
|
|
| 98 |
|
| 99 |
@app.post("/api/analyze", response_model=AnalysisResponse)
|
| 100 |
async def api_analyze(req: AnalysisRequest):
|
| 101 |
-
# Get LLM analysis
|
| 102 |
analysis = await analyze_session(req.history)
|
| 103 |
|
| 104 |
-
# Count rhythm breaks
|
| 105 |
rhythm_breaks = 0
|
| 106 |
if len(req.timestamps) >= 2:
|
| 107 |
for i in range(1, len(req.timestamps), 2):
|
|
|
|
| 1 |
"""FastAPI application for AIM Learning Companion."""
|
| 2 |
|
| 3 |
+
import re
|
|
|
|
| 4 |
from contextlib import asynccontextmanager
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
from fastapi import FastAPI
|
| 8 |
+
from fastapi.responses import FileResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
from pydantic import BaseModel
|
| 11 |
|
|
|
|
| 32 |
topic: str = ""
|
| 33 |
phase: int = 0
|
| 34 |
history: list[dict] = []
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class ChatResponse(BaseModel):
|
|
|
|
| 57 |
rhythmBreakCount: int = 0
|
| 58 |
|
| 59 |
|
| 60 |
+
@app.get("/")
|
| 61 |
async def index():
|
| 62 |
return FileResponse(str(STATIC_DIR / "index.html"))
|
| 63 |
|
| 64 |
|
| 65 |
def _detect_phase(reply: str, current_phase: int) -> int:
|
| 66 |
"""Extract phase number from the companion's reply."""
|
|
|
|
| 67 |
match = re.search(r"Phase:\s*(\d)", reply)
|
| 68 |
if match:
|
| 69 |
return int(match.group(1))
|
|
|
|
| 72 |
|
| 73 |
@app.post("/api/chat", response_model=ChatResponse)
|
| 74 |
async def api_chat(req: ChatRequest):
|
|
|
|
| 75 |
rag_chunks = retrieve(req.message)
|
|
|
|
|
|
|
| 76 |
system_prompt = build_system_prompt(req.mode, req.topic, req.phase, rag_chunks)
|
| 77 |
|
| 78 |
+
messages = [{"role": m["role"], "content": m["content"]} for m in req.history]
|
|
|
|
|
|
|
|
|
|
| 79 |
messages.append({"role": "user", "content": req.message})
|
| 80 |
|
|
|
|
| 81 |
reply = await chat(system_prompt, messages)
|
|
|
|
|
|
|
| 82 |
detected_phase = _detect_phase(reply, req.phase)
|
| 83 |
|
| 84 |
return ChatResponse(reply=reply, phase=detected_phase)
|
|
|
|
| 86 |
|
| 87 |
@app.post("/api/analyze", response_model=AnalysisResponse)
|
| 88 |
async def api_analyze(req: AnalysisRequest):
|
|
|
|
| 89 |
analysis = await analyze_session(req.history)
|
| 90 |
|
| 91 |
+
# Count rhythm breaks: user responses submitted in under 8 seconds
|
| 92 |
rhythm_breaks = 0
|
| 93 |
if len(req.timestamps) >= 2:
|
| 94 |
for i in range(1, len(req.timestamps), 2):
|
|
@@ -1,13 +1,13 @@
|
|
| 1 |
"""RAG layer: load corpus, chunk, embed, and retrieve."""
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
|
| 5 |
import chromadb
|
| 6 |
from sentence_transformers import SentenceTransformer
|
| 7 |
|
| 8 |
CORPUS_DIR = os.environ.get("CORPUS_DIR", "corpus")
|
| 9 |
CHROMA_DIR = os.environ.get("CHROMA_DIR", "chroma_data")
|
| 10 |
-
CHUNK_SIZE = 500
|
| 11 |
CHUNK_OVERLAP = 50
|
| 12 |
TOP_K = 3
|
| 13 |
|
|
@@ -60,11 +60,11 @@ def load_corpus() -> None:
|
|
| 60 |
is_persistent=True,
|
| 61 |
))
|
| 62 |
|
| 63 |
-
# Delete and recreate to pick up new documents
|
| 64 |
try:
|
| 65 |
client.delete_collection("corpus")
|
| 66 |
except Exception:
|
| 67 |
pass
|
|
|
|
| 68 |
_collection = client.create_collection(
|
| 69 |
name="corpus",
|
| 70 |
metadata={"hnsw:space": "cosine"},
|
|
@@ -78,7 +78,7 @@ def load_corpus() -> None:
|
|
| 78 |
if not os.path.isdir(CORPUS_DIR):
|
| 79 |
return
|
| 80 |
|
| 81 |
-
for filename in os.listdir(CORPUS_DIR):
|
| 82 |
filepath = os.path.join(CORPUS_DIR, filename)
|
| 83 |
if filename.lower().endswith(".txt"):
|
| 84 |
text = _read_txt(filepath)
|
|
|
|
| 1 |
"""RAG layer: load corpus, chunk, embed, and retrieve."""
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
|
| 5 |
import chromadb
|
| 6 |
from sentence_transformers import SentenceTransformer
|
| 7 |
|
| 8 |
CORPUS_DIR = os.environ.get("CORPUS_DIR", "corpus")
|
| 9 |
CHROMA_DIR = os.environ.get("CHROMA_DIR", "chroma_data")
|
| 10 |
+
CHUNK_SIZE = 500 # approximate token count (words used as proxy)
|
| 11 |
CHUNK_OVERLAP = 50
|
| 12 |
TOP_K = 3
|
| 13 |
|
|
|
|
| 60 |
is_persistent=True,
|
| 61 |
))
|
| 62 |
|
|
|
|
| 63 |
try:
|
| 64 |
client.delete_collection("corpus")
|
| 65 |
except Exception:
|
| 66 |
pass
|
| 67 |
+
|
| 68 |
_collection = client.create_collection(
|
| 69 |
name="corpus",
|
| 70 |
metadata={"hnsw:space": "cosine"},
|
|
|
|
| 78 |
if not os.path.isdir(CORPUS_DIR):
|
| 79 |
return
|
| 80 |
|
| 81 |
+
for filename in sorted(os.listdir(CORPUS_DIR)):
|
| 82 |
filepath = os.path.join(CORPUS_DIR, filename)
|
| 83 |
if filename.lower().endswith(".txt"):
|
| 84 |
text = _read_txt(filepath)
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
Critical Thinking in Professional
|
| 2 |
|
| 3 |
In professional environments where artificial intelligence systems are deployed, critical thinking becomes an essential competency that goes beyond simple technical understanding. Practitioners must evaluate AI outputs not as ground truth but as probabilistic suggestions shaped by training data, model architecture, and inference parameters. A critical thinker in this domain questions the provenance of training data, identifies potential biases embedded in model outputs, and assesses whether the confidence level reported by an AI system genuinely reflects the reliability of its predictions. This requires a combination of domain expertise, statistical literacy, and epistemological humility — the recognition that even highly performant models can fail in subtle, context-dependent ways.
|
| 4 |
|
| 5 |
-
The application of critical thinking to AI decision-making in professional training contexts demands structured reasoning frameworks. Professionals must learn to distinguish between correlation and causation when interpreting AI-generated insights, evaluate the external validity of models trained on historical data when applied to novel situations, and recognize the limits of automation in tasks requiring ethical judgment or contextual nuance. Effective critical evaluation also involves stress-testing AI recommendations against edge cases, considering what evidence would falsify a given AI-generated conclusion, and maintaining awareness of the Dunning-Kruger effect — where superficial familiarity with AI tools can create an illusion of deep understanding.
|
| 6 |
|
| 7 |
Building a culture of critical thinking around AI in organizations requires deliberate pedagogical strategies. Rather than passively accepting AI outputs, professionals should be trained to interrogate them through Socratic questioning: What assumptions does this model make? What data was excluded? Under what conditions would this recommendation fail? How would we verify this output independently? These habits of mind transform AI users from passive consumers into active evaluators, creating a more robust and trustworthy integration of artificial intelligence into professional workflows. The goal is not skepticism for its own sake, but informed trust — knowing when to rely on AI systems and when to override them based on principled reasoning.
|
|
|
|
| 1 |
+
Critical Thinking Applied to AI in Professional Training Contexts
|
| 2 |
|
| 3 |
In professional environments where artificial intelligence systems are deployed, critical thinking becomes an essential competency that goes beyond simple technical understanding. Practitioners must evaluate AI outputs not as ground truth but as probabilistic suggestions shaped by training data, model architecture, and inference parameters. A critical thinker in this domain questions the provenance of training data, identifies potential biases embedded in model outputs, and assesses whether the confidence level reported by an AI system genuinely reflects the reliability of its predictions. This requires a combination of domain expertise, statistical literacy, and epistemological humility — the recognition that even highly performant models can fail in subtle, context-dependent ways.
|
| 4 |
|
| 5 |
+
The application of critical thinking to AI decision-making in professional training contexts demands structured reasoning frameworks. Professionals must learn to distinguish between correlation and causation when interpreting AI-generated insights, evaluate the external validity of models trained on historical data when applied to novel situations, and recognize the limits of automation in tasks requiring ethical judgment or contextual nuance. Effective critical evaluation also involves stress-testing AI recommendations against edge cases, considering what evidence would falsify a given AI-generated conclusion, and maintaining awareness of the Dunning-Kruger effect — where superficial familiarity with AI tools can create an illusion of deep understanding. Training programs should embed these analytical habits into every interaction with AI tools, rather than treating critical evaluation as a separate module.
|
| 6 |
|
| 7 |
Building a culture of critical thinking around AI in organizations requires deliberate pedagogical strategies. Rather than passively accepting AI outputs, professionals should be trained to interrogate them through Socratic questioning: What assumptions does this model make? What data was excluded? Under what conditions would this recommendation fail? How would we verify this output independently? These habits of mind transform AI users from passive consumers into active evaluators, creating a more robust and trustworthy integration of artificial intelligence into professional workflows. The goal is not skepticism for its own sake, but informed trust — knowing when to rely on AI systems and when to override them based on principled reasoning.
|
|
@@ -6,9 +6,8 @@ services:
|
|
| 6 |
volumes:
|
| 7 |
- ./corpus:/app/corpus
|
| 8 |
- chroma-data:/app/chroma_data
|
| 9 |
-
|
| 10 |
-
-
|
| 11 |
-
- LLM_MODEL=${LLM_MODEL:-mistralai/mistral-7b-instruct}
|
| 12 |
restart: unless-stopped
|
| 13 |
|
| 14 |
volumes:
|
|
|
|
| 6 |
volumes:
|
| 7 |
- ./corpus:/app/corpus
|
| 8 |
- chroma-data:/app/chroma_data
|
| 9 |
+
env_file:
|
| 10 |
+
- .env
|
|
|
|
| 11 |
restart: unless-stopped
|
| 12 |
|
| 13 |
volumes:
|
|
@@ -1,845 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="fr">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>AIM Learning Companion</title>
|
| 7 |
-
<style type="text/css">
|
| 8 |
-
*, *::before, *::after {
|
| 9 |
-
margin: 0;
|
| 10 |
-
padding: 0;
|
| 11 |
-
box-sizing: border-box;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
body {
|
| 15 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
| 16 |
-
background-color: #0f1117;
|
| 17 |
-
color: #e4e6f0;
|
| 18 |
-
min-height: 100vh;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
/* === SCREENS === */
|
| 22 |
-
.screen {
|
| 23 |
-
display: none !important;
|
| 24 |
-
min-height: 100vh;
|
| 25 |
-
}
|
| 26 |
-
.screen.active {
|
| 27 |
-
display: flex !important;
|
| 28 |
-
flex-direction: column;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
/* === SETUP SCREEN === */
|
| 32 |
-
.setup-container {
|
| 33 |
-
max-width: 520px;
|
| 34 |
-
margin: 0 auto;
|
| 35 |
-
padding: 3rem 2rem;
|
| 36 |
-
text-align: center;
|
| 37 |
-
display: flex;
|
| 38 |
-
flex-direction: column;
|
| 39 |
-
align-items: center;
|
| 40 |
-
justify-content: center;
|
| 41 |
-
flex: 1;
|
| 42 |
-
}
|
| 43 |
-
.logo {
|
| 44 |
-
font-size: 2.2rem;
|
| 45 |
-
font-weight: 800;
|
| 46 |
-
margin-bottom: 0.4rem;
|
| 47 |
-
background: linear-gradient(135deg, #a29bfe, #00cec9);
|
| 48 |
-
-webkit-background-clip: text;
|
| 49 |
-
-webkit-text-fill-color: transparent;
|
| 50 |
-
background-clip: text;
|
| 51 |
-
}
|
| 52 |
-
.subtitle {
|
| 53 |
-
color: #8b8fa3;
|
| 54 |
-
font-size: 0.95rem;
|
| 55 |
-
margin-bottom: 2.5rem;
|
| 56 |
-
}
|
| 57 |
-
.setup-form {
|
| 58 |
-
text-align: left;
|
| 59 |
-
width: 100%;
|
| 60 |
-
}
|
| 61 |
-
.form-label {
|
| 62 |
-
display: block;
|
| 63 |
-
font-size: 0.75rem;
|
| 64 |
-
font-weight: 600;
|
| 65 |
-
color: #8b8fa3;
|
| 66 |
-
margin-bottom: 0.4rem;
|
| 67 |
-
margin-top: 1.5rem;
|
| 68 |
-
text-transform: uppercase;
|
| 69 |
-
letter-spacing: 0.08em;
|
| 70 |
-
}
|
| 71 |
-
.form-input {
|
| 72 |
-
width: 100%;
|
| 73 |
-
padding: 0.8rem 1rem;
|
| 74 |
-
background-color: #1a1d27;
|
| 75 |
-
border: 1px solid #2e3144;
|
| 76 |
-
border-radius: 10px;
|
| 77 |
-
color: #e4e6f0;
|
| 78 |
-
font-size: 1rem;
|
| 79 |
-
font-family: inherit;
|
| 80 |
-
outline: none;
|
| 81 |
-
transition: border-color 0.2s;
|
| 82 |
-
}
|
| 83 |
-
.form-input:focus {
|
| 84 |
-
border-color: #6c5ce7;
|
| 85 |
-
}
|
| 86 |
-
.mode-grid {
|
| 87 |
-
display: grid;
|
| 88 |
-
grid-template-columns: 1fr 1fr;
|
| 89 |
-
gap: 0.75rem;
|
| 90 |
-
margin-top: 0.25rem;
|
| 91 |
-
}
|
| 92 |
-
.mode-card {
|
| 93 |
-
background-color: #1a1d27;
|
| 94 |
-
border: 2px solid #2e3144;
|
| 95 |
-
border-radius: 12px;
|
| 96 |
-
padding: 1.2rem 0.8rem;
|
| 97 |
-
cursor: pointer;
|
| 98 |
-
text-align: center;
|
| 99 |
-
color: #e4e6f0;
|
| 100 |
-
transition: all 0.2s;
|
| 101 |
-
font-family: inherit;
|
| 102 |
-
font-size: inherit;
|
| 103 |
-
}
|
| 104 |
-
.mode-card:hover {
|
| 105 |
-
border-color: #6c5ce7;
|
| 106 |
-
background-color: #1e2130;
|
| 107 |
-
}
|
| 108 |
-
.mode-card.selected {
|
| 109 |
-
border-color: #6c5ce7;
|
| 110 |
-
background-color: rgba(108, 92, 231, 0.12);
|
| 111 |
-
}
|
| 112 |
-
.mode-card-icon {
|
| 113 |
-
font-size: 1.6rem;
|
| 114 |
-
display: block;
|
| 115 |
-
margin-bottom: 0.5rem;
|
| 116 |
-
}
|
| 117 |
-
.mode-card-title {
|
| 118 |
-
font-weight: 700;
|
| 119 |
-
font-size: 0.95rem;
|
| 120 |
-
display: block;
|
| 121 |
-
margin-bottom: 0.25rem;
|
| 122 |
-
}
|
| 123 |
-
.mode-card-desc {
|
| 124 |
-
font-size: 0.78rem;
|
| 125 |
-
color: #8b8fa3;
|
| 126 |
-
display: block;
|
| 127 |
-
}
|
| 128 |
-
.btn-primary {
|
| 129 |
-
width: 100%;
|
| 130 |
-
padding: 0.85rem;
|
| 131 |
-
background-color: #6c5ce7;
|
| 132 |
-
color: #ffffff;
|
| 133 |
-
border: none;
|
| 134 |
-
border-radius: 10px;
|
| 135 |
-
font-size: 1rem;
|
| 136 |
-
font-weight: 600;
|
| 137 |
-
cursor: pointer;
|
| 138 |
-
margin-top: 2rem;
|
| 139 |
-
font-family: inherit;
|
| 140 |
-
transition: opacity 0.2s;
|
| 141 |
-
}
|
| 142 |
-
.btn-primary:hover:not(:disabled) {
|
| 143 |
-
opacity: 0.85;
|
| 144 |
-
}
|
| 145 |
-
.btn-primary:disabled {
|
| 146 |
-
opacity: 0.35;
|
| 147 |
-
cursor: not-allowed;
|
| 148 |
-
}
|
| 149 |
-
.btn-secondary {
|
| 150 |
-
padding: 0.45rem 0.85rem;
|
| 151 |
-
background-color: #242734;
|
| 152 |
-
color: #e4e6f0;
|
| 153 |
-
border: 1px solid #2e3144;
|
| 154 |
-
border-radius: 8px;
|
| 155 |
-
font-size: 0.82rem;
|
| 156 |
-
cursor: pointer;
|
| 157 |
-
font-family: inherit;
|
| 158 |
-
transition: background-color 0.2s;
|
| 159 |
-
}
|
| 160 |
-
.btn-secondary:hover {
|
| 161 |
-
background-color: #2e3144;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
/* === CHAT SCREEN === */
|
| 165 |
-
.chat-header {
|
| 166 |
-
display: flex;
|
| 167 |
-
justify-content: space-between;
|
| 168 |
-
align-items: center;
|
| 169 |
-
padding: 0.75rem 1.25rem;
|
| 170 |
-
background-color: #1a1d27;
|
| 171 |
-
border-bottom: 1px solid #2e3144;
|
| 172 |
-
flex-shrink: 0;
|
| 173 |
-
}
|
| 174 |
-
.header-left {
|
| 175 |
-
display: flex;
|
| 176 |
-
align-items: center;
|
| 177 |
-
gap: 0.75rem;
|
| 178 |
-
}
|
| 179 |
-
.header-title {
|
| 180 |
-
font-size: 1.1rem;
|
| 181 |
-
font-weight: 700;
|
| 182 |
-
}
|
| 183 |
-
.badge-mode {
|
| 184 |
-
font-size: 0.65rem;
|
| 185 |
-
font-weight: 700;
|
| 186 |
-
padding: 0.15rem 0.55rem;
|
| 187 |
-
background-color: #6c5ce7;
|
| 188 |
-
color: #ffffff;
|
| 189 |
-
border-radius: 20px;
|
| 190 |
-
text-transform: uppercase;
|
| 191 |
-
letter-spacing: 0.05em;
|
| 192 |
-
}
|
| 193 |
-
.badge-topic {
|
| 194 |
-
font-size: 0.82rem;
|
| 195 |
-
color: #8b8fa3;
|
| 196 |
-
max-width: 220px;
|
| 197 |
-
overflow: hidden;
|
| 198 |
-
text-overflow: ellipsis;
|
| 199 |
-
white-space: nowrap;
|
| 200 |
-
}
|
| 201 |
-
.header-right {
|
| 202 |
-
display: flex;
|
| 203 |
-
gap: 0.5rem;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
/* === PHASES === */
|
| 207 |
-
.phases-bar {
|
| 208 |
-
display: flex;
|
| 209 |
-
align-items: center;
|
| 210 |
-
justify-content: center;
|
| 211 |
-
padding: 0.85rem 1rem;
|
| 212 |
-
background-color: #1a1d27;
|
| 213 |
-
border-bottom: 1px solid #2e3144;
|
| 214 |
-
flex-shrink: 0;
|
| 215 |
-
gap: 0;
|
| 216 |
-
}
|
| 217 |
-
.phase-item {
|
| 218 |
-
display: flex;
|
| 219 |
-
flex-direction: column;
|
| 220 |
-
align-items: center;
|
| 221 |
-
gap: 0.35rem;
|
| 222 |
-
}
|
| 223 |
-
.phase-dot {
|
| 224 |
-
width: 12px;
|
| 225 |
-
height: 12px;
|
| 226 |
-
border-radius: 50%;
|
| 227 |
-
background-color: #2e3144;
|
| 228 |
-
transition: all 0.3s;
|
| 229 |
-
}
|
| 230 |
-
.phase-item.is-active .phase-dot {
|
| 231 |
-
background-color: #6c5ce7;
|
| 232 |
-
box-shadow: 0 0 10px rgba(108, 92, 231, 0.6);
|
| 233 |
-
}
|
| 234 |
-
.phase-item.is-done .phase-dot {
|
| 235 |
-
background-color: #00b894;
|
| 236 |
-
}
|
| 237 |
-
.phase-name {
|
| 238 |
-
font-size: 0.65rem;
|
| 239 |
-
color: #8b8fa3;
|
| 240 |
-
white-space: nowrap;
|
| 241 |
-
}
|
| 242 |
-
.phase-item.is-active .phase-name {
|
| 243 |
-
color: #a29bfe;
|
| 244 |
-
font-weight: 700;
|
| 245 |
-
}
|
| 246 |
-
.phase-item.is-done .phase-name {
|
| 247 |
-
color: #00b894;
|
| 248 |
-
}
|
| 249 |
-
.phase-line {
|
| 250 |
-
width: 32px;
|
| 251 |
-
height: 2px;
|
| 252 |
-
background-color: #2e3144;
|
| 253 |
-
margin: 0 0.2rem;
|
| 254 |
-
margin-bottom: 1.1rem;
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
/* === MESSAGES === */
|
| 258 |
-
.messages-area {
|
| 259 |
-
flex: 1;
|
| 260 |
-
overflow-y: auto;
|
| 261 |
-
padding: 1.25rem;
|
| 262 |
-
display: flex;
|
| 263 |
-
flex-direction: column;
|
| 264 |
-
gap: 0.75rem;
|
| 265 |
-
}
|
| 266 |
-
.msg {
|
| 267 |
-
max-width: 78%;
|
| 268 |
-
padding: 0.8rem 1rem;
|
| 269 |
-
border-radius: 14px;
|
| 270 |
-
line-height: 1.55;
|
| 271 |
-
font-size: 0.93rem;
|
| 272 |
-
white-space: pre-wrap;
|
| 273 |
-
word-wrap: break-word;
|
| 274 |
-
}
|
| 275 |
-
.msg-user {
|
| 276 |
-
align-self: flex-end;
|
| 277 |
-
background-color: #6c5ce7;
|
| 278 |
-
color: #ffffff;
|
| 279 |
-
border-bottom-right-radius: 4px;
|
| 280 |
-
}
|
| 281 |
-
.msg-assistant {
|
| 282 |
-
align-self: flex-start;
|
| 283 |
-
background-color: #242734;
|
| 284 |
-
border: 1px solid #2e3144;
|
| 285 |
-
border-bottom-left-radius: 4px;
|
| 286 |
-
}
|
| 287 |
-
.msg-system {
|
| 288 |
-
align-self: center;
|
| 289 |
-
background-color: transparent;
|
| 290 |
-
color: #8b8fa3;
|
| 291 |
-
font-size: 0.82rem;
|
| 292 |
-
font-style: italic;
|
| 293 |
-
text-align: center;
|
| 294 |
-
max-width: 100%;
|
| 295 |
-
}
|
| 296 |
-
.msg-typing {
|
| 297 |
-
align-self: flex-start;
|
| 298 |
-
padding: 0.8rem 1rem;
|
| 299 |
-
background-color: #242734;
|
| 300 |
-
border: 1px solid #2e3144;
|
| 301 |
-
border-radius: 14px;
|
| 302 |
-
border-bottom-left-radius: 4px;
|
| 303 |
-
color: #8b8fa3;
|
| 304 |
-
font-style: italic;
|
| 305 |
-
font-size: 0.93rem;
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
/* === INPUT === */
|
| 309 |
-
.input-bar {
|
| 310 |
-
display: flex;
|
| 311 |
-
gap: 0.65rem;
|
| 312 |
-
padding: 0.85rem 1.25rem;
|
| 313 |
-
background-color: #1a1d27;
|
| 314 |
-
border-top: 1px solid #2e3144;
|
| 315 |
-
flex-shrink: 0;
|
| 316 |
-
}
|
| 317 |
-
.input-bar textarea {
|
| 318 |
-
flex: 1;
|
| 319 |
-
padding: 0.7rem 0.9rem;
|
| 320 |
-
background-color: #242734;
|
| 321 |
-
border: 1px solid #2e3144;
|
| 322 |
-
border-radius: 10px;
|
| 323 |
-
color: #e4e6f0;
|
| 324 |
-
font-size: 0.93rem;
|
| 325 |
-
font-family: inherit;
|
| 326 |
-
resize: none;
|
| 327 |
-
outline: none;
|
| 328 |
-
transition: border-color 0.2s;
|
| 329 |
-
}
|
| 330 |
-
.input-bar textarea:focus {
|
| 331 |
-
border-color: #6c5ce7;
|
| 332 |
-
}
|
| 333 |
-
.input-bar .btn-primary {
|
| 334 |
-
width: auto;
|
| 335 |
-
margin-top: 0;
|
| 336 |
-
padding: 0.7rem 1.4rem;
|
| 337 |
-
flex-shrink: 0;
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
/* === ANALYSIS SCREEN === */
|
| 341 |
-
.analysis-wrap {
|
| 342 |
-
max-width: 680px;
|
| 343 |
-
margin: 0 auto;
|
| 344 |
-
padding: 2rem;
|
| 345 |
-
overflow-y: auto;
|
| 346 |
-
flex: 1;
|
| 347 |
-
}
|
| 348 |
-
.analysis-title {
|
| 349 |
-
text-align: center;
|
| 350 |
-
margin-bottom: 2rem;
|
| 351 |
-
font-size: 1.5rem;
|
| 352 |
-
font-weight: 700;
|
| 353 |
-
}
|
| 354 |
-
.loading-text {
|
| 355 |
-
text-align: center;
|
| 356 |
-
color: #8b8fa3;
|
| 357 |
-
padding: 3rem 0;
|
| 358 |
-
font-style: italic;
|
| 359 |
-
}
|
| 360 |
-
.hidden {
|
| 361 |
-
display: none !important;
|
| 362 |
-
}
|
| 363 |
-
.scores-row {
|
| 364 |
-
display: grid;
|
| 365 |
-
grid-template-columns: repeat(3, 1fr);
|
| 366 |
-
gap: 0.85rem;
|
| 367 |
-
margin-bottom: 1.5rem;
|
| 368 |
-
}
|
| 369 |
-
.score-tile {
|
| 370 |
-
background-color: #1a1d27;
|
| 371 |
-
border: 1px solid #2e3144;
|
| 372 |
-
border-radius: 12px;
|
| 373 |
-
padding: 1.1rem;
|
| 374 |
-
text-align: center;
|
| 375 |
-
}
|
| 376 |
-
.score-num {
|
| 377 |
-
font-size: 2rem;
|
| 378 |
-
font-weight: 800;
|
| 379 |
-
margin-bottom: 0.2rem;
|
| 380 |
-
}
|
| 381 |
-
.score-lbl {
|
| 382 |
-
font-size: 0.72rem;
|
| 383 |
-
color: #8b8fa3;
|
| 384 |
-
text-transform: uppercase;
|
| 385 |
-
letter-spacing: 0.04em;
|
| 386 |
-
}
|
| 387 |
-
.score-track {
|
| 388 |
-
height: 4px;
|
| 389 |
-
background-color: #2e3144;
|
| 390 |
-
border-radius: 2px;
|
| 391 |
-
margin-top: 0.65rem;
|
| 392 |
-
overflow: hidden;
|
| 393 |
-
}
|
| 394 |
-
.score-fill {
|
| 395 |
-
height: 100%;
|
| 396 |
-
border-radius: 2px;
|
| 397 |
-
transition: width 0.8s ease;
|
| 398 |
-
}
|
| 399 |
-
.analysis-card {
|
| 400 |
-
background-color: #1a1d27;
|
| 401 |
-
border: 1px solid #2e3144;
|
| 402 |
-
border-radius: 12px;
|
| 403 |
-
padding: 1.2rem;
|
| 404 |
-
margin-bottom: 0.85rem;
|
| 405 |
-
}
|
| 406 |
-
.analysis-card h3 {
|
| 407 |
-
font-size: 0.8rem;
|
| 408 |
-
color: #8b8fa3;
|
| 409 |
-
text-transform: uppercase;
|
| 410 |
-
letter-spacing: 0.06em;
|
| 411 |
-
margin-bottom: 0.65rem;
|
| 412 |
-
}
|
| 413 |
-
.analysis-card p {
|
| 414 |
-
line-height: 1.6;
|
| 415 |
-
font-size: 0.93rem;
|
| 416 |
-
}
|
| 417 |
-
.analysis-card ul {
|
| 418 |
-
list-style: none;
|
| 419 |
-
padding: 0;
|
| 420 |
-
}
|
| 421 |
-
.analysis-card li {
|
| 422 |
-
padding: 0.35rem 0;
|
| 423 |
-
padding-left: 1.2rem;
|
| 424 |
-
position: relative;
|
| 425 |
-
font-size: 0.93rem;
|
| 426 |
-
}
|
| 427 |
-
.analysis-card li::before {
|
| 428 |
-
content: '';
|
| 429 |
-
position: absolute;
|
| 430 |
-
left: 0;
|
| 431 |
-
top: 0.65rem;
|
| 432 |
-
width: 6px;
|
| 433 |
-
height: 6px;
|
| 434 |
-
border-radius: 50%;
|
| 435 |
-
background-color: #6c5ce7;
|
| 436 |
-
}
|
| 437 |
-
.analysis-btns {
|
| 438 |
-
display: flex;
|
| 439 |
-
gap: 0.85rem;
|
| 440 |
-
margin-top: 1.5rem;
|
| 441 |
-
}
|
| 442 |
-
.analysis-btns .btn-primary,
|
| 443 |
-
.analysis-btns .btn-secondary {
|
| 444 |
-
flex: 1;
|
| 445 |
-
text-align: center;
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
@media (max-width: 640px) {
|
| 449 |
-
.scores-row {
|
| 450 |
-
grid-template-columns: repeat(2, 1fr);
|
| 451 |
-
}
|
| 452 |
-
.phase-name {
|
| 453 |
-
font-size: 0.55rem;
|
| 454 |
-
}
|
| 455 |
-
.phase-line {
|
| 456 |
-
width: 16px;
|
| 457 |
-
}
|
| 458 |
-
.msg {
|
| 459 |
-
max-width: 90%;
|
| 460 |
-
}
|
| 461 |
-
}
|
| 462 |
-
</style>
|
| 463 |
-
</head>
|
| 464 |
-
<body>
|
| 465 |
-
|
| 466 |
-
<!-- ============ SETUP ============ -->
|
| 467 |
-
<div id="screen-setup" class="screen active">
|
| 468 |
-
<div class="setup-container">
|
| 469 |
-
<div class="logo">AIM Learning Companion</div>
|
| 470 |
-
<p class="subtitle">Compagnon socratique pour l'apprentissage professionnel</p>
|
| 471 |
-
<div class="setup-form">
|
| 472 |
-
<label class="form-label" for="inp-topic">Sujet d'exploration</label>
|
| 473 |
-
<input class="form-input" type="text" id="inp-topic" placeholder="Ex: L'intelligence artificielle en entreprise..." autocomplete="off">
|
| 474 |
-
|
| 475 |
-
<label class="form-label">Mode</label>
|
| 476 |
-
<div class="mode-grid">
|
| 477 |
-
<button class="mode-card selected" data-mode="TUTOR">
|
| 478 |
-
<span class="mode-card-icon">⭐</span>
|
| 479 |
-
<span class="mode-card-title">TUTOR</span>
|
| 480 |
-
<span class="mode-card-desc">Accompagnement bienveillant</span>
|
| 481 |
-
</button>
|
| 482 |
-
<button class="mode-card" data-mode="CRITIC">
|
| 483 |
-
<span class="mode-card-icon">⚖️</span>
|
| 484 |
-
<span class="mode-card-title">CRITIC</span>
|
| 485 |
-
<span class="mode-card-desc">Audit logique</span>
|
| 486 |
-
</button>
|
| 487 |
-
</div>
|
| 488 |
-
|
| 489 |
-
<label class="form-label" for="inp-key">Cle API OpenRouter</label>
|
| 490 |
-
<input class="form-input" type="password" id="inp-key" placeholder="sk-or-v1-..." autocomplete="off">
|
| 491 |
-
|
| 492 |
-
<button id="btn-start" class="btn-primary" disabled>Commencer la session</button>
|
| 493 |
-
</div>
|
| 494 |
-
</div>
|
| 495 |
-
</div>
|
| 496 |
-
|
| 497 |
-
<!-- ============ CHAT ============ -->
|
| 498 |
-
<div id="screen-chat" class="screen">
|
| 499 |
-
<header class="chat-header">
|
| 500 |
-
<div class="header-left">
|
| 501 |
-
<span class="header-title">Companion</span>
|
| 502 |
-
<span id="hdr-mode" class="badge-mode">TUTOR</span>
|
| 503 |
-
<span id="hdr-topic" class="badge-topic"></span>
|
| 504 |
-
</div>
|
| 505 |
-
<div class="header-right">
|
| 506 |
-
<button id="btn-end" class="btn-secondary">Terminer</button>
|
| 507 |
-
<button id="btn-reset" class="btn-secondary">Reset</button>
|
| 508 |
-
</div>
|
| 509 |
-
</header>
|
| 510 |
-
<div class="phases-bar">
|
| 511 |
-
<div class="phase-item" data-p="0"><div class="phase-dot"></div><span class="phase-name">Ciblage</span></div>
|
| 512 |
-
<div class="phase-line"></div>
|
| 513 |
-
<div class="phase-item" data-p="1"><div class="phase-dot"></div><span class="phase-name">Clarification</span></div>
|
| 514 |
-
<div class="phase-line"></div>
|
| 515 |
-
<div class="phase-item" data-p="2"><div class="phase-dot"></div><span class="phase-name">Mecanisme</span></div>
|
| 516 |
-
<div class="phase-line"></div>
|
| 517 |
-
<div class="phase-item" data-p="3"><div class="phase-dot"></div><span class="phase-name">Verification</span></div>
|
| 518 |
-
<div class="phase-line"></div>
|
| 519 |
-
<div class="phase-item" data-p="4"><div class="phase-dot"></div><span class="phase-name">Stress-test</span></div>
|
| 520 |
-
</div>
|
| 521 |
-
<div id="messages" class="messages-area"></div>
|
| 522 |
-
<div class="input-bar">
|
| 523 |
-
<textarea id="inp-msg" placeholder="Ecris ta reponse ici..." rows="2"></textarea>
|
| 524 |
-
<button id="btn-send" class="btn-primary">Envoyer</button>
|
| 525 |
-
</div>
|
| 526 |
-
</div>
|
| 527 |
-
|
| 528 |
-
<!-- ============ ANALYSIS ============ -->
|
| 529 |
-
<div id="screen-analysis" class="screen">
|
| 530 |
-
<div class="analysis-wrap">
|
| 531 |
-
<h2 class="analysis-title">Analyse de session</h2>
|
| 532 |
-
<div id="ana-loading" class="loading-text">Analyse en cours...</div>
|
| 533 |
-
<div id="ana-content" class="hidden">
|
| 534 |
-
<div class="scores-row" id="ana-scores"></div>
|
| 535 |
-
<div class="analysis-card"><h3>Resume</h3><p id="ana-summary"></p></div>
|
| 536 |
-
<div class="analysis-card"><h3>Points forts</h3><ul id="ana-strengths"></ul></div>
|
| 537 |
-
<div class="analysis-card"><h3>Axes d'amelioration</h3><ul id="ana-weaknesses"></ul></div>
|
| 538 |
-
<div class="analysis-card"><h3>Rythme</h3><p id="ana-rhythm"></p></div>
|
| 539 |
-
<div class="analysis-btns">
|
| 540 |
-
<button id="btn-export" class="btn-primary">Exporter en JSON</button>
|
| 541 |
-
<button id="btn-new" class="btn-secondary">Nouvelle session</button>
|
| 542 |
-
</div>
|
| 543 |
-
</div>
|
| 544 |
-
</div>
|
| 545 |
-
</div>
|
| 546 |
-
|
| 547 |
-
<script>
|
| 548 |
-
(function() {
|
| 549 |
-
"use strict";
|
| 550 |
-
|
| 551 |
-
var API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
| 552 |
-
var MODEL = "mistralai/mistral-small-3.1-24b-instruct";
|
| 553 |
-
|
| 554 |
-
/* --- RAG corpus --- */
|
| 555 |
-
var CORPUS = [
|
| 556 |
-
"In professional environments where artificial intelligence systems are deployed, critical thinking becomes an essential competency that goes beyond simple technical understanding. Practitioners must evaluate AI outputs not as ground truth but as probabilistic suggestions shaped by training data, model architecture, and inference parameters. A critical thinker in this domain questions the provenance of training data, identifies potential biases embedded in model outputs, and assesses whether the confidence level reported by an AI system genuinely reflects the reliability of its predictions.",
|
| 557 |
-
"The application of critical thinking to AI decision-making in professional training contexts demands structured reasoning frameworks. Professionals must learn to distinguish between correlation and causation when interpreting AI-generated insights, evaluate the external validity of models trained on historical data when applied to novel situations, and recognize the limits of automation in tasks requiring ethical judgment or contextual nuance.",
|
| 558 |
-
"Building a culture of critical thinking around AI in organizations requires deliberate pedagogical strategies. Rather than passively accepting AI outputs, professionals should be trained to interrogate them through Socratic questioning: What assumptions does this model make? What data was excluded? Under what conditions would this recommendation fail? How would we verify this output independently?"
|
| 559 |
-
];
|
| 560 |
-
|
| 561 |
-
function retrieveContext(query) {
|
| 562 |
-
var words = query.toLowerCase().split(/\s+/);
|
| 563 |
-
var scored = [];
|
| 564 |
-
for (var i = 0; i < CORPUS.length; i++) {
|
| 565 |
-
var lower = CORPUS[i].toLowerCase();
|
| 566 |
-
var score = 0;
|
| 567 |
-
for (var j = 0; j < words.length; j++) {
|
| 568 |
-
if (words[j].length > 3 && lower.indexOf(words[j]) >= 0) score++;
|
| 569 |
-
}
|
| 570 |
-
if (score > 0) scored.push({ chunk: CORPUS[i], score: score });
|
| 571 |
-
}
|
| 572 |
-
scored.sort(function(a, b) { return b.score - a.score; });
|
| 573 |
-
return scored.slice(0, 3).map(function(s) { return s.chunk; });
|
| 574 |
-
}
|
| 575 |
-
|
| 576 |
-
/* --- System prompts --- */
|
| 577 |
-
var SYSTEM_TUTOR = "Tu es un mentor socratique bienveillant, empathique et complice.\nTu utilises systematiquement le \"TU\" pour t'adresser a l'apprenant.\nTon but est de faire accoucher l'esprit (maieutique) en guidant la reflexion pas a pas.\nRegles :\n1. Ne depasse jamais 3 a 4 phrases par message.\n2. Valide l'effort avant de rediriger. Si l'apprenant bloque, propose une analogie ou un indice progressif.\n3. Si une definition est demandee, explique en max 2 phrases puis verifie la comprehension immediatement.\n4. Des qu'une base est posee, avance vers Phase 2. Ne reste pas bloque en Phase 1.\n5. Evite les reproches. Prefere l'invitation.\nA la fin de chaque message, ajoute obligatoirement :\n---\nPhase: [Numero]\n\nMode : Tuteur (Accompagnement)";
|
| 578 |
-
|
| 579 |
-
var SYSTEM_CRITIC = "Tu es un mentor socratique bienveillant, empathique et complice.\nTu utilises systematiquement le \"TU\" pour t'adresser a l'apprenant.\nTon but est de faire accoucher l'esprit (maieutique) en guidant la reflexion pas a pas.\nRegles :\n1. Ne depasse jamais 3 a 4 phrases par message.\n2. Valide l'effort avant de rediriger.\n3. Si une definition est demandee, explique en max 2 phrases puis verifie la comprehension immediatement.\n4. Des qu'une base est posee, avance vers Phase 2.\n5. Evite les reproches.\nA la fin de chaque message, ajoute obligatoirement :\n---\nPhase: [Numero]\n\nMode : Critique (Audit Logique)\nTa mission : proposer des raisonnements fallacieux pour tester la vigilance. Reste un partenaire de jeu elegant, jamais meprisant.";
|
| 580 |
-
|
| 581 |
-
var PHASE_DESC = [
|
| 582 |
-
"Phase actuelle: 0 (Ciblage). Identifie l'objet exact de l'interrogation.",
|
| 583 |
-
"Phase actuelle: 1 (Clarification). Fais emerger les ambiguites conceptuelles.",
|
| 584 |
-
"Phase actuelle: 2 (Mecanisme). Explore les relations cause-effet.",
|
| 585 |
-
"Phase actuelle: 3 (Verification). Pousse vers des preuves et criteres testables.",
|
| 586 |
-
"Phase actuelle: 4 (Stress-test). Confronte le raisonnement avec des contre-exemples."
|
| 587 |
-
];
|
| 588 |
-
|
| 589 |
-
var STRATEGIES = "clarification, test_necessite, contre_exemple, prediction, falsifiabilite, mecanisme_causal, changement_cadre, compression, concession_controlee";
|
| 590 |
-
|
| 591 |
-
var ANALYSIS_SYSTEM = "Tu es un evaluateur pedagogique. Analyse la conversation suivante entre un mentor socratique et un apprenant.\nProduis un JSON strict avec cette structure :\n{\"reasoningScore\": 0, \"clarityScore\": 0, \"skepticismScore\": 0, \"processScore\": 0, \"reflectionScore\": 0, \"integrityScore\": 0, \"summary\": \"\", \"keyStrengths\": [], \"weaknesses\": []}\nRemplis chaque score de 0 a 100. Reponds UNIQUEMENT avec le JSON, sans texte autour.";
|
| 592 |
-
|
| 593 |
-
function buildSystem(mode, topic, phase, rag) {
|
| 594 |
-
var s = (mode === "TUTOR" ? SYSTEM_TUTOR : SYSTEM_CRITIC);
|
| 595 |
-
s += "\nSujet d'exploration : " + topic;
|
| 596 |
-
s += "\n\n" + (PHASE_DESC[phase] || PHASE_DESC[0]);
|
| 597 |
-
s += "\n\nStrategies socratiques : " + STRATEGIES;
|
| 598 |
-
if (rag.length > 0) {
|
| 599 |
-
s += "\n\nContexte documentaire (ne le montre PAS directement) :\n" + rag.join("\n---\n");
|
| 600 |
-
}
|
| 601 |
-
return s;
|
| 602 |
-
}
|
| 603 |
-
|
| 604 |
-
/* --- API --- */
|
| 605 |
-
function callLLM(key, sys, msgs) {
|
| 606 |
-
if (!key || key.length < 10) {
|
| 607 |
-
return Promise.reject(new Error("Cle API manquante ou invalide (longueur: " + (key ? key.length : 0) + ")"));
|
| 608 |
-
}
|
| 609 |
-
var body = JSON.stringify({
|
| 610 |
-
model: MODEL,
|
| 611 |
-
messages: [{ role: "system", content: sys }].concat(msgs)
|
| 612 |
-
});
|
| 613 |
-
var hdrs = new Headers();
|
| 614 |
-
hdrs.append("Content-Type", "application/json");
|
| 615 |
-
hdrs.append("Authorization", "Bearer " + key);
|
| 616 |
-
hdrs.append("HTTP-Referer", "https://rochikh.github.io/aim/");
|
| 617 |
-
hdrs.append("X-Title", "AIM Learning Companion");
|
| 618 |
-
return fetch(API_URL, {
|
| 619 |
-
method: "POST",
|
| 620 |
-
headers: hdrs,
|
| 621 |
-
body: body
|
| 622 |
-
}).then(function(r) {
|
| 623 |
-
if (!r.ok) return r.text().then(function(t) { throw new Error("API " + r.status + ": " + t); });
|
| 624 |
-
return r.json();
|
| 625 |
-
}).then(function(d) {
|
| 626 |
-
return d.choices[0].message.content;
|
| 627 |
-
});
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
/* --- State --- */
|
| 631 |
-
var S = { mode: "TUTOR", topic: "", phase: 0, history: [], ts: [], sending: false, key: "" };
|
| 632 |
-
|
| 633 |
-
/* --- DOM refs --- */
|
| 634 |
-
var screenSetup = document.getElementById("screen-setup");
|
| 635 |
-
var screenChat = document.getElementById("screen-chat");
|
| 636 |
-
var screenAnalysis = document.getElementById("screen-analysis");
|
| 637 |
-
var inpTopic = document.getElementById("inp-topic");
|
| 638 |
-
var inpKey = document.getElementById("inp-key");
|
| 639 |
-
var btnStart = document.getElementById("btn-start");
|
| 640 |
-
var messagesEl = document.getElementById("messages");
|
| 641 |
-
var inpMsg = document.getElementById("inp-msg");
|
| 642 |
-
var btnSend = document.getElementById("btn-send");
|
| 643 |
-
|
| 644 |
-
function showScreen(name) {
|
| 645 |
-
screenSetup.classList.remove("active");
|
| 646 |
-
screenChat.classList.remove("active");
|
| 647 |
-
screenAnalysis.classList.remove("active");
|
| 648 |
-
if (name === "setup") screenSetup.classList.add("active");
|
| 649 |
-
else if (name === "chat") screenChat.classList.add("active");
|
| 650 |
-
else if (name === "analysis") screenAnalysis.classList.add("active");
|
| 651 |
-
}
|
| 652 |
-
|
| 653 |
-
/* --- Setup --- */
|
| 654 |
-
function checkReady() {
|
| 655 |
-
btnStart.disabled = !(inpTopic.value.trim() && inpKey.value.trim());
|
| 656 |
-
}
|
| 657 |
-
inpTopic.addEventListener("input", checkReady);
|
| 658 |
-
inpKey.addEventListener("input", checkReady);
|
| 659 |
-
|
| 660 |
-
var modeCards = document.querySelectorAll(".mode-card");
|
| 661 |
-
for (var i = 0; i < modeCards.length; i++) {
|
| 662 |
-
modeCards[i].addEventListener("click", function() {
|
| 663 |
-
for (var j = 0; j < modeCards.length; j++) modeCards[j].classList.remove("selected");
|
| 664 |
-
this.classList.add("selected");
|
| 665 |
-
S.mode = this.getAttribute("data-mode");
|
| 666 |
-
});
|
| 667 |
-
}
|
| 668 |
-
|
| 669 |
-
btnStart.addEventListener("click", function() {
|
| 670 |
-
S.topic = inpTopic.value.trim();
|
| 671 |
-
S.key = inpKey.value.trim();
|
| 672 |
-
if (!S.topic || !S.key) return;
|
| 673 |
-
S.phase = 0;
|
| 674 |
-
S.history = [];
|
| 675 |
-
S.ts = [];
|
| 676 |
-
document.getElementById("hdr-mode").textContent = S.mode;
|
| 677 |
-
document.getElementById("hdr-topic").textContent = S.topic;
|
| 678 |
-
showScreen("chat");
|
| 679 |
-
updatePhases();
|
| 680 |
-
addMsg("system", "Session demarree - Mode: " + S.mode + " - Sujet: " + S.topic);
|
| 681 |
-
doSend("Je souhaite explorer le sujet suivant : " + S.topic);
|
| 682 |
-
});
|
| 683 |
-
|
| 684 |
-
/* --- Phases --- */
|
| 685 |
-
function updatePhases() {
|
| 686 |
-
var items = document.querySelectorAll(".phase-item");
|
| 687 |
-
for (var i = 0; i < items.length; i++) {
|
| 688 |
-
var p = parseInt(items[i].getAttribute("data-p"));
|
| 689 |
-
items[i].classList.remove("is-active", "is-done");
|
| 690 |
-
if (p === S.phase) items[i].classList.add("is-active");
|
| 691 |
-
else if (p < S.phase) items[i].classList.add("is-done");
|
| 692 |
-
}
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
/* --- Messages --- */
|
| 696 |
-
function addMsg(type, text) {
|
| 697 |
-
var div = document.createElement("div");
|
| 698 |
-
if (type === "user") div.className = "msg msg-user";
|
| 699 |
-
else if (type === "assistant") div.className = "msg msg-assistant";
|
| 700 |
-
else div.className = "msg msg-system";
|
| 701 |
-
div.textContent = text;
|
| 702 |
-
messagesEl.appendChild(div);
|
| 703 |
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 704 |
-
}
|
| 705 |
-
|
| 706 |
-
/* --- Send --- */
|
| 707 |
-
btnSend.addEventListener("click", function() { sendUserMsg(); });
|
| 708 |
-
inpMsg.addEventListener("keydown", function(e) {
|
| 709 |
-
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendUserMsg(); }
|
| 710 |
-
});
|
| 711 |
-
|
| 712 |
-
function sendUserMsg() {
|
| 713 |
-
var t = inpMsg.value.trim();
|
| 714 |
-
if (!t || S.sending) return;
|
| 715 |
-
inpMsg.value = "";
|
| 716 |
-
addMsg("user", t);
|
| 717 |
-
S.ts.push(Date.now() / 1000);
|
| 718 |
-
doSend(t);
|
| 719 |
-
}
|
| 720 |
-
|
| 721 |
-
function doSend(text) {
|
| 722 |
-
S.sending = true;
|
| 723 |
-
btnSend.disabled = true;
|
| 724 |
-
inpMsg.disabled = true;
|
| 725 |
-
|
| 726 |
-
var typing = document.createElement("div");
|
| 727 |
-
typing.className = "msg-typing";
|
| 728 |
-
typing.textContent = "Companion reflechit...";
|
| 729 |
-
messagesEl.appendChild(typing);
|
| 730 |
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 731 |
-
|
| 732 |
-
var rag = retrieveContext(text);
|
| 733 |
-
var sys = buildSystem(S.mode, S.topic, S.phase, rag);
|
| 734 |
-
var apiMsgs = S.history.concat([{ role: "user", content: text }]);
|
| 735 |
-
|
| 736 |
-
callLLM(S.key, sys, apiMsgs).then(function(reply) {
|
| 737 |
-
if (typing.parentNode) typing.parentNode.removeChild(typing);
|
| 738 |
-
var m = reply.match(/Phase:\s*(\d)/);
|
| 739 |
-
if (m) { S.phase = parseInt(m[1]); updatePhases(); }
|
| 740 |
-
S.history.push({ role: "user", content: text });
|
| 741 |
-
S.history.push({ role: "assistant", content: reply });
|
| 742 |
-
S.ts.push(Date.now() / 1000);
|
| 743 |
-
addMsg("assistant", reply);
|
| 744 |
-
}).catch(function(err) {
|
| 745 |
-
if (typing.parentNode) typing.parentNode.removeChild(typing);
|
| 746 |
-
addMsg("system", "Erreur: " + err.message);
|
| 747 |
-
}).finally(function() {
|
| 748 |
-
S.sending = false;
|
| 749 |
-
btnSend.disabled = false;
|
| 750 |
-
inpMsg.disabled = false;
|
| 751 |
-
inpMsg.focus();
|
| 752 |
-
});
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
/* --- End session --- */
|
| 756 |
-
document.getElementById("btn-end").addEventListener("click", function() {
|
| 757 |
-
if (S.history.length === 0) { addMsg("system", "Aucun message."); return; }
|
| 758 |
-
showScreen("analysis");
|
| 759 |
-
document.getElementById("ana-loading").classList.remove("hidden");
|
| 760 |
-
document.getElementById("ana-content").classList.add("hidden");
|
| 761 |
-
|
| 762 |
-
var conv = S.history.map(function(m) {
|
| 763 |
-
return (m.role === "user" ? "Apprenant" : "Companion") + ": " + m.content;
|
| 764 |
-
}).join("\n");
|
| 765 |
-
|
| 766 |
-
callLLM(S.key, ANALYSIS_SYSTEM, [{ role: "user", content: "Conversation:\n\n" + conv }]).then(function(raw) {
|
| 767 |
-
var s = raw.indexOf("{"), e = raw.lastIndexOf("}") + 1;
|
| 768 |
-
var data = {};
|
| 769 |
-
if (s >= 0 && e > s) { try { data = JSON.parse(raw.substring(s, e)); } catch(x) {} }
|
| 770 |
-
var fast = 0;
|
| 771 |
-
for (var i = 1; i < S.ts.length; i += 2) {
|
| 772 |
-
if (i + 1 < S.ts.length && S.ts[i + 1] - S.ts[i] < 8) fast++;
|
| 773 |
-
}
|
| 774 |
-
data.rhythmBreaks = fast;
|
| 775 |
-
showAnalysis(data);
|
| 776 |
-
}).catch(function(err) {
|
| 777 |
-
document.getElementById("ana-loading").textContent = "Erreur: " + err.message;
|
| 778 |
-
});
|
| 779 |
-
});
|
| 780 |
-
|
| 781 |
-
function showAnalysis(d) {
|
| 782 |
-
document.getElementById("ana-loading").classList.add("hidden");
|
| 783 |
-
document.getElementById("ana-content").classList.remove("hidden");
|
| 784 |
-
|
| 785 |
-
var defs = [
|
| 786 |
-
{ k: "reasoningScore", l: "Raisonnement", c: "#6c5ce7" },
|
| 787 |
-
{ k: "clarityScore", l: "Clarte", c: "#00cec9" },
|
| 788 |
-
{ k: "skepticismScore", l: "Scepticisme", c: "#e17055" },
|
| 789 |
-
{ k: "processScore", l: "Processus", c: "#00b894" },
|
| 790 |
-
{ k: "reflectionScore", l: "Reflexion", c: "#fdcb6e" },
|
| 791 |
-
{ k: "integrityScore", l: "Integrite", c: "#a29bfe" }
|
| 792 |
-
];
|
| 793 |
-
var grid = document.getElementById("ana-scores");
|
| 794 |
-
grid.innerHTML = "";
|
| 795 |
-
for (var i = 0; i < defs.length; i++) {
|
| 796 |
-
var v = d[defs[i].k] || 0;
|
| 797 |
-
var tile = document.createElement("div");
|
| 798 |
-
tile.className = "score-tile";
|
| 799 |
-
tile.innerHTML = '<div class="score-num" style="color:' + defs[i].c + '">' + v + '</div><div class="score-lbl">' + defs[i].l + '</div><div class="score-track"><div class="score-fill" style="width:' + v + '%;background:' + defs[i].c + '"></div></div>';
|
| 800 |
-
grid.appendChild(tile);
|
| 801 |
-
}
|
| 802 |
-
document.getElementById("ana-summary").textContent = d.summary || "Pas d'analyse disponible.";
|
| 803 |
-
|
| 804 |
-
var sl = document.getElementById("ana-strengths"); sl.innerHTML = "";
|
| 805 |
-
var strengths = d.keyStrengths || [];
|
| 806 |
-
for (var i = 0; i < strengths.length; i++) {
|
| 807 |
-
var li = document.createElement("li"); li.textContent = strengths[i]; sl.appendChild(li);
|
| 808 |
-
}
|
| 809 |
-
var wl = document.getElementById("ana-weaknesses"); wl.innerHTML = "";
|
| 810 |
-
var weaks = d.weaknesses || [];
|
| 811 |
-
for (var i = 0; i < weaks.length; i++) {
|
| 812 |
-
var li = document.createElement("li"); li.textContent = weaks[i]; wl.appendChild(li);
|
| 813 |
-
}
|
| 814 |
-
document.getElementById("ana-rhythm").textContent = "Reponses trop rapides (< 8s) : " + (d.rhythmBreaks || 0);
|
| 815 |
-
}
|
| 816 |
-
|
| 817 |
-
/* --- Export --- */
|
| 818 |
-
document.getElementById("btn-export").addEventListener("click", function() {
|
| 819 |
-
var blob = new Blob([JSON.stringify({ topic: S.topic, mode: S.mode, history: S.history, timestamps: S.ts, exported: new Date().toISOString() }, null, 2)], { type: "application/json" });
|
| 820 |
-
var a = document.createElement("a");
|
| 821 |
-
a.href = URL.createObjectURL(blob);
|
| 822 |
-
a.download = "aim-session-" + Date.now() + ".json";
|
| 823 |
-
a.click();
|
| 824 |
-
});
|
| 825 |
-
|
| 826 |
-
/* --- Reset / New --- */
|
| 827 |
-
document.getElementById("btn-reset").addEventListener("click", function() {
|
| 828 |
-
if (!confirm("Reinitialiser la session ?")) return;
|
| 829 |
-
resetAll(); showScreen("setup");
|
| 830 |
-
});
|
| 831 |
-
document.getElementById("btn-new").addEventListener("click", function() {
|
| 832 |
-
resetAll(); showScreen("setup");
|
| 833 |
-
});
|
| 834 |
-
|
| 835 |
-
function resetAll() {
|
| 836 |
-
S.phase = 0; S.history = []; S.ts = [];
|
| 837 |
-
messagesEl.innerHTML = "";
|
| 838 |
-
inpTopic.value = "";
|
| 839 |
-
btnStart.disabled = true;
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
})();
|
| 843 |
-
</script>
|
| 844 |
-
</body>
|
| 845 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,8 +1,9 @@
|
|
| 1 |
fastapi==0.115.6
|
| 2 |
uvicorn[standard]==0.34.0
|
| 3 |
-
|
| 4 |
chromadb==0.5.23
|
| 5 |
sentence-transformers==3.3.1
|
| 6 |
pydantic==2.10.4
|
| 7 |
python-multipart==0.0.20
|
| 8 |
pypdf2==3.0.1
|
|
|
|
|
|
| 1 |
fastapi==0.115.6
|
| 2 |
uvicorn[standard]==0.34.0
|
| 3 |
+
openai==1.58.1
|
| 4 |
chromadb==0.5.23
|
| 5 |
sentence-transformers==3.3.1
|
| 6 |
pydantic==2.10.4
|
| 7 |
python-multipart==0.0.20
|
| 8 |
pypdf2==3.0.1
|
| 9 |
+
python-dotenv==1.0.1
|
|
@@ -1,93 +1,115 @@
|
|
| 1 |
/**
|
| 2 |
-
* AIM Learning Companion - Frontend
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
//
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
function
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
method: "POST",
|
| 92 |
headers: { "Content-Type": "application/json" },
|
| 93 |
body: JSON.stringify({
|
|
@@ -95,184 +117,196 @@ async function sendMessage(text) {
|
|
| 95 |
mode: state.mode,
|
| 96 |
topic: state.topic,
|
| 97 |
phase: state.phase,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
history: state.history,
|
| 99 |
-
|
| 100 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
});
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
typing.remove();
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
addSystemMessage(`Erreur de connexion: ${err.message}. Vérifiez qu'Ollama est en cours d'exécution.`);
|
| 122 |
-
} finally {
|
| 123 |
-
state.sending = false;
|
| 124 |
-
sendBtn.disabled = false;
|
| 125 |
-
chatInput.disabled = false;
|
| 126 |
-
chatInput.focus();
|
| 127 |
}
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
function
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
function updatePhaseIndicator() {
|
| 147 |
-
document.querySelectorAll(".phase-step").forEach((step) => {
|
| 148 |
-
const p = parseInt(step.dataset.phase);
|
| 149 |
-
step.classList.remove("active", "done");
|
| 150 |
-
if (p === state.phase) step.classList.add("active");
|
| 151 |
-
else if (p < state.phase) step.classList.add("done");
|
| 152 |
-
});
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
function showScreen(name) {
|
| 156 |
-
[setupScreen, chatScreen, analysisScreen].forEach((s) => s.classList.remove("active"));
|
| 157 |
-
if (name === "setup") setupScreen.classList.add("active");
|
| 158 |
-
else if (name === "chat") chatScreen.classList.add("active");
|
| 159 |
-
else if (name === "analysis") analysisScreen.classList.add("active");
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
// End session
|
| 163 |
-
endSessionBtn.addEventListener("click", async () => {
|
| 164 |
-
if (state.history.length === 0) {
|
| 165 |
-
addSystemMessage("Aucun message dans la session.");
|
| 166 |
-
return;
|
| 167 |
}
|
| 168 |
-
showScreen("analysis");
|
| 169 |
-
$("#analysis-loading").classList.remove("hidden");
|
| 170 |
-
$("#analysis-content").classList.add("hidden");
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
});
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
}
|
| 188 |
-
});
|
| 189 |
-
|
| 190 |
-
function displayAnalysis(data) {
|
| 191 |
-
$("#analysis-loading").classList.add("hidden");
|
| 192 |
-
$("#analysis-content").classList.remove("hidden");
|
| 193 |
-
|
| 194 |
-
const scores = [
|
| 195 |
-
{ key: "reasoningScore", label: "Raisonnement", color: "#6c5ce7" },
|
| 196 |
-
{ key: "clarityScore", label: "Clarté", color: "#00cec9" },
|
| 197 |
-
{ key: "skepticismScore", label: "Scepticisme", color: "#e17055" },
|
| 198 |
-
{ key: "processScore", label: "Processus", color: "#00b894" },
|
| 199 |
-
{ key: "reflectionScore", label: "Réflexion", color: "#fdcb6e" },
|
| 200 |
-
{ key: "integrityScore", label: "Intégrité", color: "#a29bfe" },
|
| 201 |
-
];
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
<div class="score-bar">
|
| 213 |
-
<div class="score-bar-fill" style="width: ${val}%; background: ${color}"></div>
|
| 214 |
-
</div>
|
| 215 |
-
`;
|
| 216 |
-
grid.appendChild(card);
|
| 217 |
});
|
| 218 |
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
const li = document.createElement("li");
|
| 225 |
-
li.textContent = s;
|
| 226 |
-
strengthsList.appendChild(li);
|
| 227 |
});
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
li.textContent = w;
|
| 234 |
-
weaknessesList.appendChild(li);
|
| 235 |
});
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
//
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
showScreen("setup");
|
| 264 |
-
});
|
| 265 |
-
|
| 266 |
-
newSessionBtn.addEventListener("click", () => {
|
| 267 |
-
resetState();
|
| 268 |
-
showScreen("setup");
|
| 269 |
-
});
|
| 270 |
-
|
| 271 |
-
function resetState() {
|
| 272 |
-
state.phase = 0;
|
| 273 |
-
state.history = [];
|
| 274 |
-
state.timestamps = [];
|
| 275 |
-
chatMessages.innerHTML = "";
|
| 276 |
-
topicInput.value = "";
|
| 277 |
-
startBtn.disabled = true;
|
| 278 |
-
}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* AIM Learning Companion - Frontend
|
| 3 |
+
* Stateless: no localStorage, no cookies, no persistence.
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
(function () {
|
| 7 |
+
"use strict";
|
| 8 |
+
|
| 9 |
+
/* ===== State (in-memory only, lost on tab close) ===== */
|
| 10 |
+
var state = {
|
| 11 |
+
mode: "TUTOR",
|
| 12 |
+
topic: "",
|
| 13 |
+
phase: 0,
|
| 14 |
+
history: [], // {role, content}
|
| 15 |
+
timestamps: [], // epoch ms for every message (user & assistant alternating)
|
| 16 |
+
analysisResult: null
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
var PHASE_NAMES = [
|
| 20 |
+
"Ciblage",
|
| 21 |
+
"Clarification",
|
| 22 |
+
"Mecanisme",
|
| 23 |
+
"Verification",
|
| 24 |
+
"Stress-test"
|
| 25 |
+
];
|
| 26 |
+
|
| 27 |
+
/* ===== DOM refs ===== */
|
| 28 |
+
var setupScreen = document.getElementById("setup-screen");
|
| 29 |
+
var chatScreen = document.getElementById("chat-screen");
|
| 30 |
+
var analysisScreen = document.getElementById("analysis-screen");
|
| 31 |
+
|
| 32 |
+
var topicInput = document.getElementById("topic-input");
|
| 33 |
+
var btnStart = document.getElementById("btn-start");
|
| 34 |
+
var modeBtns = document.querySelectorAll(".mode-btn");
|
| 35 |
+
|
| 36 |
+
var modeBadge = document.getElementById("mode-badge");
|
| 37 |
+
var topicBadge = document.getElementById("topic-badge");
|
| 38 |
+
var phaseDots = document.getElementById("phase-dots");
|
| 39 |
+
var phaseLabels = document.getElementById("phase-labels");
|
| 40 |
+
var messagesEl = document.getElementById("messages");
|
| 41 |
+
var typingEl = document.getElementById("typing");
|
| 42 |
+
var chatInput = document.getElementById("chat-input");
|
| 43 |
+
var btnSend = document.getElementById("btn-send");
|
| 44 |
+
var btnEnd = document.getElementById("btn-end-session");
|
| 45 |
+
var btnReset = document.getElementById("btn-reset");
|
| 46 |
+
|
| 47 |
+
var scoresGrid = document.getElementById("scores-grid");
|
| 48 |
+
var summaryEl = document.getElementById("analysis-summary");
|
| 49 |
+
var strengthsEl = document.getElementById("analysis-strengths");
|
| 50 |
+
var weaknessesEl = document.getElementById("analysis-weaknesses");
|
| 51 |
+
var rhythmCount = document.getElementById("rhythm-count");
|
| 52 |
+
var btnExport = document.getElementById("btn-export");
|
| 53 |
+
var btnNewSession = document.getElementById("btn-new-session");
|
| 54 |
+
|
| 55 |
+
/* ===== Screen navigation ===== */
|
| 56 |
+
function showScreen(screen) {
|
| 57 |
+
setupScreen.classList.remove("active");
|
| 58 |
+
chatScreen.classList.remove("active");
|
| 59 |
+
analysisScreen.classList.remove("active");
|
| 60 |
+
screen.classList.add("active");
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ===== Phase indicator ===== */
|
| 64 |
+
function renderPhaseIndicator() {
|
| 65 |
+
phaseDots.innerHTML = "";
|
| 66 |
+
phaseLabels.innerHTML = "";
|
| 67 |
+
|
| 68 |
+
for (var i = 0; i < 5; i++) {
|
| 69 |
+
if (i > 0) {
|
| 70 |
+
var conn = document.createElement("div");
|
| 71 |
+
conn.className = "phase-connector" + (i <= state.phase ? " done" : "");
|
| 72 |
+
phaseDots.appendChild(conn);
|
| 73 |
+
}
|
| 74 |
+
var dot = document.createElement("div");
|
| 75 |
+
dot.className = "phase-dot";
|
| 76 |
+
if (i === state.phase) dot.className += " active";
|
| 77 |
+
else if (i < state.phase) dot.className += " done";
|
| 78 |
+
dot.textContent = i;
|
| 79 |
+
phaseDots.appendChild(dot);
|
| 80 |
+
|
| 81 |
+
var lbl = document.createElement("div");
|
| 82 |
+
lbl.className = "phase-label-text" + (i === state.phase ? " active" : "");
|
| 83 |
+
lbl.textContent = PHASE_NAMES[i];
|
| 84 |
+
phaseLabels.appendChild(lbl);
|
| 85 |
+
}
|
| 86 |
}
|
| 87 |
+
|
| 88 |
+
/* ===== Messages ===== */
|
| 89 |
+
function addMessage(role, content) {
|
| 90 |
+
var div = document.createElement("div");
|
| 91 |
+
div.className = "message " + role;
|
| 92 |
+
div.textContent = content;
|
| 93 |
+
messagesEl.insertBefore(div, typingEl);
|
| 94 |
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function setTyping(on) {
|
| 98 |
+
typingEl.style.display = on ? "block" : "none";
|
| 99 |
+
if (on) messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* ===== API calls ===== */
|
| 103 |
+
function sendMessage(text) {
|
| 104 |
+
state.history.push({ role: "user", content: text });
|
| 105 |
+
state.timestamps.push(Date.now());
|
| 106 |
+
addMessage("user", text);
|
| 107 |
+
|
| 108 |
+
chatInput.value = "";
|
| 109 |
+
btnSend.disabled = true;
|
| 110 |
+
setTyping(true);
|
| 111 |
+
|
| 112 |
+
fetch("/api/chat", {
|
| 113 |
method: "POST",
|
| 114 |
headers: { "Content-Type": "application/json" },
|
| 115 |
body: JSON.stringify({
|
|
|
|
| 117 |
mode: state.mode,
|
| 118 |
topic: state.topic,
|
| 119 |
phase: state.phase,
|
| 120 |
+
history: state.history.slice(0, -1) // send history before this message
|
| 121 |
+
})
|
| 122 |
+
})
|
| 123 |
+
.then(function (res) { return res.json(); })
|
| 124 |
+
.then(function (data) {
|
| 125 |
+
setTyping(false);
|
| 126 |
+
state.phase = data.phase;
|
| 127 |
+
state.history.push({ role: "assistant", content: data.reply });
|
| 128 |
+
state.timestamps.push(Date.now());
|
| 129 |
+
addMessage("assistant", data.reply);
|
| 130 |
+
renderPhaseIndicator();
|
| 131 |
+
btnSend.disabled = false;
|
| 132 |
+
chatInput.focus();
|
| 133 |
+
})
|
| 134 |
+
.catch(function (err) {
|
| 135 |
+
setTyping(false);
|
| 136 |
+
addMessage("assistant", "Erreur de connexion. Veuillez reessayer.");
|
| 137 |
+
btnSend.disabled = false;
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function requestAnalysis() {
|
| 142 |
+
btnEnd.disabled = true;
|
| 143 |
+
setTyping(true);
|
| 144 |
+
|
| 145 |
+
fetch("/api/analyze", {
|
| 146 |
+
method: "POST",
|
| 147 |
+
headers: { "Content-Type": "application/json" },
|
| 148 |
+
body: JSON.stringify({
|
| 149 |
history: state.history,
|
| 150 |
+
timestamps: state.timestamps
|
| 151 |
+
})
|
| 152 |
+
})
|
| 153 |
+
.then(function (res) { return res.json(); })
|
| 154 |
+
.then(function (data) {
|
| 155 |
+
setTyping(false);
|
| 156 |
+
state.analysisResult = data;
|
| 157 |
+
renderAnalysis(data);
|
| 158 |
+
showScreen(analysisScreen);
|
| 159 |
+
})
|
| 160 |
+
.catch(function () {
|
| 161 |
+
setTyping(false);
|
| 162 |
+
btnEnd.disabled = false;
|
| 163 |
+
alert("Erreur lors de l'analyse. Veuillez reessayer.");
|
| 164 |
});
|
| 165 |
+
}
|
| 166 |
|
| 167 |
+
/* ===== Analysis rendering ===== */
|
| 168 |
+
function renderAnalysis(data) {
|
| 169 |
+
var scores = [
|
| 170 |
+
{ key: "reasoningScore", label: "Raisonnement" },
|
| 171 |
+
{ key: "clarityScore", label: "Clarte" },
|
| 172 |
+
{ key: "skepticismScore", label: "Scepticisme" },
|
| 173 |
+
{ key: "processScore", label: "Processus" },
|
| 174 |
+
{ key: "reflectionScore", label: "Reflexion" },
|
| 175 |
+
{ key: "integrityScore", label: "Integrite" }
|
| 176 |
+
];
|
| 177 |
+
|
| 178 |
+
scoresGrid.innerHTML = "";
|
| 179 |
+
scores.forEach(function (s) {
|
| 180 |
+
var val = data[s.key] || 0;
|
| 181 |
+
var card = document.createElement("div");
|
| 182 |
+
card.className = "score-card";
|
| 183 |
+
card.innerHTML =
|
| 184 |
+
'<div class="score-value">' + val + '</div>' +
|
| 185 |
+
'<div class="score-label">' + s.label + '</div>' +
|
| 186 |
+
'<div class="score-bar"><div class="score-bar-fill" style="width:' + val + '%"></div></div>';
|
| 187 |
+
scoresGrid.appendChild(card);
|
| 188 |
+
});
|
| 189 |
|
| 190 |
+
summaryEl.textContent = data.summary || "Aucun bilan disponible.";
|
|
|
|
| 191 |
|
| 192 |
+
strengthsEl.innerHTML = "";
|
| 193 |
+
(data.keyStrengths || []).forEach(function (s) {
|
| 194 |
+
var li = document.createElement("li");
|
| 195 |
+
li.textContent = s;
|
| 196 |
+
strengthsEl.appendChild(li);
|
| 197 |
+
});
|
| 198 |
|
| 199 |
+
weaknessesEl.innerHTML = "";
|
| 200 |
+
(data.weaknesses || []).forEach(function (w) {
|
| 201 |
+
var li = document.createElement("li");
|
| 202 |
+
li.textContent = w;
|
| 203 |
+
weaknessesEl.appendChild(li);
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
rhythmCount.textContent = data.rhythmBreakCount || 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
}
|
| 208 |
+
|
| 209 |
+
/* ===== JSON export ===== */
|
| 210 |
+
function exportJSON() {
|
| 211 |
+
var payload = {
|
| 212 |
+
mode: state.mode,
|
| 213 |
+
topic: state.topic,
|
| 214 |
+
messages: state.history,
|
| 215 |
+
timestamps: state.timestamps,
|
| 216 |
+
scores: state.analysisResult
|
| 217 |
+
};
|
| 218 |
+
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
| 219 |
+
var url = URL.createObjectURL(blob);
|
| 220 |
+
var a = document.createElement("a");
|
| 221 |
+
a.href = url;
|
| 222 |
+
a.download = "aim-session-" + new Date().toISOString().slice(0, 10) + ".json";
|
| 223 |
+
a.click();
|
| 224 |
+
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
}
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
+
/* ===== Reset ===== */
|
| 228 |
+
function resetSession() {
|
| 229 |
+
state.mode = "TUTOR";
|
| 230 |
+
state.topic = "";
|
| 231 |
+
state.phase = 0;
|
| 232 |
+
state.history = [];
|
| 233 |
+
state.timestamps = [];
|
| 234 |
+
state.analysisResult = null;
|
| 235 |
+
|
| 236 |
+
topicInput.value = "";
|
| 237 |
+
chatInput.value = "";
|
| 238 |
+
messagesEl.querySelectorAll(".message").forEach(function (el) { el.remove(); });
|
| 239 |
+
|
| 240 |
+
modeBtns.forEach(function (btn) {
|
| 241 |
+
btn.classList.toggle("selected", btn.dataset.mode === "TUTOR");
|
| 242 |
});
|
| 243 |
|
| 244 |
+
btnStart.disabled = true;
|
| 245 |
+
btnEnd.disabled = false;
|
| 246 |
+
btnSend.disabled = false;
|
| 247 |
+
|
| 248 |
+
showScreen(setupScreen);
|
| 249 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
+
/* ===== Event listeners ===== */
|
| 252 |
+
|
| 253 |
+
// Mode selection
|
| 254 |
+
modeBtns.forEach(function (btn) {
|
| 255 |
+
btn.addEventListener("click", function () {
|
| 256 |
+
modeBtns.forEach(function (b) { b.classList.remove("selected"); });
|
| 257 |
+
btn.classList.add("selected");
|
| 258 |
+
state.mode = btn.dataset.mode;
|
| 259 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
});
|
| 261 |
|
| 262 |
+
// Topic input enables start button
|
| 263 |
+
topicInput.addEventListener("input", function () {
|
| 264 |
+
btnStart.disabled = !topicInput.value.trim();
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
// Start session
|
| 268 |
+
btnStart.addEventListener("click", function () {
|
| 269 |
+
var topic = topicInput.value.trim();
|
| 270 |
+
if (!topic) return;
|
| 271 |
+
|
| 272 |
+
state.topic = topic;
|
| 273 |
+
modeBadge.textContent = state.mode === "TUTOR" ? "Tuteur" : "Critique";
|
| 274 |
+
topicBadge.textContent = topic;
|
| 275 |
|
| 276 |
+
renderPhaseIndicator();
|
| 277 |
+
showScreen(chatScreen);
|
| 278 |
+
chatInput.focus();
|
|
|
|
|
|
|
|
|
|
| 279 |
});
|
| 280 |
|
| 281 |
+
// Send message
|
| 282 |
+
btnSend.addEventListener("click", function () {
|
| 283 |
+
var text = chatInput.value.trim();
|
| 284 |
+
if (text) sendMessage(text);
|
|
|
|
|
|
|
| 285 |
});
|
| 286 |
|
| 287 |
+
chatInput.addEventListener("keydown", function (e) {
|
| 288 |
+
if (e.key === "Enter") {
|
| 289 |
+
var text = chatInput.value.trim();
|
| 290 |
+
if (text) sendMessage(text);
|
| 291 |
+
}
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
// End session -> analysis
|
| 295 |
+
btnEnd.addEventListener("click", function () {
|
| 296 |
+
if (state.history.length === 0) {
|
| 297 |
+
alert("Aucun echange a analyser.");
|
| 298 |
+
return;
|
| 299 |
+
}
|
| 300 |
+
requestAnalysis();
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
// Reset
|
| 304 |
+
btnReset.addEventListener("click", resetSession);
|
| 305 |
+
|
| 306 |
+
// Export JSON
|
| 307 |
+
btnExport.addEventListener("click", exportJSON);
|
| 308 |
+
|
| 309 |
+
// New session from analysis screen
|
| 310 |
+
btnNewSession.addEventListener("click", resetSession);
|
| 311 |
+
|
| 312 |
+
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -3,119 +3,96 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>AIM
|
| 7 |
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
-
<div id="app">
|
| 11 |
-
<!-- Setup Screen -->
|
| 12 |
-
<div id="setup-screen" class="screen active">
|
| 13 |
-
<div class="setup-container">
|
| 14 |
-
<h1>AIM Learning Companion</h1>
|
| 15 |
-
<p class="subtitle">Compagnon socratique pour l'apprentissage professionnel</p>
|
| 16 |
-
<div class="setup-form">
|
| 17 |
-
<label for="topic-input">Sujet d'exploration</label>
|
| 18 |
-
<input type="text" id="topic-input" placeholder="Ex: L'intelligence artificielle en entreprise..." autocomplete="off">
|
| 19 |
-
<label for="mode-select">Mode</label>
|
| 20 |
-
<div class="mode-selector">
|
| 21 |
-
<button class="mode-btn active" data-mode="TUTOR">
|
| 22 |
-
<span class="mode-icon">★</span>
|
| 23 |
-
<span class="mode-label">TUTOR</span>
|
| 24 |
-
<span class="mode-desc">Accompagnement bienveillant</span>
|
| 25 |
-
</button>
|
| 26 |
-
<button class="mode-btn" data-mode="CRITIC">
|
| 27 |
-
<span class="mode-icon">⚖</span>
|
| 28 |
-
<span class="mode-label">CRITIC</span>
|
| 29 |
-
<span class="mode-desc">Audit logique</span>
|
| 30 |
-
</button>
|
| 31 |
-
</div>
|
| 32 |
-
<button id="start-btn" class="primary-btn" disabled>Commencer la session</button>
|
| 33 |
-
</div>
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
<h2>Companion</h2>
|
| 42 |
-
<span id="header-mode" class="mode-badge">TUTOR</span>
|
| 43 |
-
<span id="header-topic" class="topic-badge"></span>
|
| 44 |
-
</div>
|
| 45 |
-
<div class="header-right">
|
| 46 |
-
<button id="end-session-btn" class="secondary-btn">Terminer la session</button>
|
| 47 |
-
<button id="reset-btn" class="secondary-btn">Reset</button>
|
| 48 |
-
</div>
|
| 49 |
-
</header>
|
| 50 |
|
| 51 |
-
|
| 52 |
-
<
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
<
|
| 61 |
-
|
| 62 |
-
<div class="phase-connector"></div>
|
| 63 |
-
<div class="phase-step" data-phase="2">
|
| 64 |
-
<div class="phase-dot"></div>
|
| 65 |
-
<span class="phase-label">2 Mécanisme</span>
|
| 66 |
-
</div>
|
| 67 |
-
<div class="phase-connector"></div>
|
| 68 |
-
<div class="phase-step" data-phase="3">
|
| 69 |
-
<div class="phase-dot"></div>
|
| 70 |
-
<span class="phase-label">3 Vérification</span>
|
| 71 |
</div>
|
| 72 |
-
<div class="
|
| 73 |
-
|
| 74 |
-
<div class="
|
| 75 |
-
<span class="phase-label">4 Stress-test</span>
|
| 76 |
</div>
|
| 77 |
</div>
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
</div>
|
| 87 |
</div>
|
| 88 |
|
| 89 |
-
<
|
| 90 |
-
<div
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
<
|
| 95 |
-
<div class="scores-grid" id="scores-grid"></div>
|
| 96 |
-
<div class="analysis-section">
|
| 97 |
-
<h3>Résumé</h3>
|
| 98 |
-
<p id="analysis-summary"></p>
|
| 99 |
-
</div>
|
| 100 |
-
<div class="analysis-section">
|
| 101 |
-
<h3>Points forts</h3>
|
| 102 |
-
<ul id="analysis-strengths"></ul>
|
| 103 |
-
</div>
|
| 104 |
-
<div class="analysis-section">
|
| 105 |
-
<h3>Axes d'amélioration</h3>
|
| 106 |
-
<ul id="analysis-weaknesses"></ul>
|
| 107 |
-
</div>
|
| 108 |
-
<div class="analysis-section">
|
| 109 |
-
<h3>Rythme</h3>
|
| 110 |
-
<p id="analysis-rhythm"></p>
|
| 111 |
-
</div>
|
| 112 |
-
<div class="analysis-actions">
|
| 113 |
-
<button id="export-btn" class="primary-btn">Exporter en JSON</button>
|
| 114 |
-
<button id="new-session-btn" class="secondary-btn">Nouvelle session</button>
|
| 115 |
-
</div>
|
| 116 |
-
</div>
|
| 117 |
</div>
|
| 118 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
| 120 |
|
| 121 |
<script src="/static/app.js"></script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AIM - Compagnon d'apprentissage</title>
|
| 7 |
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
<!-- ===== Setup Screen ===== -->
|
| 12 |
+
<div id="setup-screen" class="screen active">
|
| 13 |
+
<h1>AIM</h1>
|
| 14 |
+
<p class="subtitle">Compagnon socratique d'apprentissage</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
<div class="form-group">
|
| 17 |
+
<label for="topic-input">Sujet d'exploration</label>
|
| 18 |
+
<input type="text" id="topic-input" placeholder="Ex : L'intelligence artificielle en formation professionnelle">
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div class="form-group">
|
| 22 |
+
<label>Mode</label>
|
| 23 |
+
<div class="mode-selector">
|
| 24 |
+
<div class="mode-btn selected" data-mode="TUTOR">
|
| 25 |
+
<div class="mode-title">Tuteur</div>
|
| 26 |
+
<div class="mode-desc">Accompagnement bienveillant, questions ouvertes</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
</div>
|
| 28 |
+
<div class="mode-btn" data-mode="CRITIC">
|
| 29 |
+
<div class="mode-title">Critique</div>
|
| 30 |
+
<div class="mode-desc">Avocat du diable, teste les failles logiques</div>
|
|
|
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
+
</div>
|
| 34 |
|
| 35 |
+
<button id="btn-start" class="btn-primary" disabled>Commencer la session</button>
|
| 36 |
+
</div>
|
| 37 |
|
| 38 |
+
<!-- ===== Chat Screen ===== -->
|
| 39 |
+
<div id="chat-screen" class="screen">
|
| 40 |
+
<div class="chat-header">
|
| 41 |
+
<div class="chat-header-left">
|
| 42 |
+
<span id="mode-badge" class="badge badge-mode"></span>
|
| 43 |
+
<span id="topic-badge" class="badge badge-topic"></span>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="chat-header-right">
|
| 46 |
+
<button id="btn-end-session" class="btn-end">Terminer la session</button>
|
| 47 |
+
<button id="btn-reset" class="btn-reset">Recommencer</button>
|
| 48 |
</div>
|
| 49 |
</div>
|
| 50 |
|
| 51 |
+
<div class="phase-indicator" id="phase-dots"></div>
|
| 52 |
+
<div class="phase-labels" id="phase-labels"></div>
|
| 53 |
+
|
| 54 |
+
<div class="messages" id="messages">
|
| 55 |
+
<div class="typing-indicator" id="typing">
|
| 56 |
+
<span></span><span></span><span></span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
+
|
| 60 |
+
<div class="input-bar">
|
| 61 |
+
<input type="text" id="chat-input" placeholder="Tape ta reflexion ici...">
|
| 62 |
+
<button id="btn-send" class="btn-send">Envoyer</button>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- ===== Analysis Screen ===== -->
|
| 67 |
+
<div id="analysis-screen" class="screen">
|
| 68 |
+
<h2>Rapport de session</h2>
|
| 69 |
+
|
| 70 |
+
<div class="scores-grid" id="scores-grid"></div>
|
| 71 |
+
|
| 72 |
+
<div class="analysis-section">
|
| 73 |
+
<h3>Bilan</h3>
|
| 74 |
+
<p id="analysis-summary"></p>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div class="analysis-section">
|
| 78 |
+
<h3>Points forts</h3>
|
| 79 |
+
<ul id="analysis-strengths"></ul>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<div class="analysis-section">
|
| 83 |
+
<h3>Axes d'amelioration</h3>
|
| 84 |
+
<ul id="analysis-weaknesses"></ul>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="analysis-section">
|
| 88 |
+
<h3>Rythme</h3>
|
| 89 |
+
<p>Reponses en moins de 8 secondes : <span id="rhythm-count" class="rhythm-badge">0</span></p>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="analysis-actions">
|
| 93 |
+
<button id="btn-export" class="btn-secondary">Exporter JSON</button>
|
| 94 |
+
<button id="btn-new-session" class="btn-primary">Nouvelle session</button>
|
| 95 |
+
</div>
|
| 96 |
</div>
|
| 97 |
|
| 98 |
<script src="/static/app.js"></script>
|
|
@@ -1,460 +1,432 @@
|
|
| 1 |
-
*
|
| 2 |
-
margin: 0;
|
| 3 |
-
padding: 0;
|
| 4 |
-
box-sizing: border-box;
|
| 5 |
-
}
|
| 6 |
-
|
| 7 |
:root {
|
| 8 |
-
--bg: #0f1117;
|
| 9 |
-
--
|
| 10 |
-
--
|
| 11 |
-
--
|
| 12 |
-
--text: #
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--primary-light: #a29bfe;
|
| 16 |
-
--accent: #00cec9;
|
| 17 |
--success: #00b894;
|
| 18 |
--warning: #fdcb6e;
|
| 19 |
--danger: #e17055;
|
| 20 |
-
--
|
| 21 |
-
--phase-done: #00b894;
|
| 22 |
-
--phase-pending: #2e3144;
|
| 23 |
--radius: 12px;
|
| 24 |
--radius-sm: 8px;
|
| 25 |
}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 29 |
-
background: var(--bg);
|
| 30 |
-
color: var(--text);
|
| 31 |
-
min-height: 100vh;
|
| 32 |
-
}
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
min-height: 100vh;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
.screen.active {
|
| 40 |
display: flex;
|
| 41 |
-
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
-
|
| 53 |
font-size: 2rem;
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
-webkit-background-clip: text;
|
| 57 |
-
-webkit-text-fill-color: transparent;
|
| 58 |
-
background-clip: text;
|
| 59 |
}
|
| 60 |
|
| 61 |
-
.subtitle {
|
| 62 |
-
color: var(--text-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
-
.
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
-
.
|
| 72 |
-
|
| 73 |
-
font-size: 0.
|
| 74 |
-
color: var(--text-
|
| 75 |
-
margin-bottom: 0.5rem;
|
| 76 |
-
margin-top: 1.25rem;
|
| 77 |
-
text-transform: uppercase;
|
| 78 |
-
letter-spacing: 0.05em;
|
| 79 |
}
|
| 80 |
|
| 81 |
-
.
|
| 82 |
-
|
| 83 |
-
padding: 0.85rem 1rem;
|
| 84 |
-
background: var(--surface);
|
| 85 |
-
border: 1px solid var(--border);
|
| 86 |
border-radius: var(--radius-sm);
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
font-size: 1rem;
|
| 89 |
outline: none;
|
| 90 |
transition: border-color 0.2s;
|
| 91 |
}
|
| 92 |
|
| 93 |
-
.
|
| 94 |
-
border-color: var(--
|
| 95 |
}
|
| 96 |
|
| 97 |
.mode-selector {
|
| 98 |
-
display:
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
margin-top: 0.25rem;
|
| 102 |
}
|
| 103 |
|
| 104 |
.mode-btn {
|
| 105 |
-
|
| 106 |
-
|
| 107 |
border-radius: var(--radius);
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
cursor: pointer;
|
| 110 |
text-align: center;
|
| 111 |
transition: all 0.2s;
|
| 112 |
-
color: var(--text);
|
| 113 |
}
|
| 114 |
|
| 115 |
-
.mode-btn:hover {
|
| 116 |
-
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.mode-btn.active {
|
| 120 |
-
border-color: var(--primary);
|
| 121 |
-
background: rgba(108, 92, 231, 0.1);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
.mode-icon {
|
| 125 |
-
font-size: 1.5rem;
|
| 126 |
-
display: block;
|
| 127 |
-
margin-bottom: 0.5rem;
|
| 128 |
-
}
|
| 129 |
|
| 130 |
-
.mode-
|
| 131 |
font-weight: 700;
|
| 132 |
-
font-size:
|
| 133 |
-
|
| 134 |
-
margin-bottom: 0.25rem;
|
| 135 |
}
|
| 136 |
|
| 137 |
-
.mode-desc {
|
| 138 |
-
font-size: 0.
|
| 139 |
-
color: var(--text-
|
| 140 |
}
|
| 141 |
|
| 142 |
-
.
|
| 143 |
-
|
| 144 |
-
padding: 0.85rem;
|
| 145 |
-
background: var(--primary);
|
| 146 |
-
color: white;
|
| 147 |
-
border: none;
|
| 148 |
border-radius: var(--radius-sm);
|
|
|
|
|
|
|
|
|
|
| 149 |
font-size: 1rem;
|
| 150 |
font-weight: 600;
|
| 151 |
cursor: pointer;
|
| 152 |
-
margin-top: 2rem;
|
| 153 |
transition: opacity 0.2s;
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
-
.
|
| 157 |
-
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
.primary-btn:disabled {
|
| 161 |
-
opacity: 0.4;
|
| 162 |
-
cursor: not-allowed;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
.secondary-btn {
|
| 166 |
-
padding: 0.5rem 1rem;
|
| 167 |
-
background: var(--surface-2);
|
| 168 |
-
color: var(--text);
|
| 169 |
-
border: 1px solid var(--border);
|
| 170 |
-
border-radius: var(--radius-sm);
|
| 171 |
-
font-size: 0.85rem;
|
| 172 |
-
cursor: pointer;
|
| 173 |
-
transition: background 0.2s;
|
| 174 |
-
}
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
-
/* Chat Header */
|
| 181 |
.chat-header {
|
|
|
|
|
|
|
| 182 |
display: flex;
|
| 183 |
-
justify-content: space-between;
|
| 184 |
align-items: center;
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
border-bottom: 1px solid var(--border);
|
| 188 |
}
|
| 189 |
|
| 190 |
-
.header-left {
|
| 191 |
display: flex;
|
| 192 |
align-items: center;
|
| 193 |
-
gap:
|
| 194 |
}
|
| 195 |
|
| 196 |
-
.
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
-
.
|
|
|
|
|
|
|
|
|
|
| 201 |
display: flex;
|
| 202 |
-
|
| 203 |
}
|
| 204 |
|
| 205 |
-
.
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
background:
|
| 210 |
-
color:
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
| 214 |
}
|
| 215 |
|
| 216 |
-
.
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
.phase-indicator {
|
| 227 |
display: flex;
|
| 228 |
align-items: center;
|
| 229 |
justify-content: center;
|
| 230 |
-
padding: 1rem 1.5rem;
|
| 231 |
-
background: var(--surface);
|
| 232 |
-
border-bottom: 1px solid var(--border);
|
| 233 |
gap: 0;
|
|
|
|
|
|
|
| 234 |
}
|
| 235 |
|
| 236 |
.phase-step {
|
| 237 |
display: flex;
|
| 238 |
-
flex-direction: column;
|
| 239 |
align-items: center;
|
| 240 |
-
gap: 0.4rem;
|
| 241 |
-
position: relative;
|
| 242 |
}
|
| 243 |
|
| 244 |
.phase-dot {
|
| 245 |
-
width:
|
| 246 |
-
height:
|
| 247 |
border-radius: 50%;
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
border: 2px solid var(--border);
|
| 250 |
transition: all 0.3s;
|
|
|
|
| 251 |
}
|
| 252 |
|
| 253 |
-
.phase-
|
| 254 |
-
background: var(--
|
| 255 |
-
|
| 256 |
-
|
| 257 |
}
|
| 258 |
|
| 259 |
-
.phase-
|
| 260 |
-
background: var(--
|
| 261 |
-
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
-
.phase-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
}
|
| 269 |
|
| 270 |
-
.phase-
|
| 271 |
-
color: var(--primary-light);
|
| 272 |
-
font-weight: 600;
|
| 273 |
-
}
|
| 274 |
|
| 275 |
-
.phase-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
-
.phase-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
margin-bottom: 1.25rem;
|
| 285 |
}
|
| 286 |
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
| 289 |
flex: 1;
|
| 290 |
overflow-y: auto;
|
| 291 |
-
padding:
|
| 292 |
display: flex;
|
| 293 |
flex-direction: column;
|
| 294 |
-
gap:
|
| 295 |
}
|
| 296 |
|
| 297 |
.message {
|
| 298 |
-
max-width:
|
| 299 |
-
padding:
|
| 300 |
border-radius: var(--radius);
|
| 301 |
line-height: 1.55;
|
| 302 |
font-size: 0.95rem;
|
| 303 |
white-space: pre-wrap;
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
.message.user {
|
| 307 |
-
align-self: flex-end;
|
| 308 |
-
background: var(--primary);
|
| 309 |
-
color: white;
|
| 310 |
-
border-bottom-right-radius: 4px;
|
| 311 |
}
|
| 312 |
|
| 313 |
.message.assistant {
|
| 314 |
-
|
| 315 |
-
background: var(--surface-2);
|
| 316 |
border: 1px solid var(--border);
|
| 317 |
-
|
| 318 |
}
|
| 319 |
|
| 320 |
-
.message.
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
font-size: 0.85rem;
|
| 325 |
-
font-style: italic;
|
| 326 |
-
text-align: center;
|
| 327 |
}
|
| 328 |
|
| 329 |
.typing-indicator {
|
| 330 |
align-self: flex-start;
|
| 331 |
-
padding:
|
| 332 |
-
background: var(--
|
| 333 |
border: 1px solid var(--border);
|
| 334 |
border-radius: var(--radius);
|
| 335 |
-
|
| 336 |
-
color: var(--text-muted);
|
| 337 |
-
font-style: italic;
|
| 338 |
}
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
border-top: 1px solid var(--border);
|
|
|
|
|
|
|
|
|
|
| 347 |
}
|
| 348 |
|
| 349 |
-
.
|
| 350 |
flex: 1;
|
| 351 |
-
padding:
|
| 352 |
-
background: var(--surface-2);
|
| 353 |
-
border: 1px solid var(--border);
|
| 354 |
border-radius: var(--radius-sm);
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
outline: none;
|
| 360 |
-
transition: border-color 0.2s;
|
| 361 |
}
|
| 362 |
|
| 363 |
-
.
|
| 364 |
-
border-color: var(--primary);
|
| 365 |
-
}
|
| 366 |
|
| 367 |
-
.
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
}
|
| 372 |
|
| 373 |
-
|
| 374 |
-
.analysis-container {
|
| 375 |
-
max-width: 680px;
|
| 376 |
-
margin: 2rem auto;
|
| 377 |
-
padding: 2rem;
|
| 378 |
-
}
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
| 384 |
}
|
| 385 |
|
| 386 |
-
|
|
|
|
| 387 |
text-align: center;
|
| 388 |
-
color: var(--text-muted);
|
| 389 |
-
padding: 3rem 0;
|
| 390 |
-
font-style: italic;
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
.hidden {
|
| 394 |
-
display: none;
|
| 395 |
}
|
| 396 |
|
| 397 |
.scores-grid {
|
| 398 |
display: grid;
|
| 399 |
grid-template-columns: repeat(3, 1fr);
|
| 400 |
-
gap:
|
| 401 |
-
|
| 402 |
}
|
| 403 |
|
| 404 |
.score-card {
|
| 405 |
-
background: var(--
|
| 406 |
border: 1px solid var(--border);
|
| 407 |
-
border-radius: var(--radius);
|
| 408 |
-
padding:
|
| 409 |
text-align: center;
|
| 410 |
}
|
| 411 |
|
| 412 |
.score-card .score-value {
|
| 413 |
font-size: 2rem;
|
| 414 |
font-weight: 700;
|
| 415 |
-
|
| 416 |
}
|
| 417 |
|
| 418 |
.score-card .score-label {
|
| 419 |
-
font-size: 0.
|
| 420 |
-
color: var(--text-
|
| 421 |
-
|
| 422 |
-
letter-spacing: 0.03em;
|
| 423 |
}
|
| 424 |
|
| 425 |
.score-bar {
|
|
|
|
| 426 |
height: 4px;
|
| 427 |
-
background: var(--
|
| 428 |
border-radius: 2px;
|
| 429 |
-
margin-top:
|
| 430 |
overflow: hidden;
|
| 431 |
}
|
| 432 |
|
| 433 |
.score-bar-fill {
|
| 434 |
height: 100%;
|
|
|
|
| 435 |
border-radius: 2px;
|
| 436 |
-
transition: width 0.
|
| 437 |
}
|
| 438 |
|
| 439 |
.analysis-section {
|
| 440 |
-
|
|
|
|
| 441 |
border: 1px solid var(--border);
|
| 442 |
border-radius: var(--radius);
|
| 443 |
-
padding:
|
| 444 |
-
margin-bottom: 1rem;
|
| 445 |
}
|
| 446 |
|
| 447 |
.analysis-section h3 {
|
| 448 |
-
font-size:
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
letter-spacing: 0.04em;
|
| 452 |
-
margin-bottom: 0.75rem;
|
| 453 |
}
|
| 454 |
|
| 455 |
-
.analysis-section p {
|
| 456 |
line-height: 1.6;
|
| 457 |
-
font-size: 0.
|
| 458 |
}
|
| 459 |
|
| 460 |
.analysis-section ul {
|
|
@@ -462,51 +434,46 @@ body {
|
|
| 462 |
padding: 0;
|
| 463 |
}
|
| 464 |
|
| 465 |
-
.analysis-section li {
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
position: relative;
|
| 469 |
-
font-size: 0.95rem;
|
| 470 |
}
|
| 471 |
|
| 472 |
-
.
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
border
|
| 480 |
-
background: var(--primary);
|
| 481 |
}
|
| 482 |
|
| 483 |
.analysis-actions {
|
| 484 |
display: flex;
|
| 485 |
-
gap:
|
| 486 |
-
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
.analysis-actions .primary-btn,
|
| 490 |
-
.analysis-actions .secondary-btn {
|
| 491 |
-
flex: 1;
|
| 492 |
-
text-align: center;
|
| 493 |
}
|
| 494 |
|
| 495 |
-
|
| 496 |
-
@media (max-width: 640px) {
|
| 497 |
-
.scores-grid {
|
| 498 |
-
grid-template-columns: repeat(2, 1fr);
|
| 499 |
-
}
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
-
|
| 506 |
-
width: 20px;
|
| 507 |
-
}
|
| 508 |
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
}
|
|
|
|
|
|
|
|
|
|
| 512 |
}
|
|
|
|
| 1 |
+
/* ===== Variables & Reset ===== */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
:root {
|
| 3 |
+
--bg-primary: #0f1117;
|
| 4 |
+
--bg-secondary: #1a1d27;
|
| 5 |
+
--bg-tertiary: #252833;
|
| 6 |
+
--text-primary: #e8e8ed;
|
| 7 |
+
--text-secondary: #9ca3af;
|
| 8 |
+
--accent: #6c5ce7;
|
| 9 |
+
--accent-light: #a29bfe;
|
|
|
|
|
|
|
| 10 |
--success: #00b894;
|
| 11 |
--warning: #fdcb6e;
|
| 12 |
--danger: #e17055;
|
| 13 |
+
--border: #2d3040;
|
|
|
|
|
|
|
| 14 |
--radius: 12px;
|
| 15 |
--radius-sm: 8px;
|
| 16 |
}
|
| 17 |
|
| 18 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 22 |
+
background: var(--bg-primary);
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
| 25 |
display: flex;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
align-items: center;
|
| 28 |
}
|
| 29 |
|
| 30 |
+
.screen { display: none; width: 100%; max-width: 720px; padding: 24px; }
|
| 31 |
+
.screen.active { display: flex; flex-direction: column; }
|
| 32 |
+
|
| 33 |
+
/* ===== Setup Screen ===== */
|
| 34 |
+
#setup-screen {
|
| 35 |
+
align-items: center;
|
| 36 |
+
gap: 32px;
|
| 37 |
+
min-height: 100vh;
|
| 38 |
+
justify-content: center;
|
| 39 |
}
|
| 40 |
|
| 41 |
+
#setup-screen h1 {
|
| 42 |
font-size: 2rem;
|
| 43 |
+
font-weight: 700;
|
| 44 |
+
text-align: center;
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
+
#setup-screen p.subtitle {
|
| 48 |
+
color: var(--text-secondary);
|
| 49 |
+
text-align: center;
|
| 50 |
+
margin-top: -16px;
|
| 51 |
+
font-size: 0.95rem;
|
| 52 |
}
|
| 53 |
|
| 54 |
+
.form-group {
|
| 55 |
+
width: 100%;
|
| 56 |
+
display: flex;
|
| 57 |
+
flex-direction: column;
|
| 58 |
+
gap: 8px;
|
| 59 |
}
|
| 60 |
|
| 61 |
+
.form-group label {
|
| 62 |
+
font-weight: 600;
|
| 63 |
+
font-size: 0.9rem;
|
| 64 |
+
color: var(--text-secondary);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
+
.form-group input {
|
| 68 |
+
padding: 14px 16px;
|
|
|
|
|
|
|
|
|
|
| 69 |
border-radius: var(--radius-sm);
|
| 70 |
+
border: 1px solid var(--border);
|
| 71 |
+
background: var(--bg-secondary);
|
| 72 |
+
color: var(--text-primary);
|
| 73 |
font-size: 1rem;
|
| 74 |
outline: none;
|
| 75 |
transition: border-color 0.2s;
|
| 76 |
}
|
| 77 |
|
| 78 |
+
.form-group input:focus {
|
| 79 |
+
border-color: var(--accent);
|
| 80 |
}
|
| 81 |
|
| 82 |
.mode-selector {
|
| 83 |
+
display: flex;
|
| 84 |
+
gap: 16px;
|
| 85 |
+
width: 100%;
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
.mode-btn {
|
| 89 |
+
flex: 1;
|
| 90 |
+
padding: 20px 16px;
|
| 91 |
border-radius: var(--radius);
|
| 92 |
+
border: 2px solid var(--border);
|
| 93 |
+
background: var(--bg-secondary);
|
| 94 |
+
color: var(--text-primary);
|
| 95 |
cursor: pointer;
|
| 96 |
text-align: center;
|
| 97 |
transition: all 0.2s;
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
+
.mode-btn:hover { border-color: var(--accent-light); }
|
| 101 |
+
.mode-btn.selected { border-color: var(--accent); background: var(--bg-tertiary); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
.mode-btn .mode-title {
|
| 104 |
font-weight: 700;
|
| 105 |
+
font-size: 1.1rem;
|
| 106 |
+
margin-bottom: 6px;
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
+
.mode-btn .mode-desc {
|
| 110 |
+
font-size: 0.82rem;
|
| 111 |
+
color: var(--text-secondary);
|
| 112 |
}
|
| 113 |
|
| 114 |
+
.btn-primary {
|
| 115 |
+
padding: 14px 32px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
border-radius: var(--radius-sm);
|
| 117 |
+
border: none;
|
| 118 |
+
background: var(--accent);
|
| 119 |
+
color: #fff;
|
| 120 |
font-size: 1rem;
|
| 121 |
font-weight: 600;
|
| 122 |
cursor: pointer;
|
|
|
|
| 123 |
transition: opacity 0.2s;
|
| 124 |
+
width: 100%;
|
| 125 |
}
|
| 126 |
|
| 127 |
+
.btn-primary:hover { opacity: 0.9; }
|
| 128 |
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
+
/* ===== Chat Screen ===== */
|
| 131 |
+
#chat-screen {
|
| 132 |
+
height: 100vh;
|
| 133 |
+
max-height: 100vh;
|
| 134 |
+
padding: 0;
|
| 135 |
+
gap: 0;
|
| 136 |
}
|
| 137 |
|
|
|
|
| 138 |
.chat-header {
|
| 139 |
+
padding: 16px 24px;
|
| 140 |
+
border-bottom: 1px solid var(--border);
|
| 141 |
display: flex;
|
|
|
|
| 142 |
align-items: center;
|
| 143 |
+
justify-content: space-between;
|
| 144 |
+
flex-shrink: 0;
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
+
.chat-header-left {
|
| 148 |
display: flex;
|
| 149 |
align-items: center;
|
| 150 |
+
gap: 12px;
|
| 151 |
}
|
| 152 |
|
| 153 |
+
.badge {
|
| 154 |
+
padding: 4px 12px;
|
| 155 |
+
border-radius: 20px;
|
| 156 |
+
font-size: 0.78rem;
|
| 157 |
+
font-weight: 600;
|
| 158 |
}
|
| 159 |
|
| 160 |
+
.badge-mode { background: var(--accent); color: #fff; }
|
| 161 |
+
.badge-topic { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
|
| 162 |
+
|
| 163 |
+
.chat-header-right {
|
| 164 |
display: flex;
|
| 165 |
+
align-items: center;
|
| 166 |
}
|
| 167 |
|
| 168 |
+
.btn-end {
|
| 169 |
+
padding: 8px 16px;
|
| 170 |
+
border-radius: var(--radius-sm);
|
| 171 |
+
border: 1px solid var(--danger);
|
| 172 |
+
background: transparent;
|
| 173 |
+
color: var(--danger);
|
| 174 |
+
font-size: 0.85rem;
|
| 175 |
+
font-weight: 600;
|
| 176 |
+
cursor: pointer;
|
| 177 |
+
transition: all 0.2s;
|
| 178 |
}
|
| 179 |
|
| 180 |
+
.btn-end:hover { background: var(--danger); color: #fff; }
|
| 181 |
+
|
| 182 |
+
.btn-reset {
|
| 183 |
+
padding: 8px 16px;
|
| 184 |
+
border-radius: var(--radius-sm);
|
| 185 |
+
border: 1px solid var(--border);
|
| 186 |
+
background: transparent;
|
| 187 |
+
color: var(--text-secondary);
|
| 188 |
+
font-size: 0.85rem;
|
| 189 |
+
cursor: pointer;
|
| 190 |
+
margin-left: 8px;
|
| 191 |
}
|
| 192 |
|
| 193 |
+
.btn-reset:hover { border-color: var(--text-secondary); }
|
| 194 |
+
|
| 195 |
+
/* Phase indicator */
|
| 196 |
.phase-indicator {
|
| 197 |
display: flex;
|
| 198 |
align-items: center;
|
| 199 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
| 200 |
gap: 0;
|
| 201 |
+
padding: 14px 24px 6px;
|
| 202 |
+
flex-shrink: 0;
|
| 203 |
}
|
| 204 |
|
| 205 |
.phase-step {
|
| 206 |
display: flex;
|
|
|
|
| 207 |
align-items: center;
|
|
|
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
.phase-dot {
|
| 211 |
+
width: 32px;
|
| 212 |
+
height: 32px;
|
| 213 |
border-radius: 50%;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
justify-content: center;
|
| 217 |
+
font-size: 0.75rem;
|
| 218 |
+
font-weight: 700;
|
| 219 |
+
background: var(--bg-tertiary);
|
| 220 |
+
color: var(--text-secondary);
|
| 221 |
border: 2px solid var(--border);
|
| 222 |
transition: all 0.3s;
|
| 223 |
+
flex-shrink: 0;
|
| 224 |
}
|
| 225 |
|
| 226 |
+
.phase-dot.active {
|
| 227 |
+
background: var(--accent);
|
| 228 |
+
color: #fff;
|
| 229 |
+
border-color: var(--accent);
|
| 230 |
}
|
| 231 |
|
| 232 |
+
.phase-dot.done {
|
| 233 |
+
background: var(--success);
|
| 234 |
+
color: #fff;
|
| 235 |
+
border-color: var(--success);
|
| 236 |
}
|
| 237 |
|
| 238 |
+
.phase-connector {
|
| 239 |
+
width: 32px;
|
| 240 |
+
height: 2px;
|
| 241 |
+
background: var(--border);
|
| 242 |
+
flex-shrink: 0;
|
| 243 |
}
|
| 244 |
|
| 245 |
+
.phase-connector.done { background: var(--success); }
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
+
.phase-labels {
|
| 248 |
+
display: flex;
|
| 249 |
+
justify-content: space-between;
|
| 250 |
+
padding: 4px 12px 10px;
|
| 251 |
+
flex-shrink: 0;
|
| 252 |
+
border-bottom: 1px solid var(--border);
|
| 253 |
}
|
| 254 |
|
| 255 |
+
.phase-label-text {
|
| 256 |
+
font-size: 0.68rem;
|
| 257 |
+
color: var(--text-secondary);
|
| 258 |
+
text-align: center;
|
| 259 |
+
flex: 1;
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
+
.phase-label-text.active { color: var(--accent-light); font-weight: 600; }
|
| 263 |
+
|
| 264 |
+
/* Messages */
|
| 265 |
+
.messages {
|
| 266 |
flex: 1;
|
| 267 |
overflow-y: auto;
|
| 268 |
+
padding: 24px;
|
| 269 |
display: flex;
|
| 270 |
flex-direction: column;
|
| 271 |
+
gap: 16px;
|
| 272 |
}
|
| 273 |
|
| 274 |
.message {
|
| 275 |
+
max-width: 85%;
|
| 276 |
+
padding: 14px 18px;
|
| 277 |
border-radius: var(--radius);
|
| 278 |
line-height: 1.55;
|
| 279 |
font-size: 0.95rem;
|
| 280 |
white-space: pre-wrap;
|
| 281 |
+
word-wrap: break-word;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
.message.assistant {
|
| 285 |
+
background: var(--bg-secondary);
|
|
|
|
| 286 |
border: 1px solid var(--border);
|
| 287 |
+
align-self: flex-start;
|
| 288 |
}
|
| 289 |
|
| 290 |
+
.message.user {
|
| 291 |
+
background: var(--accent);
|
| 292 |
+
color: #fff;
|
| 293 |
+
align-self: flex-end;
|
|
|
|
|
|
|
|
|
|
| 294 |
}
|
| 295 |
|
| 296 |
.typing-indicator {
|
| 297 |
align-self: flex-start;
|
| 298 |
+
padding: 14px 18px;
|
| 299 |
+
background: var(--bg-secondary);
|
| 300 |
border: 1px solid var(--border);
|
| 301 |
border-radius: var(--radius);
|
| 302 |
+
display: none;
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
+
.typing-indicator span {
|
| 306 |
+
display: inline-block;
|
| 307 |
+
width: 8px;
|
| 308 |
+
height: 8px;
|
| 309 |
+
background: var(--text-secondary);
|
| 310 |
+
border-radius: 50%;
|
| 311 |
+
margin: 0 2px;
|
| 312 |
+
animation: bounce 1.4s ease-in-out infinite;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
| 316 |
+
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
| 317 |
+
|
| 318 |
+
@keyframes bounce {
|
| 319 |
+
0%, 60%, 100% { transform: translateY(0); }
|
| 320 |
+
30% { transform: translateY(-6px); }
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* Input bar */
|
| 324 |
+
.input-bar {
|
| 325 |
+
padding: 16px 24px;
|
| 326 |
border-top: 1px solid var(--border);
|
| 327 |
+
display: flex;
|
| 328 |
+
gap: 12px;
|
| 329 |
+
flex-shrink: 0;
|
| 330 |
}
|
| 331 |
|
| 332 |
+
.input-bar input {
|
| 333 |
flex: 1;
|
| 334 |
+
padding: 14px 16px;
|
|
|
|
|
|
|
| 335 |
border-radius: var(--radius-sm);
|
| 336 |
+
border: 1px solid var(--border);
|
| 337 |
+
background: var(--bg-secondary);
|
| 338 |
+
color: var(--text-primary);
|
| 339 |
+
font-size: 1rem;
|
| 340 |
outline: none;
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
+
.input-bar input:focus { border-color: var(--accent); }
|
|
|
|
|
|
|
| 344 |
|
| 345 |
+
.btn-send {
|
| 346 |
+
padding: 14px 24px;
|
| 347 |
+
border-radius: var(--radius-sm);
|
| 348 |
+
border: none;
|
| 349 |
+
background: var(--accent);
|
| 350 |
+
color: #fff;
|
| 351 |
+
font-weight: 600;
|
| 352 |
+
cursor: pointer;
|
| 353 |
}
|
| 354 |
|
| 355 |
+
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
+
/* ===== Analysis Screen ===== */
|
| 358 |
+
#analysis-screen {
|
| 359 |
+
min-height: 100vh;
|
| 360 |
+
justify-content: flex-start;
|
| 361 |
+
padding-top: 40px;
|
| 362 |
+
gap: 24px;
|
| 363 |
}
|
| 364 |
|
| 365 |
+
#analysis-screen h2 {
|
| 366 |
+
font-size: 1.5rem;
|
| 367 |
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
.scores-grid {
|
| 371 |
display: grid;
|
| 372 |
grid-template-columns: repeat(3, 1fr);
|
| 373 |
+
gap: 12px;
|
| 374 |
+
width: 100%;
|
| 375 |
}
|
| 376 |
|
| 377 |
.score-card {
|
| 378 |
+
background: var(--bg-secondary);
|
| 379 |
border: 1px solid var(--border);
|
| 380 |
+
border-radius: var(--radius-sm);
|
| 381 |
+
padding: 16px;
|
| 382 |
text-align: center;
|
| 383 |
}
|
| 384 |
|
| 385 |
.score-card .score-value {
|
| 386 |
font-size: 2rem;
|
| 387 |
font-weight: 700;
|
| 388 |
+
color: var(--accent-light);
|
| 389 |
}
|
| 390 |
|
| 391 |
.score-card .score-label {
|
| 392 |
+
font-size: 0.78rem;
|
| 393 |
+
color: var(--text-secondary);
|
| 394 |
+
margin-top: 4px;
|
|
|
|
| 395 |
}
|
| 396 |
|
| 397 |
.score-bar {
|
| 398 |
+
width: 100%;
|
| 399 |
height: 4px;
|
| 400 |
+
background: var(--bg-tertiary);
|
| 401 |
border-radius: 2px;
|
| 402 |
+
margin-top: 8px;
|
| 403 |
overflow: hidden;
|
| 404 |
}
|
| 405 |
|
| 406 |
.score-bar-fill {
|
| 407 |
height: 100%;
|
| 408 |
+
background: var(--accent);
|
| 409 |
border-radius: 2px;
|
| 410 |
+
transition: width 0.6s ease;
|
| 411 |
}
|
| 412 |
|
| 413 |
.analysis-section {
|
| 414 |
+
width: 100%;
|
| 415 |
+
background: var(--bg-secondary);
|
| 416 |
border: 1px solid var(--border);
|
| 417 |
border-radius: var(--radius);
|
| 418 |
+
padding: 20px;
|
|
|
|
| 419 |
}
|
| 420 |
|
| 421 |
.analysis-section h3 {
|
| 422 |
+
font-size: 1rem;
|
| 423 |
+
margin-bottom: 12px;
|
| 424 |
+
color: var(--text-secondary);
|
|
|
|
|
|
|
| 425 |
}
|
| 426 |
|
| 427 |
+
.analysis-section p, .analysis-section li {
|
| 428 |
line-height: 1.6;
|
| 429 |
+
font-size: 0.92rem;
|
| 430 |
}
|
| 431 |
|
| 432 |
.analysis-section ul {
|
|
|
|
| 434 |
padding: 0;
|
| 435 |
}
|
| 436 |
|
| 437 |
+
.analysis-section ul li::before {
|
| 438 |
+
content: "- ";
|
| 439 |
+
color: var(--accent-light);
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
|
| 442 |
+
.rhythm-badge {
|
| 443 |
+
display: inline-block;
|
| 444 |
+
padding: 6px 14px;
|
| 445 |
+
border-radius: 20px;
|
| 446 |
+
font-size: 0.85rem;
|
| 447 |
+
font-weight: 600;
|
| 448 |
+
background: var(--bg-tertiary);
|
| 449 |
+
border: 1px solid var(--border);
|
|
|
|
| 450 |
}
|
| 451 |
|
| 452 |
.analysis-actions {
|
| 453 |
display: flex;
|
| 454 |
+
gap: 12px;
|
| 455 |
+
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
}
|
| 457 |
|
| 458 |
+
.analysis-actions button { flex: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
|
| 460 |
+
.btn-secondary {
|
| 461 |
+
padding: 14px 24px;
|
| 462 |
+
border-radius: var(--radius-sm);
|
| 463 |
+
border: 1px solid var(--border);
|
| 464 |
+
background: transparent;
|
| 465 |
+
color: var(--text-primary);
|
| 466 |
+
font-size: 0.95rem;
|
| 467 |
+
font-weight: 600;
|
| 468 |
+
cursor: pointer;
|
| 469 |
+
}
|
| 470 |
|
| 471 |
+
.btn-secondary:hover { border-color: var(--accent); }
|
|
|
|
|
|
|
| 472 |
|
| 473 |
+
/* ===== Responsive ===== */
|
| 474 |
+
@media (max-width: 600px) {
|
| 475 |
+
.scores-grid { grid-template-columns: repeat(2, 1fr); }
|
| 476 |
+
.mode-selector { flex-direction: column; }
|
| 477 |
+
.analysis-actions { flex-direction: column; }
|
| 478 |
+
.chat-header { flex-wrap: wrap; gap: 8px; }
|
| 479 |
}
|