Spaces:
Sleeping
Sleeping
File size: 10,878 Bytes
541ad39 bbee108 cb1ecf1 541ad39 cb1ecf1 ca323ef bbee108 ca323ef bbee108 cb1ecf1 50dedd0 bbee108 cb1ecf1 ca323ef 50dedd0 bbee108 ca323ef 50dedd0 ca323ef 541ad39 ca323ef 541ad39 ca323ef bbee108 ca323ef 541ad39 bbee108 541ad39 cb1ecf1 541ad39 ca323ef cb1ecf1 541ad39 bbee108 541ad39 bbee108 cb1ecf1 541ad39 bbee108 cb1ecf1 541ad39 ca323ef bbee108 cb1ecf1 ca323ef cb1ecf1 ca323ef bbee108 cb1ecf1 bbee108 541ad39 cb1ecf1 541ad39 bbee108 ca323ef bbee108 541ad39 50dedd0 541ad39 50dedd0 541ad39 50dedd0 541ad39 50dedd0 541ad39 50dedd0 541ad39 ca323ef 541ad39 50dedd0 bbee108 541ad39 50dedd0 e1e9747 bbee108 541ad39 e1e9747 bbee108 cb1ecf1 e1e9747 bbee108 541ad39 e1e9747 ca323ef 541ad39 bbee108 541ad39 ca323ef bbee108 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | 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) |