Toya0421 commited on
Commit
4c204dd
·
verified ·
1 Parent(s): 796eb3e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -202
app.py CHANGED
@@ -138,8 +138,6 @@ def get_new_passage_random(used_passages_set, target_level):
138
  if num.isdigit():
139
  all_ids.append(int(num))
140
 
141
- # ★追加:excelのflesch_scoreでフィルタ(target_fleschより低いものだけ)
142
- # passage_information.xlsx に該当行がないものは除外(スコア不明だと条件判定できないため)
143
  eligible_ids = []
144
  for pid in all_ids:
145
  row = passage_info_df[passage_info_df["Text#"] == pid]
@@ -187,16 +185,11 @@ def get_new_passage_random(used_passages_set, target_level):
187
  # ======================================================
188
 
189
  def extract_main_body(text: str) -> str:
190
- """
191
- 書き換えは行わず、「本文っぽい部分」だけを軽量なヒューリスティックで抽出する。
192
- (タイトル/著者/URL/出典/著作権/Project Gutenbergヘッダ・フッタ/注釈などを落とす)
193
- """
194
  if not text:
195
  return ""
196
 
197
  lines = text.splitlines()
198
 
199
- # ありがちなメタ情報/フッタ/ヘッダの除外パターン
200
  drop_patterns = [
201
  r"^\s*title\s*:\s*.*$",
202
  r"^\s*author\s*:\s*.*$",
@@ -208,21 +201,19 @@ def extract_main_body(text: str) -> str:
208
  r".*GUTENBERG.*",
209
  r"^\s*http[s]?://\S+.*$",
210
  r"^\s*www\.\S+.*$",
211
- r"^\s*\[.*\]\s*$", # 注釈っぽい行
212
- r"^\s*\(\s*.*\s*\)\s*$", # 注釈っぽい行
213
  r"^\s*end\s+of\s+.*$",
214
  r"^\s*\*{3}.*\*{3}\s*$",
215
  ]
216
  drop_re = re.compile("|".join(f"(?:{p})" for p in drop_patterns), re.IGNORECASE)
217
 
218
- # まず明らかなメタ行を除外
219
  kept = []
220
  for ln in lines:
221
  if drop_re.match(ln.strip()):
222
  continue
223
  kept.append(ln)
224
 
225
- # 先頭の「短いタイトル行」っぽいものを数行だけ落とす(本文が始まるまで)
226
  def is_title_like(s: str) -> bool:
227
  t = s.strip()
228
  if not t:
@@ -262,14 +253,7 @@ def extract_main_body(text: str) -> str:
262
  # ======================================================
263
 
264
  def rewrite_level(text, target_level):
265
- level_to_flesch = {
266
- 1: 90,
267
- 2: 75,
268
- 3: 65,
269
- 4: 55,
270
- 5: 40
271
- }
272
-
273
  target_flesch = level_to_flesch[int(target_level)]
274
 
275
  prompt = f"""
@@ -277,9 +261,9 @@ Rewrite the following passage so it fits about {target_flesch} Flesch Reading Ea
277
  - Extract only the portions of the text that should be read as the main body,
278
  excluding the title, author name, source information, chapter number, annotations, and footers.
279
  - When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
280
- - Preserve the original meaning faithfully.
281
- - Do not add new information or remove essential information.
282
- - Output only the rewritten passage. Do not include explanations.
283
  {text}
284
  """
285
 
@@ -338,24 +322,24 @@ def start_test(student_id, level_input, group_input, session_state):
338
  save_log(entry)
339
 
340
  return (
341
- "", # title_display
342
- "", # text_display
343
- "", # page_display
344
- json.dumps([]), # hidden_pages
345
- 0, # hidden_page_index
346
- 0, # hidden_total_pages
347
- "", # hidden_passage_id
348
- "", # hidden_orig_lev
349
- None, # hidden_assigned_lev(level)
350
- gr.update(interactive=False, visible=False), # prev_btn
351
- gr.update(interactive=False, visible=True), # next_btn
352
- gr.update(interactive=False, visible=False), # finish_btn
353
  session_state
354
  )
355
 
356
  user_id = str(student_id).strip()
357
  level = int(level_input)
358
- group = int(group_input)
359
 
360
  used_passages_set = set()
361
 
