Эх сурвалжийг харах

Version Works ! - > App works ! -> connection works !

Abdenbi 1 сар өмнө
parent
commit
2a99ff3084
49 өөрчлөгдсөн 1274 нэмэгдсэн , 801 устгасан
  1. 0 0
      03 - Scripts/.env
  2. 209 0
      03 - Scripts/Agents.py
  3. 0 0
      03 - Scripts/clean_DBSCAN.py
  4. 3 0
      03 - Scripts/code.txt
  5. 27 17
      03 - Scripts/function.py
  6. 0 0
      03 - Scripts/function_exctract.py
  7. 32 19
      03 - Scripts/main.py
  8. 0 0
      03 - Scripts/package-lock.json
  9. 0 0
      03 - Scripts/requirements.txt
  10. 221 0
      03 - Scripts/server.py
  11. 0 0
      03 - Scripts/sfcr-app/.gitignore
  12. 0 0
      03 - Scripts/sfcr-app/README.md
  13. 0 0
      03 - Scripts/sfcr-app/eslint.config.js
  14. 0 0
      03 - Scripts/sfcr-app/index.html
  15. 0 0
      03 - Scripts/sfcr-app/package-lock.json
  16. 0 0
      03 - Scripts/sfcr-app/package.json
  17. 0 0
      03 - Scripts/sfcr-app/public/favicon.svg
  18. 0 0
      03 - Scripts/sfcr-app/public/icons.svg
  19. 0 0
      03 - Scripts/sfcr-app/src/App.css
  20. 780 0
      03 - Scripts/sfcr-app/src/App.jsx
  21. 0 0
      03 - Scripts/sfcr-app/src/assets/hero.png
  22. 0 0
      03 - Scripts/sfcr-app/src/assets/react.svg
  23. 0 0
      03 - Scripts/sfcr-app/src/assets/vite.svg
  24. 0 0
      03 - Scripts/sfcr-app/src/constants.js
  25. 0 0
      03 - Scripts/sfcr-app/src/index.css
  26. 0 0
      03 - Scripts/sfcr-app/src/main.jsx
  27. 0 0
      03 - Scripts/sfcr-app/src/styles.js
  28. 0 0
      03 - Scripts/sfcr-app/vite.config.js
  29. 0 0
      03 - Scripts/test_luncher.py
  30. 0 0
      03 - Scripts/tools.py
  31. 2 2
      03 - Scripts/workflow_agents.py
  32. BIN
      04 - Outputs/2025/CNP_Assurances/Rapport_S.02_page_83.xlsx
  33. BIN
      04 - Outputs/2025/CNP_Assurances/Rapport_S.02_page_84.xlsx
  34. BIN
      04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_88.xlsx
  35. BIN
      04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_89.xlsx
  36. BIN
      04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_90.xlsx
  37. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.02_page_48.xlsx
  38. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.02_page_49.xlsx
  39. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.05_page_50.xlsx
  40. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.05_page_51.xlsx
  41. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.22_page_55.xlsx
  42. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.25_page_58.xlsx
  43. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.28_page_59.xlsx
  44. BIN
      04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/~$Rapport_S.28_page_59.xlsx
  45. 0 234
      04 - Scripts/Agents.py
  46. BIN
      04 - Scripts/Caisse_Générale_de_Prévoyance_(CGP)_2025/Rapport_S.02.xlsx
  47. BIN
      04 - Scripts/Caisse_Générale_de_Prévoyance_(CGP)_2025/Rapport_S.05.xlsx
  48. 0 72
      04 - Scripts/server.py
  49. 0 457
      04 - Scripts/sfcr-app/src/App.jsx

+ 0 - 0
04 - Scripts/.env → 03 - Scripts/.env


+ 209 - 0
03 - Scripts/Agents.py

@@ -0,0 +1,209 @@
+import os 
+from typing import Annotated , Sequence , TypedDict , Optional , List , Any
+from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
+from langgraph.graph.message import add_messages
+from dotenv import load_dotenv  
+from langchain_openai import ChatOpenAI 
+from langchain_groq import ChatGroq 
+import pandas as pd
+from langfuse import get_client
+import json
+
+from tools import  excel_code_interpreter
+
+from paddleocr import PaddleOCR
+
+from function import (
+    preparer_image_zoom_hd, 
+    extraire_donnees_ocr, 
+    nettoyage_sortie_ocr, 
+    formater_donnees_section,
+    sauvegarder_fichier_unique,
+    to_points , 
+    build_lines,
+    cluster_lines , 
+    merge_close_lines
+
+)
+
+from clean_DBSCAN import (
+    transform_to_clean_markdown
+)
+
+
+
+load_dotenv() 
+
+
+tools = [excel_code_interpreter]
+
+
+class AgentState(TypedDict) : 
+    messages : Annotated[Sequence[BaseMessage],add_messages]
+    pdf_path : str 
+    pages : List[int]
+    page : List[int]
+    entreprise_name : str 
+    section_name : str
+    lignes : str
+    markdown : str
+
+
+model_llama = ChatGroq(model="llama-3.3-70b-versatile") 
+
+model_openai = ChatOpenAI(model="gpt-4.1" , temperature=0.2) 
+
+model_ai = model_llama.bind_tools(tools)
+
+ocr = PaddleOCR(use_angle_cls=True, lang='fr', det_limit_side_len=10000, show_log=False)
+
+
+
+def agent_ocr(state: AgentState):
+    pdf_path = state.get("pdf_path")
+    # On récupère la page unique envoyée par le main
+    page_val = state.get("page")
+
+    if page_val is None:
+        raise ValueError("page est None dans state")
+
+    # Conversion en entier au cas où
+    try:
+        page_index = int(page_val) - 1 
+    except TypeError:
+        # Si c'est une liste [45], on prend le premier élément
+        page_index = int(page_val[0]) - 1
+
+    all_points = []
+    texte_accumule = []
+
+    try:
+        # Traitement de la page unique
+        img_finale = preparer_image_zoom_hd(pdf_path, page_index)
+        raw_data = extraire_donnees_ocr(img_finale, ocr)
+        data_propre = nettoyage_sortie_ocr(raw_data)
+
+        # Structure spatiale
+        points = to_points(data_propre)
+        all_points.extend(points)
+
+        # Texte pour le LLM
+        lignes_page = formater_donnees_section(data_propre, page_index)
+        texte_accumule.extend(lignes_page)
+
+        print(f" Page {page_index + 1} traitée par l'OCR.")
+
+    except Exception as e:
+        print(f"Erreur lors de l'OCR Page {page_index + 1}: {e}")
+        raise e
+
+    # DBSCAN et Markdown
+    clusters = cluster_lines(all_points, eps=0.5)
+    lignes = build_lines(clusters)
+    lignes = merge_close_lines(lignes)
+    markdown = transform_to_clean_markdown(lignes)
+    
+    contenu_final = "\n".join(texte_accumule)
+
+    return {
+        "messages": [HumanMessage(content=f"Voici les données OCR brutes :\n{contenu_final}")],
+        "clusters" : clusters , 
+        "lignes" : lignes ,
+        "markdown": markdown 
+    }
+
+
+
+
+def agent_extracteur(state: AgentState):
+    prompt = """
+    Tu es un extracteur de tableaux QRT (Solvabilité II).
+    Objectif : convertir le tableau en JSON sans perte d’information.
+
+    Règles :
+    - Utilise Rxxxx (lignes) et Cxxxx (colonnes) comme clés directement.
+    - Copie STRICTEMENT les valeurs, sans calcul.
+    - Supprime uniquement les espaces dans les nombres : "3 297 388" → 3297388.
+    - Garde toute la précision et les 0.
+    - Structure attendue : {"Cxxxx": {"Rxxxx": valeur, ...}, ...}
+
+    ⚠️ CONTRAINTE DE SORTIE :
+    - Réponds UNIQUEMENT avec l'objet JSON.
+    - PAS de markdown (```json), PAS de texte, PAS d'explications.
+
+    Sortie attendue :
+    {
+        "Cxxxx": { "Rxxxx": 123456, "Ryyyy": 789 }
+    }
+    """
+
+    markdown_content = state.get("markdown", "")
+    if not markdown_content or str(markdown_content).strip() == "":
+        backup_content = json.dumps(state.get("lignes", "Aucune donnée trouvée"))
+        input_content = f"Note : Le markdown était vide. Voici les lignes brutes :\n{backup_content}"
+    else:
+        input_content = markdown_content
+
+    msg = [
+        SystemMessage(content=prompt),
+        HumanMessage(content=json.dumps(input_content))
+    ]
+
+    response = model_openai.invoke(msg)
+    return {"messages": [response]}
+
+
+
+
+
+def agent_builder(state: AgentState):
+    print(f" Construction du fichier Excel pour : {state['entreprise_name']}...")
+
+    try:
+        import json
+        import pandas as pd
+        import os
+        import re
+
+        # 1. Extraction du contenu JSON pur
+        content = state["messages"][-1].content
+        # Nettoyage au cas où le modèle aurait ajouté des balises markdown malgré les consignes
+        content = re.sub(r'```json|```', '', content).strip()
+        data_json = json.loads(content)
+
+        # 2. Gestion du chemin de sauvegarde (identique)
+        base_outputs = os.path.join("..", "04 - Outputs")
+        match = re.search(r'(\d{4})', state['entreprise_name'])
+        annee = match.group(1) if match else "2025"
+        nom_entreprise = state['entreprise_name'].split('_')[0].replace(" ", "_")
+        target_folder = os.path.join(base_outputs, annee, nom_entreprise)
+        
+        if not os.path.exists(target_folder):
+            os.makedirs(target_folder)
+        
+        output_file = os.path.join(target_folder, f"Rapport_{state['section_name']}.xlsx")
+
+        # 3. Construction du DataFrame à partir de 'data_json' uniquement
+        # On récupère l'ensemble unique de tous les Rxxxx présents dans toutes les colonnes
+        all_rows = sorted(list(set(r for col in data_json.values() for r in col.keys())))
+        all_cols = sorted(list(data_json.keys()))
+
+        df = pd.DataFrame(index=all_rows, columns=all_cols)
+
+        for col, row_values in data_json.items():
+            for row, val in row_values.items():
+                df.at[row, col] = val
+
+        # 4. Exportation
+        with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
+            df.to_excel(writer, sheet_name="QRT_Export", index=True)
+            # Ajoute ici tes formats xlsxwriter si nécessaire
+
+        success_msg = f"✓ Sauvegardé dans : {output_file}"
+        print(success_msg)
+        return {"messages": [HumanMessage(content=success_msg)]}
+
+    except Exception as e:
+        error_msg = f" Erreur construction Excel : {str(e)}"
+        print(error_msg)
+        return {"messages": [HumanMessage(content=error_msg)]}

