Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,38 +1,41 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
from gradio_htmlinjector import HTMLInjector
|
| 3 |
|
| 4 |
-
#
|
| 5 |
-
def
|
| 6 |
-
# 1) ajoute (user_msg, question) et indique qu'il faut ouvrir la popup
|
| 7 |
-
if conv is None:
|
| 8 |
-
conv = []
|
| 9 |
-
user_msg = (user_msg or "").strip()
|
| 10 |
-
if not user_msg:
|
| 11 |
-
return conv, False, "" # pas de popup
|
| 12 |
-
|
| 13 |
-
bot_question = "Ceci est une réponse de démo.\n\n**Confirmez-vous l’action ?**"
|
| 14 |
-
new_conv = conv + [(user_msg, bot_question)]
|
| 15 |
-
confirm_needed = True
|
| 16 |
-
return new_conv, confirm_needed, "" # (maj chatbot via state_conv, flag popup, clear textbox)
|
| 17 |
-
|
| 18 |
-
def handle_choice(choice: str, conv: list[tuple[str, str]]):
|
| 19 |
-
# 3) ajoute (Oui/Non, confirmation) ; ignore les resets vides
|
| 20 |
-
if conv is None:
|
| 21 |
-
conv = []
|
| 22 |
c = (choice or "").strip().lower()
|
| 23 |
-
if not c:
|
| 24 |
-
return conv, "" # ignore le "change" déclenché par le reset
|
| 25 |
-
|
| 26 |
if c in {"oui", "o", "yes", "y"}:
|
| 27 |
-
user_choice
|
|
|
|
| 28 |
elif c in {"non", "n", "no"}:
|
| 29 |
-
user_choice
|
|
|
|
| 30 |
else:
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def inject_assets():
|
| 37 |
js = r"""
|
| 38 |
function setGradioValue(elemId, value){
|
|
@@ -45,90 +48,71 @@ def inject_assets():
|
|
| 45 |
input.dispatchEvent(new Event('input', {bubbles:true}));
|
| 46 |
input.dispatchEvent(new Event('change', {bubbles:true}));
|
| 47 |
}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
#confirm-actions{ display:flex; gap:8px; justify-content:flex-end; margin-top:14px; }
|
|
|
|
|
|
|
| 60 |
#bridge-choice{ display:none; } /* pont caché mais monté */
|
| 61 |
"""
|
| 62 |
html = r"""
|
| 63 |
<div id="confirm-modal" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
| 64 |
<div id="confirm-card">
|
| 65 |
<h3 id="confirm-title">Confirmer ?</h3>
|
| 66 |
-
<p>Souhaitez-vous
|
| 67 |
<div id="confirm-actions">
|
| 68 |
-
<button type="button"
|
| 69 |
-
|
| 70 |
-
Non
|
| 71 |
-
</button>
|
| 72 |
-
<button type="button" class="primary"
|
| 73 |
-
onclick="setGradioValue('bridge-choice','oui'); document.getElementById('confirm-modal').style.display='none'; document.body.style.overflow=''">
|
| 74 |
-
Oui
|
| 75 |
-
</button>
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
"""
|
| 80 |
return {"css": css, "js": js, "body_html": html}
|
| 81 |
|
| 82 |
-
#
|
| 83 |
-
with gr.Blocks(title="Chat + Popup
|
| 84 |
html_injector = HTMLInjector()
|
| 85 |
-
|
| 86 |
-
chat = gr.Chatbot(height=420)
|
| 87 |
user_in = gr.Textbox(placeholder="Tapez un message…", lines=2)
|
| 88 |
-
state_conv = gr.State([]) # vérité de la conversation
|
| 89 |
-
confirm_flag = gr.State(False) # flag “ouvrir popup ?”
|
| 90 |
bridge_choice = gr.Textbox(visible=True, elem_id="bridge-choice") # pont oui/non
|
| 91 |
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
inputs=[user_in, state_conv],
|
| 96 |
-
outputs=[state_conv, confirm_flag, user_in], # maj state_conv, flag, clear input
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
# (B) refléter l'état dans le Chatbot (immédiatement après)
|
| 100 |
-
evt = evt.then(
|
| 101 |
-
fn=lambda s: s, inputs=state_conv, outputs=chat
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
# (C) puis ouvrir la popup côté client *si* confirm_flag = True
|
| 105 |
-
evt.then(
|
| 106 |
-
fn=None, inputs=[confirm_flag], outputs=None,
|
| 107 |
-
js="""
|
| 108 |
-
(flag) => {
|
| 109 |
-
if(!flag) return;
|
| 110 |
-
const start = Date.now();
|
| 111 |
-
(function tick(){
|
| 112 |
-
const modal = document.getElementById('confirm-modal');
|
| 113 |
-
if (modal) {
|
| 114 |
-
modal.style.display = 'flex';
|
| 115 |
-
document.body.style.overflow = 'hidden';
|
| 116 |
-
return;
|
| 117 |
-
}
|
| 118 |
-
if (Date.now() - start < 3000) setTimeout(tick, 60);
|
| 119 |
-
})();
|
| 120 |
-
}
|
| 121 |
-
""",
|
| 122 |
-
)
|
| 123 |
|
| 124 |
-
#
|
| 125 |
-
bridge_choice.change(
|
| 126 |
-
|
| 127 |
-
inputs=[bridge_choice, state_conv],
|
| 128 |
-
outputs=[state_conv, bridge_choice],
|
| 129 |
-
).then(
|
| 130 |
-
fn=lambda s: s, inputs=state_conv, outputs=chat
|
| 131 |
-
)
|
| 132 |
|
| 133 |
demo.load(fn=inject_assets, inputs=None, outputs=[html_injector])
|
| 134 |
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from gradio_htmlinjector import HTMLInjector
|
| 3 |
|
| 4 |
+
# 1) Choix dans la popup -> ajoute (Oui/Non, confirmation)
|
| 5 |
+
def handle_choice(choice: str, chat_history: list[tuple[str, str]]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
c = (choice or "").strip().lower()
|
|
|
|
|
|
|
|
|
|
| 7 |
if c in {"oui", "o", "yes", "y"}:
|
| 8 |
+
user_choice = "Oui"
|
| 9 |
+
reply = "✅ Vous avez choisi **OUI**. Action validée."
|
| 10 |
elif c in {"non", "n", "no"}:
|
| 11 |
+
user_choice = "Non"
|
| 12 |
+
reply = "❌ Vous avez choisi **NON**. Action annulée."
|
| 13 |
else:
|
| 14 |
+
# ignore les valeurs inattendues (évite les "choix non reconnu" parasites)
|
| 15 |
+
return chat_history, ""
|
| 16 |
+
return chat_history + [(user_choice, reply)], "" # reset du pont
|
| 17 |
|
| 18 |
+
# 2) Soumission utilisateur : réponse fixe + ouverture popup
|
| 19 |
+
def on_submit(user_msg: str, chat_history: list[tuple[str, str]]):
|
| 20 |
+
if not user_msg:
|
| 21 |
+
return chat_history, "", {}
|
| 22 |
|
| 23 |
+
assistant_reply = "Ceci est une réponse de démonstration.\n\nConfirmez-vous l’action ?"
|
| 24 |
+
chat_history = chat_history + [(user_msg, assistant_reply)]
|
| 25 |
+
|
| 26 |
+
# IIFE + retry: ouvre la popup quand showModal est prêt
|
| 27 |
+
open_popup_js = """
|
| 28 |
+
(function openWhenReady(){
|
| 29 |
+
var start = Date.now();
|
| 30 |
+
(function tick(){
|
| 31 |
+
if (typeof showModal === 'function') { showModal(); return; }
|
| 32 |
+
if (Date.now() - start < 3000) { setTimeout(tick, 60); }
|
| 33 |
+
})();
|
| 34 |
+
})();
|
| 35 |
+
"""
|
| 36 |
+
return chat_history, "", {"js": open_popup_js} # clear textbox
|
| 37 |
+
|
| 38 |
+
# 3) Injection: définit showModal/hideModal + HTML/CSS de la popup
|
| 39 |
def inject_assets():
|
| 40 |
js = r"""
|
| 41 |
function setGradioValue(elemId, value){
|
|
|
|
| 48 |
input.dispatchEvent(new Event('input', {bubbles:true}));
|
| 49 |
input.dispatchEvent(new Event('change', {bubbles:true}));
|
| 50 |
}
|
| 51 |
+
let _modalKeyHandler = null, _lastFocused = null;
|
| 52 |
+
function getFocusable(el){
|
| 53 |
+
return Array.from(el.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'))
|
| 54 |
+
.filter(e => !e.hasAttribute('disabled') && e.offsetParent !== null);
|
| 55 |
+
}
|
| 56 |
+
function showModal(){
|
| 57 |
+
const modal = document.getElementById('confirm-modal'); if(!modal) return;
|
| 58 |
+
modal.style.display = 'flex'; document.body.style.overflow = 'hidden';
|
| 59 |
+
modal.setAttribute('aria-hidden','false'); _lastFocused = document.activeElement;
|
| 60 |
+
const foc = getFocusable(modal); const first=foc[0], last=foc[foc.length-1]; if(first) first.focus();
|
| 61 |
+
_modalKeyHandler = function(e){
|
| 62 |
+
if (e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); return false; }
|
| 63 |
+
if (e.key==='Tab'){
|
| 64 |
+
if (foc.length===0){ e.preventDefault(); return; }
|
| 65 |
+
if (e.shiftKey){ if (document.activeElement===first){ e.preventDefault(); last.focus(); } }
|
| 66 |
+
else { if (document.activeElement===last){ e.preventDefault(); first.focus(); } }
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
document.addEventListener('keydown', _modalKeyHandler, true);
|
| 70 |
}
|
| 71 |
+
function hideModal(){
|
| 72 |
+
const modal = document.getElementById('confirm-modal'); if(!modal) return;
|
| 73 |
+
modal.style.display = 'none'; modal.setAttribute('aria-hidden','true'); document.body.style.overflow = '';
|
| 74 |
+
if (_modalKeyHandler){ document.removeEventListener('keydown', _modalKeyHandler, true); _modalKeyHandler=null; }
|
| 75 |
+
if (_lastFocused && typeof _lastFocused.focus==='function'){ try{ _lastFocused.focus(); } catch{} }
|
| 76 |
}
|
| 77 |
+
"""
|
| 78 |
+
css = r"""
|
| 79 |
+
#confirm-modal{ display:none; position:fixed; inset:0; z-index:9999;
|
| 80 |
+
background: rgba(0,0,0,.45); align-items:center; justify-content:center; }
|
| 81 |
+
#confirm-card{ background:#fff; color:#111; border:1px solid #ccc; border-radius:10px;
|
| 82 |
+
padding:20px; min-width:320px; max-width:90vw; box-shadow:0 10px 30px rgba(0,0,0,.2); }
|
| 83 |
#confirm-actions{ display:flex; gap:8px; justify-content:flex-end; margin-top:14px; }
|
| 84 |
+
#confirm-actions button{ cursor:pointer; padding:8px 14px; border-radius:8px; border:1px solid #bbb; background:#f4f4f5; }
|
| 85 |
+
#confirm-actions button.primary{ background:#2563eb; color:#fff; border-color:transparent; }
|
| 86 |
#bridge-choice{ display:none; } /* pont caché mais monté */
|
| 87 |
"""
|
| 88 |
html = r"""
|
| 89 |
<div id="confirm-modal" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
| 90 |
<div id="confirm-card">
|
| 91 |
<h3 id="confirm-title">Confirmer ?</h3>
|
| 92 |
+
<p>Souhaitez-vous continuer ?</p>
|
| 93 |
<div id="confirm-actions">
|
| 94 |
+
<button type="button" onclick="setGradioValue('bridge-choice','non'); hideModal()">Non</button>
|
| 95 |
+
<button type="button" class="primary" onclick="setGradioValue('bridge-choice','oui'); hideModal()">Oui</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
</div>
|
| 97 |
</div>
|
| 98 |
</div>
|
| 99 |
"""
|
| 100 |
return {"css": css, "js": js, "body_html": html}
|
| 101 |
|
| 102 |
+
# 4) UI
|
| 103 |
+
with gr.Blocks(title="Démo Chat + Popup", fill_height=True) as demo:
|
| 104 |
html_injector = HTMLInjector()
|
| 105 |
+
chat = gr.Chatbot(label="Chatbot", height=420)
|
|
|
|
| 106 |
user_in = gr.Textbox(placeholder="Tapez un message…", lines=2)
|
|
|
|
|
|
|
| 107 |
bridge_choice = gr.Textbox(visible=True, elem_id="bridge-choice") # pont oui/non
|
| 108 |
|
| 109 |
+
# A) soumission -> affiche (user, question) + ouvre popup (IIFE via HTMLInjector)
|
| 110 |
+
user_in.submit(fn=on_submit, inputs=[user_in, chat],
|
| 111 |
+
outputs=[chat, user_in, html_injector])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
# B) clic Oui/Non -> ajoute (Oui/Non, confirmation)
|
| 114 |
+
bridge_choice.change(fn=handle_choice, inputs=[bridge_choice, chat],
|
| 115 |
+
outputs=[chat, bridge_choice])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
demo.load(fn=inject_assets, inputs=None, outputs=[html_injector])
|
| 118 |
|