import os, json, random, datetime import gradio as gr APP_TITLE = "100个不" TASKS_FILE = "tasks.json" TARGET_PERCENT = 100 LEVEL_LABELS = {"easy": "简单", "medium": "进阶", "hard": "挑战"} LABEL_TO_KEY = {v: k for k, v in LEVEL_LABELS.items()} LEVEL_REWARDS = {"easy": 1, "medium": 5, "hard": 10} # ====== 读取 tasks.json ====== def load_tasks_from_file(): if not os.path.exists(TASKS_FILE): return {"easy": [], "medium": [], "hard": []}, f"未找到 {TASKS_FILE},使用空题库。" try: with open(TASKS_FILE, "r", encoding="utf-8") as f: data = json.load(f) for k in ["easy", "medium", "hard"]: if k not in data or not isinstance(data[k], list): raise ValueError("JSON 结构不正确,应包含 easy/medium/hard 三个数组。") return data, None except Exception as e: return {"easy": [], "medium": [], "hard": []}, f"{TASKS_FILE} 解析失败:{e}。已使用空题库。" def init_state(): tasks, msg = load_tasks_from_file() return { "done_percent": 0, "history": [], "current": None, "current_level": None, "tasks": tasks, "load_msg": msg, "celebrated": False } def progress_text(state): pct = min(100, int(state["done_percent"])) bar = f"{'█' * (pct//5)}{'░' * (20 - pct//5)}" return f"进度:{pct}%\n{bar}" def draw_task(state, level_cn): if state["done_percent"] >= TARGET_PERCENT: return state, "🎉 你已达成 100%,可以重置开始新循环或导出记录。", progress_text(state), state.get("load_msg") or "", "" level = LABEL_TO_KEY.get(level_cn, "easy") pool = state["tasks"].get(level, []) if not pool: return state, f"({LEVEL_LABELS[level]}暂无任务,请检查 {TASKS_FILE})", progress_text(state), state.get("load_msg") or "", "" task = random.choice(pool) state["current"] = task state["current_level"] = level return state, f"难度:{LEVEL_LABELS[level]}\n\n任务:{task}", progress_text(state), "", "" def _celebration_overlay(): return """
🎉 恭喜完成 100 个不!
你已经把“拒绝”练成了勇气。去对世界说 YES 吧!
""" def complete_task(state, note): if not state["current"]: return state, "(请先抽取任务)", progress_text(state), "" level = state["current_level"] reward = LEVEL_REWARDS.get(level, 0) before = state["done_percent"] state["done_percent"] = min(TARGET_PERCENT, state["done_percent"] + reward) state["history"].append(( state["current"], level, datetime.datetime.now().isoformat(timespec="seconds"), (note or "").strip() )) msg = f"✅ 已完成:{state['current']}(+{reward}%)" state["current"], state["current_level"] = None, None cele_html = "" if before < TARGET_PERCENT and state["done_percent"] >= TARGET_PERCENT and not state["celebrated"]: state["celebrated"] = True cele_html = _celebration_overlay() return state, msg, progress_text(state), cele_html def skip_task(state): if not state["current"]: return state, "(当前没有任务)", progress_text(state), "" skipped = state["current"] state["current"], state["current_level"] = None, None return state, f"⏭ 已跳过:{skipped}", progress_text(state), "" def reset_state(): st = init_state() return st, "已重置进度。", progress_text(st), (st["load_msg"] or ""), "" def reload_tasks(state): tasks, msg = load_tasks_from_file() state["tasks"] = tasks state["current"], state["current_level"] = None, None tip = msg if msg else "已从 tasks.json 重新加载题库。" return state, tip def export_progress(state): lines = [ "# 100个不|练习记录", f"已完成:{state['done_percent']}%", "" ] for i, (task, lvl, ts, note) in enumerate(state["history"], 1): note_part = f" 反思:{note}" if note else "" lines.append(f"{i}. [{ts}] {LEVEL_LABELS[lvl]}任务:{task}{note_part}") content = "\n".join(lines) if len(lines) > 3 else "暂无记录" return gr.File.update(value=("progress.txt", content)) theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate") CSS = """ body,.gradio-container{background:#faf7fb!important;} .topbar{display:flex;align-items:center;gap:8px;padding:28px 16px;background:linear-gradient(90deg,#ffe8f3,#e8fbff); border:1px solid #eddcf3;border-radius:16px;margin-bottom:12px;} .title{font-weight:900;font-size:28px;color:#111;} .card{background:#fff;border:1px solid #eddcf3;border-radius:14px;box-shadow:0 4px 18px rgba(0,0,0,.05);padding:16px;} .taskbox{border:1px dashed #eddcf3;border-radius:10px;padding:12px;font-weight:700;} .tip{color:#6b7280;font-size:12px;} .hero{display:flex;flex-direction:column;gap:12px;align-items:flex-start;padding:24px;} .hero h1{margin:0;font-size:32px} .hero p{margin:0;color:#374151} .hero .cta{padding:10px 16px;border-radius:10px;border:1px solid #eddcf3;background:#fff;cursor:pointer;font-weight:700} .hero .cta:hover{border-color:#ff4fa3;color:#ff4fa3} .cele-overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:9999;} .cele-modal{width:min(560px,92vw);background:#fff;border:1px solid #eddcf3;border-radius:16px; box-shadow:0 20px 60px rgba(0,0,0,.18);padding:20px 18px 16px;text-align:center;position:relative;overflow:hidden;} .cele-title{font-size:22px;font-weight:900;margin-bottom:6px;} .cele-sub{color:#374151;margin-bottom:12px;} .cele-btn{padding:10px 14px;border-radius:10px;border:1px solid #eddcf3;background:#fff;cursor:pointer;font-weight:700;} .cele-btn:hover{border-color:#ff4fa3;color:#ff4fa3} .confetti{position:absolute;inset:0;pointer-events:none;} .confetti span{position:absolute;top:-10px;width:10px;height:14px;border-radius:2px;animation:fall 1.8s linear infinite;opacity:.9} .confetti span:nth-child(odd){background:#ff4fa3} .confetti span:nth-child(even){background:#5ee0ff} @keyframes fall{ 0%{transform:translateY(-20px) rotate(0deg)} 100%{transform:translateY(520px) rotate(360deg)} } """ with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS) as demo: state = gr.State(init_state()) # --- 首屏(Landing) --- with gr.Group(visible=True) as landing: gr.HTML("""
100个不
""") gr.HTML("""

A gentle social challenge for shy souls.

Collect 100 “no’s,” build real courage, and discover the world isn’t that scary after all.

""") start_btn = gr.Button("开始挑战 ▶️", variant="primary") # --- 主界面 --- with gr.Group(visible=False) as main: gr.HTML(f"""
{APP_TITLE}
""") with gr.Row(): with gr.Column(scale=5): with gr.Group(elem_classes=["card"]): gr.Markdown("#### 选择难度") level_choice = gr.Radio( label="难度", choices=[LEVEL_LABELS["easy"], LEVEL_LABELS["medium"], LEVEL_LABELS["hard"]], value=LEVEL_LABELS["easy"] ) with gr.Row(): btn_draw = gr.Button("🎲 抽取任务", variant="primary") btn_reload = gr.Button("🔁 刷新题库") with gr.Row(): btn_complete = gr.Button("✅ 标记完成") btn_skip = gr.Button("⏭ 跳过") reflect = gr.Textbox(label="反思(可选)", placeholder="完成后写一句感受", lines=2) with gr.Row(): btn_export = gr.Button("📤 导出记录") btn_reset = gr.Button("🧹 重置进度") with gr.Column(scale=7): with gr.Group(elem_classes=["card"]): gr.Markdown("#### 当前任务") current_task = gr.Markdown("(点击 🎲 抽取任务)", elem_classes=["taskbox"]) progress = gr.Markdown("进度:0%\n░░░░░░░░░░░░░░░░░░░░░░") tip = gr.Markdown("", elem_classes=["tip"]) file_out = gr.File(label="下载导出记录", visible=True) cele = gr.HTML(value="", visible=True) # ====== 事件绑定 ====== def on_start(st): first_tip = st.get("load_msg") or "" st["load_msg"] = "" return (gr.update(visible=False), gr.update(visible=True), first_tip, "") def on_draw(st, lvl_cn): st, msg, prog, info, cele_html = draw_task(st, lvl_cn) return st, msg, prog, info, cele_html def on_complete(st, note): st, msg, prog, cele_html = complete_task(st, note) return st, msg, prog, "", "", cele_html def on_skip(st): st, msg, prog, cele_html = skip_task(st) return st, msg, prog, "", cele_html def on_export(st): return export_progress(st) def on_reset(): st, msg, prog, info, cele_html = reset_state() return st, "(点击 🎲 抽取任务)", prog, info, None, cele_html def on_reload(st): st, info = reload_tasks(st) return st, info start_btn.click(on_start, inputs=[state], outputs=[landing, main, tip, cele]) btn_draw.click(on_draw, inputs=[state, level_choice], outputs=[state, current_task, progress, tip, cele]) btn_complete.click(on_complete, inputs=[state, reflect], outputs=[state, current_task, progress, tip, reflect, cele]) btn_skip.click(on_skip, inputs=[state], outputs=[state, current_task, progress, tip, cele]) btn_export.click(on_export, inputs=[state], outputs=[file_out]) btn_reset.click(on_reset, outputs=[state, current_task, progress, tip, file_out, cele]) btn_reload.click(on_reload, inputs=[state], outputs=[state, tip]) if __name__ == "__main__": demo.launch(ssr_mode=False)