sofiamak commited on
Commit
541ad39
·
verified ·
1 Parent(s): f0746b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -70
app.py CHANGED
@@ -1,8 +1,8 @@
1
- import os, json, random, datetime, io
2
  import gradio as gr
3
 
4
  APP_TITLE = "100个不"
5
- TASKS_FILE = "tasks.json" # 你的题库文件
6
  TARGET_PERCENT = 100
7
 
8
  # 难度映射与奖励
@@ -12,10 +12,6 @@ LEVEL_REWARDS = {"easy": 1, "medium": 5, "hard": 10}
12
 
13
  # ====== 读取 tasks.json ======
14
  def load_tasks_from_file():
15
- """
16
- 从 tasks.json 读取:必须包含 easy/medium/hard 三个数组。
17
- 返回 (tasks, message);message 为 None 表示正常,否则是提示信息。
18
- """
19
  if not os.path.exists(TASKS_FILE):
20
  return {"easy": [], "medium": [], "hard": []}, f"未找到 {TASKS_FILE},使用空题库。"
21
  try:
@@ -37,30 +33,52 @@ def init_state():
37
  "current": None,
38
  "current_level": None,
39
  "tasks": tasks,
40
- "load_msg": msg # 首次加载提示
 
41
  }
42
 
43
  # ====== 业务逻辑 ======
44
  def progress_text(state):
45
  pct = min(100, int(state["done_percent"]))
46
- bar = f"{'█' * (pct//5)}{'░' * (20 - pct//5)}" # 20格进度条
47
  return f"进度:{pct}%\n{bar}"
48
 
49
  def draw_task(state, level_cn):
 
 
 
50
  level = LABEL_TO_KEY.get(level_cn, "easy")
51
  pool = state["tasks"].get(level, [])
52
  if not pool:
53
- return state, f"({LEVEL_LABELS[level]}暂无任务,请检查 {TASKS_FILE})", progress_text(state), state.get("load_msg") or ""
54
  task = random.choice(pool)
55
  state["current"] = task
56
  state["current_level"] = level
57
- return state, f"难度:{LEVEL_LABELS[level]}\n\n任务:{task}", progress_text(state), ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  def complete_task(state, note):
60
  if not state["current"]:
61
- return state, "(请先抽取任务)", progress_text(state)
62
  level = state["current_level"]
63
  reward = LEVEL_REWARDS.get(level, 0)
 
64
  state["done_percent"] = min(TARGET_PERCENT, state["done_percent"] + reward)
65
  state["history"].append((
66
  state["current"], level,
@@ -69,18 +87,25 @@ def complete_task(state, note):
69
  ))
70
  msg = f"✅ 已完成:{state['current']}(+{reward}%)"
71
  state["current"], state["current_level"] = None, None
72
- return state, msg, progress_text(state)
 
 
 
 
 
 
 
73
 
74
  def skip_task(state):
75
  if not state["current"]:
76
- return state, "(当前没有任务)", progress_text(state)
77
  skipped = state["current"]
78
  state["current"], state["current_level"] = None, None
79
- return state, f"⏭ 已跳过:{skipped}", progress_text(state)
80
 
81
  def reset_state():
82
  st = init_state()
83
- return st, "已重置进度。", progress_text(st), (st["load_msg"] or "")
84
 
85
  def reload_tasks(state):
86
  tasks, msg = load_tasks_from_file()
@@ -101,88 +126,148 @@ def export_progress(state):
101
  content = "\n".join(lines) if len(lines) > 3 else "暂无记录"
102
  return gr.File.update(value=("progress.txt", content))
103
 
104
- # ====== 界面��式 ======
105
  theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate")
106
  CSS = """
107
  body,.gradio-container{background:#faf7fb!important;}
108
- .topbar{display:flex;align-items:center;gap:8px;padding:12px;background:linear-gradient(90deg,#ffe8f3,#e8fbff);border:1px solid #eddcf3;border-radius:12px;}
109
- .title{font-weight:800;font-size:20px;color:#111;}
110
- .card{background:#fff;border:1px solid #eddcf3;border-radius:12px;box-shadow:0 4px 18px rgba(0,0,0,.05);padding:14px;}
 
111
  .taskbox{border:1px dashed #eddcf3;border-radius:10px;padding:12px;font-weight:700;}
112
  .tip{color:#6b7280;font-size:12px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  """
114
 
115
- # ====== 构建 UI ======
116
  with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS) as demo:
117
- gr.HTML(f"""<div class='topbar'><div class='title'>{APP_TITLE}</div></div>""")
118
-
119
  state = gr.State(init_state())
120
 
121
- with gr.Row():
122
- with gr.Column(scale=5):
123
- with gr.Group(elem_classes=["card"]):
124
- gr.Markdown("#### 选择难度")
125
- level_choice = gr.Radio(
126
- label="难度",
127
- choices=[LEVEL_LABELS["easy"], LEVEL_LABELS["medium"], LEVEL_LABELS["hard"]],
128
- value=LEVEL_LABELS["easy"]
129
- )
130
- with gr.Row():
131
- btn_draw = gr.Button("🎲 抽取任务", variant="primary")
132
- btn_reload = gr.Button("🔁 刷新题库")
133
- with gr.Row():
134
- btn_complete = gr.Button("✅ 标记完成")
135
- btn_skip = gr.Button("⏭ 跳过")
136
- reflect = gr.Textbox(label="反思(可选)", placeholder="完成后写一句感受", lines=2)
137
- with gr.Row():
138
- btn_export = gr.Button("📤 导出记录")
139
- btn_reset = gr.Button("🧹 重置进度")
140
-
141
- with gr.Column(scale=7):
142
- with gr.Group(elem_classes=["card"]):
143
- gr.Markdown("#### 当前任务")
144
- current_task = gr.Markdown("(点击 🎲 抽取任务)", elem_classes=["taskbox"])
145
- progress = gr.Markdown("进度:0%\n░░░░░░░░░░░░░░░░░░░░░░")
146
- tip = gr.Markdown("", elem_classes=["tip"]) # 用于显示加载/刷新提示
147
- file_out = gr.File(label="下载导出记录", visible=True)
148
-
149
- # ====== 事件处理 ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def on_draw(st, lvl_cn):
151
- st["load_msg"] = "" # 清理一次性提示
152
- st, msg, prog, info = draw_task(st, lvl_cn)
153
- return st, msg, prog, (info or "")
154
 
155
  def on_complete(st, note):
156
- st, msg, prog = complete_task(st, note)
157
- return st, msg, prog, "", "" # 清空 tip 与反思框
158
 
159
  def on_skip(st):
160
- st, msg, prog = skip_task(st)
161
- return st, msg, prog, ""
162
 
163
  def on_export(st):
164
  return export_progress(st)
165
 
166
  def on_reset():
167
- st, msg, prog, info = reset_state()
168
- return st, "(点击 🎲 抽取任务)", prog, info, None
169
 
170
  def on_reload(st):
171
  st, info = reload_tasks(st)
172
  return st, info
173
 
174
- # 绑定
175
- btn_draw.click(on_draw, inputs=[state, level_choice], outputs=[state, current_task, progress, tip])
176
- btn_complete.click(on_complete, inputs=[state, reflect], outputs=[state, current_task, progress, tip, reflect])
177
- btn_skip.click(on_skip, inputs=[state], outputs=[state, current_task, progress, tip])
 
178
  btn_export.click(on_export, inputs=[state], outputs=[file_out])
179
- btn_reset.click(on_reset, outputs=[state, current_task, progress, tip, file_out])
180
  btn_reload.click(on_reload, inputs=[state], outputs=[state, tip])
181
 
182
- # ⚠️ 修正点:用 demo.load,而不是 tip.load
183
- def show_first_tip(st):
184
- return st.get("load_msg") or ""
185
- demo.load(show_first_tip, inputs=[state], outputs=[tip])
186
-
187
  if __name__ == "__main__":
188
  demo.launch(ssr_mode=False)
 
1
+ import os, json, random, datetime
2
  import gradio as gr
3
 
4
  APP_TITLE = "100个不"
5
+ TASKS_FILE = "tasks.json"
6
  TARGET_PERCENT = 100
7
 
8
  # 难度映射与奖励
 
12
 
13
  # ====== 读取 tasks.json ======
14
  def load_tasks_from_file():
 
 
 
 
15
  if not os.path.exists(TASKS_FILE):