@@ -371,19 +355,18 @@ def start_test(student_id, level_input, group_input, session_state):
371
  }
372
  save_log(entry)
373
 
374
- # ★変更:target level を渡して「難しい教材のみ」から選ぶ
375
  pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
376
  if text is None:
377
  return (
378
- "", # title_display
379
- "教材が��つかりません", # text_display
380
- "", # page_display
381
- json.dumps([]), # hidden_pages
382
- 0, # hidden_page_index
383
- 0, # hidden_total_pages
384
- "", # hidden_passage_id
385
- "", # hidden_orig_lev
386
- None, # hidden_assigned_lev(level)
387
  gr.update(interactive=False, visible=False),
388
  gr.update(interactive=False, visible=False),
389
  gr.update(interactive=False, visible=False),
@@ -407,20 +390,17 @@ def start_test(student_id, level_input, group_input, session_state):
407
  next_upd = gr.update(interactive=True, visible=True)
408
  finish_upd = gr.update(interactive=False, visible=False)
409
 
410
- page_num = 1
411
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
412
-
413
- entry2 = {
414
  "user_id": user_id,
415
  "group": group,
416
  "assigned_level": level,
417
  "passage_id": pid,
418
  "original_level": orig_lev,
419
  "action_time": now2,
420
- "action_type": f"page_displayed_{page_num}",
421
  "page_text": pages[0]
422
- }
423
- save_log(entry2)
424
 
425
  session_state = {
426
  "user_id": user_id,
@@ -453,9 +433,9 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
453
  user_id = session_state.get("user_id")
454
  level = session_state.get("level")
455
  group = session_state.get("group")
456
-
457
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
458
- entry = {
459
  "user_id": user_id,
460
  "group": group,
461
  "assigned_level": level,
@@ -464,8 +444,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
464
  "action_time": now,
465
  "action_type": "next_pushed",
466
  "page_text": None
467
- }
468
- save_log(entry)
469
 
470
  pages = json.loads(pages_json)
471
  if not pages:
@@ -478,7 +457,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
478
  new_page = min(current_page + 1, total_pages - 1)
479
 
480
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
481
- entry2 = {
482
  "user_id": user_id,
483
  "group": group,
484
  "assigned_level": level,
@@ -487,8 +466,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
487
  "action_time": now2,
488
  "action_type": f"page_displayed_{new_page+1}",
489
  "page_text": pages[new_page]
490
- }
491
- save_log(entry2)
492
 
493
  if new_page == total_pages - 1:
494
  return (
@@ -519,7 +497,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
519
  group = session_state.get("group")
520
 
521
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
522
- entry = {
523
  "user_id": user_id,
524
  "group": group,
525
  "assigned_level": level,
@@ -528,8 +506,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
528
  "action_time": now,
529
  "action_type": "prev_pushed",
530
  "page_text": None
531
- }
532
- save_log(entry)
533
 
534
  pages = json.loads(pages_json)
535
  if not pages:
@@ -547,7 +524,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
547
  finish_upd = gr.update(interactive=(not next_visible), visible=(not next_visible))
548
 
549
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
550
- entry2 = {
551
  "user_id": user_id,
552
  "group": group,
553
  "assigned_level": level,
@@ -556,8 +533,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
556
  "action_time": now2,
557
  "action_type": f"page_displayed_{new_page+1}",
558
  "page_text": pages[new_page]
559
- }
560
- save_log(entry2)
561
 
562
  return (
563
  pages[new_page],
@@ -579,7 +555,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
579
  pages = json.loads(pages_json)
580
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
581
 
582
- entry = {
583
  "user_id": user_id,
584
  "group": group,
585
  "assigned_level": level,
@@ -588,10 +564,8 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
588
  "action_time": now,
589
  "action_type": action,
590
  "page_text": None
591
- }
592
- save_log(entry)
593
 
594
- # ★変更:target level を渡して「難しい教材のみ」から選ぶ
595
  new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
596
  if new_text is None:
597
  return (
@@ -621,7 +595,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
621
  finish_upd = gr.update(interactive=False, visible=False)
622
 
623
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
624
- entry2 = {
625
  "user_id": user_id,
626
  "group": group,
627
  "assigned_level": level,
@@ -630,8 +604,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
630
  "action_time": now2,
631
  "action_type": "page_displayed_1",
632
  "page_text": new_pages[0]
633
- }
634
- save_log(entry2)
635
 
636
  session_state = {
637
  "user_id": user_id,
@@ -659,87 +632,8 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
659
  # ======================================================
660
  # UI(タイトル表示を追加。それ以外は変更しない)
661
  # ★追加:パスワード付きログCSVダウンロード
662
- # ★改善Edgeでも確実に「Group選択中」が分かるようJSでstyle直当て
663
  # ======================================================
664
-
665
- # ★差し替え:Edgeでも確実に効くハイライトJS(style直書き)
666
- radio_js = r"""
667
- () => {
668
- const root = document.getElementById("group_radio");
669
- if (!root) return;
670
-
671
- const getScheme = () => {
672
- try {
673
- return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
674
- ? "dark" : "light";
675
- } catch (e) {
676
- return "light";
677
- }
678
- };
679
-
680
- const clear = (labels) => {
681
- labels.forEach(l => {
682
- l.classList.remove("radio-selected");
683
- l.style.background = "";
684
- l.style.border = "";
685
- l.style.boxShadow = "";
686
- l.style.borderRadius = "";
687
- l.style.padding = "";
688
- });
689
- };
690
-
691
- const apply = (label) => {
692
- const scheme = getScheme();
693
- label.classList.add("radio-selected");
694
-
695
- if (scheme === "dark") {
696
- label.style.background = "#1f2937";
697
- label.style.border = "2px solid #60a5fa";
698
- label.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.06) inset";
699
- } else {
700
- label.style.background = "#eef2ff";
701
- label.style.border = "2px solid #4f46e5";
702
- label.style.boxShadow = "0 0 0 1px rgba(0,0,0,0.04) inset";
703
- }
704
- label.style.borderRadius = "12px";
705
- label.style.padding = "10px 12px";
706
- };
707
-
708
- const update = () => {
709
- const labels = root.querySelectorAll("label");
710
- if (!labels || labels.length === 0) return;
711
-
712
- clear(labels);
713
-
714
- const checked = root.querySelector('input[type="radio"]:checked');
715
- if (!checked) return;
716
-
717
- const label = checked.closest("label");
718
- if (!label) return;
719
-
720
- apply(label);
721
- };
722
-
723
- update();
724
-
725
- root.addEventListener("change", update);
726
- root.addEventListener("click", update);
727
-
728
- let n = 0;
729
- const t = setInterval(() => {
730
- update();
731
- n += 1;
732
- if (n >= 15) clearInterval(t);
733
- }, 200);
734
-
735
- try {
736
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
737
- mq.addEventListener?.("change", update);
738
- mq.addListener?.(update);
739
- } catch (e) {}
740
- };
741
- """
742
-
743
  custom_css = """
744
  /* ===============================
745
  共通(両モード)
@@ -749,22 +643,18 @@ custom_css = """
749
  line-height: 1.8 !important;
750
  font-family: "Noto Sans", sans-serif !important;
751
  }
752
-
753
- /* 教材表示ボックス */
754
  .reading-area {
755
  padding: 20px !important;
756
  border-radius: 12px !important;
757
  border: 1px solid #ccc !important;
758
  transition: background-color 0.2s ease, color 0.2s ease;
759
  }
760
-
761
- /* ★追加:Radio / Dropdown などフォーム部品の可読性を安定させる(両モード共通) */
762
  .gradio-container label, .gradio-container .wrap label {
763
  color: inherit !important;
764
  }
765
  .gradio-container input[type="radio"],
766
  .gradio-container input[type="checkbox"] {
767
- accent-color: #2563eb !important; /* 選択丸が見えにくい問題の対策 */
768
  }
769
  .gradio-container select {
770
  color: inherit !important;
@@ -778,38 +668,28 @@ custom_css = """
778
  background-color: #ffffff !important;
779
  color: #222 !important;
780
  }
781
-
782
- /* ★追加:フォーム周り(Radio/Dropdown)の背景と境界を明るい環境で見やすく */
783
  .gr-panel, .gr-box, .gr-group {
784
  background-color: #ffffff !important;
785
  border-color: #ddd !important;
786
  }
787
-
788
  .reading-area {
789
  background-color: #fafafa !important;
790
  color: #222 !important;
791
  border-color: #ddd !important;
792
  }
793
-
794
  textarea, input, .gr-textbox textarea {
795
  background-color: #ffffff !important;
796
  color: #222 !important;
797
  border: 1px solid #ccc !important;
798
  }
799
-
800
- /* ★追加:Dropdown(Select) をライトで見やすく */
801
  select, .gr-dropdown select {
802
  background-color: #ffffff !important;
803
  color: #222 !important;
804
  border: 1px solid #ccc !important;
805
  }
806
-
807
- /* ★追加:Radioの選択肢テキストが薄くなるケースの対策 */
808
  .gr-radio label, .gr-radio .wrap label, .gr-radio span {
809
  color: #222 !important;
810
  }
811
-
812
- /* ★追加:ライトモードでボタンが背景に溶ける場合の対策 */
813
  button {
814
  background-color: #f5f5f5 !important;
815
  color: #111 !important;
@@ -828,26 +708,21 @@ custom_css = """
828
  background-color: #1e1e1e !important;
829
  color: #e6e6e6 !important;
830
  }
831
-
832
  .reading-area {
833
  background-color: #2a2a2a !important;
834
  color: #f2f2f2 !important;
835
  border-color: #444 !important;
836
  }
837
-
838
  textarea, input, .gr-textbox textarea {
839
  background-color: #2c2c2c !important;
840
  color: #f0f0f0 !important;
841
  border: 1px solid #555 !important;
842
  }
843
-
844
- /* ★追加:Dropdown(Select) をダークで見やすく */
845
  select, .gr-dropdown select {
846
  background-color: #2c2c2c !important;
847
  color: #f0f0f0 !important;
848
  border: 1px solid #555 !important;
849
  }
850
-
851
  button {
852
  background-color: #3a3a3a !important;
853
  color: #f0f0f0 !important;
@@ -856,29 +731,18 @@ custom_css = """
856
  button:hover {
857
  background-color: #4a4a4a !important;
858
  }
859
-
860
  .gr-panel, .gr-box, .gr-group {
861
  background-color: #272727 !important;
862
  border-color: #444 !important;
863
  }
864
-
865
- /* ★追加:Radioの選択肢が薄くなるケースの対策 */
866
  .gr-radio label, .gr-radio .wrap label, .gr-radio span {
867
  color: #e6e6e6 !important;
868
  }
869
  }
870
 
871
- /* ★追加:Radio行が細くてハイライトが目立たない問題の保険(Edge向け) */
872
- #group_radio label {
873
- width: 100% !important;
874
- box-sizing: border-box !important;
875
- }
876
-
877
  /* ===============================
878
- 追加:Group選択中を確実にえるように(Edge対応
879
  =============================== */
880
-
881
- /* ラジオ自体がOS/ブラウザ依存で薄くなるのを防ぐ */
882
  #group_radio input[type="radio"]{
883
  appearance: auto !important;
884
  -webkit-appearance: radio !important;
@@ -886,8 +750,6 @@ custom_css = """
886
  height: 18px !important;
887
  accent-color: #2563eb !important;
888
  }
889
-
890
- /* 各選択肢を行として見やすく */
891
  #group_radio label{
892
  width: 100% !important;
893
  box-sizing: border-box !important;
@@ -899,34 +761,22 @@ custom_css = """
899
  gap: 10px !important;
900
  }
901
 
902
- /* checked のとき “行全体” をハイライト
903
- (DOM差に備えて :has / input+span / input~span を全部用意)
904
- */
905
  @media (prefers-color-scheme: light){
906
  #group_radio label:has(input[type="radio"]:checked){
907
  background: #eef2ff !important;
908
  border: 2px solid #4f46e5 !important;
909
  }
910
- #group_radio input[type="radio"]:checked + span,
911
- #group_radio input[type="radio"]:checked ~ span{
912
- font-weight: 700 !important;
913
- }
914
  }
915
-
916
  @media (prefers-color-scheme: dark){
917
  #group_radio label:has(input[type="radio"]:checked){
918
  background: #1f2937 !important;
919
  border: 2px solid #60a5fa !important;
920
  }
921
- #group_radio input[type="radio"]:checked + span,
922
- #group_radio input[type="radio"]:checked ~ span{
923
- font-weight: 700 !important;
924
- }
925
  }
926
-
927
  """
928
 
929
- with gr.Blocks(css=custom_css, js=radio_js) as demo:
930
  gr.Markdown("# 📚 Reading Exercise")
931
 
932
  session_state = gr.State({"user_id": None, "level": None, "group": 2, "used_passages": []})
@@ -934,10 +784,10 @@ with gr.Blocks(css=custom_css, js=radio_js) as demo:
934
  student_id_input = gr.Textbox(label="学生番号(必須)")
935
 
936
  group_input = gr.Radio(
937
- choices=[("Group 1", "1"), ("Group 2", "2")],
938
  label="実験グループを選択",
939
  value="2",
940
- elem_id="group_radio" # ★追加:Edgeでも確実に狙うため
941
  )
942
 
943
  level_input = gr.Dropdown(
@@ -1048,7 +898,6 @@ with gr.Blocks(css=custom_css, js=radio_js) as demo:
1048
  ]
1049
  )
1050
 
1051
- # ★追加:ログCSVダウンロード(パスワード必須)
1052
  gr.Markdown("## 🔐 管理者用:ログCSVダウンロード(パスワード必須)")
1053
  admin_password = gr.Textbox(label="Password", type="password")
1054
  download_btn = gr.Button("ログCSVを生成してダウンロード")
 
138
  if num.isdigit():
139
  all_ids.append(int(num))
140
 
 
 
141
  eligible_ids = []
142
  for pid in all_ids:
143
  row = passage_info_df[passage_info_df["Text#"] == pid]
 
185
  # ======================================================
186
 
187
  def extract_main_body(text: str) -> str:
 
 
 
 
188
  if not text:
189
  return ""
190
 
191
  lines = text.splitlines()
192
 
 
193
  drop_patterns = [
194
  r"^\s*title\s*:\s*.*$",
195
  r"^\s*author\s*:\s*.*$",
 
201
  r".*GUTENBERG.*",
202
  r"^\s*http[s]?://\S+.*$",
203
  r"^\s*www\.\S+.*$",
204
+ r"^\s*\[.*\]\s*$",
205
+ r"^\s*\(\s*.*\s*\)\s*$",
206
  r"^\s*end\s+of\s+.*$",
207
  r"^\s*\*{3}.*\*{3}\s*$",
208
  ]
209
  drop_re = re.compile("|".join(f"(?:{p})" for p in drop_patterns), re.IGNORECASE)
210
 
 
211
  kept = []
212
  for ln in lines:
213
  if drop_re.match(ln.strip()):
214
  continue
215
  kept.append(ln)
216
 
 
217
  def is_title_like(s: str) -> bool:
218
  t = s.strip()
219
  if not t:
 
253
  # ======================================================
254
 
255
  def rewrite_level(text, target_level):
256
+ level_to_flesch = {1: 90, 2: 75, 3: 65, 4: 55, 5: 40}
 
 
 
 
 
 
 
257
  target_flesch = level_to_flesch[int(target_level)]
258
 
259
  prompt = f"""
 
261
  - Extract only the portions of the text that should be read as the main body,
262
  excluding the title, author name, source information, chapter number, annotations, and footers.
263
  - When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
264
+ - Preserve the original meaning faithfully.
265
+ - Do not add new information or remove essential information.
266
+ - Output only the rewritten passage. Do not include explanations.
267
  {text}
268
  """
269
 
 
322
  save_log(entry)
323
 
324
  return (
325
+ "",
326
+ "",
327
+ "",
328
+ json.dumps([]),
329
+ 0,
330
+ 0,
331
+ "",
332
+ "",
333
+ None,
334
+ gr.update(interactive=False, visible=False),
335
+ gr.update(interactive=False, visible=True),
336
+ gr.update(interactive=False, visible=False),
337
  session_state
338
  )
339
 
340
  user_id = str(student_id).strip()
341
  level = int(level_input)
342
+ group = int(group_input) # ★group_inputは "1"/"2" でもOK
343
 
344
  used_passages_set = set()
345
 
 
355
  }
356
  save_log(entry)
357
 
 
358
  pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
359
  if text is None:
360
  return (
361
+ "",
362
+ "教材がつかりません",
363
+ "",
364
+ json.dumps([]),
365
+ 0,
366
+ 0,
367
+ "",
368
+ "",
369
+ None,
370
  gr.update(interactive=False, visible=False),
371
  gr.update(interactive=False, visible=False),
372
  gr.update(interactive=False, visible=False),
 
390
  next_upd = gr.update(interactive=True, visible=True)
391
  finish_upd = gr.update(interactive=False, visible=False)
392
 
 
393
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
394
+ save_log({
 
395
  "user_id": user_id,
396
  "group": group,
397
  "assigned_level": level,
398
  "passage_id": pid,
399
  "original_level": orig_lev,
400
  "action_time": now2,
401
+ "action_type": "page_displayed_1",
402
  "page_text": pages[0]
403
+ })
 
404
 
405
  session_state = {
406
  "user_id": user_id,
 
433
  user_id = session_state.get("user_id")
434
  level = session_state.get("level")
435
  group = session_state.get("group")
436
+
437
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
438
+ save_log({
439
  "user_id": user_id,
440
  "group": group,
441
  "assigned_level": level,
 
444
  "action_time": now,
445
  "action_type": "next_pushed",
446
  "page_text": None
447
+ })
 
448
 
449
  pages = json.loads(pages_json)
450
  if not pages:
 
457
  new_page = min(current_page + 1, total_pages - 1)
458
 
459
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
460
+ save_log({
461
  "user_id": user_id,
462
  "group": group,
463
  "assigned_level": level,
 
466
  "action_time": now2,
467
  "action_type": f"page_displayed_{new_page+1}",
468
  "page_text": pages[new_page]
469
+ })
 
470
 
471
  if new_page == total_pages - 1:
472
  return (
 
497
  group = session_state.get("group")
498
 
499
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
500
+ save_log({
501
  "user_id": user_id,
502
  "group": group,
503
  "assigned_level": level,
 
506
  "action_time": now,
507
  "action_type": "prev_pushed",
508
  "page_text": None
509
+ })
 
510
 
511
  pages = json.loads(pages_json)
512
  if not pages:
 
524
  finish_upd = gr.update(interactive=(not next_visible), visible=(not next_visible))
525
 
526
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
527
+ save_log({
528
  "user_id": user_id,
529
  "group": group,
530
  "assigned_level": level,
 
533
  "action_time": now2,
534
  "action_type": f"page_displayed_{new_page+1}",
535
  "page_text": pages[new_page]
536
+ })
 
537
 
538
  return (
539
  pages[new_page],
 
555
  pages = json.loads(pages_json)
556
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
557
 
558
+ save_log({
559
  "user_id": user_id,
560
  "group": group,
561
  "assigned_level": level,
 
564
  "action_time": now,
565
  "action_type": action,
566
  "page_text": None
567
+ })
 
568
 
 
569
  new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
570
  if new_text is None:
571
  return (
 
595
  finish_upd = gr.update(interactive=False, visible=False)
596
 
597
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
598
+ save_log({
599
  "user_id": user_id,
600
  "group": group,
601
  "assigned_level": level,
 
604
  "action_time": now2,
605
  "action_type": "page_displayed_1",
606
  "page_text": new_pages[0]
607
+ })
 
608
 
609
  session_state = {
610
  "user_id": user_id,
 
632
  # ======================================================
633
  # UI(タイトル表示を追加。それ以外は変更しない)
634
  # ★追加:パスワード付きログCSVダウンロード
635
+ # ★修正:JSは使わずCSSのみ(スタートが進まない問題を回避)
636
  # ======================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  custom_css = """
638
  /* ===============================
639
  共通(両モード)
 
643
  line-height: 1.8 !important;
644
  font-family: "Noto Sans", sans-serif !important;
645
  }
 
 
646
  .reading-area {
647
  padding: 20px !important;
648
  border-radius: 12px !important;
649
  border: 1px solid #ccc !important;
650
  transition: background-color 0.2s ease, color 0.2s ease;
651
  }
 
 
652
  .gradio-container label, .gradio-container .wrap label {
653
  color: inherit !important;
654
  }
655
  .gradio-container input[type="radio"],
656
  .gradio-container input[type="checkbox"] {
657
+ accent-color: #2563eb !important;
658
  }
659
  .gradio-container select {
660
  color: inherit !important;
 
668
  background-color: #ffffff !important;
669
  color: #222 !important;
670
  }
 
 
671
  .gr-panel, .gr-box, .gr-group {
672
  background-color: #ffffff !important;
673
  border-color: #ddd !important;
674
  }
 
675
  .reading-area {
676
  background-color: #fafafa !important;
677
  color: #222 !important;
678
  border-color: #ddd !important;
679
  }
 
680
  textarea, input, .gr-textbox textarea {
681
  background-color: #ffffff !important;
682
  color: #222 !important;
683
  border: 1px solid #ccc !important;
684
  }
 
 
685
  select, .gr-dropdown select {
686
  background-color: #ffffff !important;
687
  color: #222 !important;
688
  border: 1px solid #ccc !important;
689
  }
 
 
690
  .gr-radio label, .gr-radio .wrap label, .gr-radio span {
691
  color: #222 !important;
692
  }
 
 
693
  button {
694
  background-color: #f5f5f5 !important;
695
  color: #111 !important;
 
708
  background-color: #1e1e1e !important;
709
  color: #e6e6e6 !important;
710
  }
 
711
  .reading-area {
712
  background-color: #2a2a2a !important;
713
  color: #f2f2f2 !important;
714
  border-color: #444 !important;
715
  }
 
716
  textarea, input, .gr-textbox textarea {
717
  background-color: #2c2c2c !important;
718
  color: #f0f0f0 !important;
719
  border: 1px solid #555 !important;
720
  }
 
 
721
  select, .gr-dropdown select {
722
  background-color: #2c2c2c !important;
723
  color: #f0f0f0 !important;
724
  border: 1px solid #555 !important;
725
  }
 
726
  button {
727
  background-color: #3a3a3a !important;
728
  color: #f0f0f0 !important;
 
731
  button:hover {
732
  background-color: #4a4a4a !important;
733
  }
 
734
  .gr-panel, .gr-box, .gr-group {
735
  background-color: #272727 !important;
736
  border-color: #444 !important;
737
  }
 
 
738
  .gr-radio label, .gr-radio .wrap label, .gr-radio span {
739
  color: #e6e6e6 !important;
740
  }
741
  }
742
 
 
 
 
 
 
 
743
  /* ===============================
744
+ ★Group選択:CSSのみでやすく(EdgeでもOK
745
  =============================== */
 
 
746
  #group_radio input[type="radio"]{
747
  appearance: auto !important;
748
  -webkit-appearance: radio !important;
 
750
  height: 18px !important;
751
  accent-color: #2563eb !important;
752
  }
 
 
753
  #group_radio label{
754
  width: 100% !important;
755
  box-sizing: border-box !important;
 
761
  gap: 10px !important;
762
  }
763
 
764
+ /* :has が効く環境は行全体ハイライト */
 
 
765
  @media (prefers-color-scheme: light){
766
  #group_radio label:has(input[type="radio"]:checked){
767
  background: #eef2ff !important;
768
  border: 2px solid #4f46e5 !important;
769
  }
 
 
 
 
770
  }
 
771
  @media (prefers-color-scheme: dark){
772
  #group_radio label:has(input[type="radio"]:checked){
773
  background: #1f2937 !important;
774
  border: 2px solid #60a5fa !important;
775
  }
 
 
 
 
776
  }
 
777
  """
778
 
779
+ with gr.Blocks(css=custom_css) as demo:
780
  gr.Markdown("# 📚 Reading Exercise")
781
 
782
  session_state = gr.State({"user_id": None, "level": None, "group": 2, "used_passages": []})
 
784
  student_id_input = gr.Textbox(label="学生番号(必須)")
785
 
786
  group_input = gr.Radio(
787
+ choices=[("Group 1", "1"), ("Group 2", "2")], # ★表示問題回避のため文字列
788
  label="実験グループを選択",
789
  value="2",
790
+ elem_id="group_radio"
791
  )
792
 
793
  level_input = gr.Dropdown(
 
898
  ]
899
  )
900
 
 
901
  gr.Markdown("## 🔐 管理者用:ログCSVダウンロード(パスワード必須)")
902
  admin_password = gr.Textbox(label="Password", type="password")
903
  download_btn = gr.Button("ログCSVを生成してダウンロード")