|
|
import streamlit as st |
|
|
from PyPDF2 import PdfReader |
|
|
import pandas as pd |
|
|
from groq import Groq |
|
|
from referentials import REFERENTIALS |
|
|
import json |
|
|
import re |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
layout="wide", |
|
|
page_title="Analyse de CV - GFSI (Version 25.12)", |
|
|
page_icon="📄", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
def get_groq_client(api_key): |
|
|
""" |
|
|
Initialise et valide un client Groq avec la clé API fournie. |
|
|
|
|
|
Args: |
|
|
api_key (str): Clé API Groq |
|
|
|
|
|
Returns: |
|
|
Groq: Instance de client Groq ou None en cas d'erreur |
|
|
""" |
|
|
if not api_key or not api_key.startswith("gsk_"): |
|
|
st.error("La clé API semble invalide. Les clés Groq commencent généralement par 'gsk_'.") |
|
|
return None |
|
|
|
|
|
try: |
|
|
client = Groq(api_key=api_key) |
|
|
|
|
|
test_response = client.chat.completions.create( |
|
|
messages=[{"role": "user", "content": "Test de connexion"}], |
|
|
model="llama-3.1-8b-instant", |
|
|
max_tokens=10 |
|
|
) |
|
|
if test_response: |
|
|
return client |
|
|
except Exception as e: |
|
|
st.error(f"Erreur de connexion à l'API Groq : {str(e)}") |
|
|
return None |
|
|
|
|
|
|
|
|
def extract_text_from_pdf(file): |
|
|
""" |
|
|
Extrait le texte d'un fichier PDF avec gestion optimisée des erreurs. |
|
|
|
|
|
Args: |
|
|
file (UploadedFile): Fichier PDF téléchargé |
|
|
|
|
|
Returns: |
|
|
str: Texte extrait du PDF ou None en cas d'erreur |
|
|
""" |
|
|
try: |
|
|
with st.spinner("Extraction du texte en cours..."): |
|
|
reader = PdfReader(file) |
|
|
|
|
|
|
|
|
if len(reader.pages) == 0: |
|
|
st.warning("Le PDF ne contient aucune page.") |
|
|
return None |
|
|
|
|
|
|
|
|
text_parts = [] |
|
|
for page in reader.pages: |
|
|
page_text = page.extract_text() |
|
|
if page_text: |
|
|
|
|
|
page_text = re.sub(r'\s+', ' ', page_text) |
|
|
text_parts.append(page_text) |
|
|
|
|
|
if not text_parts: |
|
|
st.warning("Aucun texte n'a pu être extrait du PDF. Vérifiez qu'il ne s'agit pas d'un PDF scanné.") |
|
|
return None |
|
|
|
|
|
return " ".join(text_parts) |
|
|
except Exception as e: |
|
|
st.error(f"Erreur lors de l'extraction du texte : {str(e)}") |
|
|
st.info("Conseils: Vérifiez que le PDF n'est pas protégé ou qu'il ne s'agit pas d'un document scanné sans OCR.") |
|
|
return None |
|
|
|
|
|
|
|
|
def analyze_cv_with_groq(cv_text, referential, groq_client, model="openai/gpt-oss-120b"): |
|
|
""" |
|
|
Analyse approfondie du CV en fonction des référentiels GFSI avec références systématiques. |
|
|
|
|
|
Args: |
|
|
cv_text (str): Texte du CV à analyser |
|
|
referential (str): Nom du référentiel GFSI sélectionné |
|
|
groq_client (Groq): Instance de client Groq |
|
|
model (str): Modèle LLM à utiliser |
|
|
|
|
|
Returns: |
|
|
dict: Résultats structurés de l'analyse ou None en cas d'erreur |
|
|
""" |
|
|
referential_data = REFERENTIALS.get(referential, {}) |
|
|
|
|
|
|
|
|
requirements_categories = { |
|
|
"General_Requirements": referential_data.get("General_Requirements", {}), |
|
|
"Qualifications": referential_data.get("Qualifications", {}), |
|
|
"Audit_Experience": referential_data.get("Audit_Experience", {}), |
|
|
"Advanced_Requirements": referential_data.get("Advanced_Requirements", {}) |
|
|
} |
|
|
|
|
|
|
|
|
prompt = f""" |
|
|
Vous êtes un expert en évaluation des compétences selon les référentiels GFSI. |
|
|
|
|
|
TÂCHE: Analysez ce CV pour vérifier sa conformité avec le référentiel {referential} de manière systématique. |
|
|
|
|
|
INSTRUCTIONS: |
|
|
1. Ne jamais mentionner le nom du candidat - utilisez toujours "le candidat" |
|
|
2. Pour chaque exigence, fournissez: |
|
|
- La référence exacte de l'exigence (code/numéro) |
|
|
- Votre évaluation avec STATUT: CONFORME, NON CONFORME, ou PARTIELLEMENT CONFORME |
|
|
- Les éléments du CV justifiant votre évaluation |
|
|
- Des recommandations spécifiques si nécessaire |
|
|
3. Structurez votre réponse exactement selon le format JSON demandé à la fin |
|
|
|
|
|
RÉFÉRENTIEL {referential} - EXIGENCES: |
|
|
|
|
|
SECTION 1: EXIGENCES GÉNÉRALES |
|
|
{json.dumps(requirements_categories["General_Requirements"], ensure_ascii=False, indent=2)} |
|
|
|
|
|
SECTION 2: QUALIFICATIONS |
|
|
{json.dumps(requirements_categories["Qualifications"], ensure_ascii=False, indent=2)} |
|
|
|
|
|
SECTION 3: EXPÉRIENCE EN AUDIT |
|
|
{json.dumps(requirements_categories["Audit_Experience"], ensure_ascii=False, indent=2)} |
|
|
|
|
|
SECTION 4: EXIGENCES AVANCÉES |
|
|
{json.dumps(requirements_categories["Advanced_Requirements"], ensure_ascii=False, indent=2)} |
|
|
|
|
|
CV DU CANDIDAT: |
|
|
{cv_text} |
|
|
|
|
|
INSTRUCTIONS SUPPLÉMENTAIRES: |
|
|
- Vérifiez chaque formation et expérience plusieurs fois, car les terminologies peuvent varier |
|
|
- Évaluez si l'équivalence des formations est acceptable selon le référentiel |
|
|
- Identifiez précisément chaque lacune avec référence à l'exigence spécifique |
|
|
|
|
|
FORMAT DE RÉPONSE: Fournissez votre analyse au format JSON avec la structure suivante: |
|
|
{{ |
|
|
"analysis": {{ |
|
|
"general_requirements": [ |
|
|
{{ |
|
|
"reference": "REF-CODE-1", |
|
|
"requirement": "Description de l'exigence", |
|
|
"status": "CONFORME/NON CONFORME/PARTIELLEMENT CONFORME", |
|
|
"evidence": "Éléments du CV justifiant l'évaluation", |
|
|
"recommendations": "Recommandations si nécessaire" |
|
|
}} |
|
|
], |
|
|
"qualifications": [...], |
|
|
"audit_experience": [...], |
|
|
"advanced_requirements": [...] |
|
|
}}, |
|
|
"summary": {{ |
|
|
"conformant_count": 12, |
|
|
"non_conformant_count": 3, |
|
|
"partially_conformant_count": 2, |
|
|
"overall_assessment": "CONFORME/NON CONFORME/PARTIELLEMENT CONFORME", |
|
|
"key_strengths": ["Force 1", "Force 2"], |
|
|
"key_gaps": ["Lacune 1", "Lacune 2"], |
|
|
"conclusion": "Conclusion générale sur l'adéquation du candidat" |
|
|
}} |
|
|
}} |
|
|
|
|
|
IMPORTANT: Assurez-vous que votre réponse soit un JSON valide et bien structuré. |
|
|
""" |
|
|
|
|
|
try: |
|
|
with st.spinner("Analyse approfondie du CV en cours..."): |
|
|
messages = [ |
|
|
{"role": "system", "content": "Vous êtes un expert en conformité GFSI qui fournit des analyses structurées avec références systématiques aux exigences."}, |
|
|
{"role": "user", "content": prompt} |
|
|
] |
|
|
|
|
|
response = groq_client.chat.completions.create( |
|
|
messages=messages, |
|
|
model=model, |
|
|
max_tokens=4000, |
|
|
temperature=0.1, |
|
|
response_format={"type": "json_object"} |
|
|
) |
|
|
|
|
|
|
|
|
try: |
|
|
result = json.loads(response.choices[0].message.content) |
|
|
return result |
|
|
except json.JSONDecodeError: |
|
|
|
|
|
content = response.choices[0].message.content |
|
|
st.warning("L'analyse n'a pas pu être structurée correctement. Affichage des résultats bruts.") |
|
|
return {"raw_content": content} |
|
|
except Exception as e: |
|
|
st.error(f"Erreur lors de l'analyse du CV : {str(e)}") |
|
|
return None |
|
|
|
|
|
|
|
|
def display_analysis_results(analysis_result, referential): |
|
|
""" |
|
|
Affiche les résultats de l'analyse de manière structurée et visuelle. |
|
|
|
|
|
Args: |
|
|
analysis_result (dict): Résultats de l'analyse |
|
|
referential (str): Référentiel GFSI utilisé pour l'analyse |
|
|
""" |
|
|
if not analysis_result: |
|
|
return |
|
|
|
|
|
|
|
|
if "raw_content" in analysis_result: |
|
|
st.markdown( |
|
|
f""" |
|
|
<div style='font-size:18px;line-height:1.6;margin-top:20px;'> |
|
|
{analysis_result["raw_content"]} |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
return |
|
|
|
|
|
|
|
|
st.markdown(f"## Résumé de l'analyse selon le référentiel {referential}") |
|
|
|
|
|
summary = analysis_result.get("summary", {}) |
|
|
conformant = summary.get("conformant_count", 0) |
|
|
non_conformant = summary.get("non_conformant_count", 0) |
|
|
partially = summary.get("partially_conformant_count", 0) |
|
|
total = conformant + non_conformant + partially |
|
|
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
with col1: |
|
|
st.metric("Total des exigences", total) |
|
|
with col2: |
|
|
st.metric("Conformes", conformant, f"{int(conformant/total*100 if total else 0)}%") |
|
|
with col3: |
|
|
st.metric("Non conformes", non_conformant, f"{int(non_conformant/total*100 if total else 0)}%") |
|
|
with col4: |
|
|
st.metric("Partiellement conformes", partially, f"{int(partially/total*100 if total else 0)}%") |
|
|
|
|
|
|
|
|
conclusion_color = { |
|
|
"CONFORME": "#28a745", |
|
|
"NON CONFORME": "#dc3545", |
|
|
"PARTIELLEMENT CONFORME": "#ffc107" |
|
|
}.get(summary.get("overall_assessment", ""), "#6c757d") |
|
|
|
|
|
st.markdown( |
|
|
f""" |
|
|
<div style="background-color:{conclusion_color};color:white;padding:20px;border-radius:10px;margin-top:20px;font-size:22px;line-height:1.8;"> |
|
|
<strong>Évaluation générale :</strong> {summary.get("overall_assessment", "Non déterminée")} |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.markdown("### Forces principales") |
|
|
for strength in summary.get("key_strengths", []): |
|
|
st.markdown(f"✅ {strength}") |
|
|
|
|
|
with col2: |
|
|
st.markdown("### Points d'amélioration") |
|
|
for gap in summary.get("key_gaps", []): |
|
|
st.markdown(f"❌ {gap}") |
|
|
|
|
|
|
|
|
st.markdown("### Conclusion") |
|
|
st.markdown(summary.get("conclusion", "Aucune conclusion disponible.")) |
|
|
|
|
|
|
|
|
analysis = analysis_result.get("analysis", {}) |
|
|
|
|
|
|
|
|
sections = [ |
|
|
("Exigences générales", analysis.get("general_requirements", [])), |
|
|
("Qualifications", analysis.get("qualifications", [])), |
|
|
("Expérience en audit", analysis.get("audit_experience", [])), |
|
|
("Exigences avancées", analysis.get("advanced_requirements", [])) |
|
|
] |
|
|
|
|
|
|
|
|
tabs = st.tabs([section[0] for section in sections]) |
|
|
|
|
|
|
|
|
for i, (section_name, section_data) in enumerate(sections): |
|
|
with tabs[i]: |
|
|
if not section_data: |
|
|
st.info(f"Aucune donnée disponible pour la section {section_name}") |
|
|
continue |
|
|
|
|
|
for item in section_data: |
|
|
|
|
|
status_color = { |
|
|
"CONFORME": "#28a745", |
|
|
"NON CONFORME": "#dc3545", |
|
|
"PARTIELLEMENT CONFORME": "#ffc107" |
|
|
}.get(item.get("status", ""), "#6c757d") |
|
|
|
|
|
|
|
|
with st.expander(f"{item.get('reference', 'REF')} - {item.get('requirement', 'Exigence')} ({item.get('status', 'Non évalué')})"): |
|
|
st.markdown( |
|
|
f""" |
|
|
<div style="border-left: 5px solid {status_color}; padding-left: 10px;"> |
|
|
<p><strong>Référence:</strong> {item.get('reference', 'Non spécifiée')}</p> |
|
|
<p><strong>Exigence:</strong> {item.get('requirement', 'Non spécifiée')}</p> |
|
|
<p><strong>Statut:</strong> <span style="background-color:{status_color};color:white;padding:3px 6px;border-radius:3px;">{item.get('status', 'Non évalué')}</span></p> |
|
|
<p><strong>Éléments justificatifs:</strong> {item.get('evidence', 'Aucun')}</p> |
|
|
<p><strong>Recommandations:</strong> {item.get('recommendations', 'Aucune')}</p> |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
|
|
|
|
|
|
def generate_exportable_report(analysis_result, referential, cv_text): |
|
|
""" |
|
|
Génère un rapport exportable basé sur l'analyse du CV. |
|
|
|
|
|
Args: |
|
|
analysis_result (dict): Résultats de l'analyse |
|
|
referential (str): Référentiel utilisé |
|
|
cv_text (str): Texte du CV analysé |
|
|
|
|
|
Returns: |
|
|
str: HTML du rapport |
|
|
""" |
|
|
if not analysis_result or "raw_content" in analysis_result: |
|
|
return None |
|
|
|
|
|
summary = analysis_result.get("summary", {}) |
|
|
analysis = analysis_result.get("analysis", {}) |
|
|
|
|
|
html = f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<title>Rapport d'analyse CV - {referential}</title> |
|
|
<style> |
|
|
body {{ font-family: Arial, sans-serif; margin: 20px; }} |
|
|
.header {{ background-color: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px; }} |
|
|
.section {{ margin-bottom: 30px; }} |
|
|
.item {{ border-left: 5px solid #ccc; padding-left: 15px; margin-bottom: 15px; }} |
|
|
.conforme {{ border-color: #28a745; }} |
|
|
.non-conforme {{ border-color: #dc3545; }} |
|
|
.partiel {{ border-color: #ffc107; }} |
|
|
.status-badge {{ padding: 3px 6px; border-radius: 3px; color: white; font-size: 12px; }} |
|
|
.summary {{ background-color: #f8f9fa; padding: 15px; border-radius: 5px; }} |
|
|
table {{ width: 100%; border-collapse: collapse; margin-bottom: 20px; }} |
|
|
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }} |
|
|
th {{ background-color: #f2f2f2; }} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header"> |
|
|
<h1>Rapport d'analyse de CV</h1> |
|
|
<p>Référentiel: <strong>{referential}</strong></p> |
|
|
<p>Date: <strong>{pd.Timestamp.now().strftime('%d/%m/%Y')}</strong></p> |
|
|
</div> |
|
|
|
|
|
<div class="section summary"> |
|
|
<h2>Résumé de l'analyse</h2> |
|
|
<table> |
|
|
<tr> |
|
|
<th>Exigences conformes</th> |
|
|
<th>Exigences non conformes</th> |
|
|
<th>Exigences partiellement conformes</th> |
|
|
<th>Évaluation générale</th> |
|
|
</tr> |
|
|
<tr> |
|
|
<td>{summary.get('conformant_count', 0)}</td> |
|
|
<td>{summary.get('non_conformant_count', 0)}</td> |
|
|
<td>{summary.get('partially_conformant_count', 0)}</td> |
|
|
<td><strong>{summary.get('overall_assessment', 'Non déterminée')}</strong></td> |
|
|
</tr> |
|
|
</table> |
|
|
|
|
|
<h3>Forces principales</h3> |
|
|
<ul> |
|
|
{' '.join(f'<li>{strength}</li>' for strength in summary.get('key_strengths', []))} |
|
|
</ul> |
|
|
|
|
|
<h3>Points d'amélioration</h3> |
|
|
<ul> |
|
|
{' '.join(f'<li>{gap}</li>' for gap in summary.get('key_gaps', []))} |
|
|
</ul> |
|
|
|
|
|
<h3>Conclusion</h3> |
|
|
<p>{summary.get('conclusion', 'Aucune conclusion disponible.')}</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
sections = [ |
|
|
("Exigences générales", analysis.get("general_requirements", [])), |
|
|
("Qualifications", analysis.get("qualifications", [])), |
|
|
("Expérience en audit", analysis.get("audit_experience", [])), |
|
|
("Exigences avancées", analysis.get("advanced_requirements", [])) |
|
|
] |
|
|
|
|
|
for section_name, section_data in sections: |
|
|
html += f""" |
|
|
<div class="section"> |
|
|
<h2>{section_name}</h2> |
|
|
""" |
|
|
|
|
|
if not section_data: |
|
|
html += "<p>Aucune donnée disponible pour cette section</p>" |
|
|
else: |
|
|
for item in section_data: |
|
|
status = item.get('status', '') |
|
|
status_class = "" |
|
|
if "CONFORME" in status and "NON" not in status: |
|
|
status_class = "conforme" |
|
|
elif "NON CONFORME" in status: |
|
|
status_class = "non-conforme" |
|
|
elif "PARTIELLEMENT" in status: |
|
|
status_class = "partiel" |
|
|
|
|
|
html += f""" |
|
|
<div class="item {status_class}"> |
|
|
<h3>{item.get('reference', 'REF')} - {item.get('requirement', 'Exigence')}</h3> |
|
|
<p><strong>Statut:</strong> <span class="status-badge" style="background-color: {'#28a745' if status_class == 'conforme' else '#dc3545' if status_class == 'non-conforme' else '#ffc107'};">{status}</span></p> |
|
|
<p><strong>Éléments justificatifs:</strong> {item.get('evidence', 'Aucun')}</p> |
|
|
<p><strong>Recommandations:</strong> {item.get('recommendations', 'Aucune')}</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
html += "</div>" |
|
|
|
|
|
html += """ |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
def main(): |
|
|
""" |
|
|
Interface principale améliorée avec fonctionnalités d'exportation et statistiques. |
|
|
""" |
|
|
|
|
|
with st.sidebar: |
|
|
st.image("https://via.placeholder.com/150x60?text=GFSI+Analyzer", width=150) |
|
|
st.title("Configuration") |
|
|
|
|
|
|
|
|
if "api_key" not in st.session_state: |
|
|
st.session_state.api_key = "" |
|
|
|
|
|
api_key = st.text_input( |
|
|
"Clé API Groq :", |
|
|
value=st.session_state.api_key, |
|
|
type="password", |
|
|
help="La clé API commence par 'gsk_'" |
|
|
) |
|
|
|
|
|
if api_key != st.session_state.api_key: |
|
|
st.session_state.api_key = api_key |
|
|
st.session_state.groq_client = None if not api_key else get_groq_client(api_key) |
|
|
|
|
|
if "groq_client" not in st.session_state: |
|
|
st.session_state.groq_client = None if not api_key else get_groq_client(api_key) |
|
|
|
|
|
|
|
|
model_options = { |
|
|
"openai/gpt-oss-120b": "openai/gpt-oss-120b (Haute précision)", |
|
|
"llama-3.2-11b-versatile": "Llama 3.2 11B (Équilibré)", |
|
|
"llama-3.1-8b-instant": "Llama 3.1 8B (Rapide)" |
|
|
} |
|
|
selected_model = st.selectbox( |
|
|
"Modèle d'IA:", |
|
|
options=list(model_options.keys()), |
|
|
format_func=lambda x: model_options[x], |
|
|
help="Balance entre précision d'analyse et vitesse" |
|
|
) |
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown("### Options d'analyse") |
|
|
|
|
|
|
|
|
show_debug = st.checkbox("Afficher les données brutes (Debug)", False) |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown("### À propos") |
|
|
st.info(""" |
|
|
**Outil d'analyse CV - GFSI** |
|
|
Version 25.12 |
|
|
|
|
|
Référentiels supportés : BRCGS, FSSC 22000, IFS |
|
|
""") |
|
|
|
|
|
|
|
|
st.title("Analyse de CV selon les Référentiels GFSI") |
|
|
st.markdown("---") |
|
|
|
|
|
|
|
|
with st.expander("ℹ️ Guide d'utilisation", expanded=True): |
|
|
st.markdown(""" |
|
|
### Comment utiliser cet outil? |
|
|
|
|
|
1. **Configuration**: |
|
|
- Saisissez votre clé API Groq dans le panneau latéral |
|
|
- Sélectionnez le modèle d'IA à utiliser |
|
|
|
|
|
2. **Analyse**: |
|
|
- Téléchargez un CV au format PDF |
|
|
- Sélectionnez le référentiel GFSI applicable |
|
|
- Lancez l'analyse |
|
|
|
|
|
3. **Résultats**: |
|
|
- Consultez le rapport détaillé avec références aux exigences |
|
|
- Explorez les détails par section (Général, Qualifications, etc.) |
|
|
- Exportez les résultats au format HTML |
|
|
|
|
|
**Astuce**: Plus le document est clairement formaté, meilleure sera l'analyse. |
|
|
""") |
|
|
|
|
|
|
|
|
st.markdown("## Analyse de CV") |
|
|
|
|
|
|
|
|
if not st.session_state.groq_client: |
|
|
st.warning("⚠️ Veuillez configurer votre clé API Groq dans le panneau latéral pour continuer.") |
|
|
return |
|
|
|
|
|
|
|
|
uploaded_file = st.file_uploader( |
|
|
"📄 Téléchargez un fichier PDF (CV)", |
|
|
type="pdf", |
|
|
help="Formats supportés : PDF. Taille maximale: 10MB" |
|
|
) |
|
|
|
|
|
if not uploaded_file: |
|
|
st.info("Veuillez télécharger un fichier PDF pour commencer l'analyse.") |
|
|
|
|
|
st.markdown("### Aperçu des référentiels supportés") |
|
|
for ref_name, ref_data in REFERENTIALS.items(): |
|
|
with st.expander(f"Référentiel {ref_name}"): |
|
|
st.json(ref_data) |
|
|
return |
|
|
|
|
|
|
|
|
referential = st.selectbox( |
|
|
"📋 Sélectionnez un référentiel", |
|
|
list(REFERENTIALS.keys()), |
|
|
help="Choisissez le standard GFSI applicable pour ce candidat" |
|
|
) |
|
|
|
|
|
|
|
|
if uploaded_file and referential and st.session_state.groq_client: |
|
|
|
|
|
if st.button("🔍 Analyser le CV", type="primary"): |
|
|
|
|
|
cv_text = extract_text_from_pdf(uploaded_file) |
|
|
|
|
|
|
|
|
if show_debug and cv_text: |
|
|
with st.expander("Aperçu du texte extrait", expanded=False): |
|
|
st.text(cv_text[:1000] + "..." if len(cv_text) > 1000 else cv_text) |
|
|
|
|
|
if cv_text: |
|
|
|
|
|
analysis_result = analyze_cv_with_groq( |
|
|
cv_text, |
|
|
referential, |
|
|
st.session_state.groq_client, |
|
|
model=selected_model |
|
|
) |
|
|
|
|
|
|
|
|
if show_debug and analysis_result: |
|
|
with st.expander("Données brutes de l'analyse (Debug)", expanded=False): |
|
|
st.json(analysis_result) |
|
|
|
|
|
|
|
|
if analysis_result: |
|
|
|
|
|
display_analysis_results(analysis_result, referential) |
|
|
|
|
|
|
|
|
report_html = generate_exportable_report(analysis_result, referential, cv_text) |
|
|
if report_html: |
|
|
st.download_button( |
|
|
label="📥 Télécharger le rapport complet", |
|
|
data=report_html, |
|
|
file_name=f"analyse_cv_{referential}_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.html", |
|
|
mime="text/html", |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |