CodeSoft commited on
Commit
0b5f313
·
verified ·
1 Parent(s): 5d836a6

Deploy SLM Consortium Polls app

Browse files
README.md CHANGED
@@ -1,13 +1,20 @@
1
  ---
2
- title: Voting Test
3
- emoji: 🐢
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.26.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: SLM Consortium Polls
3
+ emoji: 🗳️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.25.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Create polls and vote — members only
12
+ hf_oauth: true
13
+ # NOTE: Uncomment the line below to hard-gate the entire Space at the
14
+ # platform level so only slmconsortium members can even open it.
15
+ # App-level checks in app.py already enforce this for create/vote,
16
+ # while still allowing public viewing of results.
17
+ # hf_oauth_authorized_org: slmconsortium
18
  ---
19
 
20
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SLM Consortium Polls — Hugging Face Space.
2
+
3
+ Astro static frontend (frontend/dist, React + React Bits components)
4
+ served at `/` by this Python script. Gradio backend mounted at
5
+ `/gradio` provides the poll API *and* Hugging Face OAuth — the Astro
6
+ app talks to it via @gradio/client on the same origin, so the session
7
+ cookie carries HF auth and every mutating call is org-gated.
8
+
9
+ Auth model:
10
+ - Space metadata sets `hf_oauth: true` (see README.md).
11
+ - `check_membership()` verifies `slmconsortium` membership via
12
+ `whoami(user_token)["orgs"]`, with a public
13
+ `list_organization_members` fallback.
14
+ - Signed-out users can read polls/results; only members can vote/create.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import threading
22
+ import uuid
23
+ from datetime import datetime, timezone
24
+ from typing import Dict, List, Optional, Tuple
25
+
26
+ import gradio as gr
27
+ from fastapi import FastAPI, Request
28
+ from fastapi.responses import RedirectResponse
29
+ from fastapi.staticfiles import StaticFiles
30
+ from huggingface_hub import HfApi, whoami
31
+
32
+ REQUIRED_ORG = os.getenv("REQUIRED_ORG", "slmconsortium")
33
+ MEMBERSHIP_CACHE_TTL = 300 # seconds
34
+
35
+ _HERE = os.path.dirname(os.path.abspath(__file__))
36
+ # Persistent storage on Spaces lives under /data (when enabled).
37
+ # Fall back to the app directory for local dev / ephemeral disk.
38
+ _DATA_DIR = "/data" if os.path.isdir("/data") else _HERE
39
+ POLLS_FILE = os.path.join(_DATA_DIR, "polls.json")
40
+ DIST_DIR = os.path.join(_HERE, "frontend", "dist")
41
+ _lock = threading.Lock()
42
+ _member_cache: Dict[str, Tuple[bool, float]] = {}
43
+
44
+
45
+ # ---------------------------------------------------------------- storage
46
+
47
+ def load_polls() -> Dict:
48
+ if not os.path.exists(POLLS_FILE):
49
+ return {}
50
+ try:
51
+ with open(POLLS_FILE, "r", encoding="utf-8") as f:
52
+ data = json.load(f)
53
+ return data if isinstance(data, dict) else {}
54
+ except (json.JSONDecodeError, OSError):
55
+ return {}
56
+
57
+
58
+ def save_polls(polls: Dict) -> None:
59
+ tmp = POLLS_FILE + ".tmp"
60
+ with open(tmp, "w", encoding="utf-8") as f:
61
+ json.dump(polls, f, indent=2, ensure_ascii=False)
62
+ os.replace(tmp, POLLS_FILE)
63
+
64
+
65
+ def total_votes(poll: Dict) -> int:
66
+ return len(poll.get("voters", {}))
67
+
68
+
69
+ def count_votes(poll: Dict) -> List[int]:
70
+ n = len(poll.get("options", []))
71
+ counts = [0] * n
72
+ for idx in poll.get("voters", {}).values():
73
+ if isinstance(idx, int) and 0 <= idx < n:
74
+ counts[idx] += 1
75
+ return counts
76
+
77
+
78
+ def parse_options(raw: str) -> List[str]:
79
+ options = [o.strip() for o in (raw or "").splitlines() if o.strip()]
80
+ if len(options) <= 1 and raw and "," in raw:
81
+ options = [o.strip() for o in raw.split(",") if o.strip()]
82
+ return options
83
+
84
+
85
+ def poll_to_dict(poll: Dict, username: Optional[str]) -> Dict:
86
+ counts = count_votes(poll)
87
+ n = len(poll.get("options", []))
88
+ return {
89
+ "id": poll["id"],
90
+ "question": poll["question"],
91
+ "options": poll["options"],
92
+ "counts": counts,
93
+ "total": sum(counts),
94
+ "created_by": poll.get("created_by", "?"),
95
+ "created_at": poll.get("created_at", ""),
96
+ "my_vote": (poll.get("voters", {}).get(username) if username else None),
97
+ # username -> option index (sanitized)
98
+ "voters": {
99
+ u: idx
100
+ for u, idx in poll.get("voters", {}).items()
101
+ if isinstance(idx, int) and 0 <= idx < n
102
+ },
103
+ }
104
+
105
+
106
+ # ------------------------------------------------------------------ auth
107
+
108
+ def check_membership(
109
+ profile: gr.OAuthProfile | None,
110
+ token: gr.OAuthToken | None,
111
+ ) -> Tuple[bool, str, str]:
112
+ """Return (allowed, username, status_message)."""
113
+ if profile is None:
114
+ return False, "", "🔒 Please **sign in with Hugging Face** to create polls or vote."
115
+
116
+ username = profile.username or ""
117
+ now = datetime.now(timezone.utc).timestamp()
118
+ cached = _member_cache.get(username.lower())
119
+ if cached is not None:
120
+ allowed, ts = cached
121
+ if now - ts < MEMBERSHIP_CACHE_TTL:
122
+ msg = (
123
+ f"✅ Signed in as **@{username}** — member of `{REQUIRED_ORG}`."
124
+ if allowed
125
+ else f"⛔ Signed in as **@{username}** — not a member of `{REQUIRED_ORG}`. "
126
+ "Creating polls and voting are disabled."
127
+ )
128
+ return allowed, username, msg
129
+
130
+ # 1) Preferred: whoami with the user's own OAuth token.
131
+ # Sees private org memberships too.
132
+ if token is not None and getattr(token, "token", None):
133
+ try:
134
+ info = whoami(token.token)
135
+ orgs = info.get("orgs", []) or []
136
+ names = [
137
+ (o.get("name", "") if isinstance(o, dict) else str(o)).lower()
138
+ for o in orgs
139
+ ]
140
+ allowed = REQUIRED_ORG.lower() in names
141
+ _member_cache[username.lower()] = (allowed, now)
142
+ if allowed:
143
+ return True, username, f"✅ Signed in as **@{username}** — member of `{REQUIRED_ORG}`."
144
+ return False, username, (
145
+ f"⛔ Signed in as **@{username}** — not a member of `{REQUIRED_ORG}`. "
146
+ "Creating polls and voting are disabled."
147
+ )
148
+ except Exception:
149
+ pass # fall through to public check
150
+
151
+ # 2) Fallback: public org member list (works without a user token
152
+ # for public orgs; also covers local-dev where token may be None).
153
+ try:
154
+ api = HfApi()
155
+ members = [m.username.lower() for m in api.list_organization_members(REQUIRED_ORG)]
156
+ allowed = username.lower() in members
157
+ _member_cache[username.lower()] = (allowed, now)
158
+ if allowed:
159
+ return True, username, f"✅ Signed in as **@{username}** — member of `{REQUIRED_ORG}`."
160
+ return False, username, (
161
+ f"⛔ Signed in as **@{username}** — not a member of `{REQUIRED_ORG}`. "
162
+ "Creating polls and voting are disabled."
163
+ )
164
+ except Exception as e:
165
+ return False, username, (
166
+ f"⚠️ Signed in as **@{username}**, but membership in `{REQUIRED_ORG}` "
167
+ f"could not be verified ({e}). Try again shortly."
168
+ )
169
+
170
+
171
+ # ------------------------------------------------- JSON API (Astro app)
172
+
173
+ def api_me(
174
+ profile: gr.OAuthProfile | None,
175
+ token: gr.OAuthToken | None,
176
+ ) -> Dict:
177
+ allowed, username, _ = check_membership(profile, token)
178
+ return {"username": username or None, "member": allowed}
179
+
180
+
181
+ def api_polls(
182
+ profile: gr.OAuthProfile | None,
183
+ token: gr.OAuthToken | None,
184
+ ) -> Dict:
185
+ allowed, username, _ = check_membership(profile, token) if profile else (False, "", "")
186
+ polls = load_polls()
187
+ items = sorted(polls.values(), key=lambda p: p.get("created_at", ""), reverse=True)
188
+ return {
189
+ "me": {"username": username or None, "member": allowed},
190
+ "polls": [poll_to_dict(p, username or None) for p in items],
191
+ }
192
+
193
+
194
+ def api_vote(
195
+ poll_id: str,
196
+ choice_idx: float,
197
+ profile: gr.OAuthProfile | None,
198
+ token: gr.OAuthToken | None,
199
+ ) -> Dict:
200
+ allowed, username, status = check_membership(profile, token)
201
+ polls = load_polls()
202
+ poll = polls.get(poll_id or "")
203
+ if not allowed:
204
+ return {"ok": False, "message": status, "poll": poll_to_dict(poll, None) if poll else None}
205
+ if poll is None:
206
+ return {"ok": False, "message": "That poll no longer exists — refresh the list.", "poll": None}
207
+ try:
208
+ idx = int(choice_idx)
209
+ except (TypeError, ValueError):
210
+ idx = -1
211
+ if not 0 <= idx < len(poll["options"]):
212
+ return {"ok": False, "message": "Invalid option.", "poll": poll_to_dict(poll, username)}
213
+
214
+ with _lock:
215
+ polls = load_polls()
216
+ poll = polls.get(poll_id)
217
+ if poll is None:
218
+ return {"ok": False, "message": "That poll no longer exists — refresh the list.", "poll": None}
219
+ prev = poll.setdefault("voters", {}).get(username)
220
+ poll["voters"][username] = idx
221
+ save_polls(polls)
222
+
223
+ msg = f"Vote counted for “{poll['options'][idx]}” as @{username}."
224
+ if prev is not None and prev != idx and 0 <= prev < len(poll["options"]):
225
+ msg += f" (changed from “{poll['options'][prev]}”)"
226
+ return {"ok": True, "message": msg, "poll": poll_to_dict(polls[poll_id], username)}
227
+
228
+
229
+ def api_create(
230
+ question: str,
231
+ options_raw: str,
232
+ profile: gr.OAuthProfile | None,
233
+ token: gr.OAuthToken | None,
234
+ ) -> Dict:
235
+ allowed, username, status = check_membership(profile, token)
236
+ if not allowed:
237
+ return {"ok": False, "message": status, "poll": None}
238
+
239
+ question = (question or "").strip()
240
+ options = parse_options(options_raw or "")
241
+ if len(question) < 3:
242
+ return {"ok": False, "message": "Give your poll a question (min 3 characters).", "poll": None}
243
+ if len(options) < 2:
244
+ return {"ok": False, "message": "Provide at least 2 options (one per line).", "poll": None}
245
+ if len(options) > 20:
246
+ return {"ok": False, "message": "Max 20 options per poll.", "poll": None}
247
+ if len(set(o.lower() for o in options)) != len(options):
248
+ return {"ok": False, "message": "Options must be unique.", "poll": None}
249
+
250
+ with _lock:
251
+ polls = load_polls()
252
+ pid = uuid.uuid4().hex[:8]
253
+ polls[pid] = {
254
+ "id": pid,
255
+ "question": question,
256
+ "options": options,
257
+ "created_by": username,
258
+ "created_at": datetime.now(timezone.utc).isoformat(),
259
+ "voters": {},
260
+ }
261
+ save_polls(polls)
262
+ return {
263
+ "ok": True,
264
+ "message": f"Poll created by @{username}!",
265
+ "poll": poll_to_dict(polls[pid], username),
266
+ }
267
+
268
+
269
+ # ------------------------------------------------------- classic UI views
270
+
271
+ def poll_label(poll: Dict) -> str:
272
+ return f"{poll['question']} — {total_votes(poll)} vote(s) [{poll['id'][:6]}]"
273
+
274
+
275
+ def dropdown_choices(polls: Dict) -> List[Tuple[str, str]]:
276
+ items = sorted(polls.values(), key=lambda p: p.get("created_at", ""), reverse=True)
277
+ return [(poll_label(p), p["id"]) for p in items]
278
+
279
+
280
+ def format_results(poll: Optional[Dict]) -> str:
281
+ if poll is None:
282
+ return "Select a poll to see live results."
283
+ counts = count_votes(poll)
284
+ total = sum(counts)
285
+ lines = [f"### 📊 {poll['question']}", ""]
286
+ if total == 0:
287
+ lines.append("_No votes yet — be the first!_")
288
+ by_opt: Dict[int, List[str]] = {}
289
+ for u, idx in poll.get("voters", {}).items():
290
+ if isinstance(idx, int) and 0 <= idx < len(poll["options"]):
291
+ by_opt.setdefault(idx, []).append(u)
292
+ for i, (opt, c) in enumerate(zip(poll["options"], counts)):
293
+ pct = (c / total * 100) if total else 0
294
+ bar = "█" * int(round(pct / 5)) + "░" * (20 - int(round(pct / 5)))
295
+ lines.append(f"- **{opt}** — {c} vote(s) ({pct:.1f}%) `{bar}`")
296
+ voters = sorted(by_opt.get(i, []))
297
+ if voters:
298
+ lines.append(f" - Voters: " + ", ".join(f"**@{u}**" for u in voters))
299
+ lines.append("")
300
+ lines.append(f"Total votes: **{total}** · Created by **@{poll.get('created_by', '?')}**")
301
+ return "\n".join(lines)
302
+
303
+
304
+ def refresh_polls() -> Tuple[gr.Dropdown, str]:
305
+ polls = load_polls()
306
+ choices = dropdown_choices(polls)
307
+ dd = gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
308
+ if not choices:
309
+ return dd, "No polls yet. Members of `slmconsortium` can create one in the **Create poll** tab."
310
+ poll = polls.get(choices[0][1])
311
+ return dd, format_results(poll)
312
+
313
+
314
+ def on_select_poll(
315
+ poll_id: Optional[str],
316
+ ) -> Tuple[gr.Radio, str]:
317
+ polls = load_polls()
318
+ poll = polls.get(poll_id) if poll_id else None
319
+ if poll is None:
320
+ return gr.Radio(choices=[], value=None), "Select a poll to see live results."
321
+ radio = gr.Radio(choices=poll["options"], value=None)
322
+ return radio, format_results(poll)
323
+
324
+
325
+ def on_vote(
326
+ poll_id: Optional[str],
327
+ choice: Optional[str],
328
+ profile: gr.OAuthProfile | None,
329
+ token: gr.OAuthToken | None,
330
+ ) -> Tuple[str, str]:
331
+ allowed, username, status = check_membership(profile, token)
332
+ if not allowed:
333
+ return status, format_results(load_polls().get(poll_id) if poll_id else None)
334
+ if not poll_id:
335
+ return "⚠️ Select a poll first.", "Select a poll to see live results."
336
+ if not choice:
337
+ return "⚠️ Pick an option before voting.", format_results(load_polls().get(poll_id))
338
+
339
+ with _lock:
340
+ polls = load_polls()
341
+ poll = polls.get(poll_id)
342
+ if poll is None:
343
+ return "⚠️ That poll no longer exists. Hit Refresh.", "Select a poll to see live results."
344
+ try:
345
+ idx = poll["options"].index(choice)
346
+ except ValueError:
347
+ return "⚠️ Invalid option.", format_results(poll)
348
+ prev = poll.setdefault("voters", {}).get(username)
349
+ poll["voters"][username] = idx
350
+ save_polls(polls)
351
+
352
+ msg = f"✅ Vote counted for **{choice}** as **@{username}**."
353
+ if prev is not None and prev != idx:
354
+ msg += f" (changed from **{poll['options'][prev]}**)"
355
+ elif prev == idx:
356
+ msg += " (unchanged)"
357
+ polls = load_polls()
358
+ return msg, format_results(polls.get(poll_id))
359
+
360
+
361
+ def on_create(
362
+ question: str,
363
+ options_raw: str,
364
+ profile: gr.OAuthProfile | None,
365
+ token: gr.OAuthToken | None,
366
+ ) -> Tuple[str, gr.Dropdown, gr.Radio, str, str, str]:
367
+ allowed, username, status = check_membership(profile, token)
368
+ polls = load_polls()
369
+ choices = dropdown_choices(polls)
370
+ dd = gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
371
+ current = polls.get(choices[0][1]) if choices else None
372
+ radio = gr.Radio(choices=current["options"] if current else [])
373
+ results = format_results(current)
374
+
375
+ if not allowed:
376
+ return status, dd, radio, results, question, options_raw
377
+
378
+ question = (question or "").strip()
379
+ options = parse_options(options_raw or "")
380
+
381
+ if len(question) < 3:
382
+ return "⚠️ Give your poll a question (min 3 characters).", dd, radio, results, question, options_raw
383
+ if len(options) < 2:
384
+ return "⚠️ Provide at least **2 options** (one per line).", dd, radio, results, question, options_raw
385
+ if len(options) > 20:
386
+ return "⚠️ Max 20 options per poll.", dd, radio, results, question, options_raw
387
+ if len(set(o.lower() for o in options)) != len(options):
388
+ return "⚠️ Options must be unique.", dd, radio, results, question, options_raw
389
+
390
+ with _lock:
391
+ polls = load_polls()
392
+ pid = uuid.uuid4().hex[:8]
393
+ polls[pid] = {
394
+ "id": pid,
395
+ "question": question,
396
+ "options": options,
397
+ "created_by": username,
398
+ "created_at": datetime.now(timezone.utc).isoformat(),
399
+ "voters": {},
400
+ }
401
+ save_polls(polls)
402
+ choices = dropdown_choices(polls)
403
+
404
+ dd = gr.Dropdown(choices=choices, value=pid)
405
+ radio = gr.Radio(choices=options, value=None)
406
+ results = format_results(polls[pid])
407
+ return f"✅ Poll created by **@{username}**! Share it with fellow `{REQUIRED_ORG}` members.", dd, radio, results, "", ""
408
+
409
+
410
+ def on_load(
411
+ profile: gr.OAuthProfile | None,
412
+ token: gr.OAuthToken | None,
413
+ ) -> Tuple[str, gr.Dropdown, gr.Radio, str]:
414
+ _, _, status = check_membership(profile, token) if profile else (False, "", "🔒 Please **sign in with Hugging Face** to create polls or vote.")
415
+ polls = load_polls()
416
+ choices = dropdown_choices(polls)
417
+ dd = gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
418
+ current = polls.get(choices[0][1]) if choices else None
419
+ radio = gr.Radio(choices=current["options"] if current else [])
420
+ return status, dd, radio, format_results(current)
421
+
422
+
423
+ # ------------------------------------------------------------------ blocks
424
+
425
+ with gr.Blocks(title="SLM Consortium Polls") as demo:
426
+ gr.Markdown(
427
+ "# 🗳️ SLM Consortium Polls\n"
428
+ "Create polls and vote. **Sign-in with Hugging Face is required**, and only "
429
+ f"members of the `{REQUIRED_ORG}` organization can create polls or vote. "
430
+ "Everyone can view live results.\n\n"
431
+ "Prefer the new look? Use the [Astro frontend](/) at the Space root."
432
+ )
433
+ with gr.Row():
434
+ gr.LoginButton(min_width=50)
435
+ status_md = gr.Markdown("🔒 Please **sign in with Hugging Face** to create polls or vote.")
436
+
437
+ with gr.Tab("🗳️ Vote"):
438
+ poll_dropdown = gr.Dropdown(label="Choose a poll", choices=[], interactive=True)
439
+ option_radio = gr.Radio(label="Your choice", choices=[])
440
+ with gr.Row():
441
+ vote_btn = gr.Button("Vote", variant="primary")
442
+ refresh_btn = gr.Button("🔄 Refresh")
443
+ vote_msg = gr.Markdown()
444
+ results_md = gr.Markdown("Select a poll to see live results.")
445
+
446
+ with gr.Tab("➕ Create poll"):
447
+ gr.Markdown(
448
+ f"Only `{REQUIRED_ORG}` members can create polls. "
449
+ "Put each option on its own line (or comma-separated)."
450
+ )
451
+ q_input = gr.Textbox(label="Question", placeholder="e.g. Which meeting time works best?")
452
+ o_input = gr.Textbox(
453
+ label="Options (one per line)",
454
+ lines=4,
455
+ placeholder="Monday 10:00 UTC\nTuesday 14:00 UTC\nFriday 09:00 UTC",
456
+ )
457
+ create_btn = gr.Button("Create poll", variant="primary")
458
+ create_msg = gr.Markdown()
459
+
460
+ # Wiring (classic UI)
461
+ demo.load(on_load, inputs=None, outputs=[status_md, poll_dropdown, option_radio, results_md])
462
+ poll_dropdown.change(on_select_poll, inputs=poll_dropdown, outputs=[option_radio, results_md])
463
+ refresh_btn.click(refresh_polls, inputs=None, outputs=[poll_dropdown, results_md])
464
+ vote_btn.click(on_vote, inputs=[poll_dropdown, option_radio], outputs=[vote_msg, results_md])
465
+ create_btn.click(
466
+ on_create,
467
+ inputs=[q_input, o_input],
468
+ outputs=[create_msg, poll_dropdown, option_radio, results_md, q_input, o_input],
469
+ )
470
+
471
+ # Hidden JSON API for the Astro frontend (same-origin, same HF session).
472
+ api_me_btn = gr.Button(visible=False)
473
+ api_me_out = gr.JSON(visible=False)
474
+ api_me_btn.click(api_me, inputs=None, outputs=api_me_out, api_name="me")
475
+
476
+ api_polls_btn = gr.Button(visible=False)
477
+ api_polls_out = gr.JSON(visible=False)
478
+ api_polls_btn.click(api_polls, inputs=None, outputs=api_polls_out, api_name="polls")
479
+
480
+ api_vote_poll_id = gr.Textbox(visible=False)
481
+ api_vote_choice = gr.Number(visible=False, precision=0)
482
+ api_vote_out = gr.JSON(visible=False)
483
+ api_vote_btn = gr.Button(visible=False)
484
+ api_vote_btn.click(
485
+ api_vote,
486
+ inputs=[api_vote_poll_id, api_vote_choice],
487
+ outputs=api_vote_out,
488
+ api_name="vote",
489
+ )
490
+
491
+ api_create_q = gr.Textbox(visible=False)
492
+ api_create_opts = gr.Textbox(visible=False)
493
+ api_create_out = gr.JSON(visible=False)
494
+ api_create_btn = gr.Button(visible=False)
495
+ api_create_btn.click(
496
+ api_create,
497
+ inputs=[api_create_q, api_create_opts],
498
+ outputs=api_create_out,
499
+ api_name="create_poll",
500
+ )
501
+
502
+
503
+ # ------------------------------------------------- app: static + gradio
504
+
505
+ fastapi_app = FastAPI(title="SLM Consortium Polls")
506
+
507
+ # Gradio 6 mounts don't redirect /gradio -> /gradio/, so the bare path 404s.
508
+ # Add the redirect before the mount takes over the prefix.
509
+ @fastapi_app.get("/gradio")
510
+ def gradio_redirect() -> RedirectResponse:
511
+ return RedirectResponse(url="/gradio/", status_code=307)
512
+
513
+
514
+ # Gradio's OAuth routes are hardcoded at the app root ("/login/huggingface",
515
+ # "/login/callback", "/logout") and its Spaces flow forces the callback to
516
+ # https://<space_host>/login/callback — outside the /gradio mount. Forward
517
+ # those bare paths into the mounted app so HF OAuth works.
518
+ def _forward_to_gradio(path: str):
519
+ async def _forward(request: Request) -> RedirectResponse:
520
+ qs = "&".join(
521
+ f"{k}={v}" for k, v in request.query_params.multi_items()
522
+ )
523
+ url = f"/gradio{path}" + (f"?{qs}" if qs else "")
524
+ return RedirectResponse(url, status_code=307)
525
+
526
+ return _forward
527
+
528
+
529
+ fastapi_app.get("/login/huggingface")(_forward_to_gradio("/login/huggingface"))
530
+ fastapi_app.get("/login/callback")(_forward_to_gradio("/login/callback"))
531
+ fastapi_app.get("/logout")(_forward_to_gradio("/logout"))
532
+
533
+
534
+ # root_path makes gradio generate internal links (OAuth login, etc.)
535
+ # under /gradio instead of the app root.
536
+ fastapi_app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio", root_path="/gradio")
537
+
538
+
539
+ @fastapi_app.get("/healthz")
540
+ def healthz() -> Dict[str, str]:
541
+ return {
542
+ "status": "ok",
543
+ "data_dir": _DATA_DIR,
544
+ "polls_file": POLLS_FILE,
545
+ "polls_on_disk": str(os.path.exists(POLLS_FILE)),
546
+ }
547
+
548
+
549
+ if os.path.isdir(DIST_DIR):
550
+ fastapi_app.mount("/", StaticFiles(directory=DIST_DIR, html=True), name="frontend")
551
+ else:
552
+ @fastapi_app.get("/")
553
+ def missing_build() -> Dict[str, str]:
554
+ return {
555
+ "status": "frontend not built",
556
+ "hint": "Run `npm run build` in frontend/ (output committed to frontend/dist/).",
557
+ "gradio": "/gradio",
558
+ }
559
+
560
+
561
+ if __name__ == "__main__":
562
+ import uvicorn
563
+
564
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
frontend/astro.config.mjs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'astro/config';
2
+ import react from '@astrojs/react';
3
+
4
+ // Static build — output (dist/) is committed and served by app.py,
5
+ // so the Hugging Face Space needs no Node runtime.
6
+ export default defineConfig({
7
+ integrations: [react()],
8
+ output: 'static',
9
+ outDir: './dist',
10
+ build: {
11
+ format: 'directory'
12
+ }
13
+ });
frontend/dist/_astro/PollApp.BI8OLAG4.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{r as L}from"./index.qNTDzdXh.js";var he={exports:{}},ne={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var Ne;function nt(){if(Ne)return ne;Ne=1;var e=Symbol.for("react.transitional.element"),s=Symbol.for("react.fragment");function t(n,i,a){var o=null;if(a!==void 0&&(o=""+a),i.key!==void 0&&(o=""+i.key),"key"in i){a={};for(var r in i)r!=="key"&&(a[r]=i[r])}else a=i;return i=a.ref,{$$typeof:e,type:n,key:o,ref:i!==void 0?i:null,props:a}}return ne.Fragment=s,ne.jsx=t,ne.jsxs=t,ne}var Ce;function it(){return Ce||(Ce=1,he.exports=nt()),he.exports}var p=it();const at="modulepreload",ot=function(e){return"/"+e},Oe={},Pe=function(s,t,n){let i=Promise.resolve();if(t&&t.length>0){let o=function(h){return Promise.all(h.map(g=>Promise.resolve(g).then(u=>({status:"fulfilled",value:u}),u=>({status:"rejected",reason:u}))))};document.getElementsByTagName("link");const r=document.querySelector("meta[property=csp-nonce]"),d=r?.nonce||r?.getAttribute("nonce");i=o(t.map(h=>{if(h=ot(h),h in Oe)return;Oe[h]=!0;const g=h.endsWith(".css"),u=g?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${h}"]${u}`))return;const _=document.createElement("link");if(_.rel=g?"stylesheet":at,g||(_.as="script"),_.crossOrigin="",_.href=h,d&&_.setAttribute("nonce",d),document.head.appendChild(_),g)return new Promise(($,R)=>{_.addEventListener("load",$),_.addEventListener("error",()=>R(new Error(`Unable to preload CSS for ${h}`)))})}))}function a(o){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=o,window.dispatchEvent(r),!r.defaultPrevented)throw o}return i.then(o=>{for(const r of o||[])r.status==="rejected"&&a(r.reason);return s().catch(a)})};var rt=Object.defineProperty,Ue=e=>{throw TypeError(e)},ct=(e,s,t)=>s in e?rt(e,s,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[s]=t,m=(e,s,t)=>ct(e,typeof s!="symbol"?s+"":s,t),Be=(e,s,t)=>s.has(e)||Ue("Cannot "+t),ie=(e,s,t)=>(Be(e,s,"read from private field"),s.get(e)),lt=(e,s,t)=>s.has(e)?Ue("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(e):s.set(e,t),ut=(e,s,t,n)=>(Be(e,s,"write to private field"),s.set(e,t),t),fe=new Intl.Collator(0,{numeric:1}).compare;function Ie(e,s,t){return e=e.split("."),s=s.split("."),fe(e[0],s[0])||fe(e[1],s[1])||(s[2]=s.slice(2).join("."),t=/[.-]/.test(e[2]=e.slice(2).join(".")),t==/[.-]/.test(s[2])?fe(e[2],s[2]):t?-1:1)}const dt="host",Fe="queue/data",pt="queue/join",qe="upload",ht="login",ce="config",ft="info",mt="runtime",_t="sleeptime",gt="heartbeat",wt="component_server",yt="reset",vt="cancel",bt="app_id",xt="https://gradio-space-api-fetcher-v2.hf.space/api",Je="This application is currently busy. Please try again. ",Q="Connection errored out. ",G="Could not resolve app config. ",kt="Could not get space status. ",Et="Could not get API info. ",be="Space metadata could not be loaded. ",$t="Invalid URL. A full URL path is required.",jt="Not authorized to access this space. ",We="Invalid credentials. Could not login. ",St="Login credentials are required to access this space.",Nt="File system access is only available in Node.js environments",Me="Root URL not found in client config",Ct="Error uploading file";function Ot(e,s,t){return s.startsWith("http://")||s.startsWith("https://")?t?e:s:e+s}async function Te(e,s,t){try{return(await(await fetch(`https://huggingface.co/api/spaces/${e}/jwt`,{headers:{Authorization:`Bearer ${s}`,...t?{Cookie:t}:{}}})).json()).token||!1}catch{return!1}}function Pt(e){let s={};return e.forEach(({api_name:t,id:n})=>{t&&(s[t]=n)}),s}async function qt(e){const s=this.options.hf_token?{Authorization:`Bearer ${this.options.hf_token}`}:{};if(s["Content-Type"]="application/json",typeof window<"u"&&window.gradio_config&&location.origin!=="http://localhost:9876"){if(window.gradio_config.current_page&&(e=e.substring(0,e.lastIndexOf("/"))),window.gradio_config.dev_mode){let t=we(e,this.deep_link?ce+"?deep_link="+this.deep_link:ce);const n=await this.fetch(t,{headers:s,credentials:"include"}),i=await Ae(n,e,!!this.options.auth);window.gradio_config={...i,current_page:window.gradio_config.current_page}}return window.gradio_config.root=e,{...window.gradio_config}}else if(e){let t=we(e,this.deep_link?ce+"?deep_link="+this.deep_link:ce);const n=await this.fetch(t,{headers:s,credentials:"include"});return Ae(n,e,!!this.options.auth)}throw new Error(G)}async function Ae(e,s,t){var n,i;if(e?.status===401&&!t){const a=await e.json(),o=(n=a?.detail)==null?void 0:n.auth_message;throw new Error(o||St)}else if(e?.status===401&&t)throw new Error(We);if(e?.status===200){let a=await e.json();return a.root=s,(i=a.dependencies)==null||i.forEach((o,r)=>{o.id===void 0&&(o.id=r)}),a}else if(e?.status===401)throw new Error(jt);throw new Error(G)}async function Tt(){const{http_protocol:e,host:s}=await le(this.app_reference,this.options.hf_token);try{if(this.options.auth){const t=await Ge(e,s,this.options.auth,this.fetch,this.options.hf_token);t&&this.set_cookies(t)}}catch(t){throw Error(t.message)}}async function Ge(e,s,t,n,i){const a=new FormData;a.append("username",t?.[0]),a.append("password",t?.[1]);let o={};i&&(o.Authorization=`Bearer ${i}`);const r=await n(`${e}//${s}/${ht}`,{headers:o,method:"POST",body:a,credentials:"include"});if(r.status===200)return r.headers.get("set-cookie");throw r.status===401?new Error(We):new Error(be)}function me(e){if(e.startsWith("http")){const{protocol:s,host:t,pathname:n}=new URL(e);return{ws_protocol:s==="https:"?"wss":"ws",http_protocol:s,host:t+(n!=="/"?n:"")}}return{ws_protocol:"wss",http_protocol:"https:",host:new URL(e).host}}const He=e=>{let s=[];return e.split(/,(?=\s*[^\s=;]+=[^\s=;]+)/).forEach(t=>{const[n,i]=t.split(";")[0].split("=");n&&i&&s.push(`${n.trim()}=${i.trim()}`)}),s},xe=/^[a-zA-Z0-9_\-\.]+\/[a-zA-Z0-9_\-\.]+$/,At=/.*hf\.space\/{0,1}.*$/;async function le(e,s){const t={};s&&(t.Authorization=`Bearer ${s}`);const n=e.trim().replace(/\/$/,"");if(xe.test(n))try{const i=(await(await fetch(`https://huggingface.co/api/spaces/${n}/${dt}`,{headers:t})).json()).host;return{space_id:e,...me(i)}}catch{throw new Error(be)}if(At.test(n)){const{ws_protocol:i,http_protocol:a,host:o}=me(n);return{space_id:o.split("/")[0].replace(".hf.space",""),ws_protocol:i,http_protocol:a,host:o}}return{space_id:!1,...me(n)}}const we=(...e)=>{try{return e.reduce((s,t)=>(s=s.replace(/\/+$/,""),t=t.replace(/^\/+/,""),new URL(t,s+"/").toString()))}catch{throw new Error($t)}};function Dt(e,s,t){const n={named_endpoints:{},unnamed_endpoints:{}};return Object.keys(e).forEach(i=>{(i==="named_endpoints"||i==="unnamed_endpoints")&&(n[i]={},Object.entries(e[i]).forEach(([a,{parameters:o,returns:r}])=>{var d,h,g,u;const _=((d=s.dependencies.find(c=>c.api_name===a||c.api_name===a.replace("/","")))==null?void 0:d.id)||t[a.replace("/","")]||-1,$=_!==-1?(h=s.dependencies.find(c=>c.id==_))==null?void 0:h.types:{generator:!1,cancel:!1};if(_!==-1&&((u=(g=s.dependencies.find(c=>c.id==_))==null?void 0:g.inputs)==null?void 0:u.length)!==o.length){const c=s.dependencies.find(w=>w.id==_).inputs.map(w=>{var N;return(N=s.components.find(C=>C.id===w))==null?void 0:N.type});try{c.forEach((w,N)=>{if(w==="state"){const C={component:"state",example:null,parameter_default:null,parameter_has_default:!0,parameter_name:null,hidden:!0};o.splice(N,0,C)}})}catch(w){console.error(w)}}const R=(c,w,N,C)=>({...c,description:zt(c?.type,N),type:Rt(c?.type,w,N,C)||""});n[i][a]={parameters:o.map(c=>R(c,c?.component,c?.serializer,"parameter")),returns:r.map(c=>R(c,c?.component,c?.serializer,"return")),type:$}}))}),n}function Rt(e,s,t,n){if(s==="Api")return e.type;switch(e?.type){case"string":return"string";case"boolean":return"boolean";case"number":return"number"}if(t==="JSONSerializable"||t==="StringSerializable")return"any";if(t==="ListStringSerializable")return"string[]";if(s==="Image")return n==="parameter"?"Blob | File | Buffer":"string";if(t==="FileSerializable")return e?.type==="array"?n==="parameter"?"(Blob | File | Buffer)[]":"{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}[]":n==="parameter"?"Blob | File | Buffer":"{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}";if(t==="GallerySerializable")return n==="parameter"?"[(Blob | File | Buffer), (string | null)][]":"[{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}, (string | null))][]"}function zt(e,s){return s==="GallerySerializable"?"array of [file, label] tuples":s==="ListStringSerializable"?"array of strings":s==="FileSerializable"?"array of files or single file":e?.description}function _e(e,s){switch(e.msg){case"send_data":return{type:"data"};case"send_hash":return{type:"hash"};case"queue_full":return{type:"update",status:{queue:!0,message:Je,stage:"error",code:e.code,success:e.success}};case"heartbeat":return{type:"heartbeat"};case"unexpected_error":return{type:"unexpected_error",status:{queue:!0,message:e.message,session_not_found:e.session_not_found,stage:"error",success:!1}};case"broken_connection":return{type:"broken_connection",status:{queue:!0,message:e.message,stage:"error",success:!1}};case"estimation":return{type:"update",status:{queue:!0,stage:s||"pending",code:e.code,size:e.queue_size,position:e.rank,eta:e.rank_eta,success:e.success}};case"progress":return{type:"update",status:{queue:!0,stage:"pending",code:e.code,progress_data:e.progress_data,success:e.success}};case"log":return{type:"log",data:e};case"process_generating":return{type:"generating",status:{queue:!0,message:e.success?null:e.output.error,stage:e.success?"generating":"error",code:e.code,progress_data:e.progress_data,eta:e.average_duration,changed_state_ids:e.success?e.output.changed_state_ids:void 0},data:e.success?e.output:null};case"process_streaming":return{type:"streaming",status:{queue:!0,message:e.output.error,stage:"streaming",time_limit:e.time_limit,code:e.code,progress_data:e.progress_data,eta:e.eta},data:e.output};case"process_completed":return"error"in e.output?{type:"update",status:{queue:!0,title:e.output.title,message:e.output.error,visible:e.output.visible,duration:e.output.duration,stage:"error",code:e.code,success:e.success}}:{type:"complete",status:{queue:!0,message:e.success?void 0:e.output.error,stage:e.success?"complete":"error",code:e.code,progress_data:e.progress_data,changed_state_ids:e.success?e.output.changed_state_ids:void 0},data:e.success?e.output:null};case"process_starts":return{type:"update",status:{queue:!0,stage:"pending",code:e.code,size:e.rank,position:0,success:e.success,eta:e.eta},original_msg:"process_starts"}}return{type:"none",status:{stage:"error",queue:!0}}}const Lt=(e=[],s)=>{const t=s?s.parameters:[];if(Array.isArray(e))return s&&t.length>0&&e.length>t.length&&console.warn("Too many arguments provided for the endpoint."),e;const n=[],i=Object.keys(e);return t.forEach((a,o)=>{if(e.hasOwnProperty(a.parameter_name))n[o]=e[a.parameter_name];else if(a.parameter_has_default)n[o]=a.parameter_default;else throw new Error(`No value provided for required parameter: ${a.parameter_name}`)}),i.forEach(a=>{if(!t.some(o=>o.parameter_name===a))throw new Error(`Parameter \`${a}\` is not a valid keyword argument. Please refer to the API for usage.`)}),n.forEach((a,o)=>{if(a===void 0&&!t[o].parameter_has_default)throw new Error(`No value provided for required parameter: ${t[o].parameter_name}`)}),n};async function Ut(){if(this.api_info)return this.api_info;const{hf_token:e}=this.options,{config:s}=this,t={"Content-Type":"application/json"};if(e&&(t.Authorization=`Bearer ${e}`),!!s)try{let n,i;if(typeof window<"u"&&window.gradio_api_info)i=window.gradio_api_info;else{if(Ie(s?.version||"2.0.0","3.30")<0)n=await this.fetch(xt,{method:"POST",body:JSON.stringify({serialize:!1,config:JSON.stringify(s)}),headers:t,credentials:"include"});else{const a=we(s.root,this.api_prefix,ft);n=await this.fetch(a,{headers:t,credentials:"include"})}if(!n.ok)throw new Error(Q);i=await n.json()}return"api"in i&&(i=i.api),i.named_endpoints["/predict"]&&!i.unnamed_endpoints[0]&&(i.unnamed_endpoints[0]=i.named_endpoints["/predict"]),Dt(i,s,this.api_map)}catch(n){throw new Error("Could not get API info. "+n.message)}}async function Bt(e,s,t){var n;const i={};(n=this==null?void 0:this.options)!=null&&n.hf_token&&(i.Authorization=`Bearer ${this.options.hf_token}`);const a=1e3,o=[];let r;for(let d=0;d<s.length;d+=a){const h=s.slice(d,d+a),g=new FormData;h.forEach(_=>{g.append("files",_)});try{const _=t?`${e}${this.api_prefix}/${qe}?upload_id=${t}`:`${e}${this.api_prefix}/${qe}`;r=await this.fetch(_,{method:"POST",body:g,headers:i,credentials:"include"})}catch(_){throw new Error(Q+_.message)}if(!r.ok){const _=await r.text();return{error:`HTTP ${r.status}: ${_}`}}const u=await r.json();u&&o.push(...u)}return{files:o}}async function It(e,s,t,n){let i=(Array.isArray(e)?e:[e]).map(o=>o.blob);const a=i.filter(o=>o.size>(n??1/0));if(a.length)throw new Error(`File size exceeds the maximum allowed size of ${n} bytes: ${a.map(o=>o.name).join(", ")}`);return await Promise.all(await this.upload_files(s,i,t).then(async o=>{if(o.error)throw new Error(o.error);return o.files?o.files.map((r,d)=>new ke({...e[d],path:r,url:`${s}${this.api_prefix}/file=${r}`})):[]}))}class ke{constructor({path:s,url:t,orig_name:n,size:i,blob:a,is_stream:o,mime_type:r,alt_text:d,b64:h}){m(this,"path"),m(this,"url"),m(this,"orig_name"),m(this,"size"),m(this,"blob"),m(this,"is_stream"),m(this,"mime_type"),m(this,"alt_text"),m(this,"b64"),m(this,"meta",{_type:"gradio.FileData"}),this.path=s,this.url=t,this.orig_name=n,this.size=i,this.blob=t?void 0:a,this.is_stream=o,this.mime_type=r,this.alt_text=d,this.b64=h}}class Ft{constructor(s,t){m(this,"type"),m(this,"command"),m(this,"meta"),m(this,"fileData"),this.type="command",this.command=s,this.meta=t}}typeof process<"u"&&process.versions&&process.versions.node;function De(e,s,t){for(;t.length>1;){const i=t.shift();if(typeof i=="string"||typeof i=="number")e=e[i];else throw new Error("Invalid key type")}const n=t.shift();if(typeof n=="string"||typeof n=="number")e[n]=s;else throw new Error("Invalid key type")}async function ye(e,s=void 0,t=[],n=!1,i=void 0){if(Array.isArray(e)){let a=[];return await Promise.all(e.map(async(o,r)=>{var d;let h=t.slice();h.push(String(r));const g=await ye(e[r],n?((d=i?.parameters[r])==null?void 0:d.component)||void 0:s,h,!1,i);a=a.concat(g)})),a}else{if(globalThis.Buffer&&e instanceof globalThis.Buffer||e instanceof Blob)return[{path:t,blob:new Blob([e]),type:s}];if(typeof e=="object"&&e!==null){let a=[];for(const o of Object.keys(e)){const r=[...t,o],d=e[o];a=a.concat(await ye(d,void 0,r,!1,i))}return a}}return[]}function Jt(e,s){var t,n;let i=(n=(t=s?.dependencies)==null?void 0:t.find(a=>a.id==e))==null?void 0:n.queue;return i!=null?!i:!s.enable_queue}function Wt(e,s){return new Promise((t,n)=>{const i=new MessageChannel;i.port1.onmessage=({data:a})=>{i.port1.close(),t(a)},window.parent.postMessage(e,s,[i.port2])})}function ae(e,s,t,n,i=!1){if(n==="input"&&!i)throw new Error("Invalid code path. Cannot skip state inputs for input.");if(n==="output"&&i)return e;let a=[],o=0;const r=n==="input"?s.inputs:s.outputs;for(let d=0;d<r.length;d++){const h=r[d],g=t.find(u=>u.id===h);if(g?.type==="state"){if(i)if(e.length===r.length){const u=e[o];a.push(u),o++}else a.push(null);else{o++;continue}continue}else{const u=e[o];a.push(u),o++}}return a}async function Mt(e,s,t){const n=this;await Gt(n,s);const i=await ye(s,void 0,[],!0,t);return(await Promise.all(i.map(async({path:a,blob:o,type:r})=>{if(!o)return{path:a,type:r};const d=await n.upload_files(e,[o]),h=d.files&&d.files[0];return{path:a,file_url:h,type:r,name:typeof File<"u"&&o instanceof File?o?.name:void 0}}))).forEach(({path:a,file_url:o,type:r,name:d})=>{if(r==="Gallery")De(s,o,a);else if(o){const h=new ke({path:o,orig_name:d});De(s,h,a)}}),s}async function Gt(e,s){var t,n;if(!((t=e.config)!=null&&t.root||(n=e.config)!=null&&n.root_url))throw new Error(Me);await Ve(e,s)}async function Ve(e,s,t=[]){for(const n in s)s[n]instanceof Ft?await Ht(e,s,n):typeof s[n]=="object"&&s[n]!==null&&await Ve(e,s[n],[...t,n])}async function Ht(e,s,t){var n,i;let a=s[t];const o=((n=e.config)==null?void 0:n.root)||((i=e.config)==null?void 0:i.root_url);if(!o)throw new Error(Me);try{let r,d;if(typeof process<"u"&&process.versions&&process.versions.node){const _=await Pe(()=>import("./__vite-browser-external-DYxpcVy9.BIHI7g3E.js"),[]);d=(await Pe(async()=>{const{resolve:$}=await import("./__vite-browser-external-DYxpcVy9.BIHI7g3E.js");return{resolve:$}},[])).resolve(process.cwd(),a.meta.path),r=await _.readFile(d)}else throw new Error(Nt);const h=new Blob([r],{type:"application/octet-stream"}),g=await e.upload_files(o,[h]),u=g.files&&g.files[0];if(u){const _=new ke({path:u,orig_name:a.meta.name||""});s[t]=_}}catch(r){console.error(Ct,r)}}async function Vt(e,s,t){const n={"Content-Type":"application/json"};this.options.hf_token&&(n.Authorization=`Bearer ${this.options.hf_token}`);try{var i=await this.fetch(e,{method:"POST",body:JSON.stringify(s),headers:{...n,...t},credentials:"include"})}catch{return[{error:Q},500]}let a,o;try{a=await i.json(),o=i.status}catch(r){a={error:`Could not parse server response: ${r}`},o=500}return[a,o]}async function Yt(e,s={}){let t=!1,n=!1;if(!this.config)throw new Error("Could not resolve app config");if(typeof e=="number")this.config.dependencies.find(i=>i.id==e);else{const i=e.replace(/^\//,"");this.config.dependencies.find(a=>a.id==this.api_map[i])}return new Promise(async(i,a)=>{const o=this.submit(e,s,null,null,!0);let r;for await(const d of o)d.type==="data"&&(n&&i(r),t=!0,r=d),d.type==="status"&&(d.stage==="error"&&a(d),d.stage==="complete"&&(n=!0,t&&i(r)))})}async function oe(e,s,t){let n=s==="subdomain"?`https://huggingface.co/api/spaces/by-subdomain/${e}`:`https://huggingface.co/api/spaces/${e}`,i,a;try{if(i=await fetch(n),a=i.status,a!==200)throw new Error;i=await i.json()}catch{t({status:"error",load_status:"error",message:kt,detail:"NOT_FOUND"});return}if(!i||a!==200)return;const{runtime:{stage:o},id:r}=i;switch(o){case"STOPPED":case"SLEEPING":t({status:"sleeping",load_status:"pending",message:"Space is asleep. Waking it up...",detail:o}),setTimeout(()=>{oe(e,s,t)},1e3);break;case"PAUSED":t({status:"paused",load_status:"error",message:"This space has been paused by the author. If you would like to try this demo, consider duplicating the space.",detail:o,discussions_enabled:await Re(r)});break;case"RUNNING":case"RUNNING_BUILDING":t({status:"running",load_status:"complete",message:"Space is running.",detail:o});break;case"BUILDING":t({status:"building",load_status:"pending",message:"Space is building...",detail:o}),setTimeout(()=>{oe(e,s,t)},1e3);break;case"APP_STARTING":t({status:"starting",load_status:"pending",message:"Space is starting...",detail:o}),setTimeout(()=>{oe(e,s,t)},1e3);break;default:t({status:"space_error",load_status:"error",message:"This space is experiencing an issue.",detail:o,discussions_enabled:await Re(r)});break}}const Ye=async(e,s)=>{let t=0;const n=12,i=5e3;return new Promise(a=>{oe(e,xe.test(e)?"space_name":"subdomain",o=>{s(o),o.status==="running"||o.status==="error"||o.status==="paused"||o.status==="space_error"?a():(o.status==="sleeping"||o.status==="building")&&(t<n?(t++,setTimeout(()=>{Ye(e,s).then(a)},i)):a())})})},Qt=/^(?=[^]*\b[dD]iscussions{0,1}\b)(?=[^]*\b[dD]isabled\b)[^]*$/;async function Re(e){try{const s=await fetch(`https://huggingface.co/api/spaces/${e}/discussions`,{method:"HEAD"}),t=s.headers.get("x-error-message");return!(!s.ok||t&&Qt.test(t))}catch{return!1}}async function Zt(e,s){const t={};s&&(t.Authorization=`Bearer ${s}`);try{const n=await fetch(`https://huggingface.co/api/spaces/${e}/${mt}`,{headers:t});if(n.status!==200)throw new Error("Space hardware could not be obtained.");const{hardware:i}=await n.json();return i.current}catch(n){throw new Error(n.message)}}async function Xt(e,s,t){const n={};t&&(n.Authorization=`Bearer ${t}`);const i={seconds:s};try{const a=await fetch(`https://huggingface.co/api/spaces/${e}/${_t}`,{method:"POST",headers:{"Content-Type":"application/json",...n},body:JSON.stringify(i)});if(a.status!==200)throw new Error("Could not set sleep timeout on duplicated Space. Please visit *ADD HF LINK TO SETTINGS* to set a timeout manually to reduce billing charges.");return await a.json()}catch(a){throw new Error(a.message)}}const ze=["cpu-basic","cpu-upgrade","cpu-xl","t4-small","t4-medium","a10g-small","a10g-large","a10g-largex2","a10g-largex4","a100-large","zero-a10g","h100","h100x8"];async function Kt(e,s){const{hf_token:t,private:n,hardware:i,timeout:a,auth:o}=s;if(i&&!ze.includes(i))throw new Error(`Invalid hardware type provided. Valid types are: ${ze.map(w=>`"${w}"`).join(",")}.`);const{http_protocol:r,host:d}=await le(e,t);let h=null;if(o){const w=await Ge(r,d,o,fetch);w&&(h=He(w))}const g={Authorization:`Bearer ${t}`,"Content-Type":"application/json",...h?{Cookie:h.join("; ")}:{}},u=(await(await fetch("https://huggingface.co/api/whoami-v2",{headers:g})).json()).name,_=e.split("/")[1],$={repository:`${u}/${_}`};n&&($.private=!0);let R;try{i||(R=await Zt(e,t))}catch(w){throw Error(be+w.message)}const c=i||R||"cpu-basic";$.hardware=c;try{const w=await fetch(`https://huggingface.co/api/spaces/${e}/duplicate`,{method:"POST",headers:g,body:JSON.stringify($)});if(w.status===409)try{return await ve.connect(`${u}/${_}`,s)}catch(C){throw console.error("Failed to connect Client instance:",C),C}else if(w.status!==200)throw new Error(w.statusText);const N=await w.json();return await Xt(`${u}/${_}`,a||300,t),await ve.connect(es(N.url),s)}catch(w){throw new Error(w)}}function es(e){const s=/https:\/\/huggingface.co\/spaces\/([^/]+\/[^/]+)/,t=e.match(s);if(t)return t[1]}var M;class ts extends TransformStream{constructor(s={allowCR:!1}){super({transform:(t,n)=>{for(t=ie(this,M)+t;;){const i=t.indexOf(`
10
+ `),a=s.allowCR?t.indexOf("\r"):-1;if(a!==-1&&a!==t.length-1&&(i===-1||i-1>a)){n.enqueue(t.slice(0,a)),t=t.slice(a+1);continue}if(i===-1)break;const o=t[i-1]==="\r"?i-1:i;n.enqueue(t.slice(0,o)),t=t.slice(i+1)}ut(this,M,t)},flush:t=>{if(ie(this,M)==="")return;const n=s.allowCR&&ie(this,M).endsWith("\r")?ie(this,M).slice(0,-1):ie(this,M);t.enqueue(n)}}),lt(this,M,"")}}M=new WeakMap;function ss(e){let s=new TextDecoderStream,t=new ts({allowCR:!0});return e.pipeThrough(s).pipeThrough(t)}function ns(e){let s=/[:]\s*/.exec(e),t=s&&s.index;if(t)return[e.substring(0,t),e.substring(t+s[0].length)]}function Le(e,s,t){e.get(s)||e.set(s,t)}async function*is(e,s){if(!e.body)return;let t=ss(e.body),n,i=t.getReader(),a;for(;;){if(s&&s.aborted)return i.cancel();if(n=await i.read(),n.done)return;if(!n.value){a&&(yield a),a=void 0;continue}let[o,r]=ns(n.value)||[];o&&(o==="data"?(a||(a={}),a[o]=a[o]?a[o]+`
11
+ `+r:r):o==="event"?(a||(a={}),a[o]=r):o==="id"?(a||(a={}),a[o]=+r||r):o==="retry"&&(a||(a={}),a[o]=+r||void 0))}}async function as(e,s){let t=new Request(e,s);Le(t.headers,"Accept","text/event-stream"),Le(t.headers,"Content-Type","application/json");let n=await fetch(t);if(!n.ok)throw n;return is(n,t.signal)}async function os(){let{event_callbacks:e,unclosed_events:s,pending_stream_messages:t,stream_status:n,config:i,jwt:a}=this;const o=this;if(!i)throw new Error("Could not resolve app config");n.open=!0;let r=null,d=new URLSearchParams({session_hash:this.session_hash}).toString(),h=new URL(`${i.root}${this.api_prefix}/${Fe}?${d}`);if(a&&h.searchParams.set("__sign",a),r=this.stream(h),!r){console.warn("Cannot connect to SSE endpoint: "+h.toString());return}r.onmessage=async function(g){let u=JSON.parse(g.data);if(u.msg==="close_stream"){Ee(n,o.abort_controller);return}const _=u.event_id;if(!_)await Promise.all(Object.keys(e).map($=>e[$](u)));else if(e[_]&&i){u.msg==="process_completed"&&["sse","sse_v1","sse_v2","sse_v2.1","sse_v3"].includes(i.protocol)&&s.delete(_);let $=e[_];typeof window<"u"&&typeof document<"u"?setTimeout($,0,u):$(u)}else t[_]||(t[_]=[]),t[_].push(u)},r.onerror=async function(g){console.error(g),await Promise.all(Object.keys(e).map(u=>e[u]({msg:"broken_connection",message:Q})))}}function Ee(e,s){e&&(e.open=!1,s?.abort())}function rs(e,s,t){e[s]?t.data.forEach((n,i)=>{let a=cs(e[s][i],n);e[s][i]=a,t.data[i]=a}):(e[s]=[],t.data.forEach((n,i)=>{e[s][i]=n}))}function cs(e,s){return s.forEach(([t,n,i])=>{e=ls(e,n,t,i)}),e}function ls(e,s,t,n){if(s.length===0){if(t==="replace")return n;if(t==="append")return e+n;throw new Error(`Unsupported action: ${t}`)}let i=e;for(let o=0;o<s.length-1;o++)i=i[s[o]];const a=s[s.length-1];switch(t){case"replace":i[a]=n;break;case"append":i[a]+=n;break;case"add":Array.isArray(i)?i.splice(Number(a),0,n):i[a]=n;break;case"delete":Array.isArray(i)?i.splice(Number(a),1):delete i[a];break;default:throw new Error(`Unknown action: ${t}`)}return e}function us(e,s={}){const t={close:()=>{console.warn("Method not implemented.")},onerror:null,onmessage:null,onopen:null,readyState:0,url:e.toString(),withCredentials:!1,CONNECTING:0,OPEN:1,CLOSED:2,addEventListener:()=>{throw new Error("Method not implemented.")},dispatchEvent:()=>{throw new Error("Method not implemented.")},removeEventListener:()=>{throw new Error("Method not implemented.")}};return as(e,s).then(async n=>{t.readyState=t.OPEN;try{for await(const i of n)t.onmessage&&t.onmessage(i);t.readyState=t.CLOSED}catch(i){t.onerror&&t.onerror(i),t.readyState=t.CLOSED}}).catch(n=>{console.error(n),t.onerror&&t.onerror(n),t.readyState=t.CLOSED}),t}function ds(e,s={},t,n,i){var a;try{let o=function(v){(i||Ze[v.type])&&g(v)},r=function(){for(et=!0;se.length>0;)se.shift()({value:void 0,done:!0})},d=function(v){se.length>0?se.shift()(v):de.push(v)},h=function(v){d(ps(v)),r()},g=function(v){d({value:v,done:!1})},u=function(){return de.length>0?Promise.resolve(de.shift()):new Promise(v=>se.push(v))};const{hf_token:_}=this.options,{fetch:$,app_reference:R,config:c,session_hash:w,api_info:N,api_map:C,stream_status:H,pending_stream_messages:Z,pending_diff_streams:X,event_callbacks:K,unclosed_events:V,post_data:ee,options:W,api_prefix:f}=this,x=this;if(!N)throw new Error("No API found");if(!c)throw new Error("Could not resolve app config");let{fn_index:l,endpoint_info:U,dependency:q}=hs(N,e,C,c),re=Lt(s,U),k,B,J=c.protocol??"ws",$e="",Qe=()=>$e;const y=typeof e=="number"?"/predict":e;let te,O=null,z=!1,ue={},Y=typeof window<"u"&&typeof document<"u"?new URLSearchParams(window.location.search).toString():"";const Ze=((a=W?.events)==null?void 0:a.reduce((v,I)=>(v[I]=!0,v),{}))||{};async function Xe(){let v={},I={};J==="ws"?(k&&k.readyState===0?k.addEventListener("open",()=>{k.close()}):k.close(),v={fn_index:l,session_hash:w}):(v={event_id:O},I={event_id:O,session_hash:w,fn_index:l});try{if(!c)throw new Error("Could not resolve app config");"event_id"in I&&await $(`${c.root}${f}/${vt}`,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(I)}),await $(`${c.root}${f}/${yt}`,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(v)})}catch{console.warn("The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable.")}}const Ke=async v=>{await this._resolve_heartbeat(v)};async function je(v){if(!c)return;let I=v.render_id;c.components=[...c.components.filter(P=>P.props.rendered_in!==I),...v.components],c.dependencies=[...c.dependencies.filter(P=>P.rendered_in!==I),...v.dependencies];const pe=c.components.some(P=>P.type==="state"),j=c.dependencies.some(P=>P.targets.some(T=>T[1]==="unload"));c.connect_heartbeat=pe||j,await Ke(c),o({type:"render",data:v,endpoint:y,fn_index:l})}this.handle_blob(c.root,re,U).then(async v=>{var I;if(te={data:ae(v,q,c.components,"input",!0)||[],event_data:t,fn_index:l,trigger_id:n},Jt(l,c))o({type:"status",endpoint:y,stage:"pending",queue:!1,fn_index:l,time:new Date}),ee(`${c.root}${f}/run${y.startsWith("/")?y:`/${y}`}${Y?"?"+Y:""}`,{...te,session_hash:w}).then(([j,P])=>{const T=j.data;P==200?(o({type:"data",endpoint:y,fn_index:l,data:ae(T,q,c.components,"output",W.with_null_state),time:new Date,event_data:t,trigger_id:n}),j.render_config&&je(j.render_config),o({type:"status",endpoint:y,fn_index:l,stage:"complete",eta:j.average_duration,queue:!1,time:new Date})):o({type:"status",stage:"error",endpoint:y,fn_index:l,message:j.error,queue:!1,time:new Date})}).catch(j=>{o({type:"status",stage:"error",message:j.message,endpoint:y,fn_index:l,queue:!1,time:new Date})});else if(J=="ws"){const{ws_protocol:j,host:P}=await le(R,_);o({type:"status",stage:"pending",queue:!0,endpoint:y,fn_index:l,time:new Date});let T=new URL(`${j}://${Ot(P,c.root,!0)}/queue/join${Y?"?"+Y:""}`);this.jwt&&T.searchParams.set("__sign",this.jwt),k=new WebSocket(T),k.onclose=A=>{A.wasClean||o({type:"status",stage:"error",broken:!0,message:Q,queue:!0,endpoint:y,fn_index:l,time:new Date})},k.onmessage=function(A){const D=JSON.parse(A.data),{type:S,status:E,data:b}=_e(D,ue[l]);if(S==="update"&&E&&!z)o({type:"status",endpoint:y,fn_index:l,time:new Date,...E}),E.stage==="error"&&k.close();else if(S==="hash"){k.send(JSON.stringify({fn_index:l,session_hash:w}));return}else S==="data"?k.send(JSON.stringify({...te,session_hash:w})):S==="complete"?z=E:S==="log"?o({type:"log",title:b.title,log:b.log,level:b.level,endpoint:y,duration:b.duration,visible:b.visible,fn_index:l}):S==="generating"&&o({type:"status",time:new Date,...E,stage:E?.stage,queue:!0,endpoint:y,fn_index:l});b&&(o({type:"data",time:new Date,data:ae(b.data,q,c.components,"output",W.with_null_state),endpoint:y,fn_index:l,event_data:t,trigger_id:n}),z&&(o({type:"status",time:new Date,...z,stage:E?.stage,queue:!0,endpoint:y,fn_index:l}),k.close()))},Ie(c.version||"2.0.0","3.6")<0&&addEventListener("open",()=>k.send(JSON.stringify({hash:w})))}else if(J=="sse"){o({type:"status",stage:"pending",queue:!0,endpoint:y,fn_index:l,time:new Date});var pe=new URLSearchParams({fn_index:l.toString(),session_hash:w}).toString();let j=new URL(`${c.root}${f}/${Fe}?${Y?Y+"&":""}${pe}`);if(this.jwt&&j.searchParams.set("__sign",this.jwt),B=this.stream(j),!B)return Promise.reject(new Error("Cannot connect to SSE endpoint: "+j.toString()));B.onmessage=async function(P){const T=JSON.parse(P.data),{type:A,status:D,data:S}=_e(T,ue[l]);if(A==="update"&&D&&!z)o({type:"status",endpoint:y,fn_index:l,time:new Date,...D}),D.stage==="error"&&(B?.close(),r());else if(A==="data"){let[E,b]=await ee(`${c.root}${f}/queue/data`,{...te,session_hash:w,event_id:O});b!==200&&(o({type:"status",stage:"error",message:Q,queue:!0,endpoint:y,fn_index:l,time:new Date}),B?.close(),r())}else A==="complete"?z=D:A==="log"?o({type:"log",title:S.title,log:S.log,level:S.level,endpoint:y,duration:S.duration,visible:S.visible,fn_index:l}):(A==="generating"||A==="streaming")&&o({type:"status",time:new Date,...D,stage:D?.stage,queue:!0,endpoint:y,fn_index:l});S&&(o({type:"data",time:new Date,data:ae(S.data,q,c.components,"output",W.with_null_state),endpoint:y,fn_index:l,event_data:t,trigger_id:n}),z&&(o({type:"status",time:new Date,...z,stage:D?.stage,queue:!0,endpoint:y,fn_index:l}),B?.close(),r()))}}else if(J=="sse_v1"||J=="sse_v2"||J=="sse_v2.1"||J=="sse_v3"){o({type:"status",stage:"pending",queue:!0,endpoint:y,fn_index:l,time:new Date});let j="";typeof window<"u"&&typeof document<"u"&&(j=(I=window?.location)==null?void 0:I.hostname);const P=j.includes(".dev.")?`https://moon-${j.split(".")[1]}.dev.spaces.huggingface.tech`:"https://huggingface.co";(typeof window<"u"&&typeof document<"u"&&window.parent!=window&&window.supports_zerogpu_headers?Wt("zerogpu-headers",P):Promise.resolve(null)).then(T=>ee(`${c.root}${f}/${pt}?${Y}`,{...te,session_hash:w},T)).then(async([T,A])=>{if(A===503)o({type:"status",stage:"error",message:Je,queue:!0,endpoint:y,fn_index:l,time:new Date});else if(A===422)o({type:"status",stage:"error",message:T.detail,queue:!0,endpoint:y,fn_index:l,code:"validation_error",time:new Date}),r();else if(A!==200)o({type:"status",stage:"error",broken:!1,message:T.detail,queue:!0,endpoint:y,fn_index:l,time:new Date});else{O=T.event_id,$e=O;let D=async function(S){try{const{type:E,status:b,data:F,original_msg:tt}=_e(S,ue[l]);if(E=="heartbeat")return;if(E==="update"&&b&&!z)o({type:"status",endpoint:y,fn_index:l,time:new Date,original_msg:tt,...b});else if(E==="complete")z=b;else if(E=="unexpected_error"||E=="broken_connection"){console.error("Unexpected error",b?.message);const st=E==="broken_connection";o({type:"status",stage:"error",message:b?.message||"An Unexpected Error Occurred!",queue:!0,endpoint:y,broken:st,session_not_found:b?.session_not_found,fn_index:l,time:new Date})}else if(E==="log"){o({type:"log",title:F.title,log:F.log,level:F.level,endpoint:y,duration:F.duration,visible:F.visible,fn_index:l});return}else(E==="generating"||E==="streaming")&&(o({type:"status",time:new Date,...b,stage:b?.stage,queue:!0,endpoint:y,fn_index:l}),F&&q.connection!=="stream"&&["sse_v2","sse_v2.1","sse_v3"].includes(J)&&rs(X,O,F));F&&(o({type:"data",time:new Date,data:ae(F.data,q,c.components,"output",W.with_null_state),endpoint:y,fn_index:l}),F.render_config&&await je(F.render_config),z&&(o({type:"status",time:new Date,...z,stage:b?.stage,queue:!0,endpoint:y,fn_index:l}),r())),(b?.stage==="complete"||b?.stage==="error")&&(K[O]&&delete K[O],O in X&&delete X[O])}catch(E){console.error("Unexpected client exception",E),o({type:"status",stage:"error",message:"An Unexpected Error Occurred!",queue:!0,endpoint:y,fn_index:l,time:new Date}),["sse_v2","sse_v2.1","sse_v3"].includes(J)&&(Ee(H,x.abort_controller),H.open=!1,r())}};O in Z&&(Z[O].forEach(S=>D(S)),delete Z[O]),K[O]=D,V.add(O),H.open||await this.open_stream()}})}});let et=!1;const de=[],se=[],Se={[Symbol.asyncIterator]:()=>Se,next:u,throw:async v=>(h(v),u()),return:async()=>(r(),u()),cancel:Xe,event_id:Qe};return Se}catch(o){throw console.error("Submit function encountered an error:",o),o}}function ps(e){return{then:(s,t)=>t(e)}}function hs(e,s,t,n){let i,a,o;if(typeof s=="number")i=s,a=e.unnamed_endpoints[i],o=n.dependencies.find(r=>r.id==s);else{const r=s.replace(/^\//,"");i=t[r],a=e.named_endpoints[s.trim()],o=n.dependencies.find(d=>d.id==t[r])}if(typeof i!="number")throw new Error("There is no endpoint matching that name of fn_index matching that number.");return{fn_index:i,endpoint_info:a,dependency:o}}class ve{constructor(s,t={events:["data"]}){m(this,"app_reference"),m(this,"options"),m(this,"deep_link",null),m(this,"config"),m(this,"api_prefix",""),m(this,"api_info"),m(this,"api_map",{}),m(this,"session_hash",Math.random().toString(36).substring(2)),m(this,"jwt",!1),m(this,"last_status",{}),m(this,"cookies",null),m(this,"stream_status",{open:!1}),m(this,"closed",!1),m(this,"pending_stream_messages",{}),m(this,"pending_diff_streams",{}),m(this,"event_callbacks",{}),m(this,"unclosed_events",new Set),m(this,"heartbeat_event",null),m(this,"abort_controller",null),m(this,"stream_instance",null),m(this,"current_payload"),m(this,"ws_map",{}),m(this,"view_api"),m(this,"upload_files"),m(this,"upload"),m(this,"handle_blob"),m(this,"post_data"),m(this,"submit"),m(this,"predict"),m(this,"open_stream"),m(this,"resolve_config"),m(this,"resolve_cookies");var n;this.app_reference=s,this.deep_link=((n=t.query_params)==null?void 0:n.deep_link)||null,t.events||(t.events=["data"]),this.options=t,this.current_payload={},this.view_api=Ut.bind(this),this.upload_files=Bt.bind(this),this.handle_blob=Mt.bind(this),this.post_data=Vt.bind(this),this.submit=ds.bind(this),this.predict=Yt.bind(this),this.open_stream=os.bind(this),this.resolve_config=qt.bind(this),this.resolve_cookies=Tt.bind(this),this.upload=It.bind(this),this.fetch=this.fetch.bind(this),this.handle_space_success=this.handle_space_success.bind(this),this.stream=this.stream.bind(this)}get_url_config(s=null){if(!this.config)throw new Error(G);s===null&&(s=window.location.href);const t=o=>o.replace(/^\/+|\/+$/g,"");let n=t(new URL(this.config.root).pathname),i=t(new URL(s).pathname),a;return i.startsWith(n)?a=t(i.substring(n.length)):a="",this.get_page_config(a)}get_page_config(s){if(!this.config)throw new Error(G);let t=this.config;return s in t.page||(s=""),{...t,current_page:s,layout:t.page[s].layout,components:t.components.filter(n=>t.page[s].components.includes(n.id)),dependencies:this.config.dependencies.filter(n=>t.page[s].dependencies.includes(n.id))}}fetch(s,t){const n=new Headers(t?.headers||{});if(this&&this.cookies&&n.append("Cookie",this.cookies),this&&this.options.headers)for(const i in this.options.headers)n.append(i,this.options.headers[i]);return fetch(s,{...t,headers:n})}stream(s){const t=new Headers;if(this&&this.cookies&&t.append("Cookie",this.cookies),this&&this.options.headers)for(const n in this.options.headers)t.append(n,this.options.headers[n]);return this&&this.options.hf_token&&t.append("Authorization",`Bearer ${this.options.hf_token}`),this.abort_controller=new AbortController,this.stream_instance=us(s.toString(),{credentials:"include",headers:t,signal:this.abort_controller.signal}),this.stream_instance}async init(){var s;this.options.auth&&await this.resolve_cookies(),await this._resolve_config().then(({config:t})=>this._resolve_heartbeat(t)),this.api_info=await this.view_api(),this.api_map=Pt(((s=this.config)==null?void 0:s.dependencies)||[])}async _resolve_heartbeat(s){if(s&&(this.config=s,this.api_prefix=s.api_prefix||"",this.config&&this.config.connect_heartbeat&&this.config.space_id&&this.options.hf_token&&(this.jwt=await Te(this.config.space_id,this.options.hf_token,this.cookies))),s.space_id&&this.options.hf_token&&(this.jwt=await Te(s.space_id,this.options.hf_token)),this.config&&this.config.connect_heartbeat){const t=new URL(`${this.config.root}${this.api_prefix}/${gt}/${this.session_hash}`);this.jwt&&t.searchParams.set("__sign",this.jwt),this.heartbeat_event||(this.heartbeat_event=this.stream(t))}}static async connect(s,t={events:["data"]}){const n=new this(s,t);return t.session_hash&&(n.session_hash=t.session_hash),await n.init(),n}async reconnect(){const s=new URL(`${this.config.root}${this.api_prefix}/${bt}`);let t;try{const n=await this.fetch(s);if(!n.ok)throw new Error;t=(await n.json()).app_id}catch{return"broken"}return t!==this.config.app_id?"changed":"connected"}close(){this.closed=!0,Ee(this.stream_status,this.abort_controller)}set_current_payload(s){this.current_payload=s}static async duplicate(s,t={events:["data"]}){return Kt(s,t)}async _resolve_config(){const{http_protocol:s,host:t,space_id:n}=await le(this.app_reference,this.options.hf_token),{status_callback:i}=this.options;n&&i&&await Ye(n,i);let a;try{let o=`${s}//${t}`;if(a=await this.resolve_config(o),!a)throw new Error(G);return this.config_success(a)}catch(o){if(n&&i)oe(n,xe.test(n)?"space_name":"subdomain",this.handle_space_success);else throw i&&i({status:"error",message:"Could not load this space.",load_status:"error",detail:"NOT_FOUND"}),Error(o)}}async config_success(s){if(this.config=s,this.api_prefix=s.api_prefix||"",this.config.auth_required)return this.prepare_return_obj();try{this.api_info=await this.view_api()}catch(t){console.error(Et+t.message)}return this.prepare_return_obj()}async handle_space_success(s){var t;if(!this)throw new Error(G);const{status_callback:n}=this.options;if(n&&n(s),s.status==="running")try{if(this.config=await this._resolve_config(),this.api_prefix=((t=this==null?void 0:this.config)==null?void 0:t.api_prefix)||"",!this.config)throw new Error(G);return await this.config_success(this.config)}catch(i){throw n&&n({status:"error",message:"Could not load this space.",load_status:"error",detail:"NOT_FOUND"}),i}}async component_server(s,t,n){var i;if(!this.config)throw new Error(G);const a={},{hf_token:o}=this.options,{session_hash:r}=this;o&&(a.Authorization=`Bearer ${this.options.hf_token}`);let d,h=this.config.components.find(u=>u.id===s);(i=h?.props)!=null&&i.root_url?d=h.props.root_url:d=this.config.root;let g;if("binary"in n){g=new FormData;for(const u in n.data)u!=="binary"&&g.append(u,n.data[u]);g.set("component_id",s.toString()),g.set("fn_name",t),g.set("session_hash",r)}else g=JSON.stringify({data:n,component_id:s,fn_name:t,session_hash:r}),a["Content-Type"]="application/json";o&&(a.Authorization=`Bearer ${o}`);try{const u=await this.fetch(`${d}${this.api_prefix}/${wt}/`,{method:"POST",body:g,headers:a,credentials:"include"});if(!u.ok)throw new Error("Could not connect to component server: "+u.statusText);return await u.json()}catch(u){console.warn(u)}}set_cookies(s){this.cookies=He(s).join("; ")}prepare_return_obj(){return{config:this.config,predict:this.predict,submit:this.submit,view_api:this.view_api,component_server:this.component_server}}async connect_ws(s){return new Promise((t,n)=>{let i;try{i=new WebSocket(s)}catch{this.ws_map[s]="failed";return}this.ws_map[s]="pending",i.onopen=()=>{this.ws_map[s]=i,t()},i.onerror=a=>{console.error("WebSocket error:",a),this.close_ws(s),this.ws_map[s]="failed",t()},i.onclose=()=>{this.ws_map[s]="closed"},i.onmessage=a=>{}})}async send_ws_message(s,t){if(!(s in this.ws_map))await this.connect_ws(s);else if(this.ws_map[s]==="pending"||this.ws_map[s]==="closed"||this.ws_map[s]==="failed")return;const n=this.ws_map[s];n instanceof WebSocket?n.send(JSON.stringify(t)):this.post_data(s,t)}async close_ws(s){if(s in this.ws_map){const t=this.ws_map[s];t instanceof WebSocket&&(t.close(),delete this.ws_map[s])}}}async function ge(e,s,t){return(await e.predict(s,t)).data[0]}const ms=()=>{const e=L.useRef(null),[s,t]=L.useState(!1),[n,i]=L.useState(null),[a,o]=L.useState({username:null,member:!1}),[r,d]=L.useState([]),[h,g]=L.useState(!1),[u,_]=L.useState("polls"),[$,R]=L.useState(""),[c,w]=L.useState(["",""]),[N,C]=L.useState(null),H=20,Z=(f,x)=>{w(l=>l.map((U,q)=>q===f?x:U))},X=()=>{w(f=>f.length>=H?f:[...f,""])},K=f=>{w(x=>x.length<=1?x:x.filter((l,U)=>U!==f))},V=L.useCallback(async()=>{const f=e.current;if(f)try{const x=await ge(f,"/polls",[]);o(x.me),d(x.polls),i(null)}catch{i("Cannot reach the voting backend.")}},[]);L.useEffect(()=>{let f=!1;return(async()=>{try{e.current=await ve.connect(window.location.origin+"/gradio"),f||(t(!0),await V())}catch{f||i("Cannot reach the voting backend.")}})(),()=>{f=!0}},[V]);const ee=async(f,x)=>{if(!(h||!e.current)){g(!0);try{const l=await ge(e.current,"/vote",[f,x]);C({kind:l.ok?"ok":"err",text:l.message}),l.ok&&l.poll?d(U=>U.map(q=>q.id===l.poll.id?l.poll:q)):await V()}catch{C({kind:"err",text:"Vote failed — please retry."})}finally{g(!1)}}},W=async()=>{if(!(h||!e.current)){g(!0);try{const f=await ge(e.current,"/create_poll",[$,c.filter(x=>x.trim()).join(`
12
+ `)]);C({kind:f.ok?"ok":"err",text:f.message}),f.ok&&(R(""),w(["",""]),await V(),_("polls"))}catch{C({kind:"err",text:"Could not create poll — please retry."})}finally{g(!1)}}};return p.jsxs("div",{className:"page",children:[p.jsxs("div",{className:"bg","aria-hidden":"true",children:[p.jsx("div",{className:"bg-orb bg-orb-a"}),p.jsx("div",{className:"bg-orb bg-orb-b"}),p.jsx("div",{className:"bg-orb bg-orb-c"}),p.jsx("div",{className:"bg-grid"})]}),p.jsxs("div",{className:"shell",children:[p.jsxs("header",{className:"top",children:[p.jsxs("h1",{children:[p.jsx("span",{className:"logo","aria-hidden":"true",children:"🗳️"})," SLM Consortium Polls"]}),p.jsxs("div",{className:"auth",children:[a.username?p.jsxs("span",{className:`chip ${a.member?"chip-ok":"chip-warn"}`,children:["@",a.username,a.member?"":" · not a member"]}):p.jsx("a",{className:"btn",href:"/gradio/login/huggingface?_target_url=/",children:"Sign in with Hugging Face"}),p.jsx("button",{className:"btn quiet",onClick:()=>void V(),children:"Refresh"})]})]}),p.jsxs("p",{className:"lede",children:["Hugging Face sign-in required. Only ",p.jsx("code",{children:"slmconsortium"})," members can vote or create polls. Click to vote, and click another option to change your vote."]}),p.jsxs("nav",{className:"tabs",role:"tablist","aria-label":"Sections",children:[p.jsx("button",{role:"tab","aria-selected":u==="polls",className:u==="polls"?"active":"",onClick:()=>_("polls"),children:"🗳️ Polls"}),p.jsx("button",{role:"tab","aria-selected":u==="create",className:u==="create"?"active":"",onClick:()=>_("create"),children:"➕ Create poll"})]}),n&&p.jsx("p",{className:"notice err",children:n}),N&&p.jsx("p",{className:`notice ${N.kind}`,children:N.text}),!s&&!n&&p.jsx("p",{className:"muted",children:"Loading…"}),u==="polls"&&p.jsxs("div",{className:"tabpanel",role:"tabpanel",children:[s&&r.length===0&&p.jsx("p",{className:"muted",children:"No polls yet."}),r.map(f=>p.jsxs("section",{className:"card",children:[p.jsx("h2",{children:f.question}),p.jsxs("p",{className:"meta",children:[f.total," vote",f.total===1?"":"s"," · by @",f.created_by]}),p.jsx("ul",{className:"opts",children:f.options.map((x,l)=>{const U=f.total?f.counts[l]/f.total*100:0,q=f.my_vote===l,re=Object.entries(f.voters??{}).filter(([,k])=>k===l).map(([k])=>k).sort((k,B)=>k.toLowerCase().localeCompare(B.toLowerCase()));return p.jsx("li",{children:p.jsxs("button",{className:q?"mine":"",disabled:!a.member||h,title:a.username?a.member?`Vote for ${x}`:"Only slmconsortium members can vote":"Sign in to vote",onClick:()=>void ee(f.id,l),children:[p.jsxs("span",{className:"opt-top",children:[p.jsxs("span",{children:[x,q?" ✓":""]}),p.jsxs("span",{className:"num",children:[f.counts[l]," · ",U.toFixed(0),"%"]})]}),p.jsx("span",{className:"bar",children:p.jsx("i",{style:{width:`${U}%`}})}),re.length>0&&p.jsx("span",{className:"voters",children:re.map(k=>p.jsxs("span",{className:`voter ${k===a.username?"me":""}`,children:["@",k]},k))})]})},l)})})]},f.id))]}),u==="create"&&p.jsx("div",{className:"tabpanel",role:"tabpanel",children:p.jsxs("section",{className:"card",children:[p.jsx("h2",{children:"New poll"}),!a.member&&p.jsx("p",{className:"muted",children:a.username?"Only slmconsortium members can create polls.":"Sign in first."}),p.jsxs("label",{children:["Question",p.jsx("input",{value:$,onChange:f=>R(f.target.value),placeholder:"Which meeting time works best?",disabled:!a.member})]}),p.jsxs("div",{className:"field",children:[p.jsxs("span",{className:"field-label",children:["Options",p.jsxs("span",{className:"field-hint",children:[c.length,"/",H]})]}),p.jsx("ul",{className:"opt-editor",children:c.map((f,x)=>p.jsxs("li",{children:[p.jsx("input",{value:f,onChange:l=>Z(x,l.target.value),placeholder:`Option ${x+1}`,disabled:!a.member,"aria-label":`Option ${x+1}`}),p.jsx("button",{type:"button",className:"icon-btn",onClick:()=>K(x),disabled:!a.member||c.length<=1,"aria-label":`Remove option ${x+1}`,title:c.length<=1?"Need at least one option row":"Remove option",children:"✕"})]},x))}),p.jsx("button",{type:"button",className:"btn quiet add-opt",onClick:X,disabled:!a.member||c.length>=H,children:"+ Add option"})]}),p.jsx("button",{className:"btn",disabled:!a.member||h||$.trim().length<3||c.filter(f=>f.trim()).length<2,onClick:()=>void W(),children:h?"Working…":"Create poll"})]})}),p.jsx("footer",{className:"muted",children:p.jsx("a",{href:"/gradio/",children:"Classic UI"})})]})]})};export{ms as default};
frontend/dist/_astro/__vite-browser-external-DYxpcVy9.BIHI7g3E.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const e={};export{e as default};
frontend/dist/_astro/client.BlZe1zq3.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/_astro/index.D5zPhbGI.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--bg: #06040f;--panel: rgba(12, 9, 26, .72);--panel-solid: #0c091a;--border: rgba(140, 120, 255, .22);--border-strong: rgba(160, 130, 255, .45);--text: #eceaf6;--muted: #a49fc2;--accent: #7b5cff;--accent-soft: rgba(123, 92, 255, .18);--ok: #5ee6a8;--err: #ff7a90}*{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5;min-height:100vh}.bg{position:fixed;inset:0;z-index:0;overflow:hidden;background:radial-gradient(ellipse 120% 80% at 50% -20%,#161033 0%,transparent 60%),var(--bg)}.bg-orb{position:absolute;border-radius:50%;filter:blur(90px);opacity:.55;will-change:transform}.bg-orb-a{width:55vmax;height:55vmax;left:-15vmax;top:-18vmax;background:radial-gradient(circle,rgba(82,38,255,.5) 0%,transparent 70%);animation:drift-a 26s ease-in-out infinite alternate}.bg-orb-b{width:45vmax;height:45vmax;right:-18vmax;top:10vmax;background:radial-gradient(circle,rgba(140,60,255,.35) 0%,transparent 70%);animation:drift-b 32s ease-in-out infinite alternate}.bg-orb-c{width:50vmax;height:50vmax;left:15vmax;bottom:-25vmax;background:radial-gradient(circle,rgba(40,90,255,.3) 0%,transparent 70%);animation:drift-c 38s ease-in-out infinite alternate}@keyframes drift-a{0%{transform:translateZ(0) scale(1)}to{transform:translate3d(8vmax,6vmax,0) scale(1.15)}}@keyframes drift-b{0%{transform:translateZ(0) scale(1.1)}to{transform:translate3d(-7vmax,9vmax,0) scale(.95)}}@keyframes drift-c{0%{transform:translateZ(0) scale(1)}to{transform:translate3d(-6vmax,-8vmax,0) scale(1.2)}}.bg-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(150,130,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(150,130,255,.05) 1px,transparent 1px);background-size:44px 44px;mask-image:radial-gradient(ellipse 90% 70% at 50% 35%,black 0%,transparent 75%);-webkit-mask-image:radial-gradient(ellipse 90% 70% at 50% 35%,black 0%,transparent 75%)}@media(prefers-reduced-motion:reduce){.bg-orb{animation:none}}.shell{position:relative;z-index:1;max-width:660px;margin:0 auto;padding:40px 20px 80px}.top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-bottom:1px solid var(--border);padding-bottom:16px;margin-bottom:12px}.top h1{font-size:20px;margin:0;font-weight:650;letter-spacing:-.01em}.logo{margin-right:4px}.auth{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.chip{display:inline-block;font-size:13px;padding:5px 12px;border-radius:999px;border:1px solid var(--border-strong);background:var(--accent-soft)}.chip-ok{color:var(--ok);border-color:#5ee6a866;background:#5ee6a81a}.chip-warn{color:#ffcf7a;border-color:#ffcf7a66;background:#ffcf7a14}.lede{color:var(--muted);font-size:14px}.lede code,.meta code{font-size:13px;background:var(--accent-soft);border:1px solid var(--border);border-radius:4px;padding:1px 5px}.muted{color:var(--muted);font-size:14px}a{color:var(--text)}.btn{appearance:none;background:var(--accent);color:#fff;border:1px solid transparent;border-radius:8px;padding:8px 16px;font-size:14px;font-weight:500;cursor:pointer;text-decoration:none;display:inline-block;font-family:inherit;box-shadow:0 4px 20px #7b5cff59;transition:background .15s ease,transform .1s ease}.btn:hover:not(:disabled){background:#8e73ff;transform:translateY(-1px)}.btn:disabled{opacity:.4;cursor:not-allowed}.btn.quiet{background:#ffffff0f;color:var(--text);border-color:var(--border);box-shadow:none}.btn.quiet:hover:not(:disabled){background:#ffffff1f;transform:none}.tabs{display:flex;gap:4px;margin:20px 0 16px;padding:4px;border:1px solid var(--border);border-radius:12px;background:var(--panel);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}.tabs button{flex:1;appearance:none;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--muted);font-size:14px;font-weight:500;font-family:inherit;padding:9px 12px;cursor:pointer;transition:color .15s ease,background .15s ease}.tabs button:hover{color:var(--text)}.tabs button.active{color:#fff;background:var(--accent-soft);border-color:var(--border-strong)}.notice{font-size:14px;padding:10px 14px;margin:0 0 14px;border-radius:8px;border:1px solid}.notice.err{color:var(--err);border-color:#ff7a9059;background:#ff7a9014}.notice.ok{color:var(--ok);border-color:#5ee6a859;background:#5ee6a814}.card{border:1px solid var(--border);border-radius:14px;padding:20px;margin-bottom:16px;background:var(--panel);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 32px #00000059}.card h2{font-size:17px;margin:0 0 2px;font-weight:600}.meta{color:var(--muted);font-size:13px;margin:0 0 12px}.opts{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.opts button{width:100%;text-align:left;background:#ffffff0a;color:var(--text);border:1px solid var(--border);border-radius:10px;padding:11px 14px;font-size:14px;cursor:pointer;font-family:inherit;transition:border-color .15s ease,background .15s ease}.opts button:hover:not(:disabled){border-color:var(--border-strong);background:#ffffff12}.opts button:disabled{cursor:default}.opts button.mine{border-color:var(--accent);background:var(--accent-soft);box-shadow:0 0 0 1px var(--accent) inset}.opt-top{display:flex;justify-content:space-between;gap:12px}.num{color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}.bar{display:block;height:4px;background:#ffffff14;border-radius:99px;margin-top:9px;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(90deg,#5226ff,#9a7bff);border-radius:99px;transition:width .4s ease}.voters{display:flex;flex-wrap:wrap;gap:4px;margin-top:9px}.voter{font-size:11.5px;line-height:1;padding:4px 8px;border-radius:999px;border:1px solid var(--border);background:#ffffff0f;color:var(--muted);font-weight:500}.voter.me{color:#fff;border-color:#ffffff8c;background:#7b5cff73}label{display:block;font-size:13px;color:var(--muted);margin:14px 0}input,textarea{display:block;width:100%;margin-top:5px;border:1px solid var(--border);border-radius:10px;padding:10px 13px;font-size:14px;font-family:inherit;color:var(--text);background:#ffffff0d;transition:border-color .15s ease}input::placeholder,textarea::placeholder{color:#a49fc28c}input:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px #7b5cff33}input:disabled,textarea:disabled{background:#ffffff08;color:var(--muted);cursor:not-allowed}textarea{resize:vertical}.field{display:block;font-size:13px;color:var(--muted);margin:14px 0}.field-label{display:flex;justify-content:space-between;align-items:baseline}.field-hint{font-size:12px;color:var(--muted);opacity:.7;font-variant-numeric:tabular-nums}.opt-editor{list-style:none;margin:5px 0 10px;padding:0;display:flex;flex-direction:column;gap:8px}.opt-editor li{display:flex;gap:8px;align-items:center}.opt-editor input{flex:1;margin-top:0}.icon-btn{appearance:none;flex:0 0 auto;width:38px;height:38px;border-radius:10px;border:1px solid var(--border);background:#ffffff0a;color:var(--muted);font-size:15px;line-height:1;cursor:pointer;font-family:inherit;transition:border-color .15s ease,color .15s ease,background .15s ease}.icon-btn:hover:not(:disabled){border-color:#ff7a9080;color:var(--err);background:#ff7a9014}.icon-btn:disabled{opacity:.35;cursor:not-allowed}.add-opt{margin-top:2px}footer{border-top:1px solid var(--border);margin-top:16px;padding-top:16px;font-size:13px}footer a{color:var(--muted)}footer a:hover{color:var(--text)}
frontend/dist/_astro/index.qNTDzdXh.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ var A={exports:{}},n={};/**
2
+ * @license React
3
+ * react.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var N;function J(){if(N)return n;N=1;var E=Symbol.for("react.transitional.element"),I=Symbol.for("react.portal"),M=Symbol.for("react.fragment"),x=Symbol.for("react.strict_mode"),L=Symbol.for("react.profiler"),k=Symbol.for("react.consumer"),U=Symbol.for("react.context"),q=Symbol.for("react.forward_ref"),b=Symbol.for("react.suspense"),D=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),z=Symbol.for("react.activity"),S=Symbol.iterator;function G(t){return t===null||typeof t!="object"?null:(t=S&&t[S]||t["@@iterator"],typeof t=="function"?t:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,g={};function a(t,e,u){this.props=t,this.context=e,this.refs=g,this.updater=u||h}a.prototype.isReactComponent={},a.prototype.setState=function(t,e){if(typeof t!="object"&&typeof t!="function"&&t!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,t,e,"setState")},a.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,"forceUpdate")};function H(){}H.prototype=a.prototype;function y(t,e,u){this.props=t,this.context=e,this.refs=g,this.updater=u||h}var v=y.prototype=new H;v.constructor=y,w(v,a.prototype),v.isPureReactComponent=!0;var O=Array.isArray;function R(){}var f={H:null,A:null,T:null,S:null},P=Object.prototype.hasOwnProperty;function T(t,e,u){var r=u.ref;return{$$typeof:E,type:t,key:e,ref:r!==void 0?r:null,props:u}}function K(t,e){return T(t.type,e,t.props)}function m(t){return typeof t=="object"&&t!==null&&t.$$typeof===E}function B(t){var e={"=":"=0",":":"=2"};return"$"+t.replace(/[=:]/g,function(u){return e[u]})}var j=/\/+/g;function C(t,e){return typeof t=="object"&&t!==null&&t.key!=null?B(""+t.key):e.toString(36)}function W(t){switch(t.status){case"fulfilled":return t.value;case"rejected":throw t.reason;default:switch(typeof t.status=="string"?t.then(R,R):(t.status="pending",t.then(function(e){t.status==="pending"&&(t.status="fulfilled",t.value=e)},function(e){t.status==="pending"&&(t.status="rejected",t.reason=e)})),t.status){case"fulfilled":return t.value;case"rejected":throw t.reason}}throw t}function _(t,e,u,r,o){var s=typeof t;(s==="undefined"||s==="boolean")&&(t=null);var i=!1;if(t===null)i=!0;else switch(s){case"bigint":case"string":case"number":i=!0;break;case"object":switch(t.$$typeof){case E:case I:i=!0;break;case d:return i=t._init,_(i(t._payload),e,u,r,o)}}if(i)return o=o(t),i=r===""?"."+C(t,0):r,O(o)?(u="",i!=null&&(u=i.replace(j,"$&/")+"/"),_(o,e,u,"",function(Z){return Z})):o!=null&&(m(o)&&(o=K(o,u+(o.key==null||t&&t.key===o.key?"":(""+o.key).replace(j,"$&/")+"/")+i)),e.push(o)),1;i=0;var p=r===""?".":r+":";if(O(t))for(var c=0;c<t.length;c++)r=t[c],s=p+C(r,c),i+=_(r,e,u,s,o);else if(c=G(t),typeof c=="function")for(t=c.call(t),c=0;!(r=t.next()).done;)r=r.value,s=p+C(r,c++),i+=_(r,e,u,s,o);else if(s==="object"){if(typeof t.then=="function")return _(W(t),e,u,r,o);throw e=String(t),Error("Objects are not valid as a React child (found: "+(e==="[object Object]"?"object with keys {"+Object.keys(t).join(", ")+"}":e)+"). If you meant to render a collection of children, use an array instead.")}return i}function l(t,e,u){if(t==null)return t;var r=[],o=0;return _(t,r,"","",function(s){return e.call(u,s,o++)}),r}function Q(t){if(t._status===-1){var e=t._result;e=e(),e.then(function(u){(t._status===0||t._status===-1)&&(t._status=1,t._result=u)},function(u){(t._status===0||t._status===-1)&&(t._status=2,t._result=u)}),t._status===-1&&(t._status=0,t._result=e)}if(t._status===1)return t._result.default;throw t._result}var $=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var e=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(e))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)},X={map:l,forEach:function(t,e,u){l(t,function(){e.apply(this,arguments)},u)},count:function(t){var e=0;return l(t,function(){e++}),e},toArray:function(t){return l(t,function(e){return e})||[]},only:function(t){if(!m(t))throw Error("React.Children.only expected to receive a single React element child.");return t}};return n.Activity=z,n.Children=X,n.Component=a,n.Fragment=M,n.Profiler=L,n.PureComponent=y,n.StrictMode=x,n.Suspense=b,n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=f,n.__COMPILER_RUNTIME={__proto__:null,c:function(t){return f.H.useMemoCache(t)}},n.cache=function(t){return function(){return t.apply(null,arguments)}},n.cacheSignal=function(){return null},n.cloneElement=function(t,e,u){if(t==null)throw Error("The argument must be a React element, but you passed "+t+".");var r=w({},t.props),o=t.key;if(e!=null)for(s in e.key!==void 0&&(o=""+e.key),e)!P.call(e,s)||s==="key"||s==="__self"||s==="__source"||s==="ref"&&e.ref===void 0||(r[s]=e[s]);var s=arguments.length-2;if(s===1)r.children=u;else if(1<s){for(var i=Array(s),p=0;p<s;p++)i[p]=arguments[p+2];r.children=i}return T(t.type,o,r)},n.createContext=function(t){return t={$$typeof:U,_currentValue:t,_currentValue2:t,_threadCount:0,Provider:null,Consumer:null},t.Provider=t,t.Consumer={$$typeof:k,_context:t},t},n.createElement=function(t,e,u){var r,o={},s=null;if(e!=null)for(r in e.key!==void 0&&(s=""+e.key),e)P.call(e,r)&&r!=="key"&&r!=="__self"&&r!=="__source"&&(o[r]=e[r]);var i=arguments.length-2;if(i===1)o.children=u;else if(1<i){for(var p=Array(i),c=0;c<i;c++)p[c]=arguments[c+2];o.children=p}if(t&&t.defaultProps)for(r in i=t.defaultProps,i)o[r]===void 0&&(o[r]=i[r]);return T(t,s,o)},n.createRef=function(){return{current:null}},n.forwardRef=function(t){return{$$typeof:q,render:t}},n.isValidElement=m,n.lazy=function(t){return{$$typeof:d,_payload:{_status:-1,_result:t},_init:Q}},n.memo=function(t,e){return{$$typeof:D,type:t,compare:e===void 0?null:e}},n.startTransition=function(t){var e=f.T,u={};f.T=u;try{var r=t(),o=f.S;o!==null&&o(u,r),typeof r=="object"&&r!==null&&typeof r.then=="function"&&r.then(R,$)}catch(s){$(s)}finally{e!==null&&u.types!==null&&(e.types=u.types),f.T=e}},n.unstable_useCacheRefresh=function(){return f.H.useCacheRefresh()},n.use=function(t){return f.H.use(t)},n.useActionState=function(t,e,u){return f.H.useActionState(t,e,u)},n.useCallback=function(t,e){return f.H.useCallback(t,e)},n.useContext=function(t){return f.H.useContext(t)},n.useDebugValue=function(){},n.useDeferredValue=function(t,e){return f.H.useDeferredValue(t,e)},n.useEffect=function(t,e){return f.H.useEffect(t,e)},n.useEffectEvent=function(t){return f.H.useEffectEvent(t)},n.useId=function(){return f.H.useId()},n.useImperativeHandle=function(t,e,u){return f.H.useImperativeHandle(t,e,u)},n.useInsertionEffect=function(t,e){return f.H.useInsertionEffect(t,e)},n.useLayoutEffect=function(t,e){return f.H.useLayoutEffect(t,e)},n.useMemo=function(t,e){return f.H.useMemo(t,e)},n.useOptimistic=function(t,e){return f.H.useOptimistic(t,e)},n.useReducer=function(t,e,u){return f.H.useReducer(t,e,u)},n.useRef=function(t){return f.H.useRef(t)},n.useState=function(t){return f.H.useState(t)},n.useSyncExternalStore=function(t,e,u){return f.H.useSyncExternalStore(t,e,u)},n.useTransition=function(){return f.H.useTransition()},n.version="19.2.8",n}var Y;function V(){return Y||(Y=1,A.exports=J()),A.exports}var F=V();export{V as a,F as r};
frontend/dist/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>SLM Consortium Polls</title><meta name="description" content="Create polls and vote."><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🗳️</text></svg>"><link rel="stylesheet" href="/_astro/index.D5zPhbGI.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="TlKYT" component-url="/_astro/PollApp.BI8OLAG4.js" component-export="default" renderer-url="/_astro/client.BlZe1zq3.js" props="{}" ssr client="only" opts="{&quot;name&quot;:&quot;PollApp&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island> </body></html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "slm-polls-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview"
10
+ },
11
+ "dependencies": {
12
+ "@astrojs/react": "^4.3.0",
13
+ "@gradio/client": "^1.13.0",
14
+ "astro": "^5.13.0",
15
+ "react": "^19.1.0",
16
+ "react-dom": "^19.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.1.0",
20
+ "@types/react-dom": "^19.1.0",
21
+ "typescript": "^5.9.0"
22
+ }
23
+ }
frontend/src/components/PollApp.tsx ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Client } from '@gradio/client';
3
+
4
+ interface Me {
5
+ username: string | null;
6
+ member: boolean;
7
+ }
8
+
9
+ interface Poll {
10
+ id: string;
11
+ question: string;
12
+ options: string[];
13
+ counts: number[];
14
+ total: number;
15
+ created_by: string;
16
+ created_at: string;
17
+ my_vote: number | null;
18
+ voters: Record<string, number>;
19
+ }
20
+
21
+ type Notice = { kind: 'ok' | 'err'; text: string } | null;
22
+ type Tab = 'polls' | 'create';
23
+
24
+ async function callApi<T>(client: unknown, name: string, args: unknown[]): Promise<T> {
25
+ const c = client as { predict: (n: string, a: unknown[]) => Promise<{ data: unknown[] }> };
26
+ const res = await c.predict(name, args);
27
+ return res.data[0] as T;
28
+ }
29
+
30
+ const PollApp: React.FC = () => {
31
+ const clientRef = useRef<unknown>(null);
32
+ const [ready, setReady] = useState(false);
33
+ const [backendError, setBackendError] = useState<string | null>(null);
34
+ const [me, setMe] = useState<Me>({ username: null, member: false });
35
+ const [polls, setPolls] = useState<Poll[]>([]);
36
+ const [busy, setBusy] = useState(false);
37
+ const [tab, setTab] = useState<Tab>('polls');
38
+ const [question, setQuestion] = useState('');
39
+ const [options, setOptions] = useState<string[]>(['', '']);
40
+ const [notice, setNotice] = useState<Notice>(null);
41
+
42
+ const MAX_OPTIONS = 20;
43
+
44
+ const setOption = (idx: number, value: string) => {
45
+ setOptions((prev) => prev.map((o, i) => (i === idx ? value : o)));
46
+ };
47
+ const addOption = () => {
48
+ setOptions((prev) => (prev.length >= MAX_OPTIONS ? prev : [...prev, '']));
49
+ };
50
+ const removeOption = (idx: number) => {
51
+ setOptions((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== idx)));
52
+ };
53
+
54
+ const refresh = useCallback(async () => {
55
+ const client = clientRef.current;
56
+ if (!client) return;
57
+ try {
58
+ const data = await callApi<{ me: Me; polls: Poll[] }>(client, '/polls', []);
59
+ setMe(data.me);
60
+ setPolls(data.polls);
61
+ setBackendError(null);
62
+ } catch {
63
+ setBackendError('Cannot reach the voting backend.');
64
+ }
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ let cancelled = false;
69
+ (async () => {
70
+ try {
71
+ clientRef.current = await Client.connect(window.location.origin + '/gradio');
72
+ if (!cancelled) {
73
+ setReady(true);
74
+ await refresh();
75
+ }
76
+ } catch {
77
+ if (!cancelled) setBackendError('Cannot reach the voting backend.');
78
+ }
79
+ })();
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, [refresh]);
84
+
85
+ const doVote = async (pollId: string, idx: number) => {
86
+ if (busy || !clientRef.current) return;
87
+ setBusy(true);
88
+ try {
89
+ const data = await callApi<{ ok: boolean; message: string; poll: Poll | null }>(
90
+ clientRef.current,
91
+ '/vote',
92
+ [pollId, idx]
93
+ );
94
+ setNotice({ kind: data.ok ? 'ok' : 'err', text: data.message });
95
+ if (data.ok && data.poll) {
96
+ setPolls((prev) => prev.map((p) => (p.id === data.poll!.id ? data.poll! : p)));
97
+ } else {
98
+ await refresh();
99
+ }
100
+ } catch {
101
+ setNotice({ kind: 'err', text: 'Vote failed — please retry.' });
102
+ } finally {
103
+ setBusy(false);
104
+ }
105
+ };
106
+
107
+ const doCreate = async () => {
108
+ if (busy || !clientRef.current) return;
109
+ setBusy(true);
110
+ try {
111
+ const data = await callApi<{ ok: boolean; message: string; poll: Poll | null }>(
112
+ clientRef.current,
113
+ '/create_poll',
114
+ [question, options.filter((o) => o.trim()).join('\n')]
115
+ );
116
+ setNotice({ kind: data.ok ? 'ok' : 'err', text: data.message });
117
+ if (data.ok) {
118
+ setQuestion('');
119
+ setOptions(['', '']);
120
+ await refresh();
121
+ setTab('polls');
122
+ }
123
+ } catch {
124
+ setNotice({ kind: 'err', text: 'Could not create poll — please retry.' });
125
+ } finally {
126
+ setBusy(false);
127
+ }
128
+ };
129
+
130
+ return (
131
+ <div className="page">
132
+ <div className="bg" aria-hidden="true">
133
+ <div className="bg-orb bg-orb-a" />
134
+ <div className="bg-orb bg-orb-b" />
135
+ <div className="bg-orb bg-orb-c" />
136
+ <div className="bg-grid" />
137
+ </div>
138
+
139
+ <div className="shell">
140
+ <header className="top">
141
+ <h1>
142
+ <span className="logo" aria-hidden="true">🗳️</span> SLM Consortium Polls
143
+ </h1>
144
+ <div className="auth">
145
+ {me.username ? (
146
+ <span className={`chip ${me.member ? 'chip-ok' : 'chip-warn'}`}>
147
+ @{me.username}
148
+ {me.member ? '' : ' · not a member'}
149
+ </span>
150
+ ) : (
151
+ <a
152
+ className="btn"
153
+ href="/gradio/login/huggingface?_target_url=/"
154
+ >
155
+ Sign in with Hugging Face
156
+ </a>
157
+ )}
158
+ <button className="btn quiet" onClick={() => void refresh()}>
159
+ Refresh
160
+ </button>
161
+ </div>
162
+ </header>
163
+
164
+ <p className="lede">
165
+ Hugging Face sign-in required. Only <code>slmconsortium</code> members can vote or create
166
+ polls. Click to vote, and click another option to change your vote.
167
+ </p>
168
+
169
+ <nav className="tabs" role="tablist" aria-label="Sections">
170
+ <button
171
+ role="tab"
172
+ aria-selected={tab === 'polls'}
173
+ className={tab === 'polls' ? 'active' : ''}
174
+ onClick={() => setTab('polls')}
175
+ >
176
+ 🗳️ Polls
177
+ </button>
178
+ <button
179
+ role="tab"
180
+ aria-selected={tab === 'create'}
181
+ className={tab === 'create' ? 'active' : ''}
182
+ onClick={() => setTab('create')}
183
+ >
184
+ ➕ Create poll
185
+ </button>
186
+ </nav>
187
+
188
+ {backendError && <p className="notice err">{backendError}</p>}
189
+ {notice && <p className={`notice ${notice.kind}`}>{notice.text}</p>}
190
+ {!ready && !backendError && <p className="muted">Loading…</p>}
191
+
192
+ {tab === 'polls' && (
193
+ <div className="tabpanel" role="tabpanel">
194
+ {ready && polls.length === 0 && <p className="muted">No polls yet.</p>}
195
+ {polls.map((poll) => (
196
+ <section key={poll.id} className="card">
197
+ <h2>{poll.question}</h2>
198
+ <p className="meta">
199
+ {poll.total} vote{poll.total === 1 ? '' : 's'} · by @{poll.created_by}
200
+ </p>
201
+ <ul className="opts">
202
+ {poll.options.map((opt, idx) => {
203
+ const pct = poll.total ? (poll.counts[idx] / poll.total) * 100 : 0;
204
+ const mine = poll.my_vote === idx;
205
+ const voters = Object.entries(poll.voters ?? {})
206
+ .filter(([, v]) => v === idx)
207
+ .map(([u]) => u)
208
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
209
+ return (
210
+ <li key={idx}>
211
+ <button
212
+ className={mine ? 'mine' : ''}
213
+ disabled={!me.member || busy}
214
+ title={
215
+ !me.username
216
+ ? 'Sign in to vote'
217
+ : !me.member
218
+ ? 'Only slmconsortium members can vote'
219
+ : `Vote for ${opt}`
220
+ }
221
+ onClick={() => void doVote(poll.id, idx)}
222
+ >
223
+ <span className="opt-top">
224
+ <span>
225
+ {opt}
226
+ {mine ? ' ✓' : ''}
227
+ </span>
228
+ <span className="num">
229
+ {poll.counts[idx]} · {pct.toFixed(0)}%
230
+ </span>
231
+ </span>
232
+ <span className="bar">
233
+ <i style={{ width: `${pct}%` }} />
234
+ </span>
235
+ {voters.length > 0 && (
236
+ <span className="voters">
237
+ {voters.map((u) => (
238
+ <span key={u} className={`voter ${u === me.username ? 'me' : ''}`}>
239
+ @{u}
240
+ </span>
241
+ ))}
242
+ </span>
243
+ )}
244
+ </button>
245
+ </li>
246
+ );
247
+ })}
248
+ </ul>
249
+ </section>
250
+ ))}
251
+ </div>
252
+ )}
253
+
254
+ {tab === 'create' && (
255
+ <div className="tabpanel" role="tabpanel">
256
+ <section className="card">
257
+ <h2>New poll</h2>
258
+ {!me.member && (
259
+ <p className="muted">
260
+ {!me.username ? 'Sign in first.' : 'Only slmconsortium members can create polls.'}
261
+ </p>
262
+ )}
263
+ <label>
264
+ Question
265
+ <input
266
+ value={question}
267
+ onChange={(e) => setQuestion(e.target.value)}
268
+ placeholder="Which meeting time works best?"
269
+ disabled={!me.member}
270
+ />
271
+ </label>
272
+ <div className="field">
273
+ <span className="field-label">
274
+ Options
275
+ <span className="field-hint">
276
+ {options.length}/{MAX_OPTIONS}
277
+ </span>
278
+ </span>
279
+ <ul className="opt-editor">
280
+ {options.map((opt, idx) => (
281
+ <li key={idx}>
282
+ <input
283
+ value={opt}
284
+ onChange={(e) => setOption(idx, e.target.value)}
285
+ placeholder={`Option ${idx + 1}`}
286
+ disabled={!me.member}
287
+ aria-label={`Option ${idx + 1}`}
288
+ />
289
+ <button
290
+ type="button"
291
+ className="icon-btn"
292
+ onClick={() => removeOption(idx)}
293
+ disabled={!me.member || options.length <= 1}
294
+ aria-label={`Remove option ${idx + 1}`}
295
+ title={options.length <= 1 ? 'Need at least one option row' : 'Remove option'}
296
+ >
297
+ ✕
298
+ </button>
299
+ </li>
300
+ ))}
301
+ </ul>
302
+ <button
303
+ type="button"
304
+ className="btn quiet add-opt"
305
+ onClick={addOption}
306
+ disabled={!me.member || options.length >= MAX_OPTIONS}
307
+ >
308
+ + Add option
309
+ </button>
310
+ </div>
311
+ <button
312
+ className="btn"
313
+ disabled={
314
+ !me.member ||
315
+ busy ||
316
+ question.trim().length < 3 ||
317
+ options.filter((o) => o.trim()).length < 2
318
+ }
319
+ onClick={() => void doCreate()}
320
+ >
321
+ {busy ? 'Working…' : 'Create poll'}
322
+ </button>
323
+ </section>
324
+ </div>
325
+ )}
326
+
327
+ <footer className="muted">
328
+ <a href="/gradio/">Classic UI</a>
329
+ </footer>
330
+ </div>
331
+ </div>
332
+ );
333
+ };
334
+
335
+ export default PollApp;
frontend/src/pages/index.astro ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ import PollApp from '../components/PollApp';
3
+ import '../styles/global.css';
4
+ ---
5
+
6
+ <!doctype html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
+ <title>SLM Consortium Polls</title>
12
+ <meta
13
+ name="description"
14
+ content="Create polls and vote."
15
+ />
16
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🗳️</text></svg>" />
17
+ </head>
18
+ <body>
19
+ <PollApp client:only="react" />
20
+ </body>
21
+ </html>
frontend/src/styles/global.css ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #06040f;
3
+ --panel: rgba(12, 9, 26, 0.72);
4
+ --panel-solid: #0c091a;
5
+ --border: rgba(140, 120, 255, 0.22);
6
+ --border-strong: rgba(160, 130, 255, 0.45);
7
+ --text: #eceaf6;
8
+ --muted: #a49fc2;
9
+ --accent: #7b5cff;
10
+ --accent-soft: rgba(123, 92, 255, 0.18);
11
+ --ok: #5ee6a8;
12
+ --err: #ff7a90;
13
+ }
14
+
15
+ * { box-sizing: border-box; }
16
+ html, body { margin: 0; padding: 0; }
17
+ body {
18
+ background: var(--bg);
19
+ color: var(--text);
20
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
21
+ line-height: 1.5;
22
+ min-height: 100vh;
23
+ }
24
+
25
+ /* ---- background: slow drifting glow orbs + faint grid ---- */
26
+ .bg {
27
+ position: fixed;
28
+ inset: 0;
29
+ z-index: 0;
30
+ overflow: hidden;
31
+ background:
32
+ radial-gradient(ellipse 120% 80% at 50% -20%, #161033 0%, transparent 60%),
33
+ var(--bg);
34
+ }
35
+
36
+ .bg-orb {
37
+ position: absolute;
38
+ border-radius: 50%;
39
+ filter: blur(90px);
40
+ opacity: 0.55;
41
+ will-change: transform;
42
+ }
43
+ .bg-orb-a {
44
+ width: 55vmax; height: 55vmax;
45
+ left: -15vmax; top: -18vmax;
46
+ background: radial-gradient(circle, rgba(82, 38, 255, 0.5) 0%, transparent 70%);
47
+ animation: drift-a 26s ease-in-out infinite alternate;
48
+ }
49
+ .bg-orb-b {
50
+ width: 45vmax; height: 45vmax;
51
+ right: -18vmax; top: 10vmax;
52
+ background: radial-gradient(circle, rgba(140, 60, 255, 0.35) 0%, transparent 70%);
53
+ animation: drift-b 32s ease-in-out infinite alternate;
54
+ }
55
+ .bg-orb-c {
56
+ width: 50vmax; height: 50vmax;
57
+ left: 15vmax; bottom: -25vmax;
58
+ background: radial-gradient(circle, rgba(40, 90, 255, 0.3) 0%, transparent 70%);
59
+ animation: drift-c 38s ease-in-out infinite alternate;
60
+ }
61
+ @keyframes drift-a {
62
+ from { transform: translate3d(0, 0, 0) scale(1); }
63
+ to { transform: translate3d(8vmax, 6vmax, 0) scale(1.15); }
64
+ }
65
+ @keyframes drift-b {
66
+ from { transform: translate3d(0, 0, 0) scale(1.1); }
67
+ to { transform: translate3d(-7vmax, 9vmax, 0) scale(0.95); }
68
+ }
69
+ @keyframes drift-c {
70
+ from { transform: translate3d(0, 0, 0) scale(1); }
71
+ to { transform: translate3d(-6vmax, -8vmax, 0) scale(1.2); }
72
+ }
73
+
74
+ .bg-grid {
75
+ position: absolute;
76
+ inset: 0;
77
+ background-image:
78
+ linear-gradient(rgba(150, 130, 255, 0.05) 1px, transparent 1px),
79
+ linear-gradient(90deg, rgba(150, 130, 255, 0.05) 1px, transparent 1px);
80
+ background-size: 44px 44px;
81
+ mask-image: radial-gradient(ellipse 90% 70% at 50% 35%, black 0%, transparent 75%);
82
+ -webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 35%, black 0%, transparent 75%);
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ .bg-orb { animation: none; }
87
+ }
88
+
89
+ .shell {
90
+ position: relative;
91
+ z-index: 1;
92
+ max-width: 660px;
93
+ margin: 0 auto;
94
+ padding: 40px 20px 80px;
95
+ }
96
+
97
+ /* ---- header ---- */
98
+ .top {
99
+ display: flex; align-items: center; justify-content: space-between;
100
+ gap: 12px; flex-wrap: wrap;
101
+ border-bottom: 1px solid var(--border);
102
+ padding-bottom: 16px; margin-bottom: 12px;
103
+ }
104
+ .top h1 { font-size: 20px; margin: 0; font-weight: 650; letter-spacing: -0.01em; }
105
+ .logo { margin-right: 4px; }
106
+ .auth { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
107
+
108
+ .chip {
109
+ display: inline-block;
110
+ font-size: 13px;
111
+ padding: 5px 12px;
112
+ border-radius: 999px;
113
+ border: 1px solid var(--border-strong);
114
+ background: var(--accent-soft);
115
+ }
116
+ .chip-ok { color: var(--ok); border-color: rgba(94, 230, 168, 0.4); background: rgba(94, 230, 168, 0.1); }
117
+ .chip-warn { color: #ffcf7a; border-color: rgba(255, 207, 122, 0.4); background: rgba(255, 207, 122, 0.08); }
118
+
119
+ .lede { color: var(--muted); font-size: 14px; }
120
+ .lede code, .meta code {
121
+ font-size: 13px;
122
+ background: var(--accent-soft);
123
+ border: 1px solid var(--border);
124
+ border-radius: 4px;
125
+ padding: 1px 5px;
126
+ }
127
+ .muted { color: var(--muted); font-size: 14px; }
128
+ a { color: var(--text); }
129
+
130
+ /* ---- buttons ---- */
131
+ .btn {
132
+ appearance: none;
133
+ background: var(--accent);
134
+ color: #fff;
135
+ border: 1px solid transparent;
136
+ border-radius: 8px;
137
+ padding: 8px 16px;
138
+ font-size: 14px;
139
+ font-weight: 500;
140
+ cursor: pointer;
141
+ text-decoration: none;
142
+ display: inline-block;
143
+ font-family: inherit;
144
+ box-shadow: 0 4px 20px rgba(123, 92, 255, 0.35);
145
+ transition: background 0.15s ease, transform 0.1s ease;
146
+ }
147
+ .btn:hover:not(:disabled) { background: #8e73ff; transform: translateY(-1px); }
148
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
149
+ .btn.quiet {
150
+ background: rgba(255, 255, 255, 0.06);
151
+ color: var(--text);
152
+ border-color: var(--border);
153
+ box-shadow: none;
154
+ }
155
+ .btn.quiet:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); transform: none; }
156
+
157
+ /* ---- tabs ---- */
158
+ .tabs {
159
+ display: flex;
160
+ gap: 4px;
161
+ margin: 20px 0 16px;
162
+ padding: 4px;
163
+ border: 1px solid var(--border);
164
+ border-radius: 12px;
165
+ background: var(--panel);
166
+ backdrop-filter: blur(14px);
167
+ -webkit-backdrop-filter: blur(14px);
168
+ }
169
+ .tabs button {
170
+ flex: 1;
171
+ appearance: none;
172
+ border: 1px solid transparent;
173
+ border-radius: 9px;
174
+ background: transparent;
175
+ color: var(--muted);
176
+ font-size: 14px;
177
+ font-weight: 500;
178
+ font-family: inherit;
179
+ padding: 9px 12px;
180
+ cursor: pointer;
181
+ transition: color 0.15s ease, background 0.15s ease;
182
+ }
183
+ .tabs button:hover { color: var(--text); }
184
+ .tabs button.active {
185
+ color: #fff;
186
+ background: var(--accent-soft);
187
+ border-color: var(--border-strong);
188
+ }
189
+
190
+ .notice { font-size: 14px; padding: 10px 14px; margin: 0 0 14px; border-radius: 8px; border: 1px solid; }
191
+ .notice.err { color: var(--err); border-color: rgba(255, 122, 144, 0.35); background: rgba(255, 122, 144, 0.08); }
192
+ .notice.ok { color: var(--ok); border-color: rgba(94, 230, 168, 0.35); background: rgba(94, 230, 168, 0.08); }
193
+
194
+ /* ---- poll cards ---- */
195
+ .card {
196
+ border: 1px solid var(--border);
197
+ border-radius: 14px;
198
+ padding: 20px;
199
+ margin-bottom: 16px;
200
+ background: var(--panel);
201
+ backdrop-filter: blur(14px);
202
+ -webkit-backdrop-filter: blur(14px);
203
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
204
+ }
205
+ .card h2 { font-size: 17px; margin: 0 0 2px; font-weight: 600; }
206
+ .meta { color: var(--muted); font-size: 13px; margin: 0 0 12px; }
207
+
208
+ .opts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
209
+ .opts button {
210
+ width: 100%; text-align: left;
211
+ background: rgba(255, 255, 255, 0.04);
212
+ color: var(--text);
213
+ border: 1px solid var(--border);
214
+ border-radius: 10px;
215
+ padding: 11px 14px;
216
+ font-size: 14px;
217
+ cursor: pointer;
218
+ font-family: inherit;
219
+ transition: border-color 0.15s ease, background 0.15s ease;
220
+ }
221
+ .opts button:hover:not(:disabled) { border-color: var(--border-strong); background: rgba(255, 255, 255, 0.07); }
222
+ .opts button:disabled { cursor: default; }
223
+ .opts button.mine {
224
+ border-color: var(--accent);
225
+ background: var(--accent-soft);
226
+ box-shadow: 0 0 0 1px var(--accent) inset;
227
+ }
228
+ .opt-top { display: flex; justify-content: space-between; gap: 12px; }
229
+ .num { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
230
+ .bar {
231
+ display: block; height: 4px;
232
+ background: rgba(255, 255, 255, 0.08);
233
+ border-radius: 99px;
234
+ margin-top: 9px;
235
+ overflow: hidden;
236
+ }
237
+ .bar i {
238
+ display: block; height: 100%;
239
+ background: linear-gradient(90deg, #5226ff, #9a7bff);
240
+ border-radius: 99px;
241
+ transition: width 0.4s ease;
242
+ }
243
+
244
+ /* voter chips inside each option row */
245
+ .voters {
246
+ display: flex;
247
+ flex-wrap: wrap;
248
+ gap: 4px;
249
+ margin-top: 9px;
250
+ }
251
+ .voter {
252
+ font-size: 11.5px;
253
+ line-height: 1;
254
+ padding: 4px 8px;
255
+ border-radius: 999px;
256
+ border: 1px solid var(--border);
257
+ background: rgba(255, 255, 255, 0.06);
258
+ color: var(--muted);
259
+ font-weight: 500;
260
+ }
261
+ .voter.me {
262
+ color: #fff;
263
+ border-color: rgba(255, 255, 255, 0.55);
264
+ background: rgba(123, 92, 255, 0.45);
265
+ }
266
+
267
+ /* ---- form ---- */
268
+ label { display: block; font-size: 13px; color: var(--muted); margin: 14px 0; }
269
+ input, textarea {
270
+ display: block; width: 100%; margin-top: 5px;
271
+ border: 1px solid var(--border);
272
+ border-radius: 10px;
273
+ padding: 10px 13px;
274
+ font-size: 14px;
275
+ font-family: inherit;
276
+ color: var(--text);
277
+ background: rgba(255, 255, 255, 0.05);
278
+ transition: border-color 0.15s ease;
279
+ }
280
+ input::placeholder, textarea::placeholder { color: rgba(164, 159, 194, 0.55); }
281
+ input:focus, textarea:focus {
282
+ outline: none;
283
+ border-color: var(--accent);
284
+ box-shadow: 0 0 0 3px rgba(123, 92, 255, 0.2);
285
+ }
286
+ input:disabled, textarea:disabled { background: rgba(255, 255, 255, 0.03); color: var(--muted); cursor: not-allowed; }
287
+ textarea { resize: vertical; }
288
+
289
+ /* ---- option editor ---- */
290
+ .field { display: block; font-size: 13px; color: var(--muted); margin: 14px 0; }
291
+ .field-label { display: flex; justify-content: space-between; align-items: baseline; }
292
+ .field-hint { font-size: 12px; color: var(--muted); opacity: 0.7; font-variant-numeric: tabular-nums; }
293
+ .opt-editor { list-style: none; margin: 5px 0 10px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
294
+ .opt-editor li { display: flex; gap: 8px; align-items: center; }
295
+ .opt-editor input { flex: 1; margin-top: 0; }
296
+ .icon-btn {
297
+ appearance: none;
298
+ flex: 0 0 auto;
299
+ width: 38px; height: 38px;
300
+ border-radius: 10px;
301
+ border: 1px solid var(--border);
302
+ background: rgba(255, 255, 255, 0.04);
303
+ color: var(--muted);
304
+ font-size: 15px;
305
+ line-height: 1;
306
+ cursor: pointer;
307
+ font-family: inherit;
308
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
309
+ }
310
+ .icon-btn:hover:not(:disabled) {
311
+ border-color: rgba(255, 122, 144, 0.5);
312
+ color: var(--err);
313
+ background: rgba(255, 122, 144, 0.08);
314
+ }
315
+ .icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
316
+ .add-opt { margin-top: 2px; }
317
+
318
+ footer {
319
+ border-top: 1px solid var(--border);
320
+ margin-top: 16px;
321
+ padding-top: 16px;
322
+ font-size: 13px;
323
+ }
324
+ footer a { color: var(--muted); }
325
+ footer a:hover { color: var(--text); }
frontend/tsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "jsxImportSource": "react",
6
+ "allowImportingTsExtensions": true
7
+ }
8
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio[oauth]>=4.44.0
2
+ huggingface_hub>=0.24.0
3
+ itsdangerous>=2.0
4
+ aiofiles>=23.0