+ 0 - 0
04 - Scripts/clean_DBSCAN.py → 03 - Scripts/clean_DBSCAN.py


+ 3 - 0
03 - Scripts/code.txt

@@ -0,0 +1,3 @@
+python -m flask --app server run --port 5000  
+
+npm run dev

+ 27 - 17
04 - Scripts/function.py → 03 - Scripts/function.py

@@ -38,7 +38,6 @@ def preparer_image_zoom_hd(pdf_path, page_index):
     doc = fitz.open(pdf_path)
     page = doc.load_page(page_index)
 
-    print("page fitz",page)
     
     # 1. Localisation basse résolution
     pix_low = page.get_pixmap(matrix=fitz.Matrix(1, 1))
@@ -112,40 +111,51 @@ def nettoyer_texte_ocr(text):
 
     return text.strip()
 
+
+def est_code_metier(text):
+    return bool(re.match(r'^[A-Z]\d+$', text))
+
+
 def nettoyage_sortie_ocr(data):
-    """
-    Nettoie les données OCR pour DBSCAN :
-    - supprime lignes vides
-    - supprime artefacts inutiles
-    - garde textes utiles (mots, codes R/C, nombres)
-    """
 
     cleaned = []
 
     for r in data:
-        text = nettoyer_texte_ocr(r.get("text", ""))
 
-        # ❌ ignorer lignes totalement vides
+        # 🔥 sécurité : dict OU string
+        if isinstance(r, dict):
+            text = nettoyer_texte_ocr(r.get("text", ""))
+            x = r.get("x_pct")
+            y = r.get("y_pct")
+        else:
+            text = nettoyer_texte_ocr(str(r))
+            x = None
+            y = None
+
+        # ❌ ignorer vide
         if not text:
             continue
 
-        # ❌ ignorer bruit pur (ex: "---", "...")
+        # ❌ bruit pur
         if re.match(r"^[\W_]+$", text):
             continue
 
-        # ❌ ignorer "0" isolé (souvent bruit OCR)
-        if text == "0":
+        # 🔥 garder codes métier tels quels
+        if est_code_metier(text):
+            cleaned.append({
+                "x_pct": x,
+                "y_pct": y,
+                "text": text
+            })
             continue
 
-        # garder seulement si:
-        # - contient du texte utile OU
-        # - contient chiffre OU
+        # ❌ filtrage normal
         if not re.search(r"[A-Za-z0-9]", text):
             continue
 
         cleaned.append({
-            "x_pct": float(r["x_pct"]),
-            "y_pct": float(r["y_pct"]),
+            "x_pct": x,
+            "y_pct": y,
             "text": text
         })
 

+ 0 - 0
04 - Scripts/function_exctract.py → 03 - Scripts/function_exctract.py


+ 32 - 19
04 - Scripts/main.py → 03 - Scripts/main.py

@@ -1,3 +1,11 @@
+INPUTS = {
+    "Caisse Générale de Prévoyance (CGP)_2025.pdf": {
+        "S.25": [],
+        "S.28": [],
+        "S.02": []
+    }
+}
+
 from workflow_agents import app
 from function_exctract import extract_pages, extract_pdf_path
 from dotenv import load_dotenv
@@ -8,16 +16,10 @@ langfuse_handler = CallbackHandler()
 
 load_dotenv()
 
-INPUTS = {
-    "Caisse Générale de Prévoyance (CGP)_2025.pdf": {
-        #"S.02": [],
-        "S.05": []
-    }
-}
-
 if __name__ == "__main__":
     # 1. Boucle sur chaque entreprise (Fichier PDF)
     for pdf_filename, sections_input in INPUTS.items():
+        print("----------------------------------------")
         print(f"\n DÉMARRAGE ENTREPRISE : {pdf_filename}")
 
         entreprise_clean = pdf_filename.replace(".pdf", "")
@@ -36,22 +38,33 @@ if __name__ == "__main__":
             for s in sections_input.keys()
         }
 
+        # 2. Boucle sur chaque section
         # 2. Boucle sur chaque section
         for section_name, pages in sections.items():
-            print(f"   Section : {section_name} — pages : {pages}")
+            
+            # S'assurer que pages est une liste (ex: [45, 46])
+            list_pages = pages if isinstance(pages, list) else [pages]
+
+            # --- NOUVELLE BOUCLE PAR PAGE ---
+            for p in list_pages:
+                print(f"   Traitement Section : {section_name} — PAGE INDIVIDUELLE : {p}")
 
-            initial_state = {
-                "messages": [],
-                "pdf_path": pdf_path,
-                "pages": pages,
-                "entreprise_name": entreprise_clean,
-                "section_name": section_name
-            }
+                initial_state = {
+                    "messages": [],
+                    "pdf_path": pdf_path,
+                    "page": p, # On envoie le numéro unique
+                    "entreprise_name": entreprise_clean,
+                    "section_name": f"{section_name}_page_{p}" # On change le nom pour le fichier final
+                }
 
-            try:
+                try:
+                    # L'agent est appelé pour CHAQUE page
+                    print("----------------------------------------")
                     final_state = app.invoke(initial_state, config={"callbacks": [langfuse_handler]})
-                    print(f"   Section {section_name} terminée.")
-            except Exception as e:
-                    print(f"  ❌ Erreur section {section_name}: {e}")
+                    print(f"   Page {p} de la section {section_name} terminée.")
+                except Exception as e:
+                    print(f"   Erreur sur la page {p}: {e}")
+
+                print("----------------------------------------")
 
     #print("\n✨ TOUTES LES ENTREPRISES ET SECTIONS ONT ÉTÉ TRAITÉES.")

+ 0 - 0
04 - Scripts/package-lock.json → 03 - Scripts/package-lock.json


+ 0 - 0
04 - Scripts/requirements.txt → 03 - Scripts/requirements.txt


+ 221 - 0
03 - Scripts/server.py

@@ -0,0 +1,221 @@
+from flask import Flask, request, jsonify, Response, stream_with_context
+from flask_cors import CORS
+import subprocess
+import json
+import re
+import os
+import sys
+import threading
+import queue
+import io
+
+app = Flask(__name__) # on cree object __name qui aide Flask a savoir emplacement des fichiers ! 
+CORS(app) # Perlet pour Raect Port 3000 a parler avec FLASK port 5000
+
+if sys.platform == "win32":
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+
+def build_inputs(data):
+    """Reconstruit le dict INPUTS à partir du payload React."""
+    return data.get("inputs", {})
+
+
+def update_main_py(inputs, main_path):
+    """Met à jour le bloc INPUTS dans main.py."""
+    with open(main_path, "r", encoding="utf-8") as f:
+        content = f.read()
+
+    start_idx = content.find("INPUTS = {")
+    if start_idx != -1:
+        brace_count = 0
+        end_idx = start_idx
+        for i, char in enumerate(content[start_idx:]):
+            if char == "{":
+                brace_count += 1
+            elif char == "}":
+                brace_count -= 1
+                if brace_count == 0:
+                    end_idx = start_idx + i + 1
+                    break
+        content = content[:start_idx] + content[end_idx:].lstrip()
+
+    new_inputs_str = f"INPUTS = {json.dumps(inputs, indent=4, ensure_ascii=False)}\n\n"
+    content = new_inputs_str + content
+
+    with open(main_path, "w", encoding="utf-8") as f:
+        f.write(content)
+        f.flush()
+        os.fsync(f.fileno())
+
+
+def sse_event(event_type, data):
+    """Formate un event SSE."""
+    payload = json.dumps({"type": event_type, **data})
+    return f"data: {payload}\n\n"
+
+
+@app.route("/run-extraction-stream", methods=["POST"])
+def run_extraction_stream():
+    data = request.json
+    inputs = build_inputs(data)
+
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    main_path = os.path.join(script_dir, "main.py")
+
+    def generate():
+        # 1. Mise à jour de main.py
+        try:
+            update_main_py(inputs, main_path)
+        except Exception as e:
+            yield sse_event("error", {"message": f"Erreur écriture main.py : {str(e)}"})
+            return
+
+        yield sse_event("start", {"message": "Démarrage de l'extraction..."})
+
+        # 2. Lancement du subprocess avec stdout/stderr en temps réel
+        try:
+            process = subprocess.Popen(
+                [sys.executable, "-u", main_path],  # -u = unbuffered
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,  # merge stderr dans stdout
+                text=True,
+                bufsize=1,           # line-buffered
+                cwd=script_dir,
+            )
+        except Exception as e:
+            yield sse_event("error", {"message": f"Impossible de lancer le script : {str(e)}"})
+            return
+
+        # 3. Lecture ligne par ligne et parsing des events
+        current_company = None
+        current_section = None
+
+        for raw_line in process.stdout:
+            line = raw_line.rstrip("\n")
+            if "Erreur construction Excel" in line or "Erreur sur la page" in line:
+    # On extrait le numéro de page si possible, sinon on utilise la dernière connue
+                yield sse_event("section_done", {
+                    "company": current_company,
+                    "section": current_section,
+                    "page": page, # assurez-vous que 'page' est défini
+                    "status": "error",
+                    "error": line
+                })
+                continue
+
+            # Toujours envoyer la ligne brute comme log terminal
+            yield sse_event("log", {"line": line})
+
+            # --- Parsing des marqueurs émis par main.py ---
+
+            # Nouvelle entreprise
+            m = re.search(r"DÉMARRAGE ENTREPRISE\s*:\s*(.+)", line)
+            if m:
+                current_company = m.group(1).strip()
+                yield sse_event("company_start", {"company": current_company})
+                continue
+
+            # Nouvelle page/section
+            m = re.search(r"Traitement Section\s*:\s*(\S+)\s*[—-]\s*PAGE INDIVIDUELLE\s*:\s*(\d+)", line)
+            if m:
+                current_section = m.group(1).strip()
+                page = m.group(2).strip()
+                yield sse_event("section_start", {
+                    "company": current_company,
+                    "section": current_section,
+                    "page": page,
+                })
+                continue
+
+            # Section terminée avec succès (ligne émise par agent_builder)
+            m = re.search(r"Page (\d+) de la section (\S+) terminée", line)
+            if m:
+                page = m.group(1).strip()
+                section = m.group(2).strip()
+                yield sse_event("section_done", {
+                    "company": current_company,
+                    "section": section,
+                    "page": page,
+                    "status": "success",
+                })
+                continue
+
+            # Erreur de page
+            m = re.search(r"Erreur sur la page (\d+)\s*:\s*(.+)", line)
+            if m:
+                page = m.group(1).strip()
+                error = m.group(2).strip()
+                yield sse_event("section_done", {
+                    "company": current_company,
+                    "section": current_section,
+                    "page": page,
+                    "status": "error",
+                    "error": error,
+                })
+                continue
+
+            # Fichier Excel sauvegardé (succès confirmé par agent_builder)
+            m = re.search(r"✓ Sauvegardé dans\s*:\s*(.+)", line)
+            if m:
+                path = m.group(1).strip()
+                yield sse_event("file_saved", {
+                    "company": current_company,
+                    "path": path,
+                })
+                continue
+
+        process.wait()
+        returncode = process.returncode
+
+        if returncode == 0:
+            yield sse_event("done", {"message": "Extraction terminée avec succès ✓", "returncode": 0})
+        else:
+            yield sse_event("done", {"message": f"Extraction terminée avec des erreurs (code {returncode})", "returncode": returncode})
+
+    return Response(
+        stream_with_context(generate()),
+        mimetype="text/event-stream",
+        headers={
+            "Cache-Control": "no-cache",
+            "X-Accel-Buffering": "no",  # Désactive le buffering Nginx si présent
+        },
+    )
+
+
+# Ancien endpoint conservé pour compatibilité
+@app.route("/run-extraction", methods=["POST"])
+def run_extraction():
+    data = request.json
+    inputs = build_inputs(data)
+
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    main_path = os.path.join(script_dir, "main.py")
+
+    try:
+        update_main_py(inputs, main_path)
+    except Exception as e:
+        return jsonify({"stdout": "", "stderr": f"Erreur écriture : {str(e)}", "returncode": -1})
+
+    try:
+        result = subprocess.run(
+            [sys.executable, main_path],
+            capture_output=True,
+            text=True,
+            cwd=script_dir,
+            timeout=600,
+        )
+        return jsonify({
+            "stdout": result.stdout,
+            "stderr": result.stderr,
+            "returncode": result.returncode,
+        })
+    except subprocess.TimeoutExpired:
+        return jsonify({"stdout": "", "stderr": "Timeout dépassé", "returncode": -1})
+    except Exception as e:
+        return jsonify({"stdout": "", "stderr": f"Erreur exécution : {str(e)}", "returncode": -1})
+
+
+if __name__ == "__main__":
+    app.run(debug=True, port=5000, threaded=True)

