Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -10,8 +10,11 @@ from huggingface_hub import InferenceClient
|
|
| 10 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
# CONFIG
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
CATEGORIES = [
|
| 17 |
{"key": "alimentation", "icon": "π", "fr": "Alimentation", "en": "Nutrition"},
|
|
@@ -38,7 +41,7 @@ GUIDES = {
|
|
| 38 |
},
|
| 39 |
}
|
| 40 |
|
| 41 |
-
# Few-shot
|
| 42 |
FEWSHOTS = {
|
| 43 |
"fr": {
|
| 44 |
"alimentation": {
|
|
@@ -206,6 +209,38 @@ FEWSHOTS = {
|
|
| 206 |
},
|
| 207 |
}
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 210 |
# PROMPT + MODEL HELPERS
|
| 211 |
|
|
@@ -248,7 +283,7 @@ Tu crΓ©es des cartes-question pour parler de routines du quotidien.
|
|
| 248 |
- CatΓ©gorie: {cat['fr']} {cat['icon']}.
|
| 249 |
- Focus: {guide}
|
| 250 |
- Ton: {tone_fr}
|
| 251 |
-
-
|
| 252 |
- Style: concret, bienveillant, sans jugement.
|
| 253 |
- Interdit: conseils mΓ©dicaux, diagnostics, emojis.
|
| 254 |
|
|
@@ -269,7 +304,7 @@ You create question-cards about everyday routines.
|
|
| 269 |
- Category: {cat['en']} {cat['icon']}.
|
| 270 |
- Focus: {guide}
|
| 271 |
- Tone: {tone_en}
|
| 272 |
-
- Format: 4 questions + 2 micro-actions,
|
| 273 |
- Style: concrete, kind, non-judgmental.
|
| 274 |
- Forbidden: medical advice, diagnoses, emojis.
|
| 275 |
|
|
@@ -339,7 +374,6 @@ def normalize_output(
|
|
| 339 |
q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
|
| 340 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 341 |
|
| 342 |
-
# exactly 4 questions, 2 micro-actions
|
| 343 |
q = (q + [""] * 4)[:4]
|
| 344 |
m = (m + [""] * 2)[:2]
|
| 345 |
|
|
@@ -363,7 +397,6 @@ def normalize_output(
|
|
| 363 |
|
| 364 |
def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
| 365 |
prompt = build_prompt(lang, category_key, variant)
|
| 366 |
-
|
| 367 |
raw_text = None
|
| 368 |
try:
|
| 369 |
raw_text = model_call(prompt)
|
|
@@ -374,30 +407,92 @@ def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
|
| 374 |
|
| 375 |
if parsed:
|
| 376 |
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 377 |
-
|
| 378 |
-
# Fallback: sample from local examples so it never breaks
|
| 379 |
-
few = FEWSHOTS[lang][category_key]
|
| 380 |
-
questions_pool = few["questions"][:]
|
| 381 |
-
micro_pool = few["micro_actions"][:]
|
| 382 |
-
random.shuffle(questions_pool)
|
| 383 |
-
random.shuffle(micro_pool)
|
| 384 |
-
normalized = {
|
| 385 |
-
"category": category_key,
|
| 386 |
-
"language": lang,
|
| 387 |
-
"questions": (questions_pool + [""] * 4)[:4],
|
| 388 |
-
"micro_actions": (micro_pool + [""] * 2)[:2],
|
| 389 |
-
"tone": "fallback",
|
| 390 |
-
"safety_notes": "",
|
| 391 |
-
}
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
return {
|
| 394 |
-
"
|
| 395 |
-
"
|
| 396 |
-
"
|
|
|
|
|
|
|
|
|
|
| 397 |
}
|
| 398 |
|
|
|
|
| 399 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 400 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
|
| 403 |
CUSTOM_CSS = """
|
|
@@ -407,11 +502,11 @@ CUSTOM_CSS = """
|
|
| 407 |
--nv-border: #e5d8c7;
|
| 408 |
--nv-accent: #f38a6b;
|
| 409 |
--nv-accent-soft: #ffe4d4;
|
| 410 |
-
--nv-accent-teal: #9fcfd1;
|
| 411 |
--nv-text-main: #262626;
|
| 412 |
--nv-text-muted: #6c6459;
|
| 413 |
}
|
| 414 |
|
|
|
|
| 415 |
.gradio-container {
|
| 416 |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
| 417 |
background: radial-gradient(circle at top left, #fff6ee 0, #f7f2ec 50%, #f2ece6 100%);
|
|
@@ -421,7 +516,7 @@ CUSTOM_CSS = """
|
|
| 421 |
position: relative;
|
| 422 |
}
|
| 423 |
|
| 424 |
-
/*
|
| 425 |
@keyframes nvBlobFloat {
|
| 426 |
0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
|
| 427 |
50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
|
|
@@ -450,7 +545,7 @@ CUSTOM_CSS = """
|
|
| 450 |
animation-delay: 4s;
|
| 451 |
}
|
| 452 |
|
| 453 |
-
/* main
|
| 454 |
@keyframes nvCardIn {
|
| 455 |
0% { opacity: 0; transform: translateY(8px) scale(0.98); }
|
| 456 |
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
@@ -517,7 +612,7 @@ CUSTOM_CSS = """
|
|
| 517 |
transform: translateY(-1px);
|
| 518 |
}
|
| 519 |
|
| 520 |
-
/* button */
|
| 521 |
@keyframes nvPulse {
|
| 522 |
0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
|
| 523 |
50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(243, 138, 107, 0.40); }
|
|
@@ -539,39 +634,56 @@ CUSTOM_CSS = """
|
|
| 539 |
animation-duration: 1.4s;
|
| 540 |
}
|
| 541 |
|
| 542 |
-
/* cards */
|
| 543 |
-
@keyframes nvCardFloat {
|
| 544 |
-
0% { transform: translateY(0); }
|
| 545 |
-
50% { transform: translateY(-2px); }
|
| 546 |
-
100% { transform: translateY(0); }
|
| 547 |
-
}
|
| 548 |
.nv-card-grid {
|
| 549 |
display: grid;
|
| 550 |
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
| 551 |
gap: 10px;
|
| 552 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
.nv-card {
|
| 554 |
-
background: #ffffff;
|
| 555 |
border-radius: 18px;
|
| 556 |
padding: 10px 11px;
|
| 557 |
font-size: 0.9rem;
|
| 558 |
border: 1px solid rgba(0,0,0,0.03);
|
| 559 |
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
}
|
| 562 |
-
.nv-card
|
| 563 |
-
background: radial-gradient(circle at 0 0, #
|
| 564 |
-
border-color: #
|
| 565 |
}
|
| 566 |
-
.nv-card
|
| 567 |
-
background: radial-gradient(circle at 0 0, #
|
| 568 |
-
border-color: #
|
| 569 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
.nv-card-title {
|
| 571 |
font-size: 0.72rem;
|
| 572 |
letter-spacing: 0.14em;
|
| 573 |
text-transform: uppercase;
|
| 574 |
-
color: #
|
| 575 |
margin-bottom: 4px;
|
| 576 |
}
|
| 577 |
|
|
@@ -604,20 +716,57 @@ def _map_category(choice: str) -> str:
|
|
| 604 |
return mapping.get(choice, "alimentation")
|
| 605 |
|
| 606 |
|
| 607 |
-
def
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
|
| 613 |
-
def card_html(kind: str, title: str, body: str) -> str:
|
| 614 |
-
kind_attr = "question" if kind == "q" else "micro"
|
| 615 |
-
return f"<div class='nv-card' data-kind='{kind_attr}'><div class='nv-card-title'>{title}</div><div>{body}</div></div>"
|
| 616 |
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
-
return (*q_cards, *m_cards, out["raw_json"])
|
| 621 |
|
| 622 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 623 |
# GRADIO APP
|
|
@@ -626,6 +775,8 @@ def update_cards(lang: str, category_choice: str, variant: str):
|
|
| 626 |
with gr.Blocks(title="Neurovie β Question Studio") as demo:
|
| 627 |
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
|
| 628 |
|
|
|
|
|
|
|
| 629 |
with gr.Column(elem_classes="nv-shell"):
|
| 630 |
gr.HTML(
|
| 631 |
"""
|
|
@@ -633,7 +784,7 @@ with gr.Blocks(title="Neurovie β Question Studio") as demo:
|
|
| 633 |
<div class="nv-badge">NEUROVIE Β· FINGER</div>
|
| 634 |
<div class="nv-title">Question Studio</div>
|
| 635 |
<div class="nv-subtitle">
|
| 636 |
-
Minimal prompts for rich conversations β
|
| 637 |
</div>
|
| 638 |
</div>
|
| 639 |
"""
|
|
@@ -701,8 +852,8 @@ with gr.Blocks(title="Neurovie β Question Studio") as demo:
|
|
| 701 |
|
| 702 |
btn.click(
|
| 703 |
update_cards,
|
| 704 |
-
[lang, category, variant],
|
| 705 |
-
[q1, q2, q3, q4, m1, m2, raw_json],
|
| 706 |
)
|
| 707 |
|
| 708 |
if __name__ == "__main__":
|
|
|
|
| 10 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
# CONFIG
|
| 12 |
|
| 13 |
+
# You can hard-code the model here, or set MODEL_ID as a Space secret.
|
| 14 |
+
MODEL_ID = os.environ.get("MODEL_ID")
|
| 15 |
+
HF_TOKEN = os.environ.get("HF_TOKEN") # optional for this public model
|
| 16 |
+
|
| 17 |
+
REPO_PATH = "/data/questions.json" # where we store generated questions
|
| 18 |
|
| 19 |
CATEGORIES = [
|
| 20 |
{"key": "alimentation", "icon": "π", "fr": "Alimentation", "en": "Nutrition"},
|
|
|
|
| 41 |
},
|
| 42 |
}
|
| 43 |
|
| 44 |
+
# Few-shot + fallback pools
|
| 45 |
FEWSHOTS = {
|
| 46 |
"fr": {
|
| 47 |
"alimentation": {
|
|
|
|
| 209 |
},
|
| 210 |
}
|
| 211 |
|
| 212 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
+
# REPOSITORY HELPERS (questions only, per category)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _default_repo() -> Dict[str, List[str]]:
|
| 217 |
+
return {c["key"]: [] for c in CATEGORIES}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def load_repo() -> Dict[str, List[str]]:
|
| 221 |
+
os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True)
|
| 222 |
+
if not os.path.exists(REPO_PATH):
|
| 223 |
+
data = _default_repo()
|
| 224 |
+
with open(REPO_PATH, "w", encoding="utf-8") as f:
|
| 225 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 226 |
+
return data
|
| 227 |
+
try:
|
| 228 |
+
with open(REPO_PATH, "r", encoding="utf-8") as f:
|
| 229 |
+
data = json.load(f)
|
| 230 |
+
except Exception:
|
| 231 |
+
data = _default_repo()
|
| 232 |
+
# Ensure all categories exist
|
| 233 |
+
base = _default_repo()
|
| 234 |
+
base.update({k: v for k, v in data.items() if isinstance(v, list)})
|
| 235 |
+
return base
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def save_repo(data: Dict[str, List[str]]) -> None:
|
| 239 |
+
os.makedirs(os.path.dirname(REPO_PATH), exist_ok=True)
|
| 240 |
+
with open(REPO_PATH, "w", encoding="utf-8") as f:
|
| 241 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 245 |
# PROMPT + MODEL HELPERS
|
| 246 |
|
|
|
|
| 283 |
- CatΓ©gorie: {cat['fr']} {cat['icon']}.
|
| 284 |
- Focus: {guide}
|
| 285 |
- Ton: {tone_fr}
|
| 286 |
+
- Format: 4 questions + 2 micro-actions, 1 phrase courte chacune.
|
| 287 |
- Style: concret, bienveillant, sans jugement.
|
| 288 |
- Interdit: conseils mΓ©dicaux, diagnostics, emojis.
|
| 289 |
|
|
|
|
| 304 |
- Category: {cat['en']} {cat['icon']}.
|
| 305 |
- Focus: {guide}
|
| 306 |
- Tone: {tone_en}
|
| 307 |
+
- Format: 4 questions + 2 micro-actions, one short sentence each.
|
| 308 |
- Style: concrete, kind, non-judgmental.
|
| 309 |
- Forbidden: medical advice, diagnoses, emojis.
|
| 310 |
|
|
|
|
| 374 |
q = [str(x).strip() for x in data.get("questions", []) if str(x).strip()]
|
| 375 |
m = [str(x).strip() for x in data.get("micro_actions", []) if str(x).strip()]
|
| 376 |
|
|
|
|
| 377 |
q = (q + [""] * 4)[:4]
|
| 378 |
m = (m + [""] * 2)[:2]
|
| 379 |
|
|
|
|
| 397 |
|
| 398 |
def ai_generate(lang: str, category_key: str, variant: str) -> Dict[str, Any]:
|
| 399 |
prompt = build_prompt(lang, category_key, variant)
|
|
|
|
| 400 |
raw_text = None
|
| 401 |
try:
|
| 402 |
raw_text = model_call(prompt)
|
|
|
|
| 407 |
|
| 408 |
if parsed:
|
| 409 |
normalized = normalize_output(parsed, lang, category_key, variant)
|
| 410 |
+
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
+
# fallback: shuffle fewshots
|
| 413 |
+
few = FEWSHOTS[lang][category_key]
|
| 414 |
+
q_pool = few["questions"][:]
|
| 415 |
+
m_pool = few["micro_actions"][:]
|
| 416 |
+
random.shuffle(q_pool)
|
| 417 |
+
random.shuffle(m_pool)
|
| 418 |
return {
|
| 419 |
+
"category": category_key,
|
| 420 |
+
"language": lang,
|
| 421 |
+
"questions": (q_pool + [""] * 4)[:4],
|
| 422 |
+
"micro_actions": (m_pool + [""] * 2)[:2],
|
| 423 |
+
"tone": "fallback",
|
| 424 |
+
"safety_notes": "",
|
| 425 |
}
|
| 426 |
|
| 427 |
+
|
| 428 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 429 |
+
# MAIN LOGIC: REPO + SESSION "SEEN" QUESTIONS
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def get_questions_and_micro(
|
| 433 |
+
lang: str,
|
| 434 |
+
category_key: str,
|
| 435 |
+
variant: str,
|
| 436 |
+
seen: List[str],
|
| 437 |
+
) -> Dict[str, Any]:
|
| 438 |
+
"""
|
| 439 |
+
1. Load /data/questions.json
|
| 440 |
+
2. If repo has >=4 unseen questions -> sample 4 from repo, micro from local pool.
|
| 441 |
+
3. Else -> call AI, store any new questions into repo, use AI's questions + micro.
|
| 442 |
+
4. Update seen list so this session won't see the same question twice.
|
| 443 |
+
"""
|
| 444 |
+
seen_set = set(seen or [])
|
| 445 |
+
repo = load_repo()
|
| 446 |
+
repo_qs = repo.get(category_key, [])
|
| 447 |
+
|
| 448 |
+
unseen_repo = [q for q in repo_qs if q and q not in seen_set]
|
| 449 |
+
|
| 450 |
+
used_ai = False
|
| 451 |
+
|
| 452 |
+
if len(unseen_repo) >= 4:
|
| 453 |
+
# entirely from repo, no AI call
|
| 454 |
+
random.shuffle(unseen_repo)
|
| 455 |
+
questions = unseen_repo[:4]
|
| 456 |
+
# micro-actions from local fewshot pool (cheap)
|
| 457 |
+
m_pool = FEWSHOTS[lang][category_key]["micro_actions"][:]
|
| 458 |
+
random.shuffle(m_pool)
|
| 459 |
+
micro = (m_pool + ["", ""])[:2]
|
| 460 |
+
else:
|
| 461 |
+
# need fresh AI content
|
| 462 |
+
ai_out = ai_generate(lang, category_key, variant)
|
| 463 |
+
questions = ai_out["questions"]
|
| 464 |
+
micro = ai_out["micro_actions"]
|
| 465 |
+
used_ai = True
|
| 466 |
+
|
| 467 |
+
# store new questions in repo
|
| 468 |
+
new_qs = [q for q in questions if q and q not in repo_qs]
|
| 469 |
+
if new_qs:
|
| 470 |
+
repo_qs_extended = repo_qs + new_qs
|
| 471 |
+
repo[category_key] = repo_qs_extended
|
| 472 |
+
save_repo(repo)
|
| 473 |
+
|
| 474 |
+
# update seen for this session
|
| 475 |
+
for q in questions:
|
| 476 |
+
if q:
|
| 477 |
+
seen_set.add(q)
|
| 478 |
+
|
| 479 |
+
payload = {
|
| 480 |
+
"category": category_key,
|
| 481 |
+
"language": lang,
|
| 482 |
+
"questions": questions,
|
| 483 |
+
"micro_actions": micro,
|
| 484 |
+
"source": "ai" if used_ai else "repo",
|
| 485 |
+
}
|
| 486 |
+
return {
|
| 487 |
+
"questions": questions,
|
| 488 |
+
"micro_actions": micro,
|
| 489 |
+
"raw_json": json.dumps(payload, ensure_ascii=False, indent=2),
|
| 490 |
+
"seen": list(seen_set),
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 495 |
+
# UI β pastel, animated, color-coded cards
|
| 496 |
|
| 497 |
|
| 498 |
CUSTOM_CSS = """
|
|
|
|
| 502 |
--nv-border: #e5d8c7;
|
| 503 |
--nv-accent: #f38a6b;
|
| 504 |
--nv-accent-soft: #ffe4d4;
|
|
|
|
| 505 |
--nv-text-main: #262626;
|
| 506 |
--nv-text-muted: #6c6459;
|
| 507 |
}
|
| 508 |
|
| 509 |
+
/* page */
|
| 510 |
.gradio-container {
|
| 511 |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
| 512 |
background: radial-gradient(circle at top left, #fff6ee 0, #f7f2ec 50%, #f2ece6 100%);
|
|
|
|
| 516 |
position: relative;
|
| 517 |
}
|
| 518 |
|
| 519 |
+
/* floating blobs */
|
| 520 |
@keyframes nvBlobFloat {
|
| 521 |
0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.6; }
|
| 522 |
50% { transform: translate3d(10px, -8px, 0) scale(1.04); opacity: 0.9; }
|
|
|
|
| 545 |
animation-delay: 4s;
|
| 546 |
}
|
| 547 |
|
| 548 |
+
/* main panel */
|
| 549 |
@keyframes nvCardIn {
|
| 550 |
0% { opacity: 0; transform: translateY(8px) scale(0.98); }
|
| 551 |
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
|
|
| 612 |
transform: translateY(-1px);
|
| 613 |
}
|
| 614 |
|
| 615 |
+
/* button (breathing) */
|
| 616 |
@keyframes nvPulse {
|
| 617 |
0% { transform: translateY(0) scale(1); box-shadow: 0 10px 22px rgba(243, 138, 107, 0.30); }
|
| 618 |
50% { transform: translateY(-1px) scale(1.02); box-shadow: 0 14px 26px rgba(243, 138, 107, 0.40); }
|
|
|
|
| 634 |
animation-duration: 1.4s;
|
| 635 |
}
|
| 636 |
|
| 637 |
+
/* cards grid */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
.nv-card-grid {
|
| 639 |
display: grid;
|
| 640 |
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
| 641 |
gap: 10px;
|
| 642 |
}
|
| 643 |
+
|
| 644 |
+
/* deal animation */
|
| 645 |
+
@keyframes nvCardDeal {
|
| 646 |
+
0% { opacity: 0; transform: translate3d(0, 0, 0) scale(0.7); }
|
| 647 |
+
100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
|
| 648 |
+
}
|
| 649 |
.nv-card {
|
|
|
|
| 650 |
border-radius: 18px;
|
| 651 |
padding: 10px 11px;
|
| 652 |
font-size: 0.9rem;
|
| 653 |
border: 1px solid rgba(0,0,0,0.03);
|
| 654 |
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
|
| 655 |
+
opacity: 0;
|
| 656 |
+
transform: scale(0.7);
|
| 657 |
+
animation: nvCardDeal 420ms ease-out forwards;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
/* color-coded by category */
|
| 661 |
+
.nv-card--cat-alimentation {
|
| 662 |
+
background: radial-gradient(circle at 0 0, #e3f7df 0, #ffffff 60%);
|
| 663 |
+
border-color: #c3e5bc;
|
| 664 |
}
|
| 665 |
+
.nv-card--cat-mouvement {
|
| 666 |
+
background: radial-gradient(circle at 0 0, #dbeeff 0, #ffffff 60%);
|
| 667 |
+
border-color: #b9d8ff;
|
| 668 |
}
|
| 669 |
+
.nv-card--cat-cerveau {
|
| 670 |
+
background: radial-gradient(circle at 0 0, #ffd6d6 0, #ffffff 60%);
|
| 671 |
+
border-color: #f7b6b6;
|
| 672 |
}
|
| 673 |
+
.nv-card--cat-liens {
|
| 674 |
+
background: radial-gradient(circle at 0 0, #ffead1 0, #ffffff 60%);
|
| 675 |
+
border-color: #f6d2a3;
|
| 676 |
+
}
|
| 677 |
+
.nv-card--cat-bien-etre {
|
| 678 |
+
background: radial-gradient(circle at 0 0, #ffe1f2 0, #ffffff 60%);
|
| 679 |
+
border-color: #f3c3e4;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
.nv-card-title {
|
| 683 |
font-size: 0.72rem;
|
| 684 |
letter-spacing: 0.14em;
|
| 685 |
text-transform: uppercase;
|
| 686 |
+
color: #7c6a9a;
|
| 687 |
margin-bottom: 4px;
|
| 688 |
}
|
| 689 |
|
|
|
|
| 716 |
return mapping.get(choice, "alimentation")
|
| 717 |
|
| 718 |
|
| 719 |
+
def _card_html(category_key: str, kind: str, title: str, body: str, delay_s: float) -> str:
|
| 720 |
+
kind_attr = "question" if kind == "q" else "micro"
|
| 721 |
+
cat_class = f"nv-card--cat-{category_key}"
|
| 722 |
+
# each card has its own animation delay => "dealing" feel
|
| 723 |
+
return (
|
| 724 |
+
f"<div class='nv-card {cat_class}' data-kind='{kind_attr}' "
|
| 725 |
+
f"style='animation-delay:{delay_s:.2f}s'><div class='nv-card-title'>{title}</div>"
|
| 726 |
+
f"<div>{body}</div></div>"
|
| 727 |
+
)
|
| 728 |
|
|
|
|
|
|
|
|
|
|
| 729 |
|
| 730 |
+
def update_cards(lang: str, category_choice: str, variant: str, seen: List[str]):
|
| 731 |
+
category_key = _map_category(category_choice)
|
| 732 |
+
result = get_questions_and_micro(lang, category_key, variant, seen or [])
|
| 733 |
+
questions = result["questions"]
|
| 734 |
+
micro = result["micro_actions"]
|
| 735 |
+
raw_json = result["raw_json"]
|
| 736 |
+
new_seen = result["seen"]
|
| 737 |
+
|
| 738 |
+
# Stagger cards a bit
|
| 739 |
+
delays_q = [0.05, 0.10, 0.15, 0.20]
|
| 740 |
+
delays_m = [0.25, 0.30]
|
| 741 |
+
|
| 742 |
+
q_htmls = []
|
| 743 |
+
for i in range(4):
|
| 744 |
+
text = questions[i] if i < len(questions) else ""
|
| 745 |
+
q_htmls.append(
|
| 746 |
+
_card_html(
|
| 747 |
+
category_key,
|
| 748 |
+
"q",
|
| 749 |
+
f"Question {i+1}",
|
| 750 |
+
text,
|
| 751 |
+
delays_q[i],
|
| 752 |
+
)
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
m_htmls = []
|
| 756 |
+
for i in range(2):
|
| 757 |
+
text = micro[i] if i < len(micro) else ""
|
| 758 |
+
m_htmls.append(
|
| 759 |
+
_card_html(
|
| 760 |
+
category_key,
|
| 761 |
+
"m",
|
| 762 |
+
f"Micro-action {i+1}",
|
| 763 |
+
text,
|
| 764 |
+
delays_m[i],
|
| 765 |
+
)
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
return (*q_htmls, *m_htmls, raw_json, new_seen)
|
| 769 |
|
|
|
|
| 770 |
|
| 771 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 772 |
# GRADIO APP
|
|
|
|
| 775 |
with gr.Blocks(title="Neurovie β Question Studio") as demo:
|
| 776 |
gr.HTML(f"<style>{CUSTOM_CSS}</style>")
|
| 777 |
|
| 778 |
+
seen_state = gr.State([]) # per-session list of seen questions
|
| 779 |
+
|
| 780 |
with gr.Column(elem_classes="nv-shell"):
|
| 781 |
gr.HTML(
|
| 782 |
"""
|
|
|
|
| 784 |
<div class="nv-badge">NEUROVIE Β· FINGER</div>
|
| 785 |
<div class="nv-title">Question Studio</div>
|
| 786 |
<div class="nv-subtitle">
|
| 787 |
+
Minimal prompts for rich conversations β 4 questions and 2 micro-actions par tirage.
|
| 788 |
</div>
|
| 789 |
</div>
|
| 790 |
"""
|
|
|
|
| 852 |
|
| 853 |
btn.click(
|
| 854 |
update_cards,
|
| 855 |
+
[lang, category, variant, seen_state],
|
| 856 |
+
[q1, q2, q3, q4, m1, m2, raw_json, seen_state],
|
| 857 |
)
|
| 858 |
|
| 859 |
if __name__ == "__main__":
|