Spaces:
Sleeping
Sleeping
File size: 29,270 Bytes
4221702 57bdb0e 4221702 57bdb0e 2124c30 4221702 57bdb0e 66306a1 57bdb0e 8ac8a6b 4221702 57bdb0e d9205dd 4221702 bfd0050 600dc0c 4221702 588066d 4221702 0094b41 4221702 0094b41 4221702 600dc0c 0094b41 78d67d8 0094b41 78d67d8 0094b41 4221702 d9205dd 4221702 2124c30 4221702 0014db3 2124c30 4221702 c83ed77 4221702 bfd0050 c83ed77 a9a4693 4221702 bcd9b0f 4221702 bcd9b0f 600dc0c 4221702 bfd0050 4221702 f840dc6 a9a4693 2124c30 4221702 a9a4693 f840dc6 4221702 f840dc6 4221702 21e7831 4221702 2124c30 4221702 bb3e5e4 4221702 2124c30 4221702 2124c30 20b0397 4221702 20b0397 4221702 2124c30 4221702 2124c30 4221702 8ac8a6b 4221702 a94d0c1 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 e230cce 4221702 bb3e5e4 4221702 2124c30 4221702 d187cb3 4221702 8ac8a6b d187cb3 4221702 d187cb3 57bdb0e d187cb3 2124c30 4221702 2124c30 d187cb3 4221702 2124c30 4221702 d187cb3 4221702 2124c30 d187cb3 4221702 2124c30 4221702 d187cb3 2124c30 d187cb3 4221702 d187cb3 57bdb0e 2124c30 d187cb3 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 8ac8a6b 4221702 2124c30 4221702 2124c30 4221702 ba7973e 4221702 ba7973e 4221702 bfd0050 f194a79 2124c30 4221702 f194a79 588066d f194a79 2124c30 4221702 1d82f0c 4221702 1d82f0c 4221702 2124c30 f194a79 4221702 f194a79 4221702 f194a79 6a4203f f194a79 4221702 f194a79 2124c30 f194a79 66306a1 4221702 f194a79 66306a1 6a4203f 66306a1 f194a79 2124c30 66306a1 2124c30 4221702 66306a1 f194a79 2124c30 4221702 f194a79 4221702 2124c30 4221702 f194a79 4221702 2124c30 f194a79 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 4221702 2124c30 66306a1 2124c30 4221702 66306a1 2124c30 4221702 2124c30 4221702 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | # app_bilingual_parts.py
"""
Streamlit First Aid Assistant — Bilingual (EN/VN)
Single-file app divided into 3 labeled parts below.
Drop this into your Hugging Face Space as `app.py` or merge into your existing file.
PART 1: imports, session flags, i18n, small helpers
PART 2: Firebase/data rendering and simple config checks
PART 3: OpenAI integration, forms, saving victim info, chat UI
"""
# ------------------
# PART 1: Imports, session flags, translations, helpers
# ------------------
import os
import json
import traceback
from datetime import datetime
from pathlib import Path
import streamlit as st
# Optional imports that may fail at runtime; we handle missing packages gracefully
try:
import pyrebase
except Exception:
pyrebase = None
# Session defaults
if "firebase_record_id" not in st.session_state:
st.session_state.firebase_record_id = None
if "victim_saved" not in st.session_state:
st.session_state.victim_saved = False
if "basic_done" not in st.session_state:
st.session_state.basic_done = False
# ------------------
# Simple i18n (EN / VN)
# ------------------
LANGS = {"en": "English", "vn": "Tiếng Việt"}
if "lang" not in st.session_state:
st.session_state.lang = os.getenv("DEFAULT_LANG", "en")
# Short translations table. Add keys as you expand the app.
TRANSLATIONS = {
"sidebar_title": {"en": "🎓 Quick Guide", "vn": "🎓 Hướng dẫn nhanh"},
"capture_line": {
"en": "Capture: Turn on edge kit → Capture. Take clear close-up (snake photo only if safe).",
"vn": "Chụp: Bật edge kit → Chụp. Chụp cận (ảnh rắn chỉ khi an toàn).",
},
"first_aid_line": {
"en": "First aid: Start top 3 actions immediately (call, immobilize, stay calm).",
"vn": "Sơ cứu: Bắt đầu 3 hành động chính ngay (gọi, cố định, giữ bình tĩnh).",
},
"symptoms_line": {
"en": "Symptoms: Optional — tick checklist, add age/weight/time, then submit.",
"vn": "Triệu chứng: Tùy chọn — chọn danh sách, thêm tuổi/cân nặng/thời gian rồi gửi.",
},
"monitor_line": {
"en": "Monitor: Watch status page, answer prompts, follow guidance.",
"vn": "Giám sát: Xem trang trạng thái, trả lời câu hỏi, theo hướng dẫn.",
},
"escalation_line": {
"en": "Escalation: High-risk cases notify hospitals automatically.",
"vn": "Tăng cường: Các ca nguy cơ cao sẽ tự động thông báo bệnh viện.",
},
"emergency_call": {
"en": "**Emergency:** If the victim collapses or cannot breathe, **CALL EMERGENCY SERVICES NOW**.",
"vn": "**Khẩn cấp:** Nếu nạn nhân ngã hoặc không thở, **GỌI CẤP CỨU NGAY**.",
},
# UI labels used later
"save_info": {"en": "💾 Save info & get advanced guidance", "vn": "💾 Lưu & nhận hướng dẫn nâng cao"},
"emergency_btn": {"en": "🚨 Emergency — Call local services", "vn": "🚨 Khẩn cấp — Gọi dịch vụ địa phương"},
"ask_btn": {"en": "Ask", "vn": "Hỏi"},
"assistant_guidance": {"en": "Assistant guidance:", "vn": "Hướng dẫn trợ lý:"},
"victim_name": {"en": "Victim name (optional)", "vn": "Tên nạn nhân (không bắt buộc)"},
"phone": {"en": "Phone number", "vn": "Số điện thoại"},
"age": {"en": "Age (years)", "vn": "Tuổi (năm)"},
"no_firebase": {"en": "Firebase configuration not found in secrets.toml", "vn": "Không tìm thấy cấu hình Firebase trong secrets.toml"},
"no_data": {"en": "No valid data found in Firebase.", "vn": "Không tìm thấy dữ liệu hợp lệ trong Firebase."},
"weight": {"en": "Weight (kg)", "vn": "Cân Nặng (kg)"},
"height": {"en": "Height (cm)", "vn": "Chiều Cao (cm)"},
"weight_help": {"en": "Enter a number or leave blank", "vn": "Nhập số hoặc để trống"},
"height_help": {"en": "Enter a number or leave blank", "vn": "Nhập số hoặc để trống"},
}
def t(key: str, **fmt):
"""Translate key for current language; fallback to English or the key itself."""
lang = st.session_state.get("lang", "en")
entry = TRANSLATIONS.get(key, {})
text = entry.get(lang) or entry.get("en") or key
if fmt:
try:
return text.format(**fmt)
except Exception:
return text
return text
# Sidebar language chooser + compact guide
with st.sidebar:
st.selectbox(
"Language / Ngôn ngữ",
options=list(LANGS.keys()),
format_func=lambda k: LANGS[k],
key="lang",
help="Choose UI language"
)
st.sidebar.title(t("sidebar_title"))
st.sidebar.markdown(
f"## Quick steps\n"
f"- {t('capture_line')}\n"
f"- {t('first_aid_line')}\n"
f"- {t('symptoms_line')}\n"
f"- {t('monitor_line')}\n"
f"- {t('escalation_line')}\n\n"
f"---\n{t('emergency_call')}"
)
# ===============================
# ⭐ Evaluation button (NEW)
# ===============================
st.sidebar.markdown("---")
eval_label = (
"⭐ Give Feedback"
if st.session_state.lang == "en"
else "⭐ Gửi phản hồi"
)
st.sidebar.link_button(
eval_label,
"https://docs.google.com/forms/d/e/1FAIpQLSfGD5MlrP9VMCXh_QxVtss68is4k_aRBjegjQsLc1NVEwckqg/viewform?usp=header",
use_container_width=True,
)
# ------------------
# PART 2: Paths, simple first-aid content (localized), Firebase setup, read/render functions
# ------------------
APP_DIR = Path.cwd()
DATA_DIR = APP_DIR / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DATA_FILE = DATA_DIR / "victims.json" # local fallback / cache
# Localized step lists
SNAKEBITE_STEPS = {
"en": [
"Keep the victim calm and still — limit movement.",
"Call emergency services immediately.",
"Position the affected limb at or slightly below heart level.",
"Remove tight clothing or jewelry near the bite area.",
"Clean the wound gently with soap and water; do NOT cut or suck the bite.",
"Cover with a clean, loose dressing. Monitor breathing and consciousness.",
],
"vn": [
"Giữ nạn nhân bình tĩnh và cố định — hạn chế di chuyển.",
"Gọi cấp cứu ngay lập tức.",
"Đặt chi bị thương ngang hoặc thấp hơn tim một chút.",
"Tháo quần áo chật hoặc trang sức gần vết cắn.",
"Rửa nhẹ vết thương bằng xà phòng và nước; KHÔNG cắt hoặc hút vết cắn.",
"Băng lỏng vết thương sạch. Giám sát hô hấp và ý thức.",
],
}
NORMAL_WOUND_STEPS = {
"en": [
"Wash your hands if possible.",
"Apply gentle pressure with a clean cloth to stop bleeding.",
"Rinse the wound with clean water (do not use strong antiseptics directly).",
"Apply an antiseptic wipe or ointment if available.",
"Cover with a sterile dressing or bandage.",
"If bleeding continues or wound is deep, seek medical help.",
],
"vn": [
"Rửa tay nếu có thể.",
"Dùng khăn sạch ép nhẹ để cầm máu.",
"Rửa vết thương bằng nước sạch (không dùng chất tẩy mạnh trực tiếp).",
"Dùng khăn sát trùng hoặc thuốc mỡ nếu có.",
"Băng vết thương bằng băng sạch.",
"Nếu chảy máu không dứt hoặc vết thương sâu, đi khám y tế.",
],
}
def get_steps_localized(wound_type: str):
lang = st.session_state.get("lang", "en")
wound_type_norm = (wound_type or "").strip().lower()
if wound_type_norm in ("snakebite", "poisoned"):
return SNAKEBITE_STEPS.get(lang, SNAKEBITE_STEPS["en"])
return NORMAL_WOUND_STEPS.get(lang, NORMAL_WOUND_STEPS["en"])
# Firebase init (uses st.secrets if present)
firebase = None
db = None
if pyrebase and "firebase" in st.secrets:
try:
fb_cfg = st.secrets["firebase"]
firebase_config = {
"apiKey": fb_cfg.get("apiKey"),
"authDomain": fb_cfg.get("authDomain"),
"projectId": fb_cfg.get("projectId"),
"storageBucket": fb_cfg.get("storageBucket"),
"messagingSenderId": fb_cfg.get("messagingSenderId"),
"appId": fb_cfg.get("appId"),
"measurementId": fb_cfg.get("measurementId"),
"databaseURL": fb_cfg.get("databaseURL"),
}
firebase = pyrebase.initialize_app(firebase_config)
db = firebase.database()
except Exception as e:
st.warning(f"Firebase init error: {e}")
else:
if not pyrebase:
st.info("pyrebase not installed; Firebase disabled.")
else:
st.info(t("no_firebase"))
# Read latest wound data from Firebase (safe fallback to local file)
def get_latest_wound_data():
# Try Firebase first
if db:
try:
data = db.child("data").get().val()
if data and isinstance(data, dict):
latest_key = list(data.keys())[-1]
latest_data = data[latest_key]
if isinstance(latest_data, dict):
return latest_key, latest_data
except Exception as e:
st.warning(f"Firebase read error: {e}")
# Fallback: read local cache file (if exists)
if DATA_FILE.exists():
try:
all_v = json.loads(DATA_FILE.read_text(encoding="utf-8") or "{}")
if isinstance(all_v, dict):
# return last inserted
k = list(all_v.keys())[-1] if all_v else None
return (k, all_v[k]) if k else (None, None)
except Exception as e:
st.warning(f"Local cache read error: {e}")
return None, None
def render_latest_firebase_data(show_toast=False):
if show_toast:
try:
st.toast("Refreshing data...", icon="🔁")
except Exception:
pass
record_id, data = get_latest_wound_data()
if not record_id or not data:
st.warning(t("no_data"))
return
st.session_state.firebase_record_id = record_id
st.session_state.victim_saved = False
st.subheader("📡 Latest record")
st.json(data)
st.markdown("---")
wound_type = str(data.get("Wound", data.get("wound", ""))).strip().lower()
st.session_state.wound_type = wound_type
st.session_state.basic_done = True
steps = get_steps_localized(wound_type)
if wound_type in ("snakebite", "poisoned"):
st.subheader("🐍 " + ("Snakebite" if st.session_state.lang == "en" else "Rắn cắn"))
else:
st.subheader("🩹 " + ("Normal wound" if st.session_state.lang == "en" else "Vết thương thông thường"))
st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(steps)]))
# ------------------
# PART 3: OpenAI integration, save, forms, chat UI
# ------------------
# OpenAI config from secrets (Spaces: set in Secrets panel)
OPENAI_api_key = st.secrets.get("OPENAI_API_KEY")
OPENAI_model = st.secrets.get("OPENAI_MODEL", "gpt-4o-mini")
# Minimal robust client creation supporting modern + legacy SDKs
def make_openai_client(api_key):
if not api_key:
return None, "OpenAI API key not set. Set OPENAI_API_KEY in secrets."
try:
from openai import OpenAI
client = OpenAI(api_key=api_key)
return client, None
except Exception:
try:
import openai
openai.api_key = api_key
return openai, None
except Exception as e:
return None, f"Failed to create OpenAI client: {e}"
# Simple extractor (keeps many shapes)
def extract_text_from_response(resp):
try:
if resp is None:
return ""
if isinstance(resp, str):
return resp.strip()
# dict-like shapes
if isinstance(resp, dict):
# responses API often has 'output' or 'choices'
if "output_text" in resp and isinstance(resp["output_text"], str):
return resp["output_text"].strip()
if "choices" in resp and isinstance(resp["choices"], list) and resp["choices"]:
c = resp["choices"][0]
if isinstance(c, dict):
m = c.get("message") or c
if isinstance(m, dict) and isinstance(m.get("content"), str):
return m["content"].strip()
if isinstance(m.get("text"), str):
return m.get("text").strip()
# responses API: 'output' might be list
if "output" in resp:
out = resp["output"]
if isinstance(out, list) and out:
first = out[0]
if isinstance(first, dict):
c = first.get("content") or first.get("text")
if isinstance(c, list) and c:
f = c[0]
if isinstance(f, dict) and isinstance(f.get("text"), str):
return f.get("text").strip()
if isinstance(c, str):
return c.strip()
# objects with attributes
for attr in ("output_text", "text", "content"):
if hasattr(resp, attr):
val = getattr(resp, attr)
if isinstance(val, str):
return val.strip()
try:
return str(val).strip()
except Exception:
pass
return str(resp).strip()
except Exception:
try:
return str(resp)
except Exception:
return ""
def normalize_assistant_text(x):
if x is None:
return ""
if isinstance(x, str):
return x.strip()
try:
return extract_text_from_response(x)
except Exception:
return str(x)
# Query wrapper: tries modern chat, responses, legacy shapes
def query_openai(system_prompt: str, user_message: str, max_tokens=800, temperature=0.2):
"""
Robust OpenAI query that supports:
- modern client.chat.create(...)
- legacy openai.ChatCompletion.create(...)
- client.responses.create(...) (tries several kwarg combinations)
- older Completion.create(...)
Returns {"text": "..."} on success or {"error": "..."} on failure.
"""
client, err = make_openai_client(OPENAI_api_key)
if err:
return {"error": err}
model = OPENAI_model or "gpt-4o-mini"
try:
# 1) Modern OpenAI SDK: client.chat.create(...)
if hasattr(client, "chat") and hasattr(client.chat, "create"):
resp = client.chat.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
temperature=temperature,
max_tokens=max_tokens,
)
return {"text": extract_text_from_response(resp)}
# 2) Legacy openai package: ChatCompletion.create(...)
if hasattr(client, "ChatCompletion") and hasattr(client.ChatCompletion, "create"):
resp = client.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
temperature=temperature,
max_tokens=max_tokens,
)
return {"text": extract_text_from_response(resp)}
# 3) Responses API: client.responses.create(...)
if hasattr(client, "responses") and hasattr(client.responses, "create"):
prompt = system_prompt + "\n\n" + user_message
response = None
last_exc = None
# Try several kwarg shapes because different SDK versions accept different names
candidate_kw_sets = [
{"model": model, "input": prompt, "max_output_tokens": max_tokens, "temperature": temperature},
{"model": model, "input": prompt, "max_tokens": max_tokens, "temperature": temperature},
{"model": model, "input": prompt, "temperature": temperature},
{"model": model, "input": prompt},
]
for kw in candidate_kw_sets:
try:
response = client.responses.create(**kw)
break
except TypeError as te:
# argument mismatch for this kw set — try next
last_exc = te
continue
except Exception as e:
# other runtime errors (auth/quota) -> return as error
return {"error": f"OpenAI responses request failed: {e}"}
if response is None:
return {"error": f"OpenAI responses client didn't accept tried args. Last error: {last_exc}"}
return {"text": extract_text_from_response(response)}
# 4) Very old Completion API fallback
if hasattr(client, "Completion") and hasattr(client.Completion, "create"):
prompt = system_prompt + "\n\n" + user_message
resp = client.Completion.create(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature)
return {"text": extract_text_from_response(resp)}
return {"error": "OpenAI client present but no compatible chat method found. Consider upgrading the openai package."}
except Exception as e:
return {"error": f"OpenAI request failed: {e}"}
# System prompt builder that respects UI language
def make_system_prompt(wound_type: str, victim_info: dict) -> str:
lang = st.session_state.get("lang", "en")
lang_instruction = {"en": "Please reply in English.", "vn": "Vui lòng trả lời bằng tiếng Việt."}
base = (
f"You are an expert first aid assistant. The victim has wound_type={wound_type}.\n"
"- Give clear, actionable, short steps that a non-professional can follow.\n"
"- Mention when to call emergency services or go to hospital.\n"
f"- Use the victim info below to adjust guidance:\n{json.dumps(victim_info, indent=2)}\n"
"Keep language concise. Use numbered short steps when appropriate.\n"
)
return base + lang_instruction.get(lang, "Please reply in English.")
# Save victim info to Firebase (requires db initialized)
def save_victim_info(victim: dict):
if not db:
# Fallback: append to local DATA_FILE
try:
all_v = json.loads(DATA_FILE.read_text(encoding="utf-8") or "{}")
except Exception:
all_v = {}
record_id = datetime.utcnow().strftime("%Y%m%d%H%M%S")
all_v[record_id] = victim
DATA_FILE.write_text(json.dumps(all_v, ensure_ascii=False, indent=2), encoding="utf-8")
return victim
record_id = st.session_state.get("firebase_record_id")
if not record_id:
# If we don't have an id, generate one under victims
record_id = datetime.utcnow().strftime("%Y%m%d%H%M%S")
victim_data = victim.copy()
victim_data["timestamp"] = datetime.utcnow().isoformat() + "Z"
try:
db.child("victims").child(record_id).update(victim_data)
return victim_data
except Exception as e:
raise RuntimeError(f"Failed to save victim info: {e}")
# ------------------
# Streamlit UI & Flow
# ------------------
st.title("🩺 First Aid Assistant — Firebase Connected (EN/VN)")
# Auto display latest on load
render_latest_firebase_data()
st.markdown("---")
if st.button("🔄 " + ("Refresh Firebase data" if st.session_state.lang == "en" else "Làm mới dữ liệu Firebase")):
render_latest_firebase_data(show_toast=True)
# -----------------------
# Advanced assistance block (fixed: adds weight/height, VN/EN, saves to victim profile)
# -----------------------
if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
st.header("Advanced assistance")
st.markdown(f"**Wound type:** `{st.session_state.wound_type}`")
with st.form("victim_form", clear_on_submit=True):
st.subheader(t("victim_name"))
name = st.text_input(t("victim_name"))
phone = st.text_input(t("phone"),
help=("Emergency contact number" if st.session_state.lang == "en" else "Số liên hệ khẩn cấp"))
age = st.text_input(t("age"),
help=("Enter an integer or leave blank" if st.session_state.lang == "en" else "Nhập số nguyên hoặc để trống"))
# new localized weight / height fields
weight = st.text_input(t("weight"), help=t("weight_help"))
height = st.text_input(t("height"), help=t("height_help"))
# localized notes label (falls back if t("notes") missing)
notes_label = t("notes") if "notes" in TRANSLATIONS else (
"Notes (allergies, meds, conscious status)" if st.session_state.lang == "en"
else "Ghi chú thêm (dị ứng, thuốc đang dùng, tình trạng tỉnh táo)"
)
notes = st.text_area(notes_label, max_chars=500)
col1, col2 = st.columns(2)
with col1:
submit = st.form_submit_button(t("save_info"))
with col2:
emergency_submit = st.form_submit_button(t("emergency_btn"))
# common validation (now parses weight & height as floats)
def validate_fields(age_str, phone_str, weight_str, height_str):
age_v = None
weight_v = None
height_v = None
# age
if age_str and age_str.strip():
try:
age_v = int(age_str.strip())
except Exception:
st.error(("Age must be an integer (or leave blank)." if st.session_state.lang == "en"
else "Tuổi phải là số nguyên (hoặc để trống)."))
st.stop()
# weight
if weight_str and weight_str.strip():
try:
weight_v = float(weight_str.strip())
except Exception:
st.error(("Weight must be a number (kg) or leave blank." if st.session_state.lang == "en"
else "Cân nặng phải là số (kg) hoặc để trống."))
st.stop()
# height
if height_str and height_str.strip():
try:
height_v = float(height_str.strip())
except Exception:
st.error(("Height must be a number (cm) or leave blank." if st.session_state.lang == "en"
else "Chiều cao phải là số (cm) hoặc để trống."))
st.stop()
# phone
phone_clean = phone_str.strip()
if phone_clean and not phone_clean.replace("+", "").isdigit():
st.error(("Phone number should contain digits only." if st.session_state.lang == "en"
else "Số điện thoại chỉ chứa chữ số."))
st.stop()
return age_v, weight_v, height_v, phone_clean
if (submit or emergency_submit) and not st.session_state.victim_saved:
# validate including weight/height
age_v, weight_v, height_v, phone_clean = validate_fields(age, phone, weight, height)
victim = {
"name": name or "unknown",
"phone": phone_clean or None,
"age": age_v,
"weight_kg": weight_v,
"height_cm": height_v,
"notes": notes or "",
"wound_type": st.session_state.wound_type,
"saved_at": datetime.utcnow().isoformat() + "Z",
"emergency": bool(emergency_submit),
}
try:
record = save_victim_info(victim)
st.session_state.last_saved_record = record
st.session_state.victim_saved = True
if emergency_submit:
st.error(("EMERGENCY recorded. Call local emergency services now." if st.session_state.lang == "en"
else "Đã ghi nhận KHẨN CẤP. Gọi dịch vụ khẩn cấp ngay."))
else:
st.success(("Victim info saved." if st.session_state.lang == "en" else "Đã lưu thông tin nạn nhân."))
except Exception as e:
st.error(str(e))
st.write(traceback.format_exc())
# For non-emergency: ask OpenAI for guidance
if submit and st.session_state.victim_saved:
system_prompt = make_system_prompt(st.session_state.wound_type, victim)
initial_user_msg = (
"The patient has completed the basic steps. Provide the next, in-depth first aid guidance and any monitoring steps. Keep guidance concise and list immediate priorities first."
if st.session_state.lang == "en"
else "Bệnh nhân đã hoàn thành các bước cơ bản. Cung cấp hướng dẫn sơ cứu chi tiết tiếp theo và các bước giám sát. Giữ gọn và nêu ưu tiên trước."
)
with st.spinner(("Contacting assistant..." if st.session_state.lang == "en" else "Đang liên hệ trợ lý...")):
resp = query_openai(system_prompt, initial_user_msg)
if "error" in resp:
st.error(f"Assistant error: {resp['error']}")
else:
assistant_text = normalize_assistant_text(resp.get("text"))
st.session_state.assistant_text = assistant_text
st.session_state.chat_history = [{"role": "assistant", "text": assistant_text}]
st.markdown("**" + t("assistant_guidance") + "**")
st.text_area(t("assistant_guidance"), value=assistant_text, height=250, key="assistant_output")
# Chat follow-ups
st.subheader(("Ask follow-up questions" if st.session_state.lang == "en" else "Hỏi thêm"))
st.markdown(("Ask the assistant further questions related to the victim or care. Keep questions concise." if st.session_state.lang == "en" else "Hỏi trợ lý các câu liên quan tới nạn nhân hoặc chăm sóc. Giữ câu hỏi ngắn."))
col_in, col_btn = st.columns([4, 1])
with col_in:
user_q = st.text_input(("Your question" if st.session_state.lang == "en" else "Câu hỏi của bạn"), key="chat_input")
with col_btn:
if st.button(t("ask_btn")):
if not user_q or user_q.strip() == "":
st.warning(("Type a question first." if st.session_state.lang == "en" else "Nhập câu hỏi trước."))
else:
victim_info = st.session_state.get("last_saved_record") or {}
system_prompt = make_system_prompt(st.session_state.wound_type, victim_info)
with st.spinner(("Assistant is replying..." if st.session_state.lang == "en" else "Trợ lý đang trả lời...")):
resp = query_openai(system_prompt, user_q)
if "error" in resp:
st.error(f"Assistant error: {resp['error']}")
else:
assistant_reply = normalize_assistant_text(resp.get("text"))
history = st.session_state.get("chat_history", [])
history.append({"role": "user", "text": user_q})
history.append({"role": "assistant", "text": assistant_reply})
st.session_state.chat_history = history
st.success(("Assistant responded. See conversation below." if st.session_state.lang == "en" else "Trợ lý đã trả lời. Xem cuộc trò chuyện bên dưới."))
# Conversation display
if st.session_state.get("chat_history"):
st.subheader(("Conversation log" if st.session_state.lang == "en" else "Nhật ký cuộc trò chuyện"))
for turn in st.session_state.chat_history:
role = turn.get("role", "assistant")
text = turn.get("text", "")
if role == "assistant":
st.markdown(f"**Assistant:** {text}")
else:
st.markdown(f"**User:** {text}")
else:
st.info(("Start by reviewing the quick steps for Snakebite or Normal wound and confirm completion using the checkbox, then click Next → Advanced." if st.session_state.lang == "en" else "Bắt đầu bằng cách xem các bước nhanh cho rắn cắn hoặc vết thương thông thường và xác nhận, sau đó nhấp Tiếp → Nâng cao."))
st.markdown("---")
st.caption(
("🔒 Security: Prefer setting OPENAI_API_KEY as a secret. If you paste a key into the UI it is stored only in session memory and not written to disk." if st.session_state.lang == "en" else "🔒 Bảo mật: Ưu tiên đặt OPENAI_API_KEY trong Secrets. Nếu dán vào UI thì chỉ lưu trong session và không viết ra đĩa.")
)
# END of file |