BhargavMN commited on
Commit
b0f6ffc
Β·
1 Parent(s): 538af1c

feat(ui): audio journal and photo correction to maintain conistency

Browse files
Files changed (2) hide show
  1. app.py +73 -86
  2. 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 _wall_buttons(session_id: str, team_id: str):
835
  s = SESSION_STORE.get(session_id) or {}
836
- tasks = (s.get("game") or {}).get("tasks", [])[:12]
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 wall_tick(session_id, screen, team_id, taskids):
 
 
 
 
 
 
1083
  if screen != "play" or not session_id:
1084
- return [gr.update()] * 12 + [taskids]
1085
- updates, ids = _wall_buttons(session_id, team_id)
1086
- return updates + [json.dumps(ids)]
 
 
 
 
 
 
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 = 20 + 12
1215
 
1216
 
1217
  def _open_task(session_id, team_id, tid):
1218
- """Render the full task-detail view for one task. Shared by the wall cards,
1219
- the horizontal task tabs, and the prev/next arrows so every entry point
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=False), # wall view
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
- return gr.update(visible=True), gr.update(visible=False), "βœ… Task completed β€” nice work!"
 
 
1369
  gr.Warning("Add a photo or voice journal first to complete this task.")
1370
- return gr.update(), gr.update(visible=True), "πŸ”’ Add a photo or journal first."
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
- with gr.Group(visible=True) as g_wall:
1692
- gr.HTML("<div class='section-label' style='margin-top:14px'>Task wall β€” tap a task to open it</div>")
1693
- with gr.Group(elem_classes=["cq-wall"]):
1694
- task_btn = [gr.Button("", visible=False, elem_classes=["cq-tcard"]) for _ in range(12)]
1695
- play_finish = gr.Button("🏁 Finish & wait for others", variant="primary",
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
- # Collapsed by default β€” most players add a photo, not a
1716
- # recording, so this stays out of the way until tapped.
1717
- with gr.Accordion("πŸŽ™οΈ Voice journal", open=False,
1718
- elem_classes=["voice-journal-accordion"]):
1719
- # show_recording_waveform=False makes Gradio use the native
1720
- # browser <audio> player instead of wavesurfer.js. wavesurfer
1721
- # decodes the whole clip and paints a waveform on the main
1722
- # thread (decodeAudioData), which on weaker laptops blocks
1723
- # long enough to trigger the browser's "Page Unresponsive"
1724
- # dialog. We don't need the waveform, so turn it off.
1725
- d_audio = gr.Audio(sources=["microphone", "upload"], type="filepath",
1726
- format="wav", show_label=False,
1727
- elem_classes=["cq-audio"],
1728
- waveform_options=gr.WaveformOptions(
1729
- show_recording_waveform=False))
1730
- d_lang = gr.Dropdown(
1731
- ["en", "fr", "de", "it", "es", "pt", "el", "nl", "pl",
1732
- "zh", "ja", "ko", "vi", "ar"],
1733
- value="en", label="Spoken language")
1734
- with gr.Row():
1735
- d_transcribe = gr.Button("Transcribe", variant="secondary",
1736
- elem_id="td-transcribe",
1737
- elem_classes=["btn-secondary-action"])
1738
- d_savejournal = gr.Button("Save journal", variant="secondary",
1739
- elem_id="td-savejournal",
1740
- elem_classes=["btn-primary-action"])
1741
- d_transcript = gr.Textbox(label="Your notes", lines=3,
1742
- placeholder="Transcribe your audio, or type a note…")
1743
- d_journal_log = gr.Markdown("")
1744
- with gr.Column():
1745
- gr.HTML("<div class='section-label'>Photos &amp; 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 wall / tabs β†’ detail ─────────────────────────────────────────
1861
- # Order must match _open_task's return tuple (17 fixed + 12 tab buttons).
1862
- detail_open_outputs = [s_task, g_wall, g_detail, detail_md, detail_hint, detail_safety,
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
- # Both the wall cards and the in-detail task tabs index into s_taskids.
1868
- for i, tb in enumerate(task_btn):
1869
- tb.click(open_task_factory(i), inputs=[s_session, s_team, s_taskids], outputs=detail_open_outputs)
 
 
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=[g_wall, g_detail, d_capture_status])
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 &amp; 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;