Rochane Claude commited on
Commit
2e63e98
·
unverified ·
1 Parent(s): dbd0ec1

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>

Files changed (14) hide show
  1. .env.example +7 -0
  2. .gitignore +1 -0
  3. Dockerfile +5 -2
  4. README.md +91 -30
  5. app/llm.py +57 -70
  6. app/main.py +5 -18
  7. app/rag.py +4 -4
  8. corpus/sample.txt +2 -2
  9. docker-compose.yml +2 -3
  10. index.html +0 -845
  11. requirements.txt +2 -1
  12. static/app.js +281 -247
  13. static/index.html +75 -98
  14. static/style.css +275 -308
.env.example CHANGED
@@ -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
.gitignore CHANGED
@@ -2,3 +2,4 @@
2
  __pycache__/
3
  *.pyc
4
  chroma_data/
 
 
2
  __pycache__/
3
  *.pyc
4
  chroma_data/
5
+ *.egg-info/
Dockerfile CHANGED
@@ -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 8000
15
 
16
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
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"]
README.md CHANGED
@@ -1,53 +1,114 @@
1
- # AIM Learning Companion
 
 
 
 
 
 
 
 
2
 
3
- A Socratic AI companion for adult learners in professional training contexts. Uses a strict 5-phase Socratic protocol to guide learners through critical thinking, grounded in course materials via RAG.
 
 
4
 
5
  ## Architecture
6
 
7
- - **Backend**: FastAPI (Python)
8
- - **LLM**: Ollama with `mistral:instruct` (local, no external API calls)
9
- - **Vector Store**: ChromaDB (local)
10
- - **Embeddings**: sentence-transformers (`all-MiniLM-L6-v2`, local)
11
- - **Frontend**: Vanilla HTML/CSS/JS
12
 
13
- ## Prerequisites
14
 
15
- 1. **Install Ollama**: https://ollama.ai
16
- 2. **Pull the model**:
17
  ```bash
18
- ollama pull mistral:instruct
19
  ```
20
- 3. **Ensure Ollama is running**:
 
 
 
21
  ```bash
22
- ollama serve
23
  ```
24
 
25
- ## Quick Start
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
 
27
  ```bash
28
- docker-compose up --build
 
29
  ```
30
 
31
- Open http://localhost:8000 in your browser.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  ## Corpus
34
 
35
- Place `.pdf` and `.txt` files in the `/corpus` directory. The RAG pipeline will load and index them on startup. A sample file is included for testing.
 
 
36
 
37
- The `/corpus` directory is volume-mounted you can add or swap documents without rebuilding the container.
38
 
39
- ## Features
40
 
41
- - **Two modes**: TUTOR (guided learning) and CRITIC (logical audit)
42
- - **5-phase Socratic protocol**: Ciblage → Clarification → MécanismeVérification → Stress-test
43
- - **RAG-grounded questioning**: Questions are informed by course materials
44
- - **Session analysis**: End-of-session cognitive assessment with 6 scored dimensions
45
- - **JSON export**: Download full session data
46
- - **Privacy**: All processing is local no external API calls, no data retention
47
 
48
- ## Privacy
49
 
50
- - No external API calls during inference
51
- - All processing in-memory or local filesystem
52
- - No retention of learner inputs after session ends
53
- - No login, no authentication
 
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 → MecanismeVerification → 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
app/llm.py CHANGED
@@ -1,46 +1,67 @@
1
- """LLM interaction via OpenRouter (OpenAI-compatible API)."""
2
 
 
3
  import os
4
- import httpx
5
 
6
- OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
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. Si l'apprenant·e bloque, propose une analogie ou un indice progressif.
18
- 3. Si une définition est demandée, explique en max 2 phrases puis vérifie la compréhension immédiatement.
19
- 4. Dès qu'une base est posée, avance vers Phase 2. Ne reste pas bloqué en Phase 1.
20
- 5. Évite les reproches. Préfère l'invitation : "Ce point semble complexe, essayons un autre angle..."
 
