|
@@ -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 }}>></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>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|