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

Ingestion happens in the separate feedback-api Space (/submit), which writes
submissions into requests.jsonl or feedback.jsonl (see submissions.py) in the
hugging-science/feedback dataset. This Space reads both files, lets the
reviewer filter the queue by type, and approve (-> opens a GitHub PR),
acknowledge, or reject each pending item.
"""

import gradio as gr

import formatters
import github_pr
import submissions
from about import ADMIN_USERS

# Types that map onto a src/data/*.js file and can go through the PR flow.
PR_TYPES = list(formatters.TARGETS.keys())  # organization, model, dataset, blog
# Everything else (e.g. "challenge") is acknowledge/reject only.

# Every type the feedback-api Space can write, for the type filter dropdowns.
FILTER_TYPES = ["All", "feedback", "collaboration", *PR_TYPES, "challenge"]

FIELD_VISIBILITY = {
    "organization": {"id", "name", "link", "description", "tags"},
    "model": {"id", "slug", "name", "orgId", "entry_type", "description", "tags"},
    "dataset": {"id", "slug", "orgId", "entry_type", "description", "tags"},
    "blog": {"id", "title", "slug", "orgId", "date", "excerpt", "link", "tags", "featured", "upvotes"},
}

ALL_FIELD_KEYS = [
    "id", "name", "title", "slug", "orgId", "entry_type",
    "description", "excerpt", "link", "date", "featured", "upvotes", "tags",
]


def is_admin(profile: gr.OAuthProfile | None) -> bool:
    return profile is not None and profile.username in ADMIN_USERS


def _guess_slug(title: str | None) -> str:
    if title and "/" in title:
        return title.strip()
    return ""


def _type_filter(type_filter: str | None) -> str | None:
    return None if not type_filter or type_filter == "All" else type_filter


def pending_choices(type_filter: str | None = None):
    rows = submissions.list_submissions(status="pending", type_=_type_filter(type_filter))
    return [(f"[{r['type']}] {r['title'] or r['description'][:40]}", r["id"]) for r in rows]


def history_rows(type_filter: str | None = None):
    rows = [
        r for r in submissions.list_submissions(type_=_type_filter(type_filter))
        if r["status"] != "pending"
    ]
    return [
        [r["type"], r["title"], r["description"], r["status"], r.get("reviewed_at"), r.get("pr_url") or r.get("reject_reason")]
        for r in rows
    ]


def load_submission(submission_id: str):
    """Populate the detail panel + editable fields for a selected submission."""
    if not submission_id:
        return (
            gr.update(value=""),
            *[gr.update(visible=False) for _ in ALL_FIELD_KEYS],
            gr.update(visible=False),
            gr.update(visible=False),
        )

    row = submissions.get_submission(submission_id)
    if row is None:
        return (
            gr.update(value="Not found."),
            *[gr.update(visible=False) for _ in ALL_FIELD_KEYS],
            gr.update(visible=False),
            gr.update(visible=False),
        )

    detail_md = (
        f"**Type:** {row['type']}  \n"
        f"**Title/link:** {row['title'] or '_none given_'}  \n"
        f"**Submitted:** {row['submitted_at']} via {row['source']}  \n"
    )
    if row["type"] == "collaboration":
        detail_md += (
            f"**Email:** {row.get('email') or '_none given_'}  \n"
            f"**Institution:** {row.get('institution') or '_none given_'}  \n"
        )
    if row["type"] in PR_TYPES:
        detail_md += (
            "**Ready for PR** — fields below come straight from the "
            "submission; edit anything that looks off, then approve.  \n"
        )
    detail_md += f"\n{row['description']}"

    # Submissions from the current website form carry these structured
    # fields directly (see the request-api Space); older/legacy pending
    # rows won't have them, so fall back to guessing from title/link.
    visible = FIELD_VISIBILITY.get(row["type"], set())
    guessed_slug = _guess_slug(row["title"])
    defaults = {
        "id": row.get("entry_id") or guessed_slug.replace("/", "-").lower(),
        "name": row.get("name") or row["title"] or "",
        "title": row["title"] or "",
        "slug": row.get("slug") or guessed_slug,
        "orgId": row.get("org_id") or "",
        "entry_type": row.get("entry_type") or "",
        "description": row["description"],
        "excerpt": row["description"],
        "link": row.get("link")
        or (row["title"] if row["title"] and row["title"].startswith("http") else ""),
        "date": row.get("date") or "",
        "tags": row.get("tags") or [],
    }
    field_updates = []
    for key in ALL_FIELD_KEYS:
        if key in visible:
            field_updates.append(gr.update(visible=True, value=defaults.get(key, "" if key != "tags" else [])))
        else:
            field_updates.append(gr.update(visible=False))

    show_pr_button = row["type"] in PR_TYPES
    show_ack_button = row["type"] not in PR_TYPES
    return (
        gr.update(value=detail_md),
        *field_updates,
        gr.update(visible=show_pr_button),
        gr.update(visible=show_ack_button),
    )


with gr.Blocks(title="Hugging Science — requests review") as demo:
    gr.Markdown("## Hugging Science — requests review")
    login_btn = gr.LoginButton()

    with gr.Column(visible=False) as admin_panel:
        with gr.Tab("Pending"):
            with gr.Row():
                type_dd = gr.Dropdown(label="Filter by type", choices=FILTER_TYPES, value="All", scale=1)
                refresh_btn = gr.Button("Refresh", scale=0)
            pending_dd = gr.Dropdown(label="Pending requests", choices=[], interactive=True)
            detail = gr.Markdown()

            with gr.Group():
                f_id = gr.Textbox(label="id", visible=False)
                f_name = gr.Textbox(label="name", visible=False)
                f_title = gr.Textbox(label="title", visible=False)
                f_slug = gr.Textbox(label="slug (org/repo)", visible=False)
                f_orgId = gr.Textbox(label="orgId", visible=False)
                f_entry_type = gr.Textbox(label="type (e.g. Genomics, Foundation Model)", visible=False)
                f_description = gr.Textbox(label="description", lines=3, visible=False)
                f_excerpt = gr.Textbox(label="excerpt", lines=3, visible=False)
                f_link = gr.Textbox(label="link", visible=False)
                f_date = gr.Textbox(label="date (YYYY-MM-DD)", visible=False)
                f_featured = gr.Checkbox(label="featured", visible=False)
                f_upvotes = gr.Number(label="upvotes", visible=False)
                f_tags = gr.CheckboxGroup(label="tags", choices=formatters.VALID_TAGS, visible=False)

            all_fields = [f_id, f_name, f_title, f_slug, f_orgId, f_entry_type,
                          f_description, f_excerpt, f_link, f_date, f_featured, f_upvotes, f_tags]

            with gr.Row():
                approve_btn = gr.Button("Approve → open PR", variant="primary", visible=False)
                ack_btn = gr.Button("Mark as reviewed", variant="primary", visible=False)
                reject_reason = gr.Textbox(label="Reject reason (optional)", scale=2)
                reject_btn = gr.Button("Reject", variant="stop")

            result_md = gr.Markdown()

            def do_refresh(type_filter):
                return gr.update(choices=pending_choices(type_filter), value=None)

            refresh_btn.click(do_refresh, inputs=[type_dd], outputs=pending_dd)
            type_dd.change(do_refresh, inputs=[type_dd], outputs=pending_dd)
            demo.load(do_refresh, inputs=[type_dd], outputs=pending_dd)

            pending_dd.change(load_submission, inputs=pending_dd, outputs=[detail, *all_fields, approve_btn, ack_btn])

            def do_approve(submission_id, type_filter, id_, name, title, slug, org_id, entry_type,
                            description, excerpt, link, date, featured, upvotes, tags):
                row = submissions.get_submission(submission_id)
                if row is None:
                    return "Submission not found.", gr.update(choices=pending_choices(type_filter))
                fields = {
                    "id": id_, "name": name, "title": title, "slug": slug, "orgId": org_id or None,
                    "type": entry_type, "description": description, "excerpt": excerpt,
                    "link": link, "date": date, "featured": featured, "upvotes": upvotes,
                    "tags": tags or [],
                }
                missing = formatters.missing_fields(row["type"], fields)
                if missing:
                    return f"Missing required fields: {', '.join(missing)}", gr.update()
                try:
                    pr_url = github_pr.open_pr_for_submission(row, fields)
                except Exception as exc:  # surfaced to the reviewer, not swallowed
                    return f"Failed to open PR: {exc}", gr.update()
                submissions.mark_approved(submission_id, pr_url)
                return f"Approved — [PR opened]({pr_url})", gr.update(choices=pending_choices(type_filter), value=None)

            approve_btn.click(
                do_approve,
                inputs=[pending_dd, type_dd, *all_fields],
                outputs=[result_md, pending_dd],
            )

            def do_ack(submission_id, type_filter):
                if not submission_id:
                    return "Nothing selected.", gr.update()
                submissions.mark_acknowledged(submission_id)
                return "Marked as reviewed.", gr.update(choices=pending_choices(type_filter), value=None)

            ack_btn.click(do_ack, inputs=[pending_dd, type_dd], outputs=[result_md, pending_dd])

            def do_reject(submission_id, reason, type_filter):
                if not submission_id:
                    return "Nothing selected.", gr.update()
                submissions.mark_rejected(submission_id, reason or None)
                return "Rejected.", gr.update(choices=pending_choices(type_filter), value=None)

            reject_btn.click(
                do_reject,
                inputs=[pending_dd, reject_reason, type_dd],
                outputs=[result_md, pending_dd],
            )

        with gr.Tab("History"):
            with gr.Row():
                history_type_dd = gr.Dropdown(label="Filter by type", choices=FILTER_TYPES, value="All", scale=1)
                history_refresh = gr.Button("Refresh", scale=0)
            history_df = gr.Dataframe(
                headers=["type", "title", "description", "status", "reviewed_at", "pr_url / reason"],
                interactive=False,
            )
            history_refresh.click(history_rows, inputs=[history_type_dd], outputs=history_df)
            history_type_dd.change(history_rows, inputs=[history_type_dd], outputs=history_df)
            demo.load(history_rows, inputs=[history_type_dd], outputs=history_df)

    unauthorized = gr.Markdown("Log in with an allow-listed Hugging Face account to review requests.")

    def toggle_panel(profile: gr.OAuthProfile | None):
        admin = is_admin(profile)
        return gr.update(visible=admin), gr.update(visible=not admin)

    demo.load(toggle_panel, outputs=[admin_panel, unauthorized])


if __name__ == "__main__":
    demo.launch()