16
  return {"easy": [], "medium": [], "hard": []}, f"未找到 {TASKS_FILE},使用空题库。"
17
  try:
 
33
  "current": None,
34
  "current_level": None,
35
  "tasks": tasks,
36
+ "load_msg": msg, # 首次加载提示(可能为 None)
37
+ "celebrated": False # 是否已经庆祝过 100%
38
  }
39
 
40
  # ====== 业务逻辑 ======
41
  def progress_text(state):
42
  pct = min(100, int(state["done_percent"]))
43
+ bar = f"{'█' * (pct//5)}{'░' * (20 - pct//5)}" # 20格条
44
  return f"进度:{pct}%\n{bar}"
45
 
46
  def draw_task(state, level_cn):
47
+ # 若已满 100%,提示先庆祝或重置
48
+ if state["done_percent"] >= TARGET_PERCENT:
49
+ return state, "🎉 你已达成 100%,可以重置开始新循环或导出记录。", progress_text(state), state.get("load_msg") or "", ""
50
  level = LABEL_TO_KEY.get(level_cn, "easy")
51
  pool = state["tasks"].get(level, [])
52
  if not pool:
53
+ return state, f"({LEVEL_LABELS[level]}暂无任务,请检查 {TASKS_FILE})", progress_text(state), state.get("load_msg") or "", ""
54
  task = random.choice(pool)
55
  state["current"] = task
56
  state["current_level"] = level
57
+ return state, f"难度:{LEVEL_LABELS[level]}\n\n任务:{task}", progress_text(state), "", ""
58
+
59
+ def _celebration_overlay():
60
+ # 纯 CSS/HTML 彩带 + 祝贺语;点击遮罩或按钮关闭
61
+ return """
62
+ <div class="cele-overlay" onclick="this.remove()">
63
+ <div class="cele-modal" onclick="event.stopPropagation()">
64
+ <div class="cele-title">🎉 恭喜完成 100 个不!</div>
65
+ <div class="cele-sub">你已经把“拒绝”练成了勇气。去对世界说 YES 吧!</div>
66
+ <div class="confetti">
67
+ <span></span><span></span><span></span><span></span><span></span>
68
+ <span></span><span></span><span></span><span></span><span></span>
69
+ <span></span><span></span><span></span><span></span><span></span>
70
+ </div>
71
+ <button class="cele-btn" onclick="document.querySelector('.cele-overlay')?.remove()">关闭</button>
72
+ </div>
73
+ </div>
74
+ """
75
 
76
  def complete_task(state, note):
77
  if not state["current"]:
78
+ return state, "(请先抽取任务)", progress_text(state), ""
79
  level = state["current_level"]
80
  reward = LEVEL_REWARDS.get(level, 0)
81
+ before = state["done_percent"]
82
  state["done_percent"] = min(TARGET_PERCENT, state["done_percent"] + reward)
83
  state["history"].append((
84
  state["current"], level,
 
87
  ))
88
  msg = f"✅ 已完成:{state['current']}(+{reward}%)"
89
  state["current"], state["current_level"] = None, None
90
+
91
+ # 触发庆祝:从 <100 提升到 >=100 且未庆祝过
92
+ cele_html = ""
93
+ if before < TARGET_PERCENT and state["done_percent"] >= TARGET_PERCENT and not state["celebrated"]:
94
+ state["celebrated"] = True
95
+ cele_html = _celebration_overlay()
96
+
97
+ return state, msg, progress_text(state), cele_html
98
 
99
  def skip_task(state):
100
  if not state["current"]:
101
+ return state, "(当前没有任务)", progress_text(state), ""
102
  skipped = state["current"]
103
  state["current"], state["current_level"] = None, None
104
+ return state, f"⏭ 已跳过:{skipped}", progress_text(state), ""
105
 
106
  def reset_state():
107
  st = init_state()
108
+ return st, "已重置进度。", progress_text(st), (st["load_msg"] or ""), ""
109
 
110
  def reload_tasks(state):
111
  tasks, msg = load_tasks_from_file()
 
126
  content = "\n".join(lines) if len(lines) > 3 else "暂无记录"
127
  return gr.File.update(value=("progress.txt", content))
128
 
129
+ # ====== 样式 ======
130
  theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate")
