OneHundredNo / app.py
sofiamak's picture
Update app.py
50dedd0 verified
Raw History Blame Contribute Delete
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)