+ 0 - 0
04 - Scripts/sfcr-app/.gitignore → 03 - Scripts/sfcr-app/.gitignore


+ 0 - 0
04 - Scripts/sfcr-app/README.md → 03 - Scripts/sfcr-app/README.md


+ 0 - 0
04 - Scripts/sfcr-app/eslint.config.js → 03 - Scripts/sfcr-app/eslint.config.js


+ 0 - 0
04 - Scripts/sfcr-app/index.html → 03 - Scripts/sfcr-app/index.html


+ 0 - 0
04 - Scripts/sfcr-app/package-lock.json → 03 - Scripts/sfcr-app/package-lock.json


+ 0 - 0
04 - Scripts/sfcr-app/package.json → 03 - Scripts/sfcr-app/package.json


+ 0 - 0
04 - Scripts/sfcr-app/public/favicon.svg → 03 - Scripts/sfcr-app/public/favicon.svg


+ 0 - 0
04 - Scripts/sfcr-app/public/icons.svg → 03 - Scripts/sfcr-app/public/icons.svg


+ 0 - 0
04 - Scripts/sfcr-app/src/App.css → 03 - Scripts/sfcr-app/src/App.css


+ 780 - 0
03 - Scripts/sfcr-app/src/App.jsx

@@ -0,0 +1,780 @@
+import { useState, useMemo, useRef, useEffect } from "react";
+import { ALL_COMPANIES, SFCR_SECTIONS, YEARS, CURRENT_YEAR, sectionLabels } from "./constants";
+import { styles, cssString } from "./styles";
+import { C } from "./styles";
+
+// ─────────────────────────────────────────────
+// VUE EXTRACTION TEMPS RÉEL
+// ─────────────────────────────────────────────
+const ExtractionStatusView = ({ progress, logs, onClose, isDone }) => {
+  const logsEndRef = useRef(null);
+
+  useEffect(() => {
+    logsEndRef.current?.scrollIntoView({ behavior: "smooth" });
+  }, [logs]);
+
+  return (
+    <div style={styles.extractionWrapper}>
+      {/* Header */}
+      <div style={styles.extractionHeader}>
+        <div>
+          <h2 style={styles.pageTitle}>Analyse Multi-Agents</h2>
+          <p style={styles.pageSubtitle}>
+            {isDone ? "Extraction terminée." : "Traitement des rapports Solvabilité II en cours..."}
+          </p>
+        </div>
+        <button onClick={onClose} style={styles.showMoreBtn}>
+          {isDone ? "← Retour" : "Interrompre"}
+        </button>
+      </div>
+
+      {/* Cartes entreprises */}
+      <div style={styles.extractionList}>
+        {Object.entries(progress).map(([company, companyData]) => {
+          const sections = Object.entries(companyData.sections || {});
+          const allDone = sections.length > 0 && sections.every(([, s]) => s.status !== "pending" && s.status !== "running");
+          const hasError = sections.some(([, s]) => s.status === "error");
+
+            return (
+              <div key={company} style={{
+                ...styles.extractionCard,
+                // La bordure devient rouge vif si hasError est vrai
+                borderColor: allDone 
+                  ? (hasError ? "#ef4444" : "rgba(16,185,129,0.35)") 
+                  : C.border,
+                borderWidth: hasError ? 2 : 1, // Optionnel : épaissir un peu si erreur
+              }}>   
+              {/* Company header */}
+              <div style={styles.companyStatusHeader}>
+                <div style={{
+                  ...styles.companyAvatar,
+                  background: allDone
+                    ? hasError ? "rgba(239,68,68,0.15)" : "rgba(16,185,129,0.15)"
+                    : "linear-gradient(135deg, #1e3a5f, #2d5a8e)",
+                }}>
+                  {company.slice(0, 2).toUpperCase()}
+                </div>
+                <div style={{ flex: 1 }}>
+                  <h4 style={{ margin: 0, fontSize: 14, color: C.text }}>{company}</h4>
+                  <span style={{ fontSize: 11, color: allDone ? (hasError ? "#ef4444" : C.green) : C.accent }}>
+                    {allDone
+                      ? hasError ? "⚠ Terminé avec des erreurs" : "✓ Terminé avec succès"
+                      : companyData.currentSection
+                        ? `Traitement ${companyData.currentSection}...`
+                        : "En attente..."}
+                  </span>
+                </div>
+                {/* Progress pill */}
+                <span style={{
+                  fontSize: 11, fontWeight: 700, padding: "3px 10px", borderRadius: 20,
+                  background: allDone ? (hasError ? "rgba(239,68,68,0.1)" : C.greenSoft) : C.accentSoft,
+                  color: allDone ? (hasError ? "#ef4444" : C.green) : C.accent,
+                }}>
+                  {sections.filter(([, s]) => s.status === "success").length}/{sections.length} sections
+                </span>
+              </div>
+
+              {/* Sections timeline */}
+              <div style={{ display: "flex", gap: 8, flexWrap: "wrap", padding: "0 4px" }}>
+                {sections.map(([key, sec]) => (
+                  <div key={key} style={{
+                    display: "flex", flexDirection: "column", alignItems: "center", gap: 5,
+                    padding: "10px 14px", borderRadius: 8, fontSize: 11, fontWeight: 600,
+                    border: `1px solid`,
+                    borderColor: sec.status === "success"
+                      ? "rgba(16,185,129,0.4)"
+                      : sec.status === "error"
+                        ? "rgba(239,68,68,0.4)"
+                        : sec.status === "running"
+                          ? "rgba(59,130,246,0.4)"
+                          : C.border,
+                    background: sec.status === "success"
+                      ? "rgba(16,185,129,0.08)"
+                      : sec.status === "error"
+                        ? "rgba(239,68,68,0.08)"
+                        : sec.status === "running"
+                          ? C.accentSoft
+                          : C.bg,
+                    color: sec.status === "success"
+                      ? C.green
+                      : sec.status === "error"
+                        ? "#ef4444"
+                        : sec.status === "running"
+                          ? C.accent
+                          : C.textMuted,
+                    minWidth: 70,
+                  }}>
+                    <span style={{ fontSize: 16 }}>
+                      {sec.status === "success" ? "✅"
+                        : sec.status === "error" ? "❌"
+                        : sec.status === "running" ? "⚙️"
+                        : "⏳"}
+                    </span>
+                    <span>{sec.section}</span>
+                    {sec.page && <span style={{ fontSize: 10, opacity: 0.7 }}>p.{sec.page}</span>}
+                    {sec.status === "error" && sec.error && (
+                      <span style={{
+                        fontSize: 9, color: "#ef4444", maxWidth: 120,
+                        textAlign: "center", lineHeight: 1.3, marginTop: 2,
+                        wordBreak: "break-word",
+                      }}>
+                        {sec.error.slice(0, 60)}{sec.error.length > 60 ? "…" : ""}
+                      </span>
+                    )}
+                  </div>
+                ))}
+              </div>
+            </div>
+          );
+        })}
+
+        {Object.keys(progress).length === 0 && (
+          <div style={{ color: C.textMuted, textAlign: "center", padding: 40 }}>
+            ⏳ Connexion au serveur...
+          </div>
+        )}
+      </div>
+
+      {/* Terminal logs */}
+      <div style={{
+        marginTop: 24,
+        background: "#0a0c10",
+        border: `1px solid ${C.border}`,
+        borderRadius: 10,
+        overflow: "hidden",
+      }}>
+        <div style={{
+          padding: "8px 14px",
+          borderBottom: `1px solid ${C.border}`,
+          fontSize: 11, fontWeight: 700,
+          color: C.textMuted, letterSpacing: "1px",
+          textTransform: "uppercase",
+          display: "flex", alignItems: "center", gap: 8,
+        }}>
+          <span style={{ color: "#ef4444" }}>●</span>
+          <span style={{ color: "#f59e0b" }}>●</span>
+          <span style={{ color: C.green }}>●</span>
+          <span style={{ marginLeft: 8 }}>Terminal</span>
+        </div>
+        <div style={{
+          height: 220,
+          overflowY: "auto",
+          padding: "12px 16px",
+          fontFamily: "'Fira Code', 'Courier New', monospace",
+          fontSize: 12,
+          lineHeight: 1.6,
+          color: "#a3e635",
+        }}>
+          {logs.map((line, i) => (
+            <div key={i} style={{
+              color: line.includes("Erreur") || line.includes("Error") || line.includes("❌")
+                ? "#f87171"
+                : line.includes("✓") || line.includes("terminée") || line.includes("success")
+                  ? "#86efac"
+                  : line.includes("DÉMARRAGE") || line.includes("Traitement")
+                    ? "#93c5fd"
+                    : "#a3e635",
+            }}>
+              <span style={{ opacity: 0.4, marginRight: 8 }}>&gt;</span>{line}
+            </div>
+          ))}
+          <div ref={logsEndRef} />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+// ─────────────────────────────────────────────
+// COMPOSANT PRINCIPAL
+// ─────────────────────────────────────────────
+export default function App() {
+  const [activeNav, setActiveNav] = useState("extraction");
+  const [search, setSearch] = useState("");
+  const [showAll, setShowAll] = useState(false);
+  const [selectedCompanies, setSelectedCompanies] = useState([]);
+  const [selectedSections, setSelectedSections] = useState({});
+  const [selectedYears, setSelectedYears] = useState({});
+  const [globalSections, setGlobalSections] = useState([]);
+  const [globalYear, setGlobalYear] = useState(CURRENT_YEAR);
+  const [sectionMode, setSectionMode] = useState("global");
+  const [yearMode, setYearMode] = useState("global");
+  const [prompt, setPrompt] = useState("");
+  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+  const [isExtracting, setIsExtracting] = useState(false);
+  const [isDone, setIsDone] = useState(false);
+
+  // État temps réel
+  // progress: { [companyName]: { currentSection, sections: { [key]: { section, page, status, error } } } }
+  const [progress, setProgress] = useState({});
+  const [logs, setLogs] = useState([]);
+
+  const filtered = useMemo(() => {
+    const base = ALL_COMPANIES.filter((c) =>
+      c.name.toLowerCase().includes(search.toLowerCase())
+    );
+    return showAll ? base : base.slice(0, 5);
+  }, [search, showAll]);
+
+  const toggleCompany = (id) =>
+    setSelectedCompanies((prev) =>
+      prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
+    );
+
+  const toggleAll = () => {
+    if (selectedCompanies.length === filtered.length) setSelectedCompanies([]);
+    else setSelectedCompanies(filtered.map((c) => c.id));
+  };
+
+  const toggleGlobalSection = (s) =>
+    setGlobalSections((prev) =>
+      prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]
+    );
+
+  const toggleCompanySection = (companyId, s) =>
+    setSelectedSections((prev) => {
+      const cur = prev[companyId] || [];
+      return {
+        ...prev,
+        [companyId]: cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s],
+      };
+    });
+
+  const setCompanyYear = (companyId, year) =>
+    setSelectedYears((prev) => ({ ...prev, [companyId]: year }));
+
+  const selectedCompanyObjects = useMemo(
+    () => ALL_COMPANIES.filter((c) => selectedCompanies.includes(c.id)),
+    [selectedCompanies]
+  );
+
+  const handleExtraction = async () => {
+    if (selectedCompanies.length === 0) {
+      alert("Veuillez sélectionner au moins une entreprise.");
+      return;
+    }
+
+    const inputs = {};
+    selectedCompanyObjects.forEach((company) => {
+      const sections =
+        sectionMode === "global"
+          ? globalSections
+          : selectedSections[company.id] || [];
+      const year =
+        yearMode === "global"
+          ? globalYear
+          : selectedYears[company.id] || CURRENT_YEAR;
+
+      if (sections.length > 0) {
+        const filename = `${company.name}_${year}.pdf`;
+        inputs[filename] = {};
+        sections.forEach((s) => {
+          inputs[filename][s] = [];
+        });
+      }
+    });
+
+    // Reset et affichage immédiat
+    setProgress({});
+    setLogs([]);
+    setIsDone(false);
+    setIsExtracting(true);
+
+    // SSE streaming
+    try {
+      const response = await fetch("http://localhost:5000/run-extraction-stream", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ inputs }),
+      });
+
+      const reader = response.body.getReader();
+      const decoder = new TextDecoder();
+      let buffer = "";
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split("\n");
+        buffer = lines.pop(); // Garde l'éventuel fragment incomplet
+
+        for (const line of lines) {
+          if (!line.startsWith("data: ")) continue;
+          try {
+            const event = JSON.parse(line.slice(6));
+            handleSSEEvent(event);
+          } catch (_) {}
+        }
+      }
+    } catch (err) {
+      setLogs((prev) => [...prev, `❌ Erreur de connexion : ${err.message}`]);
+      setIsDone(true);
+    }
+  };
+
+  const handleSSEEvent = (event) => {
+    switch (event.type) {
+      case "log":
+        setLogs((prev) => [...prev, event.line]);
+        break;
+
+      case "company_start":
+        setProgress((prev) => ({
+          ...prev,
+          [event.company]: { currentSection: null, sections: {} },
+        }));
+        break;
+
+      case "section_start":
+        setProgress((prev) => {
+          const key = `${event.section}_p${event.page}`;
+          const comp = prev[event.company] || { sections: {} };
+          return {
+            ...prev,
+            [event.company]: {
+              ...comp,
+              currentSection: event.section,
+              sections: {
+                ...comp.sections,
+                [key]: { section: event.section, page: event.page, status: "running" },
+              },
+            },
+          };
+        });
+        break;
+
+      case "section_done":
+        setProgress((prev) => {
+          const key = `${event.section}_p${event.page}`;
+          const comp = prev[event.company] || { sections: {} };
+          return {
+            ...prev,
+            [event.company]: {
+              ...comp,
+              currentSection: event.status === "success" ? null : comp.currentSection,
+              sections: {
+                ...comp.sections,
+                [key]: {
+                  section: event.section,
+                  page: event.page,
+                  status: event.status,
+                  error: event.error || null,
+                },
+              },
+            },
+          };
+        });
+        break;
+
+      case "done":
+        setLogs((prev) => [...prev, `\n${event.message}`]);
+        setIsDone(true);
+        break;
+
+      case "error":
+        setLogs((prev) => [...prev, `❌ ${event.message}`]);
+        setIsDone(true);
+        break;
+
+      default:
+        break;
+    }
+  };
+
+  return (
+    <div style={styles.root}>
+      <style>{cssString}</style>
+
+      {/* Sidebar */}
+      <aside style={{ ...styles.sidebar, width: sidebarCollapsed ? 64 : 220 }}>
+        <div style={styles.sidebarHeader}>
+          {!sidebarCollapsed && (
+            <span style={styles.brandText}>
+              <span style={styles.brandAccent}>SFCR</span>
+              <span style={styles.brandSub}>·extract</span>
+            </span>
+          )}
+          <button
+            onClick={() => setSidebarCollapsed((v) => !v)}
+            style={styles.collapseBtn}
+          >
+            {sidebarCollapsed ? "›" : "‹"}
+          </button>
+        </div>
+
+        <nav style={styles.nav}>
+          {[
+            { id: "search", icon: "⊕", label: "Search Sources" },
+            { id: "extraction", icon: "⊞", label: "Extraction" },
+          ].map((item) => (
+            <button
+              key={item.id}
+              onClick={() => {
+                setActiveNav(item.id);
+                if (item.id === "search") setIsExtracting(false);
+              }}
+              style={{
+                ...styles.navBtn,
+                ...(activeNav === item.id ? styles.navBtnActive : {}),
+              }}
+              className="nav-btn"
+            >
+              <span style={styles.navIcon}>{item.icon}</span>
+              {!sidebarCollapsed && (
+                <span style={styles.navLabel}>{item.label}</span>
+              )}
+            </button>
+          ))}
+        </nav>
+
+        <div style={styles.sidebarFooter}>
+          {!sidebarCollapsed && (
+            <span style={styles.footerBadge}>Beta</span>
+          )}
+        </div>
+      </aside>
+
+      {/* Main */}
+      <main style={styles.main}>
+        {activeNav === "search" ? (
+          <div style={styles.placeholderPanel}>
+            <div style={styles.placeholderIcon}>⊕</div>
+            <h2 style={styles.placeholderTitle}>Search Sources</h2>
+            <p style={styles.placeholderDesc}>
+              Section dédiée à la recherche de sources SFCR.
+            </p>
+          </div>
+        ) : isExtracting ? (
+          <ExtractionStatusView
+            progress={progress}
+            logs={logs}
+            isDone={isDone}
+            onClose={() => setIsExtracting(false)}
+          />
+        ) : (
+          <div style={styles.extractionPanel}>
+            <div style={styles.pageHeader}>
+              <div>
+                <h1 style={styles.pageTitle}>Extraction SFCR</h1>
+                <p style={styles.pageSubtitle}>
+                  Sélectionnez les entreprises et paramétrez l'extraction
+                </p>
+              </div>
+              <button
+                style={styles.extractBtn}
+                className="extract-btn"
+                onClick={handleExtraction}
+              >
+                <span>▶</span> Lancer l'extraction
+              </button>
+            </div>
+
+            <div style={styles.twoCol}>
+              <div style={styles.leftCol}>
+                {/* 01. SELECTION ENTREPRISES */}
+                <section style={styles.card}>
+                  <div style={styles.cardHeader}>
+                    <span style={styles.cardBadge}>01</span>
+                    <h2 style={styles.cardTitle}>Sélection des entreprises</h2>
+                    {selectedCompanies.length > 0 && (
+                      <span style={styles.countPill}>
+                        {selectedCompanies.length} sélectionné
+                        {selectedCompanies.length > 1 ? "s" : ""}
+                      </span>
+                    )}
+                  </div>
+
+                  <div style={styles.searchRow}>
+                    <span style={styles.searchIcon}>🔍</span>
+                    <input
+                      style={styles.searchInput}
+                      placeholder="Rechercher une entreprise…"
+                      value={search}
+                      onChange={(e) => setSearch(e.target.value)}
+                    />
+                  </div>
+
+                  <div style={styles.selectAllRow}>
+                    <label style={styles.checkLabel} className="check-label">
+                      <input
+                        type="checkbox"
+                        checked={
+                          filtered.length > 0 &&
+                          filtered.every((c) =>
+                            selectedCompanies.includes(c.id)
+                          )
+                        }
+                        onChange={toggleAll}
+                        style={styles.hiddenCheck}
+                      />
+                      <span
+                        style={{
+                          ...styles.customCheck,
+                          ...(filtered.length > 0 &&
+                          filtered.every((c) =>
+                            selectedCompanies.includes(c.id)
+                          )
+                            ? styles.customCheckChecked
+                            : {}),
+                        }}
+                      >
+                        {filtered.length > 0 &&
+                          filtered.every((c) =>
+                            selectedCompanies.includes(c.id)
+                          ) && "✓"}
+                      </span>
+                      <span style={styles.checkLabelText}>Tout sélectionner</span>
+                    </label>
+                  </div>
+
+                  <div style={{ maxHeight: 300, overflowY: "auto", paddingRight: 5 }}>
+                    <div style={styles.companyList}>
+                      {filtered.map((company) => {
+                        const checked = selectedCompanies.includes(company.id);
+                        return (
+                          <label
+                            key={company.id}
+                            style={{
+                              ...styles.companyRow,
+                              ...(checked ? styles.companyRowChecked : {}),
+                            }}
+                            className="company-row"
+                          >
+                            <input
+                              type="checkbox"
+                              checked={checked}
+                              onChange={() => toggleCompany(company.id)}
+                              style={styles.hiddenCheck}
+                            />
+                            <span style={styles.companyAvatar}>
+                              {company.logo}
+                            </span>
+                            <span style={styles.companyName}>{company.name}</span>
+                            <span
+                              style={{
+                                ...styles.customCheck,
+                                ...(checked ? styles.customCheckChecked : {}),
+                              }}
+                            >
+                              {checked && "✓"}
+                            </span>
+                          </label>
+                        );
+                      })}
+                    </div>
+                  </div>
+
+                  {!showAll && ALL_COMPANIES.length > 10 && (
+                    <button
+                      style={styles.showMoreBtn}
+                      onClick={() => setShowAll(true)}
+                    >
+                      Afficher plus ({ALL_COMPANIES.length - 10} entreprises)
+                    </button>
+                  )}
+                </section>
+
+                {/* 04. ANNEE */}
+                {selectedCompanies.length > 0 && (
+                  <section style={styles.card}>
+                    <div style={styles.cardHeader}>
+                      <span style={styles.cardBadge}>04</span>
+                      <h2 style={styles.cardTitle}>Année de rapport</h2>
+                    </div>
+                    <div style={styles.modeToggle}>
+                      <button
+                        style={{
+                          ...styles.modeBtn,
+                          ...(yearMode === "global" ? styles.modeBtnActive : {}),
+                        }}
+                        onClick={() => setYearMode("global")}
+                      >
+                        Toutes
+                      </button>
+                      <button
+                        style={{
+                          ...styles.modeBtn,
+                          ...(yearMode === "individual"
+                            ? styles.modeBtnActive
+                            : {}),
+                        }}
+                        onClick={() => setYearMode("individual")}
+                      >
+                        Par entreprise
+                      </button>
+                    </div>
+                    {yearMode === "global" ? (
+                      <div style={styles.yearGrid}>
+                        {YEARS.map((y) => (
+                          <button
+                            key={y}
+                            style={{
+                              ...styles.yearChip,
+                              ...(globalYear === y ? styles.yearChipActive : {}),
+                            }}
+                            onClick={() => setGlobalYear(y)}
+                          >
+                            {y}
+                          </button>
+                        ))}
+                      </div>
+                    ) : (
+                      <div style={styles.perCompanyList}>
+                        {selectedCompanyObjects.map((c) => (
+                          <div key={c.id} style={styles.perCompanyRow}>
+                            <span style={styles.perCompanyName}>{c.name}</span>
+                            <div style={styles.miniYearRow}>
+                              {YEARS.map((y) => (
+                                <button
+                                  key={y}
+                                  style={{
+                                    ...styles.miniYearChip,
+                                    ...((selectedYears[c.id] || CURRENT_YEAR) === y
+                                      ? styles.miniYearChipActive
+                                      : {}),
+                                  }}
+                                  onClick={() => setCompanyYear(c.id, y)}
+                                >
+                                  {y}
+                                </button>
+                              ))}
+                            </div>
+                          </div>
+                        ))}
+                      </div>
+                    )}
+                  </section>
+                )}
+              </div>
+
+              <div style={styles.rightCol}>
+                {/* 03. SECTIONS */}
+                {selectedCompanies.length > 0 && (
+                  <section style={styles.card}>
+                    <div style={styles.cardHeader}>
+                      <span style={styles.cardBadge}>03</span>
+                      <h2 style={styles.cardTitle}>Sections SFCR</h2>
+                    </div>
+                    <div style={styles.modeToggle}>
+                      <button
+                        style={{
+                          ...styles.modeBtn,
+                          ...(sectionMode === "global"
+                            ? styles.modeBtnActive
+                            : {}),
+                        }}
+                        onClick={() => setSectionMode("global")}
+                      >
+                        Toutes
+                      </button>
+                      <button
+                        style={{
+                          ...styles.modeBtn,
+                          ...(sectionMode === "individual"
+                            ? styles.modeBtnActive
+                            : {}),
+                        }}
+                        onClick={() => setSectionMode("individual")}
+                      >
+                        Par entreprise
+                      </button>
+                    </div>
+                    {sectionMode === "global" ? (
+                      <div style={styles.sectionGrid}>
+                        {SFCR_SECTIONS.map((s) => (
+                          <button
+                            key={s}
+                            onClick={() => toggleGlobalSection(s)}
+                            style={{
+                              ...styles.sectionChip,
+                              ...(globalSections.includes(s)
+                                ? styles.sectionChipActive
+                                : {}),
+                            }}
+                          >
+                            <span style={styles.sectionCode}>{s}</span>
+                            <span style={styles.sectionLabel}>
+                              {sectionLabels[s]}
+                            </span>
+                          </button>
+                        ))}
+                      </div>
+                    ) : (
+                      <div style={styles.perCompanyList}>
+                        {selectedCompanyObjects.map((c) => (
+                          <div key={c.id} style={styles.perCompanyBlock}>
+                            <div style={styles.perCompanyHeader}>
+                              <span style={styles.perCompanyName}>{c.name}</span>
+                            </div>
+                            <div style={styles.miniSectionGrid}>
+                              {SFCR_SECTIONS.map((s) => (
+                                <button
+                                  key={s}
+                                  onClick={() => toggleCompanySection(c.id, s)}
+                                  style={{
+                                    ...styles.miniSectionChip,
+                                    ...(selectedSections[c.id]?.includes(s)
+                                      ? styles.miniSectionChipActive
+                                      : {}),
+                                  }}
+                                >
+                                  {s}
+                                </button>
+                              ))}
+                            </div>
+                          </div>
+                        ))}
+                      </div>
+                    )}
+                  </section>
+                )}
+
+                {/* 02. PROMPT */}
+                <section style={styles.card}>
+                  <div style={styles.cardHeader}>
+                    <span style={styles.cardBadge}>02</span>
+                    <h2 style={styles.cardTitle}>Prompt d'extraction</h2>
+                  </div>
+                  <div style={styles.promptWrapper}>
+                    <textarea
+                      style={styles.promptInput}
+                      placeholder="Instructions spécifiques..."
+                      value={prompt}
+                      onChange={(e) => setPrompt(e.target.value)}
+                      rows={4}
+                    />
+                  </div>
+                </section>
+
+                {/* RECAPITULATIF */}
+                {selectedCompanies.length > 0 && (
+                  <section style={styles.summaryCard}>
+                    <h3 style={styles.summaryTitle}>Récapitulatif</h3>
+                    <div style={styles.summaryGrid}>
+                      <div style={styles.summaryItem}>
+                        <span style={styles.summaryNum}>
+                          {selectedCompanies.length}
+                        </span>
+                        <span style={styles.summaryItemLabel}>Entreprise(s)</span>
+                      </div>
+                      <div style={styles.summaryItem}>
+                        <span style={styles.summaryNum}>
+                          {sectionMode === "global"
+                            ? globalSections.length
+                            : "Varié"}
+                        </span>
+                        <span style={styles.summaryItemLabel}>Section(s)</span>
+                      </div>
+                    </div>
+                  </section>
+                )}
+              </div>
+            </div>
+          </div>
+        )}
+      </main>
+    </div>
+  );
+}