131
  CSS = """
132
  body,.gradio-container{background:#faf7fb!important;}
133
+ .topbar{display:flex;align-items:center;gap:8px;padding:28px 16px;background:linear-gradient(90deg,#ffe8f3,#e8fbff);
134
+ border:1px solid #eddcf3;border-radius:16px;margin-bottom:12px;}
135
+ .title{font-weight:900;font-size:28px;color:#111;}
136
+ .card{background:#fff;border:1px solid #eddcf3;border-radius:14px;box-shadow:0 4px 18px rgba(0,0,0,.05);padding:16px;}
137
  .taskbox{border:1px dashed #eddcf3;border-radius:10px;padding:12px;font-weight:700;}
138
  .tip{color:#6b7280;font-size:12px;}
139
+ .hero{display:flex;flex-direction:column;gap:12px;align-items:flex-start;padding:24px;}
140
+ .hero h1{margin:0;font-size:32px}
141
+ .hero p{margin:0;color:#374151}
142
+ .hero .cta{padding:10px 16px;border-radius:10px;border:1px solid #eddcf3;background:#fff;cursor:pointer;font-weight:700}
143
+ .hero .cta:hover{border-color:#ff4fa3;color:#ff4fa3}
144
+
145
+ /* === 庆祝叠加层 === */
146
+ .cele-overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:9999;}
147
+ .cele-modal{width:min(560px,92vw);background:#fff;border:1px solid #eddcf3;border-radius:16px;
148
+ box-shadow:0 20px 60px rgba(0,0,0,.18);padding:20px 18px 16px;text-align:center;position:relative;overflow:hidden;}
149
+ .cele-title{font-size:22px;font-weight:900;margin-bottom:6px;}
150
+ .cele-sub{color:#374151;margin-bottom:12px;}
151
+ .cele-btn{padding:10px 14px;border-radius:10px;border:1px solid #eddcf3;background:#fff;cursor:pointer;font-weight:700;}
152
+ .cele-btn:hover{border-color:#ff4fa3;color:#ff4fa3}
153
+
154
+ /* 简易 confetti 动效(纯 CSS) */
155
+ .confetti{position:absolute;inset:0;pointer-events:none;}
156
+ .confetti span{position:absolute;top:-10px;width:10px;height:14px;border-radius:2px;animation:fall 1.8s linear infinite;opacity:.9}
157
+ .confetti span:nth-child(3n){width:8px;height:12px}
158
+ .confetti span:nth-child(5n){width:6px;height:10px}
159
+ .confetti span:nth-child(odd){background:#ff4fa3}
160
+ .confetti span:nth-child(even){background:#5ee0ff}
161
+ .confetti span:nth-child(3n+2){background:#ffd166}
162
+ .confetti span:nth-child(1){left:8%}
163
+ .confetti span:nth-child(2){left:18%}
164
+ .confetti span:nth-child(3){left:28%}
165
+ .confetti span:nth-child(4){left:38%}
166
+ .confetti span:nth-child(5){left:48%}
167
+ .confetti span:nth-child(6){left:58%}
168
+ .confetti span:nth-child(7){left:68%}
169
+ .confetti span:nth-child(8){left:78%}
170
+ .confetti span:nth-child(9){left:88%}
171
+ .confetti span:nth-child(10){left:96%}
172
+ @keyframes fall{
173
+ 0%{transform:translateY(-20px) rotate(0deg)}
174
+ 100%{transform:translateY(520px) rotate(360deg)}
175
+ }
176
  """
177
 
178
+ # ====== UI ======
179
  with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS) as demo:
 
 
180
  state = gr.State(init_state())
181
 
