Deploy SLM Consortium Polls app
Browse files- README.md +13 -6
- app.py +564 -0
- frontend/astro.config.mjs +13 -0
- frontend/dist/_astro/PollApp.BI8OLAG4.js +12 -0
- frontend/dist/_astro/__vite-browser-external-DYxpcVy9.BIHI7g3E.js +1 -0
- frontend/dist/_astro/client.BlZe1zq3.js +0 -0
- frontend/dist/_astro/index.D5zPhbGI.css +1 -0
- frontend/dist/_astro/index.qNTDzdXh.js +9 -0
- frontend/dist/index.html +1 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +23 -0
- frontend/src/components/PollApp.tsx +335 -0
- frontend/src/pages/index.astro +21 -0
- frontend/src/styles/global.css +325 -0
- frontend/tsconfig.json +8 -0
- requirements.txt +4 -0
README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 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="{"name":"PollApp","value":"react"}"></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
|