+ 0 - 0
04 - Scripts/sfcr-app/src/assets/hero.png → 03 - Scripts/sfcr-app/src/assets/hero.png


+ 0 - 0
04 - Scripts/sfcr-app/src/assets/react.svg → 03 - Scripts/sfcr-app/src/assets/react.svg


+ 0 - 0
04 - Scripts/sfcr-app/src/assets/vite.svg → 03 - Scripts/sfcr-app/src/assets/vite.svg


+ 0 - 0
04 - Scripts/sfcr-app/src/constants.js → 03 - Scripts/sfcr-app/src/constants.js


+ 0 - 0
04 - Scripts/sfcr-app/src/index.css → 03 - Scripts/sfcr-app/src/index.css


+ 0 - 0
04 - Scripts/sfcr-app/src/main.jsx → 03 - Scripts/sfcr-app/src/main.jsx


+ 0 - 0
04 - Scripts/sfcr-app/src/styles.js → 03 - Scripts/sfcr-app/src/styles.js


+ 0 - 0
04 - Scripts/sfcr-app/vite.config.js → 03 - Scripts/sfcr-app/vite.config.js


+ 0 - 0
04 - Scripts/test_luncher.py → 03 - Scripts/test_luncher.py


+ 0 - 0
04 - Scripts/tools.py → 03 - Scripts/tools.py


