import chainlit as cl import asyncio import zipfile import httpx import json import os import io # --- FONCTION DE RENDU DE L'UI SELON LE STATUT --- async def render_workflow_state(new_state: dict): """ Fonction centrale pour aiguiller l'affichage Chainlit en fonction du statut renvoyé par LangGraph. """ current_status = new_state.get("status") if current_status == "spec_incomplete": spec = new_state.get("spec", {}) question = spec.get("clarifying_question") await cl.Message(content=f"**Spécifications incomplètes**\n\n{question}").send() elif current_status == "spec_ready": spec = new_state.get("spec", {}) summary = "### Éléments importants à retenir de ton projet :\n\n" summary += f"- **Nom du projet** : {spec.get('title')}\n" summary += f"- **Description** : {spec.get('description')}\n" summary += "- **Actions** :\n" summary += "\n".join(f" - {req}" for req in spec.get("requirements", [])) + "\n" summary += "- **Contraintes** :\n" summary += "\n".join(f" - {constraint}" for constraint in spec.get("constraints", [])) + "\n" summary += f"- **Langage** : {spec.get('language')}\n\n" summary += "**Est-ce que cela vous convient ?**" res = await cl.AskActionMessage( content=summary, actions=[ cl.Action(name="oui", payload={"value": "oui"}, label="Oui, c'est parfait 👍"), cl.Action(name="non", payload={"value": "non"}, label="Non, modifier ❌") ], timeout=3600 ).send() if res is None: await cl.Message( content="⏰ **Session expirée.** Si tu es toujours là, envoie un message pour relancer l'analyse." ).send() return if res and res.get("name") == "oui": await cl.Message(content="🚀 **Spécifications validées !** Lancement de la génération du code...").send() new_state["status"] = "spec_approved" cl.user_session.set("graph_state", new_state) async with httpx.AsyncClient() as client: response = await client.post( "http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=600.0 ) final_state = response.json() cl.user_session.set("graph_state", final_state) # Rappel de la fonction pour traiter le nouvel état de review await render_workflow_state(final_state) else: new_state["status"] = "spec_incomplete" cl.user_session.set("graph_state", new_state) await cl.Message( content="🔄 **Compris.** Qu'est-ce qui ne convient pas ? S'il te plaît, précise les éléments manquants ou à corriger :" ).send() elif current_status in ["wait_human_review", "approved_by_human"]: dev_data = new_state.get("generated_code", {}) qa_res = new_state.get("qa_result", {}) raw_files = dev_data.get("files", {}) files = {} if isinstance(raw_files, list): for f in raw_files: if isinstance(f, dict): path = f.get("path") or f.get("filename") or f.get("name") content = f.get("content") or f.get("code") or "" if path: files[path] = content elif isinstance(raw_files, dict): files = raw_files is_safe = qa_res.get("is_complete_and_safe", False) badge_qa = "✅ CONFORME" if is_safe else "⚠️ SÉCURITÉ/QUALITÉ À VÉRIFIER" gitea_url = dev_data.get("repo_url", "") # Correction de l'URL pour y accéder depuis ton navigateur (hors réseau Docker) if gitea_url and "git-arc:3000" in gitea_url: gitea_url = gitea_url.replace("git-arc:3000", "localhost:3000") access_md = f"**Résultat global QA :** {badge_qa}\n\n" access_md += "### 🔗 Accès au projet\n\n" if gitea_url: access_md += f"🔗 **Lien vers le dépôt Gitea :** [Accéder au dépôt]({gitea_url})\n\n" # 2. Gestion du bouton de téléchargement ZIP (Correction finale de la variable) project_title = new_state.get("generated_code", {}).get("spec_title", "projet") message_elements = [] if files: zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: for filepath, file_content in files.items(): zip_file.writestr(filepath, file_content) zip_bytes = zip_buffer.getvalue() message_elements.append( cl.File(name=f"{project_title}.zip", content=zip_bytes, display="inline") ) else: access_md += "⚠️ Aucun fichier source trouvé pour générer le ZIP.\n" await cl.Message(content=access_md, elements=message_elements).send() # 3. Code Source Produit (Regroupé dans un seul message avec ses actions) cl.user_session.set("current_files", files) file_actions = [] for filename in files.keys(): file_actions.append( cl.Action( name="open_file", payload={"filename": filename}, label=f"📄 {filename}" ) ) files_message = cl.Message( content="### 📂 Code Source Produit\n\nClique sur un fichier pour l'afficher :", actions=file_actions ) await files_message.send() cl.user_session.set("files_message", files_message) # 4. Demande de validation (Boutons Valider / Refuser) res = await cl.AskActionMessage( content="**Souhaitez-vous valider et packager ce livrable ?**", actions=[ cl.Action(name="valider_projet", payload={"value": "approve"}, label="✅ Valider & Livrer", description="Génère l'archive et indexe le projet"), cl.Action(name="refuser_projet", payload={"value": "refuse"}, label="❌ Refuser et corriger", description="Renvoie le projet au PM avec vos commentaires") ], timeout=3600 ).send() if res and res.get("name") == "valider_projet": await cl.Message(content="🎉 **Projet validé.**").send() new_state["status"] = "human_approved" cl.user_session.set("graph_state", new_state) async with httpx.AsyncClient() as client: resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=600.0) final_state = resp.json() cl.user_session.set("graph_state", final_state) elif res and res.get("name") == "refuser_projet": feedback_user = await cl.AskUserMessage( content="Veuillez décrire les corrections ou les modifications à apporter au projet.", timeout=600 ).send() if feedback_user: new_state["status"] = "human_refused" new_state["user_feedback"] = feedback_user["output"] cl.user_session.set("graph_state", new_state) await cl.Message(content="🔄 **Feedback transmis.** Prise en compte des modifications...").send() async with httpx.AsyncClient() as client: resp = await client.post("http://127.0.0.1:8000/api/workflow/run", json=new_state, timeout=600.0) loop_state = resp.json() cl.user_session.set("graph_state", loop_state) await render_workflow_state(loop_state) elif current_status == "delivered": await cl.Message(content="✅ Ce projet a déjà été traité et livré.").send() else: await cl.Message( content=f"Résultat du traitement :\n```json\n{json.dumps(new_state, indent=2, ensure_ascii=False)}\n```" ).send() # --- ÉVÉNEMENTS CHAINLIT STANDARDS --- @cl.action_callback("open_file") async def on_open_file(action: cl.Action): filename = action.payload.get("filename") files = cl.user_session.get("current_files", {}) content = files.get(filename, "") if content is None: content = "" if not str(content).strip(): content = "(Fichier vide)" ext = filename.split(".")[-1] if "." in filename else "text" is_md = ext in ("md", "markdown") files_message = cl.user_session.get("files_message") message_id = files_message.id if files_message else None old_viewer = cl.user_session.get("side_code_viewer") if old_viewer: await old_viewer.remove() new_viewer = cl.Text( name=filename, content=str(content), language=None if is_md else ext, display="side", ) await new_viewer.send(for_id=message_id) cl.user_session.set("side_code_viewer", new_viewer) @cl.on_chat_start async def on_chat_start(): initial_state = { "user_input": "", "user_feedback": None, "chat_history": [], "spec": {}, "status": "start", "loop_count": 0, "existing_project": None, "generated_code": None, "qa_result": None, "is_completed": False } cl.user_session.set("graph_state", initial_state) await cl.Message(content="Bonjour 👋 Je suis ARC. Décris-moi ton besoin logiciel.").send() @cl.on_message async def on_message(message: cl.Message): state = cl.user_session.get("graph_state") if "chat_history" not in state: state["chat_history"] = [] if "status" not in state: state["status"] = "start" if state.get("status") == "spec_incomplete": state["user_feedback"] = message.content else: state["user_input"] = message.content state["user_feedback"] = None async with httpx.AsyncClient() as client: response = await client.post( "http://127.0.0.1:8000/api/workflow/run", json=state, timeout=600.0 ) new_state = response.json() cl.user_session.set("graph_state", new_state) # Appel du rendu await render_workflow_state(new_state)