ui+ux: header epure / file preserve sur error / submit Drops / pending sobre / flush right
Browse files5 ajustements demandes par l'utilisateur :
1. HEADER FILE EPURE — retire « (append-only, mis a jour a chaque
consolidation) » du header des .enrich envoyes a JDM. C'etait
du verbiage interne, JDM n'en a pas besoin. Header simple :
« # Soumission JeuxDeMots — fichier d'enrichissement. »
2. FILE PRESERVE SUR ERREUR — le yield d'erreur agent dans
run_jarvis_flow utilise maintenant _current_file_path() au lieu
de last_file_path. Quand l'API LLM crashe, le fichier de
consolidation (canonical_path peuple par auto-append) reste
affiche en bas de la page, en plus d'etre visible dans Drops.
L'utilisateur ne perd plus son travail en cas d'incident.
3. SUBMIT DANS DROPS — nouveau bouton « 📤 Soumettre ce fichier
a JDM (LLMDrops) » dans le sous-onglet Drops. Devient
interactive des qu'un fichier est selectionne ET qu'une cle
LLMDrops est dispo (input bandeau ou env). Reutilise
jarvis.submit_existing_file. Statut affiche apres l'upload
(success + nom serveur ou erreur). Permet de submettre un
fichier d'un run precedent sans relancer le flow.
4. PENDING LINE SOBRE HORS ENRICH — « ⏳ Generation en cours… »
reservait le compteur « (X consolides) » a l'enrichissement
mais affichait quand meme « (X consolides) » sans /Y sur les
autres flows (audit, gap, signalement, stats) — trompeur car
ces flows n'ont pas la semantique de consolidation. Maintenant
purement « ⏳ Generation en cours… » sans compteur quand
consolidation_target=None (= tous les flows hors enrich).
5. DROPS FLUSH A DROITE + RENAME — l'onglet « 📁 Productions »
devient « 📥 Drops » (nom plus court et plus parlant —
« les drops/soumissions JDM »). pushAideTabRight() etendu pour
accepter une liste de targets [« Aide », « Drops »] → applique
margin-left:auto a tous les tabs matching, peu importe leur
niveau (top-level pour Aide, sous-onglet Jarvis pour Drops).
Tests : 35/35 verts.
- app.py +83 -7
- jarvis.py +14 -8
- src/jdm_agent/enrich/validators.py +1 -2
|
@@ -2127,18 +2127,24 @@ _HEAD_JS = """
|
|
| 2127 |
setTimeout(bindChatbotObserver, 800);
|
| 2128 |
setTimeout(bindChatbotObserver, 2000);
|
| 2129 |
|
| 2130 |
-
// ----------
|
| 2131 |
// CSS pur ne marche pas de façon fiable (structure DOM Gradio v5
|
| 2132 |
// varie). On cherche tous les boutons role="tab" qui contiennent
|
| 2133 |
-
//
|
|
|
|
|
|
|
| 2134 |
function pushAideTabRight() {
|
| 2135 |
var tabs = document.querySelectorAll('[role="tab"]');
|
| 2136 |
var done = false;
|
|
|
|
| 2137 |
for (var i = 0; i < tabs.length; i++) {
|
| 2138 |
var label = (tabs[i].textContent || '').trim();
|
| 2139 |
-
|
| 2140 |
-
|
| 2141 |
-
|
|
|
|
|
|
|
|
|
|
| 2142 |
}
|
| 2143 |
}
|
| 2144 |
return done;
|
|
@@ -3876,13 +3882,15 @@ with gr.Blocks(theme=THEME, title="JDMAgent Demo", head=_HEAD_JS, css=_CHATBOT_C
|
|
| 3876 |
# viewer adaptatif à droite (iframe pour HTML, code pour text,
|
| 3877 |
# télécharge pour le reste). Pas d'écrasement : tout file producer
|
| 3878 |
# détecte les collisions et suffixe (_2, _3…).
|
| 3879 |
-
with gr.Tab("
|
| 3880 |
gr.Markdown(
|
| 3881 |
"**Tous les fichiers produits** par l'agent (sous-graphes, "
|
| 3882 |
"enrichissements, audits, signalements, stats) sont listés "
|
| 3883 |
"ici, du PLUS RÉCENT au PLUS ANCIEN. Aucun écrasement : les "
|
| 3884 |
"collisions de nom sont suffixées automatiquement (`_2`, `_3`…). "
|
| 3885 |
-
"La liste se rafraîchit toute seule toutes les 3 secondes."
|
|
|
|
|
|
|
| 3886 |
)
|
| 3887 |
|
| 3888 |
def _scan_productions_choices():
|
|
@@ -3947,6 +3955,16 @@ with gr.Blocks(theme=THEME, title="JDMAgent Demo", head=_HEAD_JS, css=_CHATBOT_C
|
|
| 3947 |
visible=False, label="📥 Télécharger ce fichier",
|
| 3948 |
interactive=False,
|
| 3949 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3950 |
|
| 3951 |
def _render_production_file(selected_path):
|
| 3952 |
"""Affiche le fichier selon son extension :
|
|
@@ -4034,6 +4052,64 @@ with gr.Blocks(theme=THEME, title="JDMAgent Demo", head=_HEAD_JS, css=_CHATBOT_C
|
|
| 4034 |
prod_text_viewer, prod_download],
|
| 4035 |
)
|
| 4036 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4037 |
def _refresh_choices():
|
| 4038 |
"""Re-scanne PRODUCTIONS_DIR et met à jour les choices
|
| 4039 |
du Dropdown. Préserve la sélection courante si le
|
|
|
|
| 2127 |
setTimeout(bindChatbotObserver, 800);
|
| 2128 |
setTimeout(bindChatbotObserver, 2000);
|
| 2129 |
|
| 2130 |
+
// ---------- Onglets « Aide » et « Drops » flush à droite ----------
|
| 2131 |
// CSS pur ne marche pas de façon fiable (structure DOM Gradio v5
|
| 2132 |
// varie). On cherche tous les boutons role="tab" qui contiennent
|
| 2133 |
+
// un des labels cibles (Aide en top-level, Drops en sous-onglet
|
| 2134 |
+
// Jarvis) et on leur applique margin-left:auto pour les pousser
|
| 2135 |
+
// au bout à droite de leur tablist parent.
|
| 2136 |
function pushAideTabRight() {
|
| 2137 |
var tabs = document.querySelectorAll('[role="tab"]');
|
| 2138 |
var done = false;
|
| 2139 |
+
var targets = ['Aide', 'Drops'];
|
| 2140 |
for (var i = 0; i < tabs.length; i++) {
|
| 2141 |
var label = (tabs[i].textContent || '').trim();
|
| 2142 |
+
for (var t = 0; t < targets.length; t++) {
|
| 2143 |
+
if (label.indexOf(targets[t]) >= 0) {
|
| 2144 |
+
tabs[i].style.marginLeft = 'auto';
|
| 2145 |
+
done = true;
|
| 2146 |
+
break;
|
| 2147 |
+
}
|
| 2148 |
}
|
| 2149 |
}
|
| 2150 |
return done;
|
|
|
|
| 3882 |
# viewer adaptatif à droite (iframe pour HTML, code pour text,
|
| 3883 |
# télécharge pour le reste). Pas d'écrasement : tout file producer
|
| 3884 |
# détecte les collisions et suffixe (_2, _3…).
|
| 3885 |
+
with gr.Tab("📥 Drops", id="jarvis-drops"):
|
| 3886 |
gr.Markdown(
|
| 3887 |
"**Tous les fichiers produits** par l'agent (sous-graphes, "
|
| 3888 |
"enrichissements, audits, signalements, stats) sont listés "
|
| 3889 |
"ici, du PLUS RÉCENT au PLUS ANCIEN. Aucun écrasement : les "
|
| 3890 |
"collisions de nom sont suffixées automatiquement (`_2`, `_3`…). "
|
| 3891 |
+
"La liste se rafraîchit toute seule toutes les 3 secondes. "
|
| 3892 |
+
"Sélectionne un fichier puis **📤 Soumettre à JDM** pour le "
|
| 3893 |
+
"pousser au LLMDrops directement depuis ici."
|
| 3894 |
)
|
| 3895 |
|
| 3896 |
def _scan_productions_choices():
|
|
|
|
| 3955 |
visible=False, label="📥 Télécharger ce fichier",
|
| 3956 |
interactive=False,
|
| 3957 |
)
|
| 3958 |
+
# Bouton soumission JDM — actif si fichier
|
| 3959 |
+
# sélectionné ET clé LLMDrops dispo (env ou
|
| 3960 |
+
# input). Sinon dégrise pour signaler.
|
| 3961 |
+
with gr.Row():
|
| 3962 |
+
prod_submit_btn = gr.Button(
|
| 3963 |
+
"📤 Soumettre ce fichier à JDM (LLMDrops)",
|
| 3964 |
+
variant="primary",
|
| 3965 |
+
interactive=False,
|
| 3966 |
+
)
|
| 3967 |
+
prod_submit_status = gr.Markdown(visible=False)
|
| 3968 |
|
| 3969 |
def _render_production_file(selected_path):
|
| 3970 |
"""Affiche le fichier selon son extension :
|
|
|
|
| 4052 |
prod_text_viewer, prod_download],
|
| 4053 |
)
|
| 4054 |
|
| 4055 |
+
def _toggle_submit_btn(selected_path, drops_key):
|
| 4056 |
+
"""Active le bouton submit si fichier sélectionné
|
| 4057 |
+
ET (clé fournie dans le bandeau OU env active)."""
|
| 4058 |
+
from jarvis import has_drops_key as _hk
|
| 4059 |
+
ok = bool(selected_path) and _hk(drops_key)
|
| 4060 |
+
return gr.update(interactive=ok)
|
| 4061 |
+
|
| 4062 |
+
# Réactive à chaque changement de sélection OU de clé
|
| 4063 |
+
prod_file_dropdown.change(
|
| 4064 |
+
_toggle_submit_btn,
|
| 4065 |
+
inputs=[prod_file_dropdown, jarvis_drops_key],
|
| 4066 |
+
outputs=[prod_submit_btn],
|
| 4067 |
+
)
|
| 4068 |
+
jarvis_drops_key.change(
|
| 4069 |
+
_toggle_submit_btn,
|
| 4070 |
+
inputs=[prod_file_dropdown, jarvis_drops_key],
|
| 4071 |
+
outputs=[prod_submit_btn],
|
| 4072 |
+
)
|
| 4073 |
+
|
| 4074 |
+
def _submit_production_file(selected_path, drops_key, jarvis_model_v):
|
| 4075 |
+
"""Upload le fichier sélectionné vers LLMDrops via
|
| 4076 |
+
jarvis.submit_existing_file (gère env override de
|
| 4077 |
+
JDM_DROPS_API_KEY le temps de l'appel)."""
|
| 4078 |
+
if not selected_path:
|
| 4079 |
+
return gr.update(visible=True,
|
| 4080 |
+
value="⚠️ Aucun fichier sélectionné.")
|
| 4081 |
+
try:
|
| 4082 |
+
from jarvis import submit_existing_file
|
| 4083 |
+
res = submit_existing_file(
|
| 4084 |
+
file_path=selected_path,
|
| 4085 |
+
drops_key=(drops_key or ""),
|
| 4086 |
+
model_name=(jarvis_model_v or "manual_submission"),
|
| 4087 |
+
)
|
| 4088 |
+
if isinstance(res, dict) and res.get("ok"):
|
| 4089 |
+
uploaded = res.get("uploaded_as") or Path(selected_path).name
|
| 4090 |
+
return gr.update(
|
| 4091 |
+
visible=True,
|
| 4092 |
+
value=f"✅ **Soumis avec succès** sous le nom "
|
| 4093 |
+
f"`{uploaded}`. Réponse serveur : "
|
| 4094 |
+
f"`{str(res.get('response') or '')[:200]}`",
|
| 4095 |
+
)
|
| 4096 |
+
err = (res or {}).get("error") if isinstance(res, dict) else str(res)
|
| 4097 |
+
return gr.update(
|
| 4098 |
+
visible=True,
|
| 4099 |
+
value=f"❌ **Échec de soumission** : {err}",
|
| 4100 |
+
)
|
| 4101 |
+
except Exception as e:
|
| 4102 |
+
return gr.update(
|
| 4103 |
+
visible=True,
|
| 4104 |
+
value=f"❌ Erreur soumission : {e}",
|
| 4105 |
+
)
|
| 4106 |
+
|
| 4107 |
+
prod_submit_btn.click(
|
| 4108 |
+
_submit_production_file,
|
| 4109 |
+
inputs=[prod_file_dropdown, jarvis_drops_key, jarvis_model],
|
| 4110 |
+
outputs=[prod_submit_status],
|
| 4111 |
+
)
|
| 4112 |
+
|
| 4113 |
def _refresh_choices():
|
| 4114 |
"""Re-scanne PRODUCTIONS_DIR et met à jour les choices
|
| 4115 |
du Dropdown. Préserve la sélection courante si le
|
|
@@ -1226,15 +1226,16 @@ def run_jarvis_flow(
|
|
| 1226 |
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
| 1227 |
|
| 1228 |
def _pending_line() -> str:
|
| 1229 |
-
"""Ligne « génération en cours »
|
| 1230 |
-
triplets consolidés
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
-
|
| 1235 |
if consolidation_target:
|
|
|
|
| 1236 |
return f"*⏳ Génération en cours… ({n}/{consolidation_target} consolidés)*"
|
| 1237 |
-
return
|
| 1238 |
|
| 1239 |
def _current_file_path() -> Optional[str]:
|
| 1240 |
"""Renvoie canonical_path dès qu'il existe sur disque (auto-append
|
|
@@ -1979,11 +1980,16 @@ def run_jarvis_flow(
|
|
| 1979 |
f"({len(progress_full)})</summary>\n\n"
|
| 1980 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1981 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1982 |
yield (
|
| 1983 |
[{"role": "user", "content": user_display},
|
| 1984 |
{"role": "assistant",
|
| 1985 |
"content": f"❌ Erreur agent : {e}" + err_block}],
|
| 1986 |
-
|
| 1987 |
)
|
| 1988 |
return
|
| 1989 |
|
|
|
|
| 1226 |
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
| 1227 |
|
| 1228 |
def _pending_line() -> str:
|
| 1229 |
+
"""Ligne « génération en cours ». Le compteur cumulatif des
|
| 1230 |
+
triplets consolidés est affiché UNIQUEMENT pour l'enrichissement
|
| 1231 |
+
(= seul flow où consolidation_target est défini). Les autres
|
| 1232 |
+
flows (audit, gap, signalement, stats) n'ont pas la sémantique
|
| 1233 |
+
de « consolidation » → on n'expose pas un compteur trompeur.
|
| 1234 |
+
"""
|
| 1235 |
if consolidation_target:
|
| 1236 |
+
n = count_consolidations()
|
| 1237 |
return f"*⏳ Génération en cours… ({n}/{consolidation_target} consolidés)*"
|
| 1238 |
+
return "*⏳ Génération en cours…*"
|
| 1239 |
|
| 1240 |
def _current_file_path() -> Optional[str]:
|
| 1241 |
"""Renvoie canonical_path dès qu'il existe sur disque (auto-append
|
|
|
|
| 1980 |
f"({len(progress_full)})</summary>\n\n"
|
| 1981 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1982 |
)
|
| 1983 |
+
# PRÉSERVATION DU FICHIER SUR ERREUR : on yield
|
| 1984 |
+
# _current_file_path() (priorité canonical_path
|
| 1985 |
+
# si auto-append a écrit quelque chose) pour
|
| 1986 |
+
# que l'utilisateur garde son fichier de
|
| 1987 |
+
# consolidation même quand l'API LLM crashe.
|
| 1988 |
yield (
|
| 1989 |
[{"role": "user", "content": user_display},
|
| 1990 |
{"role": "assistant",
|
| 1991 |
"content": f"❌ Erreur agent : {e}" + err_block}],
|
| 1992 |
+
_current_file_path(), _read_file_preview(_current_file_path()),
|
| 1993 |
)
|
| 1994 |
return
|
| 1995 |
|
|
@@ -177,8 +177,7 @@ def _append_consolidation_to_file(term: str, relation: str, target: str,
|
|
| 177 |
with p.open("a", encoding="utf-8") as f:
|
| 178 |
if write_header:
|
| 179 |
f.write(
|
| 180 |
-
"# Soumission JeuxDeMots — fichier d'enrichissement
|
| 181 |
-
"(append-only, mis à jour à chaque consolidation).\n"
|
| 182 |
"# Format : terme | relation | cible | annotation < explication >\n\n"
|
| 183 |
)
|
| 184 |
_CONSOLIDATION_OUTPUT_HEADER_WRITTEN = True
|
|
|
|
| 177 |
with p.open("a", encoding="utf-8") as f:
|
| 178 |
if write_header:
|
| 179 |
f.write(
|
| 180 |
+
"# Soumission JeuxDeMots — fichier d'enrichissement.\n"
|
|
|
|
| 181 |
"# Format : terme | relation | cible | annotation < explication >\n\n"
|
| 182 |
)
|
| 183 |
_CONSOLIDATION_OUTPUT_HEADER_WRITTEN = True
|