+ 2 - 2
04 - Scripts/workflow_agents.py → 03 - Scripts/workflow_agents.py

@@ -36,9 +36,9 @@ workflow.add_edge("tools",END)
 app = workflow.compile() 
 
 # 6. Graph
-try:
+"""try:
     with open("graph_workflow.png", "wb") as f:
         f.write(app.get_graph().draw_mermaid_png())
     print(" Graphique du workflow généré sous : graph_workflow.png")
 except Exception as e:
-    print(f" Erreur génération image : {e}")
+    print(f" Erreur génération image : {e}")"""

BIN
04 - Outputs/2025/CNP_Assurances/Rapport_S.02_page_83.xlsx


BIN
04 - Outputs/2025/CNP_Assurances/Rapport_S.02_page_84.xlsx


BIN
04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_88.xlsx


BIN
04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_89.xlsx


BIN
04 - Outputs/2025/CNP_Assurances/Rapport_S.12_page_90.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.02_page_48.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.02_page_49.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.05_page_50.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.05_page_51.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.22_page_55.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.25_page_58.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/Rapport_S.28_page_59.xlsx


BIN
04 - Outputs/2025/Caisse_Générale_de_Prévoyance_(CGP)/~$Rapport_S.28_page_59.xlsx


+ 0 - 234
04 - Scripts/Agents.py

