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 """
""" 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(""" """) gr.HTML("""Collect 100 “no’s,” build real courage, and discover the world isn’t that scary after all.