BhargavMN commited on
Commit Β·
b0f6ffc
1
Parent(s): 538af1c
feat(ui): audio journal and photo correction to maintain conistency
Browse files- app.py +73 -86
- app/ui/theme.py +9 -0
app.py
CHANGED
|
@@ -831,26 +831,9 @@ def scoreboard_md(session_id: str) -> str:
|
|
| 831 |
return "\n".join(lines)
|
| 832 |
|
| 833 |
|
| 834 |
-
def
|
| 835 |
s = SESSION_STORE.get(session_id) or {}
|
| 836 |
-
|
| 837 |
-
wall = rooms.task_wall_state(session_id, team_id)
|
| 838 |
-
updates, ids = [], []
|
| 839 |
-
for i in range(12):
|
| 840 |
-
if i < len(tasks):
|
| 841 |
-
t = tasks[i]
|
| 842 |
-
tid = t["task_id"]
|
| 843 |
-
ids.append(tid)
|
| 844 |
-
done = wall.get(tid, False)
|
| 845 |
-
mark = "β" if done else "β"
|
| 846 |
-
updates.append(gr.update(
|
| 847 |
-
value=f"{mark} {t.get('title', 'Task')} Β· {t.get('points', 0)} pts",
|
| 848 |
-
visible=True,
|
| 849 |
-
variant="primary" if done else "secondary",
|
| 850 |
-
))
|
| 851 |
-
else:
|
| 852 |
-
updates.append(gr.update(visible=False))
|
| 853 |
-
return updates, ids
|
| 854 |
|
| 855 |
|
| 856 |
def _task_by_id(session_id: str, task_id: str) -> dict:
|
|
@@ -1079,11 +1062,23 @@ def poll_tick(session_id, screen, team_id, player_name, is_host, poll_state):
|
|
| 1079 |
play_timer, play_status, wait_status, win_score, timer_update, new_state)
|
| 1080 |
|
| 1081 |
|
| 1082 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
if screen != "play" or not session_id:
|
| 1084 |
-
return
|
| 1085 |
-
|
| 1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
|
| 1088 |
|
| 1089 |
# ββ Screen-action handlers ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1211,18 +1206,17 @@ def do_leave():
|
|
| 1211 |
|
| 1212 |
# Number of outputs produced by _open_task / consumed by DETAIL_OPEN_OUTPUTS.
|
| 1213 |
# 17 fixed slots + 12 task-tab buttons.
|
| 1214 |
-
_N_DETAIL_OUTPUTS =
|
| 1215 |
|
| 1216 |
|
| 1217 |
def _open_task(session_id, team_id, tid):
|
| 1218 |
-
"""Render the full task-detail view for one task. Shared by the
|
| 1219 |
-
the horizontal task tabs, and the prev/next arrows so every entry
|
| 1220 |
-
produces an identical, consistent detail screen."""
|
| 1221 |
can = rooms.can_complete(session_id, team_id, tid)
|
| 1222 |
return (
|
| 1223 |
tid,
|
| 1224 |
-
gr.update(visible=
|
| 1225 |
-
gr.update(visible=True), # detail view
|
| 1226 |
task_detail_md(session_id, team_id, tid),
|
| 1227 |
task_hint_html(session_id, team_id, tid),
|
| 1228 |
task_safety_html(session_id, team_id, tid),
|
|
@@ -1365,9 +1359,11 @@ def do_complete(session_id, team_id, task_id):
|
|
| 1365 |
if rooms.can_complete(session_id, team_id, task_id):
|
| 1366 |
complete_task(session_id, task_id, team_id)
|
| 1367 |
gr.Info("Task completed β nice work! π")
|
| 1368 |
-
|
|
|
|
|
|
|
| 1369 |
gr.Warning("Add a photo or voice journal first to complete this task.")
|
| 1370 |
-
return
|
| 1371 |
|
| 1372 |
|
| 1373 |
def do_finish(session_id, team_id):
|
|
@@ -1688,16 +1684,11 @@ with gr.Blocks(title="CityQuest Β· AI") as demo:
|
|
| 1688 |
gr.HTML("<div class='section-label' style='margin-top:14px'>Teams</div>")
|
| 1689 |
play_status = gr.HTML("")
|
| 1690 |
|
| 1691 |
-
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
elem_id="play-finish")
|
| 1697 |
-
|
| 1698 |
-
with gr.Group(visible=False, elem_classes=["cq-reveal"]) as g_detail:
|
| 1699 |
-
# Horizontal task tabs β tap any to switch tasks in place (replaces
|
| 1700 |
-
# the old "β Back to task wall" round-trip).
|
| 1701 |
with gr.Row(elem_classes=["task-tab-row"]):
|
| 1702 |
detail_tab_btn = [gr.Button("", visible=False, size="sm",
|
| 1703 |
elem_classes=["task-tab"]) for _ in range(12)]
|
|
@@ -1710,39 +1701,36 @@ with gr.Blocks(title="CityQuest Β· AI") as demo:
|
|
| 1710 |
detail_md = gr.Markdown("")
|
| 1711 |
detail_hint = gr.HTML("")
|
| 1712 |
detail_safety = gr.HTML("")
|
| 1713 |
-
with gr.Row():
|
| 1714 |
-
with gr.Column():
|
| 1715 |
-
|
| 1716 |
-
#
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
|
| 1720 |
-
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
d_journal_log = gr.Markdown("")
|
| 1744 |
-
with gr.Column():
|
| 1745 |
-
gr.HTML("<div class='section-label'>Photos & taglines</div>")
|
| 1746 |
d_photo = gr.Image(type="filepath", show_label=False, height=170)
|
| 1747 |
d_tagline = gr.Textbox(label="Caption", placeholder="A caption for this photo")
|
| 1748 |
d_addphoto = gr.Button("Add photo", variant="secondary",
|
|
@@ -1806,8 +1794,6 @@ with gr.Blocks(title="CityQuest Β· AI") as demo:
|
|
| 1806 |
outputs=nav_groups + [s_screen, lobby_code, lobby_roster, lobby_start, lobby_hint,
|
| 1807 |
play_timer, play_status, wait_status, win_scoreboard,
|
| 1808 |
poll, s_poll_state])
|
| 1809 |
-
poll.tick(wall_tick, inputs=[s_session, s_screen, s_team, s_taskids],
|
| 1810 |
-
outputs=task_btn + [s_taskids])
|
| 1811 |
|
| 1812 |
# ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1813 |
create_inputs = [c_name, c_type, c_city, c_area, c_teams, c_duration, c_players]
|
|
@@ -1857,16 +1843,18 @@ with gr.Blocks(title="CityQuest Β· AI") as demo:
|
|
| 1857 |
outputs=nav_groups + [s_screen, play_timer, play_status, lobby_prep]
|
| 1858 |
).then(_btn_on, None, [lobby_start])
|
| 1859 |
|
| 1860 |
-
# ββ Task
|
| 1861 |
-
# Order must match _open_task's return tuple (
|
| 1862 |
-
detail_open_outputs = [s_task,
|
| 1863 |
d_gallery, d_proof, d_complete, completion_instruction,
|
| 1864 |
d_transcript, d_audio, d_tagline, d_ask_a, d_capture_status,
|
| 1865 |
d_journal_log, task_position_label,
|
| 1866 |
d_points, d_hint_log, d_ask_btn] + detail_tab_btn
|
| 1867 |
-
#
|
| 1868 |
-
|
| 1869 |
-
|
|
|
|
|
|
|
| 1870 |
for i, dtb in enumerate(detail_tab_btn):
|
| 1871 |
dtb.click(open_task_factory(i), inputs=[s_session, s_team, s_taskids], outputs=detail_open_outputs)
|
| 1872 |
prev_task_btn.click(go_adjacent_factory(-1), inputs=[s_session, s_team, s_taskids, s_task],
|
|
@@ -1905,11 +1893,10 @@ with gr.Blocks(title="CityQuest Β· AI") as demo:
|
|
| 1905 |
on_get_hint, inputs=[d_ask_q, s_session, s_team, s_task],
|
| 1906 |
outputs=[d_ask_a, d_points, d_ask_btn, d_hint_log]
|
| 1907 |
)
|
|
|
|
|
|
|
| 1908 |
d_complete.click(do_complete, inputs=[s_session, s_team, s_task],
|
| 1909 |
-
outputs=
|
| 1910 |
-
|
| 1911 |
-
play_finish.click(do_finish, inputs=[s_session, s_team],
|
| 1912 |
-
outputs=nav_groups + [s_screen, wait_status])
|
| 1913 |
|
| 1914 |
# ββ Wait-lobby actions ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1915 |
wait_funny_recap.click(_btn_off, None, [wait_funny_recap]).then(
|
|
|
|
| 831 |
return "\n".join(lines)
|
| 832 |
|
| 833 |
|
| 834 |
+
def _task_id_list(session_id: str) -> list[str]:
|
| 835 |
s = SESSION_STORE.get(session_id) or {}
|
| 836 |
+
return [t["task_id"] for t in (s.get("game") or {}).get("tasks", [])[:12]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
|
| 838 |
|
| 839 |
def _task_by_id(session_id: str, task_id: str) -> dict:
|
|
|
|
| 1062 |
play_timer, play_status, wait_status, win_score, timer_update, new_state)
|
| 1063 |
|
| 1064 |
|
| 1065 |
+
def play_tick(session_id, screen, team_id, taskids, cur_task):
|
| 1066 |
+
"""On the play screen keep the task-id list current and, on first entry
|
| 1067 |
+
(no task open yet), open the first task in place. This covers both the host
|
| 1068 |
+
(who clicks Start) and guests (who auto-transition to play on the poll), so
|
| 1069 |
+
the detail view is always populated without a separate landing screen.
|
| 1070 |
+
|
| 1071 |
+
Returns the full detail-open tuple plus the refreshed ``s_taskids``."""
|
| 1072 |
if screen != "play" or not session_id:
|
| 1073 |
+
return (gr.update(),) * _N_DETAIL_OUTPUTS + (taskids,)
|
| 1074 |
+
ids = _task_id_list(session_id)
|
| 1075 |
+
ids_json = json.dumps(ids)
|
| 1076 |
+
if not cur_task and ids:
|
| 1077 |
+
# First time on the play screen for this client: open task 1.
|
| 1078 |
+
return _open_task(session_id, team_id, ids[0]) + (ids_json,)
|
| 1079 |
+
# Already viewing a task β leave the detail view untouched (don't reset the
|
| 1080 |
+
# player's in-progress photo/transcript every tick), just sync the id list.
|
| 1081 |
+
return (gr.update(),) * _N_DETAIL_OUTPUTS + (ids_json,)
|
| 1082 |
|
| 1083 |
|
| 1084 |
# ββ Screen-action handlers ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1206 |
|
| 1207 |
# Number of outputs produced by _open_task / consumed by DETAIL_OPEN_OUTPUTS.
|
| 1208 |
# 17 fixed slots + 12 task-tab buttons.
|
| 1209 |
+
_N_DETAIL_OUTPUTS = 19 + 12
|
| 1210 |
|
| 1211 |
|
| 1212 |
def _open_task(session_id, team_id, tid):
|
| 1213 |
+
"""Render the full task-detail view for one task. Shared by the play-screen
|
| 1214 |
+
entry, the horizontal task tabs, and the prev/next arrows so every entry
|
| 1215 |
+
point produces an identical, consistent detail screen."""
|
| 1216 |
can = rooms.can_complete(session_id, team_id, tid)
|
| 1217 |
return (
|
| 1218 |
tid,
|
| 1219 |
+
gr.update(visible=True), # detail view (always shown on the play screen)
|
|
|
|
| 1220 |
task_detail_md(session_id, team_id, tid),
|
| 1221 |
task_hint_html(session_id, team_id, tid),
|
| 1222 |
task_safety_html(session_id, team_id, tid),
|
|
|
|
| 1359 |
if rooms.can_complete(session_id, team_id, task_id):
|
| 1360 |
complete_task(session_id, task_id, team_id)
|
| 1361 |
gr.Info("Task completed β nice work! π")
|
| 1362 |
+
# Re-render the same task so its tab shows β and the completed state
|
| 1363 |
+
# is reflected in place (there's no wall to return to).
|
| 1364 |
+
return _open_task(session_id, team_id, task_id)
|
| 1365 |
gr.Warning("Add a photo or voice journal first to complete this task.")
|
| 1366 |
+
return (gr.update(),) * _N_DETAIL_OUTPUTS
|
| 1367 |
|
| 1368 |
|
| 1369 |
def do_finish(session_id, team_id):
|
|
|
|
| 1684 |
gr.HTML("<div class='section-label' style='margin-top:14px'>Teams</div>")
|
| 1685 |
play_status = gr.HTML("")
|
| 1686 |
|
| 1687 |
+
# The play screen lands directly in the task detail; the horizontal task
|
| 1688 |
+
# tabs below are the only task switcher (the old vertical task wall was
|
| 1689 |
+
# a redundant second copy of the same list, so it's gone).
|
| 1690 |
+
with gr.Group(visible=True, elem_classes=["cq-reveal"]) as g_detail:
|
| 1691 |
+
# Horizontal task tabs β tap any to switch tasks in place.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1692 |
with gr.Row(elem_classes=["task-tab-row"]):
|
| 1693 |
detail_tab_btn = [gr.Button("", visible=False, size="sm",
|
| 1694 |
elem_classes=["task-tab"]) for _ in range(12)]
|
|
|
|
| 1701 |
detail_md = gr.Markdown("")
|
| 1702 |
detail_hint = gr.HTML("")
|
| 1703 |
detail_safety = gr.HTML("")
|
| 1704 |
+
with gr.Row(equal_height=True):
|
| 1705 |
+
with gr.Column(elem_classes=["proof-col"]):
|
| 1706 |
+
gr.HTML("<div class='section-label'>ποΈ Voice journal</div>")
|
| 1707 |
+
# show_recording_waveform=False makes Gradio use the native
|
| 1708 |
+
# browser <audio> player instead of wavesurfer.js. wavesurfer
|
| 1709 |
+
# decodes the whole clip and paints a waveform on the main
|
| 1710 |
+
# thread (decodeAudioData), which on weaker laptops blocks
|
| 1711 |
+
# long enough to trigger the browser's "Page Unresponsive"
|
| 1712 |
+
# dialog. We don't need the waveform, so turn it off.
|
| 1713 |
+
d_audio = gr.Audio(sources=["microphone", "upload"], type="filepath",
|
| 1714 |
+
format="wav", show_label=False,
|
| 1715 |
+
elem_classes=["cq-audio"],
|
| 1716 |
+
waveform_options=gr.WaveformOptions(
|
| 1717 |
+
show_recording_waveform=False))
|
| 1718 |
+
d_lang = gr.Dropdown(
|
| 1719 |
+
["en", "fr", "de", "it", "es", "pt", "el", "nl", "pl",
|
| 1720 |
+
"zh", "ja", "ko", "vi", "ar"],
|
| 1721 |
+
value="en", label="Spoken language")
|
| 1722 |
+
d_transcript = gr.Textbox(label="Your notes", lines=3,
|
| 1723 |
+
placeholder="Transcribe your audio, or type a noteβ¦")
|
| 1724 |
+
with gr.Row():
|
| 1725 |
+
d_transcribe = gr.Button("Transcribe", variant="secondary",
|
| 1726 |
+
elem_id="td-transcribe",
|
| 1727 |
+
elem_classes=["btn-secondary-action"])
|
| 1728 |
+
d_savejournal = gr.Button("Save journal", variant="secondary",
|
| 1729 |
+
elem_id="td-savejournal",
|
| 1730 |
+
elem_classes=["btn-primary-action"])
|
| 1731 |
+
d_journal_log = gr.Markdown("")
|
| 1732 |
+
with gr.Column(elem_classes=["proof-col"]):
|
| 1733 |
+
gr.HTML("<div class='section-label'>πΈ Photos & taglines</div>")
|
|
|
|
|
|
|
|
|
|
| 1734 |
d_photo = gr.Image(type="filepath", show_label=False, height=170)
|
| 1735 |
d_tagline = gr.Textbox(label="Caption", placeholder="A caption for this photo")
|
| 1736 |
d_addphoto = gr.Button("Add photo", variant="secondary",
|
|
|
|
| 1794 |
outputs=nav_groups + [s_screen, lobby_code, lobby_roster, lobby_start, lobby_hint,
|
| 1795 |
play_timer, play_status, wait_status, win_scoreboard,
|
| 1796 |
poll, s_poll_state])
|
|
|
|
|
|
|
| 1797 |
|
| 1798 |
# ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1799 |
create_inputs = [c_name, c_type, c_city, c_area, c_teams, c_duration, c_players]
|
|
|
|
| 1843 |
outputs=nav_groups + [s_screen, play_timer, play_status, lobby_prep]
|
| 1844 |
).then(_btn_on, None, [lobby_start])
|
| 1845 |
|
| 1846 |
+
# ββ Task tabs β detail ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1847 |
+
# Order must match _open_task's return tuple (19 fixed + 12 tab buttons).
|
| 1848 |
+
detail_open_outputs = [s_task, g_detail, detail_md, detail_hint, detail_safety,
|
| 1849 |
d_gallery, d_proof, d_complete, completion_instruction,
|
| 1850 |
d_transcript, d_audio, d_tagline, d_ask_a, d_capture_status,
|
| 1851 |
d_journal_log, task_position_label,
|
| 1852 |
d_points, d_hint_log, d_ask_btn] + detail_tab_btn
|
| 1853 |
+
# Keep the task-id list fresh and auto-open the first task when a client
|
| 1854 |
+
# first reaches the play screen (host via Start, guests via poll).
|
| 1855 |
+
poll.tick(play_tick, inputs=[s_session, s_screen, s_team, s_taskids, s_task],
|
| 1856 |
+
outputs=detail_open_outputs + [s_taskids])
|
| 1857 |
+
# The in-detail horizontal task tabs index into s_taskids.
|
| 1858 |
for i, dtb in enumerate(detail_tab_btn):
|
| 1859 |
dtb.click(open_task_factory(i), inputs=[s_session, s_team, s_taskids], outputs=detail_open_outputs)
|
| 1860 |
prev_task_btn.click(go_adjacent_factory(-1), inputs=[s_session, s_team, s_taskids, s_task],
|
|
|
|
| 1893 |
on_get_hint, inputs=[d_ask_q, s_session, s_team, s_task],
|
| 1894 |
outputs=[d_ask_a, d_points, d_ask_btn, d_hint_log]
|
| 1895 |
)
|
| 1896 |
+
# After completing, re-render the same task in place so the β shows on its
|
| 1897 |
+
# tab and the completed state is reflected (no wall to bounce back to).
|
| 1898 |
d_complete.click(do_complete, inputs=[s_session, s_team, s_task],
|
| 1899 |
+
outputs=detail_open_outputs)
|
|
|
|
|
|
|
|
|
|
| 1900 |
|
| 1901 |
# ββ Wait-lobby actions ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1902 |
wait_funny_recap.click(_btn_off, None, [wait_funny_recap]).then(
|
app/ui/theme.py
CHANGED
|
@@ -699,6 +699,15 @@ ul.options[role="listbox"] li {
|
|
| 699 |
transform: scale(1.02) !important;
|
| 700 |
}
|
| 701 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
/* ββ In-game guide (hints) ββ */
|
| 703 |
.guide-header {
|
| 704 |
font-size: 1rem !important;
|
|
|
|
| 699 |
transform: scale(1.02) !important;
|
| 700 |
}
|
| 701 |
|
| 702 |
+
/* ββ Proof columns: voice journal & photos read as matching side-by-side
|
| 703 |
+
panels (same surface, padding, and header) ββ */
|
| 704 |
+
.proof-col {
|
| 705 |
+
background: var(--paper) !important;
|
| 706 |
+
border: 1px solid var(--line) !important;
|
| 707 |
+
border-radius: 12px !important;
|
| 708 |
+
padding: 14px !important;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
/* ββ In-game guide (hints) ββ */
|
| 712 |
.guide-header {
|
| 713 |
font-size: 1rem !important;
|