@@ -1,234 +0,0 @@
-import os 
-from typing import Annotated , Sequence , TypedDict , Optional , List , Any
-from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
-from langgraph.graph.message import add_messages
-from dotenv import load_dotenv  
-from langchain_openai import ChatOpenAI 
-from langchain_groq import ChatGroq 
-import pandas as pd
-from langfuse import get_client
-import json
-
-from tools import  excel_code_interpreter
-
-from paddleocr import PaddleOCR
-
-from function import (
-    preparer_image_zoom_hd, 
-    extraire_donnees_ocr, 
-    nettoyage_sortie_ocr, 
-    formater_donnees_section,
-    sauvegarder_fichier_unique,
-    to_points , 
-    build_lines,
-    cluster_lines , 
-    merge_close_lines
-
-)
-
-from clean_DBSCAN import (
-    transform_to_clean_markdown
-)
-
-
-
-load_dotenv() 
-
-
-tools = [excel_code_interpreter]
-
-
-class AgentState(TypedDict) : 
-    messages : Annotated[Sequence[BaseMessage],add_messages]
-    pdf_path : str 
-    pages : List[int]
-    entreprise_name : str 
-    section_name : str
-    markdown : str
-
-
-model_llama = ChatGroq(model="llama-3.3-70b-versatile") 
-
-model_openai = ChatOpenAI(model="gpt-4.1" , temperature=0.2) 
-
-model_ai = model_llama.bind_tools(tools)
-
-ocr = PaddleOCR(use_angle_cls=True, lang='fr', det_limit_side_len=10000, show_log=False)
-
-
-
-def agent_ocr(state: AgentState):
-    pdf_path = state.get("pdf_path")
-    pages = state.get("pages")
-
-    
-
-    texte_accumule = []
-    all_points = []
-
-    for page_index in pages:
-
-        page_index = page_index -1
-        
-        try:
-            img_finale = preparer_image_zoom_hd(pdf_path, page_index)
-            raw_data = extraire_donnees_ocr(img_finale, ocr)
-
-            data_propre = nettoyage_sortie_ocr(raw_data)
-
-            # 🔥 garder structure brute
-            points = to_points(data_propre)
-            all_points.extend(points)
-
-            # debug texte
-            lignes_page = formater_donnees_section(data_propre, page_index)
-            texte_accumule.extend(lignes_page)
-
-            print(f"✅ Page {page_index + 1} traitée.")
-
-        except Exception as e:
-            print(f"❌ Erreur Page {page_index + 1}: {e}")
-
-    # 🔥 DBSCAN GLOBAL (beaucoup plus robuste)
-    clusters = cluster_lines(all_points, eps=0.5)
-    lignes = build_lines(clusters)
-    lignes = merge_close_lines(lignes)
-
-    # 🔥 TEXTE pour LLM
-    contenu_final = "\n".join(texte_accumule)
-
-    message_ocr = HumanMessage(
-        content=f"Voici les données OCR brutes :\n{contenu_final}"
-    )
-  
-    markdown = transform_to_clean_markdown(lignes)
-    
-    
-
-    return {
-        "messages": [message_ocr],
-        "lignes" : lignes,
-        "markdown": markdown 
-    } 
-
-
-
-
-def agent_extracteur(state:AgentState) : 
-
-    prompt = """
-    Tu es un extracteur de tableaux QRT (Solvabilité II).
-
-    Ton but est de transcrire les coordonnées spatiales OCR en une hiérarchie logique de données.
-
-    Objectif : convertir le tableau en JSON sans perte d’information.
-
-    Règles :
-    - Utilise Rxxxx (lignes) et Cxxxx (colonnes) comme clés.
-    - Copie STRICTEMENT les valeurs, sans calcul ni modification.
-    - Supprime uniquement les espaces dans les nombres :
-    "3 297 388" → 3297388
-    - Ne tronque pas, n’arrondis pas, garde toute la précision.
-    - Garde les 0.
-    - Si pas de libellé pour un Rxxxx → "".
-    - Optimise la structure (par lignes ou colonnes) pour réduire la taille.
-
-    ⚠️ CONTRAINTE DE SORTIE (OBLIGATOIRE) :
-    - Réponds UNIQUEMENT avec un objet JSON valide
-    - NE PAS ajouter de texte avant ou après
-    - NE PAS utiliser ```json ou ```
-    - NE PAS commenter
-    - NE PAS expliquer
-
-    Sortie attendue (exemple strict) :
-    {
-    "labels": { "Rxxxx": "Rxxxx" },
-    "data": {
-        "Cxxxx": { "Rxxxx": 123456 }
-    }
-    }
-    """
-
-    msg = [
-    SystemMessage(content=prompt),
-    HumanMessage(content=json.dumps(state["markdown"]))
-     ]
-
-    response = model_llama.invoke(msg)
-
-    return{"messages" : [response]}
-
-
-
-
-
-def agent_builder(state: AgentState):
-    """
-    Agent Builder (Version non-IA) : 
-    Prend le JSON de l'extracteur et génère physiquement le fichier Excel.
-    """
-    print(f"🏗️ Construction du fichier Excel pour : {state['entreprise_name']}...")
-
-    try:
-        # 1. Récupération du JSON depuis le dernier message de l'extracteur
-        # On suppose que l'extracteur a renvoyé un dictionnaire ou une string JSON
-        import json
-        last_message_content = state["messages"][-1].content
-        
-        # Nettoyage si le LLM a mis des balises ```json
-        if "```json" in last_message_content:
-            last_message_content = last_message_content.split("```json")[1].split("```")[0].strip()
-        
-        payload = json.loads(last_message_content)
-        
-        labels = payload.get("labels", {})
-        data_json = payload.get("data", {})
-
-        # 2. Préparation de la structure du dossier
-        folder = state['entreprise_name'].replace(" ", "_")
-        if not os.path.exists(folder):
-            os.makedirs(folder)
-        
-        output_file = os.path.join(folder, f"Rapport_{state['section_name']}.xlsx")
-
-        # 3. Construction du DataFrame
-        # On récupère toutes les colonnes et toutes les lignes uniques
-        all_rows = list(labels.keys())
-        all_cols = list(data_json.keys())
-
-        # Création d'une matrice vide remplie de NaN
-        df = pd.DataFrame(index=all_rows, columns=all_cols)
-
-        # Remplissage des données
-        for col, row_values in data_json.items():
-            for row, val in row_values.items():
-                df.at[row, col] = val
-
-        # 4. Ajout de la colonne Libellé au début
-        #df.insert(0, "Libellé", df.index.map(labels))
-
-        # 5. Exportation avec formatage pro via XlsxWriter
-        with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
-            df.to_excel(writer, sheet_name="QRT_Export", index=True)
-            
-            workbook  = writer.book
-            worksheet = writer.sheets["QRT_Export"]
-
-            # --- Ajout de formats ---
-            header_fmt = workbook.add_format({'bold': True, 'bg_color': '#1F3864', 'font_color': 'white', 'border': 1})
-            num_fmt    = workbook.add_format({'num_format': '#,##0', 'border': 1})
-            label_fmt  = workbook.add_format({'bg_color': '#D6E4F0', 'border': 1})
-
-            # Appliquer les formats aux colonnes
-            worksheet.set_column(1, 1, 60) # Libellé
-            worksheet.set_column(0, 0, 15) # Code R
-            
-            worksheet.set_column(2, len(df.columns)+1, 18, num_fmt) # Valeurs
-
-        success_msg = f"✓ Fichier Excel généré : {output_file} ({len(df)} lignes x {len(all_cols)} colonnes)"
-        return {"messages": [HumanMessage(content=success_msg)]}
-
-    except Exception as e:
-        error_msg = f"❌ Erreur lors de la construction Excel : {str(e)}"
-        print(error_msg)
-        return {"messages": [HumanMessage(content=error_msg)]}

BIN
04 - Scripts/Caisse_Générale_de_Prévoyance_(CGP)_2025/Rapport_S.02.xlsx