182
+ # --- 首屏(Landing) ---
183
+ with gr.Group(visible=True) as landing:
184
+ gr.HTML("""
185
+ <div class='topbar'><div class='title'>100个不</div></div>
186
+ """)
187
+ gr.HTML("""
188
+ <div class="card hero">
189
+ <h1>从一次“��”,开始对世界说“是”</h1>
190
+ <p>每天一个可控的小挑战,练习被看见、被拒绝也没关系的勇气。</p>
191
+ <p>完成 100 次小暴露,社交恐惧会一点点消退。</p>
192
+ </div>
193
+ """)
194
+ start_btn = gr.Button("开始挑战 ▶️", variant="primary")
195
+
196
+ # --- 主界面(隐藏,点击开始后显示) ---
197
+ with gr.Group(visible=False) as main:
198
+ gr.HTML(f"""<div class='topbar'><div class='title'>{APP_TITLE}</div></div>""")
199
+
200
+ with gr.Row():
201
+ with gr.Column(scale=5):
202
+ with gr.Group(elem_classes=["card"]):
203
+ gr.Markdown("#### 选择难度")
204
+ level_choice = gr.Radio(
205
+ label="难度",
206
+ choices=[LEVEL_LABELS["easy"], LEVEL_LABELS["medium"], LEVEL_LABELS["hard"]],
207
+ value=LEVEL_LABELS["easy"]
208
+ )
209
+ with gr.Row():
210
+ btn_draw = gr.Button("🎲 抽取任务", variant="primary")
211
+ btn_reload = gr.Button("🔁 刷新题库")
212
+ with gr.Row():
213
+ btn_complete = gr.Button("✅ 标记完成")
214
+ btn_skip = gr.Button("⏭ 跳过")
215
+ reflect = gr.Textbox(label="反思(可选)", placeholder="完成后写一句感受", lines=2)
216
+ with gr.Row():
217
+ btn_export = gr.Button("📤 导出记录")
218
+ btn_reset = gr.Button("🧹 重置进度")
219
+
220
+ with gr.Column(scale=7):
221
+ with gr.Group(elem_classes=["card"]):
222
+ gr.Markdown("#### 当前任务")
223
+ current_task = gr.Markdown("(点击 🎲 抽取任务)", elem_classes=["taskbox"])
224
+ progress = gr.Markdown("进度:0%\n░░░░░░░░░░░░░░░░░░░░░░")
225
+ tip = gr.Markdown("", elem_classes=["tip"])
226
+ file_out = gr.File(label="下载导出记录", visible=True)
227
+
228
+ # 庆祝叠加层(默认空)
229
+ cele = gr.HTML(value="", visible=True)
230
+
231
+ # ====== 行为函数 ======
232
+ def on_start(st):
233
+ first_tip = st.get("load_msg") or ""
234
+ st["load_msg"] = "" # 只显示一次
235
+ return (gr.update(visible=False), # 隐藏 landing
236
+ gr.update(visible=True), # 显示 main
237
+ first_tip, # 主界面右侧提示
238
+ "") # 清空庆祝层
239
+
240
  def on_draw(st, lvl_cn):
241
+ st, msg, prog, info, cele_html = draw_task(st, lvl_cn)
242
+ return st, msg, prog, (info or ""), cele_html
 
243
 
244
  def on_complete(st, note):
245
+ st, msg, prog, cele_html = complete_task(st, note)
246
+ return st, msg, prog, "", "", cele_html # 清 tip/反思;可能弹出庆祝
247
 
248
  def on_skip(st):
249
+ st, msg, prog, cele_html = skip_task(st)
250
+ return st, msg, prog, "", cele_html
251
 
252
  def on_export(st):
253
  return export_progress(st)
254
 
255
  def on_reset():
256
+ st, msg, prog, info, cele_html = reset_state()
257
+ return st, "(点击 🎲 抽取任务)", prog, info, None, cele_html
258
 
259
  def on_reload(st):
260
  st, info = reload_tasks(st)
261
  return st, info
262
 
263
+ # ====== 事件绑定 ======
264
+ start_btn.click(on_start, inputs=[state], outputs=[landing, main, tip, cele])
265
+ btn_draw.click(on_draw, inputs=[state, level_choice], outputs=[state, current_task, progress, tip, cele])
266
+ btn_complete.click(on_complete, inputs=[state, reflect], outputs=[state, current_task, progress, tip, reflect, cele])
267
+ btn_skip.click(on_skip, inputs=[state], outputs=[state, current_task, progress, tip, cele])
268
  btn_export.click(on_export, inputs=[state], outputs=[file_out])
269
+ btn_reset.click(on_reset, outputs=[state, current_task, progress, tip, file_out, cele])
270
  btn_reload.click(on_reload, inputs=[state], outputs=[state, tip])
271
 
 
 
 
 
 
272
  if __name__ == "__main__":
273
  demo.launch(ssr_mode=False)