21
  À la fin de chaque message, ajoute obligatoirement :
22
  ---
23
  Phase: [Numéro]
24
-
25
- Mode : Tuteur (Accompagnement)
26
- Sujet d'exploration : "{topic}" """
 
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. Si l'apprenant·e bloque, propose une analogie ou un indice progressif.
34
- 3. Si une définition est demandée, explique en max 2 phrases puis vérifie la compréhension immédiatement.
35
- 4. Dès qu'une base est posée, avance vers Phase 2. Ne reste pas bloqué en Phase 1.
36
- 5. Évite les reproches. Préfère l'invitation : "Ce point semble complexe, essayons un autre angle..."
 
37
  À la fin de chaque message, ajoute obligatoirement :
38
  ---
39
  Phase: [Numéro]
 
 
 
 
 
 
40
 
41
- Mode : Critique (Audit Logique)
42
- Ta mission : proposer des raisonnements fallacieux pour tester la vigilance. Reste un partenaire de jeu élégant, jamais méprisant.
43
- Sujet d'exploration : "{topic}" """
 
 
 
 
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": "<150 mots max : évaluation de la progression cognitive>",
55
  "keyStrengths": ["...", "..."],
56
  "weaknesses": ["...", "..."]
57
  }
58
  Réponds UNIQUEMENT avec le JSON, sans texte autour."""
59
 
60
 
61
- PHASE_GUIDANCE = {
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
- system += f"\n\nStratégies socratiques disponibles : {', '.join(STRATEGIES)}. Choisis la plus pertinente pour ce tour."
 
84
 
85
- if rag_context:
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 system
90
 
91
 
92
  async def chat(system_prompt: str, messages: list[dict]) -> str:
93
- """Send chat to OpenRouter and return assistant response."""
94
- api_messages = [{"role": "system", "content": system_prompt}]
95
- api_messages.extend(messages)
96
-
97
- async with httpx.AsyncClient(timeout=TIMEOUT) as client:
98
- response = await client.post(
99
- f"{OPENROUTER_BASE_URL}/chat/completions",
100
- headers={
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 json_mod.loads(raw[start:end])
136
- except (json_mod.JSONDecodeError, ValueError):
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 {
app/main.py CHANGED
@@ -1,12 +1,11 @@
1
  """FastAPI application for AIM Learning Companion."""
2
 
3
- import json
4
- import time
5
  from contextlib import asynccontextmanager
6
  from pathlib import Path
7
 
8
  from fastapi import FastAPI
9
- from fastapi.responses import HTMLResponse, FileResponse
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("/", response_class=HTMLResponse)
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
- # Build message history for LLM
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 (responses under 8 seconds)
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):
app/rag.py CHANGED
@@ -1,13 +1,13 @@
1
  """RAG layer: load corpus, chunk, embed, and retrieve."""
2
 
3
  import os
4
- import re
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 # tokens (approximated as words for simplicity)
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)
corpus/sample.txt CHANGED
@@ -1,7 +1,7 @@
1
- Critical Thinking in Professional AI 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.
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.
docker-compose.yml CHANGED
@@ -6,9 +6,8 @@ services:
6
  volumes:
7
  - ./corpus:/app/corpus
8
  - chroma-data:/app/chroma_data
9
- environment:
10
- - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
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:
index.html DELETED
@@ -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">&#11088;</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">&#9878;&#65039;</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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,8 +1,9 @@
1
  fastapi==0.115.6
2
  uvicorn[standard]==0.34.0
3
- httpx==0.28.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
 
 
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
static/app.js CHANGED
@@ -1,93 +1,115 @@
1
  /**
2
- * AIM Learning Companion - Frontend Application
 
3
  */
4
 
5
- const state = {
6
- mode: "TUTOR",
7
- topic: "",
8
- phase: 0,
9
- history: [], // {role, content}
10
- timestamps: [], // timestamps for each message
11
- sending: false,
12
- };
13
-
14
- // DOM elements
15
- const $ = (sel) => document.querySelector(sel);
16
- const setupScreen = $("#setup-screen");
17
- const chatScreen = $("#chat-screen");
18
- const analysisScreen = $("#analysis-screen");
19
- const topicInput = $("#topic-input");
20
- const startBtn = $("#start-btn");
21
- const chatMessages = $("#chat-messages");
22
- const chatInput = $("#chat-input");
23
- const sendBtn = $("#send-btn");
24
- const endSessionBtn = $("#end-session-btn");
25
- const resetBtn = $("#reset-btn");
26
- const exportBtn = $("#export-btn");
27
- const newSessionBtn = $("#new-session-btn");
28
-
29
- // Setup: mode selection
30
- document.querySelectorAll(".mode-btn").forEach((btn) => {
31
- btn.addEventListener("click", () => {
32
- document.querySelectorAll(".mode-btn").forEach((b) => b.classList.remove("active"));
33
- btn.classList.add("active");
34
- state.mode = btn.dataset.mode;
35
- });
36
- });
37
-
38
- // Setup: enable start when topic is entered
39
- topicInput.addEventListener("input", () => {
40
- startBtn.disabled = !topicInput.value.trim();
41
- });
42
-
43
- // Start session
44
- startBtn.addEventListener("click", () => {
45
- state.topic = topicInput.value.trim();
46
- if (!state.topic) return;
47
- state.phase = 0;
48
- state.history = [];
49
- state.timestamps = [];
50
- $("#header-mode").textContent = state.mode;
51
- $("#header-topic").textContent = state.topic;
52
- showScreen("chat");
53
- updatePhaseIndicator();
54
- addSystemMessage(`Session démarrée Mode: ${state.mode} — Sujet: ${state.topic}`);
55
- // Send initial message to get companion's opening question
56
- sendMessage(`Je souhaite explorer le sujet suivant : ${state.topic}`);
57
- });
58
-
59
- // Send message
60
- sendBtn.addEventListener("click", () => sendUserMessage());
61
- chatInput.addEventListener("keydown", (e) => {
62
- if (e.key === "Enter" && !e.shiftKey) {
63
- e.preventDefault();
64
- sendUserMessage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
- });
67
-
68
- function sendUserMessage() {
69
- const text = chatInput.value.trim();
70
- if (!text || state.sending) return;
71
- chatInput.value = "";
72
- addMessage("user", text);
73
- state.timestamps.push(Date.now() / 1000);
74
- sendMessage(text);
75
- }
76
-
77
- async function sendMessage(text) {
78
- state.sending = true;
79
- sendBtn.disabled = true;
80
- chatInput.disabled = true;
81
-
82
- // Show typing indicator
83
- const typing = document.createElement("div");
84
- typing.className = "typing-indicator";
85
- typing.textContent = "Companion réfléchit...";
86
- chatMessages.appendChild(typing);
87
- chatMessages.scrollTop = chatMessages.scrollHeight;
88
-
89
- try {
90
- const resp = await fetch("/api/chat", {
 
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
- timestamp: Date.now() / 1000,
100
- }),
 
 
 
 
 
 
 
 
 
 
 
 
101
  });
 
102
 
103
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
104
- const data = await resp.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- // Remove typing indicator
107
- typing.remove();
108
 
109
- // Update phase
110
- state.phase = data.phase;
111
- updatePhaseIndicator();
 
 
 
112
 
113
- // Add to history and display
114
- state.history.push({ role: "user", content: text });
115
- state.history.push({ role: "assistant", content: data.reply });
116
- state.timestamps.push(Date.now() / 1000);
117
-
118
- addMessage("assistant", data.reply);
119
- } catch (err) {
120
- typing.remove();
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 addMessage(role, content) {
131
- const div = document.createElement("div");
132
- div.className = `message ${role}`;
133
- div.textContent = content;
134
- chatMessages.appendChild(div);
135
- chatMessages.scrollTop = chatMessages.scrollHeight;
136
- }
137
-
138
- function addSystemMessage(text) {
139
- const div = document.createElement("div");
140
- div.className = "message system";
141
- div.textContent = text;
142
- chatMessages.appendChild(div);
143
- chatMessages.scrollTop = chatMessages.scrollHeight;
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
- try {
173
- const resp = await fetch("/api/analyze", {
174
- method: "POST",
175
- headers: { "Content-Type": "application/json" },
176
- body: JSON.stringify({
177
- history: state.history,
178
- timestamps: state.timestamps,
179
- }),
 
 
 
 
 
 
 
180
  });
181
 
182
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
183
- const data = await resp.json();
184
- displayAnalysis(data);
185
- } catch (err) {
186
- $("#analysis-loading").textContent = `Erreur: ${err.message}`;
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
- const grid = $("#scores-grid");
204
- grid.innerHTML = "";
205
- scores.forEach(({ key, label, color }) => {
206
- const val = data[key] || 0;
207
- const card = document.createElement("div");
208
- card.className = "score-card";
209
- card.innerHTML = `
210
- <div class="score-value" style="color: ${color}">${val}</div>
211
- <div class="score-label">${label}</div>
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
- $("#analysis-summary").textContent = data.summary || "Aucune analyse disponible.";
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- const strengthsList = $("#analysis-strengths");
222
- strengthsList.innerHTML = "";
223
- (data.keyStrengths || []).forEach((s) => {
224
- const li = document.createElement("li");
225
- li.textContent = s;
226
- strengthsList.appendChild(li);
227
  });
228
 
229
- const weaknessesList = $("#analysis-weaknesses");
230
- weaknessesList.innerHTML = "";
231
- (data.weaknesses || []).forEach((w) => {
232
- const li = document.createElement("li");
233
- li.textContent = w;
234
- weaknessesList.appendChild(li);
235
  });
236
 
237
- $("#analysis-rhythm").textContent =
238
- `Nombre de réponses avec un rythme anormalement rapide (< 8s) : ${data.rhythmBreakCount || 0}`;
239
- }
240
-
241
- // Export JSON
242
- exportBtn.addEventListener("click", () => {
243
- const exportData = {
244
- topic: state.topic,
245
- mode: state.mode,
246
- history: state.history,
247
- timestamps: state.timestamps,
248
- exportedAt: new Date().toISOString(),
249
- };
250
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" });
251
- const url = URL.createObjectURL(blob);
252
- const a = document.createElement("a");
253
- a.href = url;
254
- a.download = `aim-session-${Date.now()}.json`;
255
- a.click();
256
- URL.revokeObjectURL(url);
257
- });
258
-
259
- // Reset / New session
260
- resetBtn.addEventListener("click", () => {
261
- if (!confirm("Réinitialiser la session ? Toutes les données seront perdues.")) return;
262
- resetState();
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
+ })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/index.html CHANGED
@@ -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 Learning Companion</title>
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">&#9733;</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">&#9878;</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
- <!-- Chat Screen -->
38
- <div id="chat-screen" class="screen">
39
- <header class="chat-header">
40
- <div class="header-left">
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
- <!-- Phase Indicator -->
52
- <div class="phase-indicator">
53
- <div class="phase-step" data-phase="0">
54
- <div class="phase-dot"></div>
55
- <span class="phase-label">0 Ciblage</span>
56
- </div>
57
- <div class="phase-connector"></div>
58
- <div class="phase-step" data-phase="1">
59
- <div class="phase-dot"></div>
60
- <span class="phase-label">1 Clarification</span>
61
- </div>
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="phase-connector"></div>
73
- <div class="phase-step" data-phase="4">
74
- <div class="phase-dot"></div>
75
- <span class="phase-label">4 Stress-test</span>
76
  </div>
77
  </div>
 
78
 
79
- <!-- Chat Messages -->
80
- <div id="chat-messages" class="chat-messages"></div>
81
 
82
- <!-- Input Area -->
83
- <div class="chat-input-area">
84
- <textarea id="chat-input" placeholder="Écris ta réponse ici..." rows="2"></textarea>
85
- <button id="send-btn" class="primary-btn">Envoyer</button>
 
 
 
 
 
 
86
  </div>
87
  </div>
88
 
89
- <!-- Analysis Screen -->
90
- <div id="analysis-screen" class="screen">
91
- <div class="analysis-container">
92
- <h2>Analyse de session</h2>
93
- <div id="analysis-loading" class="loading">Analyse en cours...</div>
94
- <div id="analysis-content" class="hidden">
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>
static/style.css CHANGED
@@ -1,460 +1,432 @@
1
- * {
2
- margin: 0;
3
- padding: 0;
4
- box-sizing: border-box;
5
- }
6
-
7
  :root {
8
- --bg: #0f1117;
9
- --surface: #1a1d27;
10
- --surface-2: #242734;
11
- --border: #2e3144;
12
- --text: #e4e6f0;
13
- --text-muted: #8b8fa3;
14
- --primary: #6c5ce7;
15
- --primary-light: #a29bfe;
16
- --accent: #00cec9;
17
  --success: #00b894;
18
  --warning: #fdcb6e;
19
  --danger: #e17055;
20
- --phase-active: #6c5ce7;
21
- --phase-done: #00b894;
22
- --phase-pending: #2e3144;
23
  --radius: 12px;
24
  --radius-sm: 8px;
25
  }
26
 
27
- body {
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
- .screen {
35
- display: none;
 
 
36
  min-height: 100vh;
37
- }
38
-
39
- .screen.active {
40
  display: flex;
41
- flex-direction: column;
 
42
  }
43
 
44
- /* Setup Screen */
45
- .setup-container {
46
- max-width: 520px;
47
- margin: auto;
48
- padding: 2rem;
49
- text-align: center;
 
 
 
50
  }
51
 
52
- .setup-container h1 {
53
  font-size: 2rem;
54
- margin-bottom: 0.5rem;
55
- background: linear-gradient(135deg, var(--primary-light), var(--accent));
56
- -webkit-background-clip: text;
57
- -webkit-text-fill-color: transparent;
58
- background-clip: text;
59
  }
60
 
61
- .subtitle {
62
- color: var(--text-muted);
63
- margin-bottom: 2.5rem;
64
- font-size: 1rem;
 
65
  }
66
 
67
- .setup-form {
68
- text-align: left;
 
 
 
69
  }
70
 
71
- .setup-form label {
72
- display: block;
73
- font-size: 0.85rem;
74
- color: var(--text-muted);
75
- margin-bottom: 0.5rem;
76
- margin-top: 1.25rem;
77
- text-transform: uppercase;
78
- letter-spacing: 0.05em;
79
  }
80
 
81
- .setup-form input {
82
- width: 100%;
83
- padding: 0.85rem 1rem;
84
- background: var(--surface);
85
- border: 1px solid var(--border);
86
  border-radius: var(--radius-sm);
87
- color: var(--text);
 
 
88
  font-size: 1rem;
89
  outline: none;
90
  transition: border-color 0.2s;
91
  }
92
 
93
- .setup-form input:focus {
94
- border-color: var(--primary);
95
  }
96
 
97
  .mode-selector {
98
- display: grid;
99
- grid-template-columns: 1fr 1fr;
100
- gap: 1rem;
101
- margin-top: 0.25rem;
102
  }
103
 
104
  .mode-btn {
105
- background: var(--surface);
106
- border: 2px solid var(--border);
107
  border-radius: var(--radius);
108
- padding: 1.25rem 1rem;
 
 
109
  cursor: pointer;
110
  text-align: center;
111
  transition: all 0.2s;
112
- color: var(--text);
113
  }
114
 
115
- .mode-btn:hover {
116
- border-color: var(--primary);
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-label {
131
  font-weight: 700;
132
- font-size: 0.95rem;
133
- display: block;
134
- margin-bottom: 0.25rem;
135
  }
136
 
137
- .mode-desc {
138
- font-size: 0.8rem;
139
- color: var(--text-muted);
140
  }
141
 
142
- .primary-btn {
143
- width: 100%;
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
- .primary-btn:hover:not(:disabled) {
157
- opacity: 0.9;
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
- .secondary-btn:hover {
177
- background: var(--border);
 
 
 
 
178
  }
179
 
180
- /* Chat Header */
181
  .chat-header {
 
 
182
  display: flex;
183
- justify-content: space-between;
184
  align-items: center;
185
- padding: 0.75rem 1.5rem;
186
- background: var(--surface);
187
- border-bottom: 1px solid var(--border);
188
  }
189
 
190
- .header-left {
191
  display: flex;
192
  align-items: center;
193
- gap: 0.75rem;
194
  }
195
 
196
- .header-left h2 {
197
- font-size: 1.1rem;
 
 
 
198
  }
199
 
200
- .header-right {
 
 
 
201
  display: flex;
202
- gap: 0.5rem;
203
  }
204
 
205
- .mode-badge {
206
- font-size: 0.7rem;
207
- font-weight: 700;
208
- padding: 0.2rem 0.6rem;
209
- background: var(--primary);
210
- color: white;
211
- border-radius: 20px;
212
- text-transform: uppercase;
213
- letter-spacing: 0.05em;
 
214
  }
215
 
216
- .topic-badge {
217
- font-size: 0.8rem;
218
- color: var(--text-muted);
219
- max-width: 250px;
220
- overflow: hidden;
221
- text-overflow: ellipsis;
222
- white-space: nowrap;
 
 
 
 
223
  }
224
 
225
- /* Phase Indicator */
 
 
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: 14px;
246
- height: 14px;
247
  border-radius: 50%;
248
- background: var(--phase-pending);
 
 
 
 
 
 
249
  border: 2px solid var(--border);
250
  transition: all 0.3s;
 
251
  }
252
 
253
- .phase-step.active .phase-dot {
254
- background: var(--phase-active);
255
- border-color: var(--phase-active);
256
- box-shadow: 0 0 12px rgba(108, 92, 231, 0.5);
257
  }
258
 
259
- .phase-step.done .phase-dot {
260
- background: var(--phase-done);
261
- border-color: var(--phase-done);
 
262
  }
263
 
264
- .phase-label {
265
- font-size: 0.7rem;
266
- color: var(--text-muted);
267
- white-space: nowrap;
 
268
  }
269
 
270
- .phase-step.active .phase-label {
271
- color: var(--primary-light);
272
- font-weight: 600;
273
- }
274
 
275
- .phase-step.done .phase-label {
276
- color: var(--success);
 
 
 
 
277
  }
278
 
279
- .phase-connector {
280
- width: 40px;
281
- height: 2px;
282
- background: var(--border);
283
- margin: 0 0.25rem;
284
- margin-bottom: 1.25rem;
285
  }
286
 
287
- /* Chat Messages */
288
- .chat-messages {
 
 
289
  flex: 1;
290
  overflow-y: auto;
291
- padding: 1.5rem;
292
  display: flex;
293
  flex-direction: column;
294
- gap: 1rem;
295
  }
296
 
297
  .message {
298
- max-width: 80%;
299
- padding: 0.85rem 1.1rem;
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
- align-self: flex-start;
315
- background: var(--surface-2);
316
  border: 1px solid var(--border);
317
- border-bottom-left-radius: 4px;
318
  }
319
 
320
- .message.system {
321
- align-self: center;
322
- background: transparent;
323
- color: var(--text-muted);
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: 0.85rem 1.1rem;
332
- background: var(--surface-2);
333
  border: 1px solid var(--border);
334
  border-radius: var(--radius);
335
- border-bottom-left-radius: 4px;
336
- color: var(--text-muted);
337
- font-style: italic;
338
  }
339
 
340
- /* Chat Input */
341
- .chat-input-area {
342
- display: flex;
343
- gap: 0.75rem;
344
- padding: 1rem 1.5rem;
345
- background: var(--surface);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  border-top: 1px solid var(--border);
 
 
 
347
  }
348
 
349
- .chat-input-area textarea {
350
  flex: 1;
351
- padding: 0.75rem 1rem;
352
- background: var(--surface-2);
353
- border: 1px solid var(--border);
354
  border-radius: var(--radius-sm);
355
- color: var(--text);
356
- font-size: 0.95rem;
357
- font-family: inherit;
358
- resize: none;
359
  outline: none;
360
- transition: border-color 0.2s;
361
  }
362
 
363
- .chat-input-area textarea:focus {
364
- border-color: var(--primary);
365
- }
366
 
367
- .chat-input-area .primary-btn {
368
- width: auto;
369
- margin-top: 0;
370
- padding: 0.75rem 1.5rem;
 
 
 
 
371
  }
372
 
373
- /* Analysis Screen */
374
- .analysis-container {
375
- max-width: 680px;
376
- margin: 2rem auto;
377
- padding: 2rem;
378
- }
379
 
380
- .analysis-container h2 {
381
- text-align: center;
382
- margin-bottom: 2rem;
383
- font-size: 1.5rem;
 
 
384
  }
385
 
386
- .loading {
 
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: 1rem;
401
- margin-bottom: 2rem;
402
  }
403
 
404
  .score-card {
405
- background: var(--surface);
406
  border: 1px solid var(--border);
407
- border-radius: var(--radius);
408
- padding: 1.25rem;
409
  text-align: center;
410
  }
411
 
412
  .score-card .score-value {
413
  font-size: 2rem;
414
  font-weight: 700;
415
- margin-bottom: 0.25rem;
416
  }
417
 
418
  .score-card .score-label {
419
- font-size: 0.8rem;
420
- color: var(--text-muted);
421
- text-transform: uppercase;
422
- letter-spacing: 0.03em;
423
  }
424
 
425
  .score-bar {
 
426
  height: 4px;
427
- background: var(--border);
428
  border-radius: 2px;
429
- margin-top: 0.75rem;
430
  overflow: hidden;
431
  }
432
 
433
  .score-bar-fill {
434
  height: 100%;
 
435
  border-radius: 2px;
436
- transition: width 0.8s ease;
437
  }
438
 
439
  .analysis-section {
440
- background: var(--surface);
 
441
  border: 1px solid var(--border);
442
  border-radius: var(--radius);
443
- padding: 1.25rem;
444
- margin-bottom: 1rem;
445
  }
446
 
447
  .analysis-section h3 {
448
- font-size: 0.9rem;
449
- color: var(--text-muted);
450
- text-transform: uppercase;
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.95rem;
458
  }
459
 
460
  .analysis-section ul {
@@ -462,51 +434,46 @@ body {
462
  padding: 0;
463
  }
464
 
465
- .analysis-section li {
466
- padding: 0.4rem 0;
467
- padding-left: 1.25rem;
468
- position: relative;
469
- font-size: 0.95rem;
470
  }
471
 
472
- .analysis-section li::before {
473
- content: '';
474
- position: absolute;
475
- left: 0;
476
- top: 0.7rem;
477
- width: 6px;
478
- height: 6px;
479
- border-radius: 50%;
480
- background: var(--primary);
481
  }
482
 
483
  .analysis-actions {
484
  display: flex;
485
- gap: 1rem;
486
- margin-top: 2rem;
487
- }
488
-
489
- .analysis-actions .primary-btn,
490
- .analysis-actions .secondary-btn {
491
- flex: 1;
492
- text-align: center;
493
  }
494
 
495
- /* Responsive */
496
- @media (max-width: 640px) {
497
- .scores-grid {
498
- grid-template-columns: repeat(2, 1fr);
499
- }
500
 
501
- .phase-label {
502
- font-size: 0.6rem;
503
- }
 
 
 
 
 
 
 
504
 
505
- .phase-connector {
506
- width: 20px;
507
- }
508
 
509
- .message {
510
- max-width: 90%;
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
  }