BIN
04 - Scripts/Caisse_Générale_de_Prévoyance_(CGP)_2025/Rapport_S.05.xlsx


+ 0 - 72
04 - Scripts/server.py

@@ -1,72 +0,0 @@
-from flask import Flask, request, jsonify
-from flask_cors import CORS
-import subprocess
-import json
-import re
-import os
-
-app = Flask(__name__)
-CORS(app)
-
-import sys # Ajoute cet import en haut de ton fichier
-
-@app.route("/run-extraction", methods=["POST"])
-def run_extraction():
-    data = request.json
-    inputs = data.get("inputs", {})
-
-    script_dir = os.path.dirname(os.path.abspath(__file__))
-    main_path = os.path.join(script_dir, "main.py")
-
-    # 1. Mise à jour du fichier main.py
-    try:
-        with open(main_path, "r", encoding="utf-8") as f:
-            content = f.read()
-
-        # Nettoyage de l'ancien bloc INPUTS (ton regex/logique actuelle)
-        start_idx = content.find("INPUTS = {")
-        if start_idx != -1:
-            brace_count = 0
-            end_idx = start_idx
-            for i, char in enumerate(content[start_idx:]):
-                if char == "{": brace_count += 1
-                elif char == "}":
-                    brace_count -= 1
-                    if brace_count == 0:
-                        end_idx = start_idx + i + 1
-                        break
-            content = content[:start_idx] + content[end_idx:].lstrip()
-
-        # Insertion des nouveaux inputs
-        new_inputs_str = f"INPUTS = {json.dumps(inputs, indent=4, ensure_ascii=False)}\n\n"
-        content = new_inputs_str + content
-
-        with open(main_path, "w", encoding="utf-8") as f:
-            f.write(content)
-            f.flush() # Force l'écriture sur le disque
-            os.fsync(f.fileno()) # Sécurité supplémentaire pour Linux/Mac
-            
-    except Exception as e:
-        return jsonify({"stdout": "", "stderr": f"Erreur écriture : {str(e)}", "returncode": -1})
-
-    # 2. Exécution du script
-    try:
-        # Utilisation de sys.executable pour éviter les erreurs de chemin Python
-        result = subprocess.run(
-            [sys.executable, main_path], 
-            capture_output=True,
-            text=True,
-            cwd=script_dir,
-            timeout=60
-        )
-        
-        return jsonify({
-            "stdout": result.stdout,
-            "stderr": result.stderr,
-            "returncode": result.returncode
-        })
-
-    except subprocess.TimeoutExpired:
-        return jsonify({"stdout": "", "stderr": "Timeout dépassé (60s)", "returncode": -1})
-    except Exception as e:
-        return jsonify({"stdout": "", "stderr": f"Erreur exécution : {str(e)}", "returncode": -1})

+ 0 - 457
04 - Scripts/sfcr-app/src/App.jsx

