Spaces:
Sleeping
Sleeping
Download app.py from sofiamak/OneHundredNo: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/sofiamak/OneHundredNo/resolve/main/app.py
- Command line
-
hf download hf://spaces/sofiamak/OneHundredNo/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sofiamak/OneHundredNo/resolve/main/app.py
10.9 kB
| 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 """ | |
| <div class="cele-overlay" onclick="this.remove()"> | |
| <div class="cele-modal" onclick="event.stopPropagation()"> | |
| <div class="cele-title">🎉 恭喜完成 100 个不!</div> | |
| <div class="cele-sub">你已经把“拒绝”练成了勇气。去对世界说 YES 吧!</div> | |
| <div class="confetti"> | |
| <span></span><span></span><span></span><span></span><span></span> | |
| <span></span><span></span><span></span><span></span><span></span> | |
| <span></span><span></span><span></span><span></span><span></span> | |
| </div> | |
| <button class="cele-btn" onclick="document.querySelector('.cele-overlay')?.remove()">关闭</button> | |
| </div> | |
| </div> | |
| """ | |
| 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(""" | |
| <div class='topbar'><div class='title'>100个不</div></div> | |
| """) | |
| gr.HTML(""" | |
| <div class="card hero"> | |
| <h1>A gentle social challenge for shy souls.</h1> | |
| <p>Collect 100 “no’s,” build real courage, and discover the world isn’t that scary after all.</p> | |
| </div> | |
| """) | |
| start_btn = gr.Button("开始挑战 ▶️", variant="primary") | |
| # --- 主界面 --- | |
| with gr.Group(visible=False) as main: | |
| gr.HTML(f"""<div class='topbar'><div class='title'>{APP_TITLE}</div></div>""") | |
| 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) |