@@ -1,457 +0,0 @@
-import { useState, useMemo } from "react";
-import { ALL_COMPANIES, SFCR_SECTIONS, YEARS, CURRENT_YEAR, sectionLabels } from "./constants";
-import { styles, cssString } from "./styles";
-import { C } from "./styles"; // Import des couleurs pour usage direct si besoin
-
-// --- COMPOSANT DE SUIVI (Externe à App pour la performance) ---
-const ExtractionStatusView = ({ selectedCompanies, globalSections, onClose }) => {
-  return (
-    <div style={styles.extractionWrapper}>
-      <div style={styles.extractionHeader}>
-        <div>
-          <h2 style={styles.pageTitle}>Analyse Multi-Agents</h2>
-          <p style={styles.pageSubtitle}>Traitement des rapports Solvabilité II en cours...</p>
-        </div>
-        <button onClick={onClose} style={styles.showMoreBtn}>Interrompre l'analyse</button>
-      </div>
-
-      <div style={styles.extractionList}>
-        {selectedCompanies.map((company) => (
-          <div key={company.id} style={styles.extractionCard}>
-            <div style={styles.companyStatusHeader}>
-              <div style={styles.companyAvatar}>{company.logo}</div>
-              <div style={{ flex: 1 }}>
-                <h4 style={{ margin: 0, fontSize: '14px', color: '#e8ecf4' }}>{company.name}</h4>
-                <span style={{ fontSize: '11px', color: C.accent }}>
-                  Extraction en cours... <span className="loader-dots"></span>
-                </span>
-              </div>
-            </div>
-
-            <div style={styles.stepsTimeline}>
-              <div style={styles.stepItemActive}>
-                <span style={styles.stepIcon}>📄</span>
-                <span>Fichier PDF chargé</span>
-              </div>
-              
-              {globalSections.length > 0 ? (
-                globalSections.map(section => (
-                  <div key={section} style={styles.stepItemPending}>
-                    <span style={styles.stepIcon}>⚙️</span>
-                    <span>Analyse {section}</span>
-                  </div>
-                ))
-              ) : (
-                <div style={styles.stepItemPending}>
-                  <span style={styles.stepIcon}>⚙️</span>
-                  <span>Analyse automatique</span>
-                </div>
-              )}
-
-              <div style={styles.stepItemPending}>
-                <span style={styles.stepIcon}>✅</span>
-                <span>Finalisation</span>
-              </div>
-            </div>
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-};
-
-// --- COMPOSANT PRINCIPAL ---
-export default function App() {
-  const [activeNav, setActiveNav] = useState("extraction");
-  const [search, setSearch] = useState("");
-  const [showAll, setShowAll] = useState(false);
-  const [selectedCompanies, setSelectedCompanies] = useState([]);
-  const [selectedSections, setSelectedSections] = useState({});
-  const [selectedYears, setSelectedYears] = useState({});
-  const [globalSections, setGlobalSections] = useState([]);
-  const [globalYear, setGlobalYear] = useState(CURRENT_YEAR);
-  const [sectionMode, setSectionMode] = useState("global");
-  const [yearMode, setYearMode] = useState("global");
-  const [prompt, setPrompt] = useState("");
-  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
-  
-  // État pour basculer l'interface
-  const [isExtracting, setIsExtracting] = useState(false);
-
-  const filtered = useMemo(() => {
-    const base = ALL_COMPANIES.filter((c) =>
-      c.name.toLowerCase().includes(search.toLowerCase())
-    );
-    return showAll ? base : base.slice(0, 5);
-  }, [search, showAll]);
-
-  const toggleCompany = (id) => {
-    setSelectedCompanies((prev) =>
-      prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
-    );
-  };
-
-  const toggleAll = () => {
-    if (selectedCompanies.length === filtered.length) {
-      setSelectedCompanies([]);
-    } else {
-      setSelectedCompanies(filtered.map((c) => c.id));
-    }
-  };
-
-  const toggleGlobalSection = (s) => {
-    setGlobalSections((prev) =>
-      prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]
-    );
-  };
-
-  const toggleCompanySection = (companyId, s) => {
-    setSelectedSections((prev) => {
-      const cur = prev[companyId] || [];
-      return {
-        ...prev,
-        [companyId]: cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s],
-      };
-    });
-  };
-
-  const setCompanyYear = (companyId, year) => {
-    setSelectedYears((prev) => ({ ...prev, [companyId]: year }));
-  };
-
-  const selectedCompanyObjects = useMemo(() => 
-    ALL_COMPANIES.filter((c) => selectedCompanies.includes(c.id)),
-    [selectedCompanies]
-  );
-
-  const handleExtraction = async () => {
-    if (selectedCompanies.length === 0) {
-      alert("Veuillez sélectionner au moins une entreprise.");
-      return;
-    }
-
-    // 1. Afficher immédiatement l'interface d'extraction
-    setIsExtracting(true);
-
-    const inputs = {};
-    selectedCompanyObjects.forEach((company) => {
-      const sections = sectionMode === "global"
-        ? globalSections
-        : (selectedSections[company.id] || []);
-
-      const year = yearMode === "global"
-        ? globalYear
-        : (selectedYears[company.id] || CURRENT_YEAR);
-
-      if (sections.length > 0) {
-        const filename = `${company.name}_${year}.pdf`;
-        inputs[filename] = {};
-        sections.forEach((s) => {
-          inputs[filename][s] = [];
-        });
-      }
-    });
-
-    try {
-      const response = await fetch("http://localhost:5000/run-extraction", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify({ inputs }),
-      });
-
-      const result = await response.json();
-      console.log("Résultat:", result);
-      
-      // On garde l'interface affichée mais on prévient que c'est fini
-      alert("Extraction terminée avec succès !");
-      // setIsExtracting(false); // Décommenter si tu veux revenir au menu automatiquement
-    } catch (err) {
-      setIsExtracting(false);
-      alert("Erreur : impossible de contacter le serveur Python.");
-    }
-  };
-
-  return (
-    <div style={styles.root}>
-      <style>{cssString}</style>
-
-      {/* Sidebar - Toujours présente */}
-      <aside style={{ ...styles.sidebar, width: sidebarCollapsed ? 64 : 220 }}>
-        <div style={styles.sidebarHeader}>
-          {!sidebarCollapsed && (
-            <span style={styles.brandText}>
-              <span style={styles.brandAccent}>SFCR</span>
-              <span style={styles.brandSub}>·extract</span>
-            </span>
-          )}
-          <button onClick={() => setSidebarCollapsed((v) => !v)} style={styles.collapseBtn}>
-            {sidebarCollapsed ? "›" : "‹"}
-          </button>
-        </div>
-
-        <nav style={styles.nav}>
-          {[
-            { id: "search", icon: "⊕", label: "Search Sources" },
-            { id: "extraction", icon: "⊞", label: "Extraction" },
-          ].map((item) => (
-            <button
-              key={item.id}
-              onClick={() => {
-                setActiveNav(item.id);
-                if (item.id === "search") setIsExtracting(false); // Reset si on change d'onglet
-              }}
-              style={{
-                ...styles.navBtn,
-                ...(activeNav === item.id ? styles.navBtnActive : {}),
-              }}
-              className="nav-btn"
-            >
-              <span style={styles.navIcon}>{item.icon}</span>
-              {!sidebarCollapsed && <span style={styles.navLabel}>{item.label}</span>}
-            </button>
-          ))}
-        </nav>
-      </aside>
-
-      {/* Main Content Area */}
-      <main style={styles.main}>
-        {activeNav === "search" ? (
-          <div style={styles.placeholderPanel}>
-            <div style={styles.placeholderIcon}>⊕</div>
-            <h2 style={styles.placeholderTitle}>Search Sources</h2>
-            <p style={styles.placeholderDesc}>Section dédiée à la recherche de sources SFCR.</p>
-          </div>
-        ) : isExtracting ? (
-          // --- VUE SUIVI (Affiche les agents en action) ---
-          <ExtractionStatusView 
-            selectedCompanies={selectedCompanyObjects}
-            globalSections={globalSections}
-            onClose={() => setIsExtracting(false)}
-          />
-        ) : (
-          // --- VUE CONFIGURATION (Interface normale) ---
-          <div style={styles.extractionPanel}>
-            <div style={styles.pageHeader}>
-              <div>
-                <h1 style={styles.pageTitle}>Extraction SFCR</h1>
-                <p style={styles.pageSubtitle}>Sélectionnez les entreprises et paramétrez l'extraction</p>
-              </div>
-              <button style={styles.extractBtn} className="extract-btn" onClick={handleExtraction}>
-                <span>▶</span> Lancer l'extraction
-              </button>
-            </div>
-
-            <div style={styles.twoCol}>
-              <div style={styles.leftCol}>
-                {/* 01. SELECTION ENTREPRISES */}
-               <section style={styles.card}>
-                  <div style={styles.cardHeader}>
-                    <span style={styles.cardBadge}>01</span>
-                    <h2 style={styles.cardTitle}>Sélection des entreprises</h2>
-                    {selectedCompanies.length > 0 && (
-                      <span style={styles.countPill}>
-                        {selectedCompanies.length} sélectionné
-                        {selectedCompanies.length > 1 ? "s" : ""}
-                      </span>
-                    )}
-                  </div>
-
-                  <div style={styles.searchRow}>
-                    <span style={styles.searchIcon}>🔍</span>
-                    <input
-                      style={styles.searchInput}
-                      placeholder="Rechercher une entreprise…"
-                      value={search}
-                      onChange={(e) => setSearch(e.target.value)}
-                    />
-                  </div>
-
-                  <div style={styles.selectAllRow}>
-                    <label style={styles.checkLabel} className="check-label">
-                      <input
-                        type="checkbox"
-                        checked={
-                          filtered.length > 0 &&
-                          filtered.every((c) => selectedCompanies.includes(c.id))
-                        }
-                        onChange={toggleAll}
-                        style={styles.hiddenCheck}
-                      />
-                      <span
-                        style={{
-                          ...styles.customCheck,
-                          ...(filtered.length > 0 &&
-                          filtered.every((c) => selectedCompanies.includes(c.id))
-                            ? styles.customCheckChecked
-                            : {}),
-                        }}
-                      >
-                        {filtered.length > 0 &&
-                          filtered.every((c) => selectedCompanies.includes(c.id)) &&
-                          "✓"}
-                      </span>
-                      <span style={styles.checkLabelText}>Tout sélectionner</span>
-                    </label>
-                  </div>
-
-                  {/* --- ZONE DE SCROLL AJOUTÉE ICI --- */}
-                  <div 
-                    style={{ 
-                      maxHeight: "300px", // Tu peux ajuster la hauteur (ex: 250px ou 400px)
-                      overflowY: "auto", 
-                      paddingRight: "5px" // Petit espace pour ne pas coller à la barre de scroll
-                    }}
-                  >
-                    <div style={styles.companyList}>
-                      {filtered.map((company) => {
-                        const checked = selectedCompanies.includes(company.id);
-                        return (
-                          <label
-                            key={company.id}
-                            style={{
-                              ...styles.companyRow,
-                              ...(checked ? styles.companyRowChecked : {}),
-                            }}
-                            className="company-row"
-                          >
-                            <input
-                              type="checkbox"
-                              checked={checked}
-                              onChange={() => toggleCompany(company.id)}
-                              style={styles.hiddenCheck}
-                            />
-                            <span style={styles.companyAvatar}>{company.logo}</span>
-                            <span style={styles.companyName}>{company.name}</span>
-                            <span
-                              style={{
-                                ...styles.customCheck,
-                                ...(checked ? styles.customCheckChecked : {}),
-                              }}
-                            >
-                              {checked && "✓"}
-                            </span>
-                          </label>
-                        );
-                      })}
-                    </div>
-                  </div>
-                  {/* --- FIN DE LA ZONE DE SCROLL --- */}
-
-                  {!showAll && ALL_COMPANIES.length > 10 && (
-                    <button
-                      style={styles.showMoreBtn}
-                      onClick={() => setShowAll(true)}
-                    >
-                      Afficher plus ({ALL_COMPANIES.length - 10} entreprises)
-                    </button>
-                  )}
-                </section>
-
-                {/* 04. ANNEE */}
-                {selectedCompanies.length > 0 && (
-                  <section style={styles.card}>
-                    <div style={styles.cardHeader}>
-                      <span style={styles.cardBadge}>04</span>
-                      <h2 style={styles.cardTitle}>Année de rapport</h2>
-                    </div>
-                    <div style={styles.modeToggle}>
-                      <button style={{...styles.modeBtn, ...(yearMode === "global" ? styles.modeBtnActive : {})}} onClick={() => setYearMode("global")}>Toutes</button>
-                      <button style={{...styles.modeBtn, ...(yearMode === "individual" ? styles.modeBtnActive : {})}} onClick={() => setYearMode("individual")}>Par entreprise</button>
-                    </div>
-                    {yearMode === "global" ? (
-                      <div style={styles.yearGrid}>
-                        {YEARS.map(y => (
-                          <button key={y} style={{...styles.yearChip, ...(globalYear === y ? styles.yearChipActive : {})}} onClick={() => setGlobalYear(y)}>{y}</button>
-                        ))}
-                      </div>
-                    ) : (
-                      <div style={styles.perCompanyList}>
-                        {selectedCompanyObjects.map(c => (
-                          <div key={c.id} style={styles.perCompanyRow}>
-                            <span style={styles.perCompanyName}>{c.name}</span>
-                            <div style={styles.miniYearRow}>
-                              {YEARS.map(y => (
-                                <button key={y} style={{...styles.miniYearChip, ...((selectedYears[c.id] || CURRENT_YEAR) === y ? styles.miniYearChipActive : {})}} onClick={() => setCompanyYear(c.id, y)}>{y}</button>
-                              ))}
-                            </div>
-                          </div>
-                        ))}
-                      </div>
-                    )}
-                  </section>
-                )}
-              </div>
-
-              <div style={styles.rightCol}>
-                {/* 03. SECTIONS */}
-                {selectedCompanies.length > 0 && (
-                  <section style={styles.card}>
-                    <div style={styles.cardHeader}>
-                      <span style={styles.cardBadge}>03</span>
-                      <h2 style={styles.cardTitle}>Sections SFCR</h2>
-                    </div>
-                    <div style={styles.modeToggle}>
-                      <button style={{...styles.modeBtn, ...(sectionMode === "global" ? styles.modeBtnActive : {})}} onClick={() => setSectionMode("global")}>Toutes</button>
-                      <button style={{...styles.modeBtn, ...(sectionMode === "individual" ? styles.modeBtnActive : {})}} onClick={() => setSectionMode("individual")}>Par entreprise</button>
-                    </div>
-                    {sectionMode === "global" ? (
-                      <div style={styles.sectionGrid}>
-                        {SFCR_SECTIONS.map(s => (
-                          <button key={s} onClick={() => toggleGlobalSection(s)} style={{...styles.sectionChip, ...(globalSections.includes(s) ? styles.sectionChipActive : {})}}>
-                            <span style={styles.sectionCode}>{s}</span>
-                            <span style={styles.sectionLabel}>{sectionLabels[s]}</span>
-                          </button>
-                        ))}
-                      </div>
-                    ) : (
-                      <div style={styles.perCompanyList}>
-                        {selectedCompanyObjects.map(c => (
-                          <div key={c.id} style={styles.perCompanyBlock}>
-                            <div style={styles.perCompanyHeader}><span style={styles.perCompanyName}>{c.name}</span></div>
-                            <div style={styles.miniSectionGrid}>
-                              {SFCR_SECTIONS.map(s => (
-                                <button key={s} onClick={() => toggleCompanySection(c.id, s)} style={{...styles.miniSectionChip, ...(selectedSections[c.id]?.includes(s) ? styles.miniSectionChipActive : {})}}>{s}</button>
-                              ))}
-                            </div>
-                          </div>
-                        ))}
-                      </div>
-                    )}
-                  </section>
-                )}
-
-                {/* 02. PROMPT */}
-                <section style={styles.card}>
-                  <div style={styles.cardHeader}>
-                    <span style={styles.cardBadge}>02</span>
-                    <h2 style={styles.cardTitle}>Prompt d'extraction</h2>
-                  </div>
-                  <div style={styles.promptWrapper}>
-                    <textarea style={styles.promptInput} placeholder="Instructions spécifiques..." value={prompt} onChange={(e) => setPrompt(e.target.value)} rows={4} />
-                  </div>
-                </section>
-
-                {/* RECAPITULATIF */}
-                {selectedCompanies.length > 0 && (
-                  <section style={styles.summaryCard}>
-                    <h3 style={styles.summaryTitle}>Récapitulatif</h3>
-                    <div style={styles.summaryGrid}>
-                      <div style={styles.summaryItem}>
-                        <span style={styles.summaryNum}>{selectedCompanies.length}</span>
-                        <span style={styles.summaryItemLabel}>Entreprise(s)</span>
-                      </div>
-                      <div style={styles.summaryItem}>
-                        <span style={styles.summaryNum}>{sectionMode === "global" ? globalSections.length : "Varié"}</span>
-                        <span style={styles.summaryItemLabel}>Section(s)</span>
-                      </div>
-                    </div>
-                  </section>
-                )}
-              </div>
-            </div>
-          </div>
-        )}
-      </main>
-    </div>
-  );
-}