Real setup steps instead of a download that never existed; mobile notice, veil and control fixes
Browse files- APPLY2.bat +14 -0
- distinct_server/presentation.py +61 -4
- distinct_server/render.py +61 -17
- distinct_server/ui.py +50 -69
- pyproject.toml +5 -0
- scripts/demo_server.py +1 -1
- scripts/live_e2e.py +11 -3
APPLY2.bat
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cd /d "%~dp0"
|
| 2 |
+
@echo off
|
| 3 |
+
(
|
| 4 |
+
echo ===== APPLY 2 =====
|
| 5 |
+
powershell -NoProfile -Command "Expand-Archive -LiteralPath distinct-update2.zip -DestinationPath . -Force"
|
| 6 |
+
echo EXPAND_EXIT=%ERRORLEVEL%
|
| 7 |
+
git add -A
|
| 8 |
+
git -c user.name=User1342 -c user.email=User1342@users.noreply.huggingface.co commit -m "Real setup steps instead of a download that never existed; mobile notice, veil and control fixes"
|
| 9 |
+
echo COMMIT_EXIT=%ERRORLEVEL%
|
| 10 |
+
git push origin main
|
| 11 |
+
echo PUSH_EXIT=%ERRORLEVEL%
|
| 12 |
+
git log --oneline -1
|
| 13 |
+
del /q distinct-update2.zip
|
| 14 |
+
) >> push-log8.txt 2>&1
|
distinct_server/presentation.py
CHANGED
|
@@ -406,19 +406,29 @@ footer,.gradio-container footer{display:none !important;}
|
|
| 406 |
was the loudest colour on it. */
|
| 407 |
.c-notice{background:var(--c-surface); border:1px solid var(--c-hairline);
|
| 408 |
border-radius:var(--r-md); padding:var(--s3) var(--s4);}
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
flex-wrap:wrap; cursor:pointer; list-style:none;
|
| 411 |
font-size:var(--t-label); color:var(--c-text-2);
|
| 412 |
line-height:1.5;}
|
|
|
|
| 413 |
.c-notice__summary::-webkit-details-marker{display:none;}
|
| 414 |
.c-notice__summary strong{color:var(--c-text); font-weight:600;}
|
| 415 |
.c-notice__mark{flex:none; width:16px; height:16px; border-radius:50%;
|
| 416 |
-
|
| 417 |
border:1.5px solid var(--c-warn-rule); color:var(--c-warn-rule);
|
| 418 |
font-size:10px; font-weight:700; display:grid; place-items:center;}
|
| 419 |
.c-notice__toggle{margin-left:auto; white-space:nowrap; font-size:var(--t-micro);
|
| 420 |
color:var(--c-muted); text-decoration:underline;
|
| 421 |
text-underline-offset:2px; padding-left:var(--s3);}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
.c-notice__body{margin:var(--s3) 0 var(--s1) 28px; font-size:var(--t-micro);
|
| 423 |
line-height:1.6; color:var(--c-text-2); max-width:var(--measure);}
|
| 424 |
.c-notice__summary:focus-visible{outline:3px solid var(--c-sage); outline-offset:3px;
|
|
@@ -1290,6 +1300,29 @@ details[open] .c-libhelp__toggle::after{content:" -";}
|
|
| 1290 |
border-radius:var(--r-md); padding:var(--s3); font-family:var(--mono);
|
| 1291 |
font-size:11.5px; line-height:1.6; overflow-x:auto; white-space:pre;
|
| 1292 |
color:var(--c-text-2); user-select:all;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
.c-join__qr{margin-top:var(--s3);}
|
| 1294 |
.c-join__qr svg{width:160px; height:160px; display:block;
|
| 1295 |
border:1px solid var(--c-hairline); border-radius:var(--r-md);
|
|
@@ -1441,6 +1474,8 @@ details[open] .c-libhelp__toggle::after{content:" -";}
|
|
| 1441 |
rather than force the panel wider than the screen. */
|
| 1442 |
.c-join__cmd{white-space:pre-wrap !important; overflow-wrap:anywhere !important;}
|
| 1443 |
.c-join__code{overflow-wrap:anywhere;}
|
|
|
|
|
|
|
| 1444 |
.c-panel code,.c-panel pre{overflow-wrap:anywhere;}
|
| 1445 |
.c-join__qr svg{width:128px; height:128px;}
|
| 1446 |
}
|
|
@@ -1625,6 +1660,14 @@ details[open] .c-libhelp__toggle::after{content:" -";}
|
|
| 1625 |
.c-veil__panel{pointer-events:auto;}
|
| 1626 |
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-conversation > *:not(.c-veil):not(:first-child){
|
| 1627 |
opacity:.45; filter:saturate(.7);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1628 |
.c-veil__panel{max-width:380px; text-align:center; background:var(--c-surface);
|
| 1629 |
border:1px solid var(--c-hairline); border-radius:var(--r-lg);
|
| 1630 |
box-shadow:var(--e2); padding:var(--s5);}
|
|
@@ -1632,12 +1675,26 @@ details[open] .c-libhelp__toggle::after{content:" -";}
|
|
| 1632 |
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-veil{
|
| 1633 |
padding:var(--s5) var(--s4);}
|
| 1634 |
.c-veil__panel{padding:var(--s4);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1635 |
}
|
| 1636 |
.c-veil__title{margin:0 0 6px; font-weight:650; font-size:var(--t-lg); color:var(--c-text);}
|
| 1637 |
.c-veil__body{margin:0 0 var(--s4); color:var(--c-text-2); line-height:1.55;}
|
| 1638 |
-
.c-veil__action{display:inline-block;
|
| 1639 |
color:#fff; padding:10px 22px; border-radius:var(--r-md); font-weight:600;}
|
| 1640 |
-
.c-veil__action:hover{background:var(--c-accent-hover);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1641 |
|
| 1642 |
/* ---- Professional polish pass ----------------------------------------- */
|
| 1643 |
/* Calmer sections: the tail accordions read as pages, not widgets. */
|
|
|
|
| 406 |
was the loudest colour on it. */
|
| 407 |
.c-notice{background:var(--c-surface); border:1px solid var(--c-hairline);
|
| 408 |
border-radius:var(--r-md); padding:var(--s3) var(--s4);}
|
| 409 |
+
/* The mark belongs to the sentence, so it must never be able to wrap away
|
| 410 |
+
from it. On a phone the row wrapped into three lines and left a bare "!"
|
| 411 |
+
floating above the text like a rendering fault. The mark and the sentence
|
| 412 |
+
are now one non-breaking pair that shrinks together; only the "What is
|
| 413 |
+
kept" toggle is allowed to drop to its own line. */
|
| 414 |
+
.c-notice__summary{display:flex; align-items:flex-start; gap:var(--s3);
|
| 415 |
flex-wrap:wrap; cursor:pointer; list-style:none;
|
| 416 |
font-size:var(--t-label); color:var(--c-text-2);
|
| 417 |
line-height:1.5;}
|
| 418 |
+
.c-notice__summary>span:nth-child(2){flex:1 1 180px; min-width:0;}
|
| 419 |
.c-notice__summary::-webkit-details-marker{display:none;}
|
| 420 |
.c-notice__summary strong{color:var(--c-text); font-weight:600;}
|
| 421 |
.c-notice__mark{flex:none; width:16px; height:16px; border-radius:50%;
|
| 422 |
+
margin-top:2px;
|
| 423 |
border:1.5px solid var(--c-warn-rule); color:var(--c-warn-rule);
|
| 424 |
font-size:10px; font-weight:700; display:grid; place-items:center;}
|
| 425 |
.c-notice__toggle{margin-left:auto; white-space:nowrap; font-size:var(--t-micro);
|
| 426 |
color:var(--c-muted); text-decoration:underline;
|
| 427 |
text-underline-offset:2px; padding-left:var(--s3);}
|
| 428 |
+
@media (max-width:700px){
|
| 429 |
+
/* Its own line, aligned with the sentence it opens rather than the mark. */
|
| 430 |
+
.c-notice__toggle{flex:1 0 100%; margin:var(--s2) 0 0 28px; padding-left:0;}
|
| 431 |
+
}
|
| 432 |
.c-notice__body{margin:var(--s3) 0 var(--s1) 28px; font-size:var(--t-micro);
|
| 433 |
line-height:1.6; color:var(--c-text-2); max-width:var(--measure);}
|
| 434 |
.c-notice__summary:focus-visible{outline:3px solid var(--c-sage); outline-offset:3px;
|
|
|
|
| 1300 |
border-radius:var(--r-md); padding:var(--s3); font-family:var(--mono);
|
| 1301 |
font-size:11.5px; line-height:1.6; overflow-x:auto; white-space:pre;
|
| 1302 |
color:var(--c-text-2); user-select:all;}
|
| 1303 |
+
.c-join__note{margin:0; font-size:var(--t-micro); color:var(--c-muted);
|
| 1304 |
+
line-height:1.55; max-width:56ch;}
|
| 1305 |
+
.c-join__note code{font-family:var(--mono); font-size:11px;
|
| 1306 |
+
background:var(--c-fill); border-radius:4px; padding:1px 5px;}
|
| 1307 |
+
.c-join__note .c-join__code{display:inline-block; padding:2px 8px;
|
| 1308 |
+
font-size:11.5px; margin-top:4px;}
|
| 1309 |
+
|
| 1310 |
+
/* SETTING UP A WORKER, AS NUMBERED STEPS.
|
| 1311 |
+
Three things happen in order and each can fail on its own, so each is
|
| 1312 |
+
drawn as its own step with its own command. Told as one paragraph, a
|
| 1313 |
+
volunteer who got half way could not tell which half they were in. */
|
| 1314 |
+
.c-steps-num{list-style:none; margin:0; padding:0; display:flex;
|
| 1315 |
+
flex-direction:column; gap:var(--s5);}
|
| 1316 |
+
.c-step-num{display:flex; gap:var(--s3); align-items:flex-start;}
|
| 1317 |
+
.c-step-num__n{flex:none; width:24px; height:24px; border-radius:50%;
|
| 1318 |
+
display:grid; place-items:center; margin-top:1px;
|
| 1319 |
+
background:var(--c-mint); color:var(--c-sage-deep);
|
| 1320 |
+
font-size:12px; font-weight:700;}
|
| 1321 |
+
.c-step-num__body{flex:1 1 auto; min-width:0; display:flex;
|
| 1322 |
+
flex-direction:column; gap:var(--s2);}
|
| 1323 |
+
.c-step-num__head{margin:0; font-size:var(--t-body); font-weight:650;
|
| 1324 |
+
color:var(--c-text); letter-spacing:-.01em;}
|
| 1325 |
+
|
| 1326 |
.c-join__qr{margin-top:var(--s3);}
|
| 1327 |
.c-join__qr svg{width:160px; height:160px; display:block;
|
| 1328 |
border:1px solid var(--c-hairline); border-radius:var(--r-md);
|
|
|
|
| 1474 |
rather than force the panel wider than the screen. */
|
| 1475 |
.c-join__cmd{white-space:pre-wrap !important; overflow-wrap:anywhere !important;}
|
| 1476 |
.c-join__code{overflow-wrap:anywhere;}
|
| 1477 |
+
.c-steps-num{gap:var(--s4);}
|
| 1478 |
+
.c-step-num{gap:var(--s2);}
|
| 1479 |
.c-panel code,.c-panel pre{overflow-wrap:anywhere;}
|
| 1480 |
.c-join__qr svg{width:128px; height:128px;}
|
| 1481 |
}
|
|
|
|
| 1660 |
.c-veil__panel{pointer-events:auto;}
|
| 1661 |
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-conversation > *:not(.c-veil):not(:first-child){
|
| 1662 |
opacity:.45; filter:saturate(.7);}
|
| 1663 |
+
/* THE VEIL REPLACES THE EMPTY STATE, IT DOES NOT SIT ON TOP OF IT.
|
| 1664 |
+
"No agents available" and "No runs yet · pick a model and a worker" are the
|
| 1665 |
+
same news told twice, and on a phone the panel landed across the second
|
| 1666 |
+
one and cut a sentence in half. While the veil is up the transcript's own
|
| 1667 |
+
placeholder stands down. It is hidden rather than removed, so the column
|
| 1668 |
+
keeps its height and nothing jumps when a worker appears. */
|
| 1669 |
+
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-chat .c-empty{
|
| 1670 |
+
visibility:hidden;}
|
| 1671 |
.c-veil__panel{max-width:380px; text-align:center; background:var(--c-surface);
|
| 1672 |
border:1px solid var(--c-hairline); border-radius:var(--r-lg);
|
| 1673 |
box-shadow:var(--e2); padding:var(--s5);}
|
|
|
|
| 1675 |
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-veil{
|
| 1676 |
padding:var(--s5) var(--s4);}
|
| 1677 |
.c-veil__panel{padding:var(--s4);}
|
| 1678 |
+
/* With the placeholder stood down there is nothing behind the veil, and a
|
| 1679 |
+
transcript sized for a conversation it cannot yet hold left a phone
|
| 1680 |
+
screen of blank card under the panel. It keeps only the height the panel
|
| 1681 |
+
needs until a worker arrives. `!important` because the minimum is an
|
| 1682 |
+
inline style the component writes for itself. */
|
| 1683 |
+
.c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-chat{
|
| 1684 |
+
min-height:300px !important; max-height:300px !important;}
|
| 1685 |
}
|
| 1686 |
.c-veil__title{margin:0 0 6px; font-weight:650; font-size:var(--t-lg); color:var(--c-text);}
|
| 1687 |
.c-veil__body{margin:0 0 var(--s4); color:var(--c-text-2); line-height:1.55;}
|
| 1688 |
+
.c-veil__action{display:inline-block; background:var(--c-accent);
|
| 1689 |
color:#fff; padding:10px 22px; border-radius:var(--r-md); font-weight:600;}
|
| 1690 |
+
.c-veil__action:hover{background:var(--c-accent-hover); color:#fff;}
|
| 1691 |
+
/* A control is a control: the label inside it is never underlined. Gradio's
|
| 1692 |
+
prose styles underline every anchor, which turned the primary action into
|
| 1693 |
+
underlined text on a dark slab and the nav pills into links wearing a
|
| 1694 |
+
button's clothes. */
|
| 1695 |
+
.c-veil__action,.c-veil__action:visited,
|
| 1696 |
+
.c-quicknav__link,.c-quicknav__link:visited,
|
| 1697 |
+
.c-libcard a,.gradio-container button a{text-decoration:none !important;}
|
| 1698 |
|
| 1699 |
/* ---- Professional polish pass ----------------------------------------- */
|
| 1700 |
/* Calmer sections: the tail accordions read as pages, not widgets. */
|
distinct_server/render.py
CHANGED
|
@@ -177,13 +177,22 @@ def no_tools_note(notice_text: str) -> str:
|
|
| 177 |
)
|
| 178 |
|
| 179 |
|
| 180 |
-
def join_this_server(
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
|
| 188 |
``addresses`` empty is a real state and is reported as one: a server bound
|
| 189 |
to loopback is reachable by this machine and nothing else, and an agent on
|
|
@@ -204,16 +213,41 @@ def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local"
|
|
| 204 |
f'<li class="c-join__alt">{esc(url)}</li>' for url in list(addresses)[1:]
|
| 205 |
)
|
| 206 |
alternates = (
|
| 207 |
-
f'<p class="c-
|
| 208 |
if others
|
| 209 |
else ""
|
| 210 |
)
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
if code
|
| 215 |
-
else '<p class="c-
|
| 216 |
-
"command with it.</p>"
|
| 217 |
)
|
| 218 |
# One line on purpose. The previous form used backslash continuations,
|
| 219 |
# which are bash syntax: pasted into PowerShell they became stray
|
|
@@ -226,12 +260,22 @@ def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local"
|
|
| 226 |
picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
|
| 227 |
return (
|
| 228 |
'<div class="c-join">'
|
| 229 |
-
'<
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
f
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
f'<pre class="c-join__cmd">{esc(command)}</pre>'
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
"</div>"
|
| 236 |
)
|
| 237 |
|
|
|
|
| 177 |
)
|
| 178 |
|
| 179 |
|
| 180 |
+
def join_this_server(
|
| 181 |
+
*, addresses, code: str, qr: str = "", tools: str = "local", source: str = ""
|
| 182 |
+
) -> str:
|
| 183 |
+
"""The whole path from nothing to a running worker, in the order it happens.
|
| 184 |
+
|
| 185 |
+
This panel used to be an address, a code and one command, sitting beside a
|
| 186 |
+
"Download agent" button that pointed at release artifacts nobody builds.
|
| 187 |
+
The button could only ever fail, and the command it accompanied assumed
|
| 188 |
+
the code was already on the machine. What a volunteer actually needs is
|
| 189 |
+
the three steps in between, so those are what this says: get the code,
|
| 190 |
+
install it, run it.
|
| 191 |
|
| 192 |
+
``source`` is where the code lives — this Space's own repository when the
|
| 193 |
+
server knows it. Absent, the step says so plainly instead of guessing at
|
| 194 |
+
a URL, because sending somebody to a repository that may not exist is
|
| 195 |
+
worse than admitting the server cannot name one.
|
| 196 |
|
| 197 |
``addresses`` empty is a real state and is reported as one: a server bound
|
| 198 |
to loopback is reachable by this machine and nothing else, and an agent on
|
|
|
|
| 213 |
f'<li class="c-join__alt">{esc(url)}</li>' for url in list(addresses)[1:]
|
| 214 |
)
|
| 215 |
alternates = (
|
| 216 |
+
f'<p class="c-join__note">Also reachable at {", ".join(esc(str(u)) for u in list(addresses)[1:])}.</p>'
|
| 217 |
if others
|
| 218 |
else ""
|
| 219 |
)
|
| 220 |
+
|
| 221 |
+
# STEP ONE: the code. A git clone, because a Space is a git repository and
|
| 222 |
+
# a clone is the one form that also updates later with `git pull`.
|
| 223 |
+
if source:
|
| 224 |
+
get_it = (
|
| 225 |
+
f'<pre class="c-join__cmd">git clone {esc(source)} distinct</pre>'
|
| 226 |
+
'<p class="c-join__note">Or use "Download repository" on the Files tab '
|
| 227 |
+
"and unzip it. Either way you end up with a folder of Python.</p>"
|
| 228 |
+
)
|
| 229 |
+
else:
|
| 230 |
+
get_it = (
|
| 231 |
+
'<p class="c-join__note">This server does not know its own repository '
|
| 232 |
+
"address. Ask whoever runs it where the code lives.</p>"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# STEP TWO: the install. `-e` so the worker runs from the folder that was
|
| 236 |
+
# cloned, which is also the folder `git pull` updates.
|
| 237 |
+
install = (
|
| 238 |
+
'<pre class="c-join__cmd">cd distinct\npython -m pip install -e ".[agent]"</pre>'
|
| 239 |
+
'<p class="c-join__note">Python 3.10 or newer. On Windows use '
|
| 240 |
+
"<code>py -3.12 -m pip</code> if <code>python</code> is an older one.</p>"
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# STEP THREE: the run, with this server's address and this browser's own
|
| 244 |
+
# library defaults already in it.
|
| 245 |
+
code_note = (
|
| 246 |
+
f'<p class="c-join__note">Pairing code, one use and ten minutes: '
|
| 247 |
+
f'<code class="c-join__code">{esc(code)}</code></p>'
|
| 248 |
if code
|
| 249 |
+
else '<p class="c-join__note">Generate a pairing code below, then run the '
|
| 250 |
+
"command with it in place of YOUR-CODE.</p>"
|
| 251 |
)
|
| 252 |
# One line on purpose. The previous form used backslash continuations,
|
| 253 |
# which are bash syntax: pasted into PowerShell they became stray
|
|
|
|
| 260 |
picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
|
| 261 |
return (
|
| 262 |
'<div class="c-join">'
|
| 263 |
+
'<ol class="c-steps-num">'
|
| 264 |
+
'<li class="c-step-num"><span class="c-step-num__n">1</span>'
|
| 265 |
+
'<div class="c-step-num__body"><p class="c-step-num__head">Get the code</p>'
|
| 266 |
+
f"{get_it}</div></li>"
|
| 267 |
+
'<li class="c-step-num"><span class="c-step-num__n">2</span>'
|
| 268 |
+
'<div class="c-step-num__body"><p class="c-step-num__head">Install it</p>'
|
| 269 |
+
f"{install}</div></li>"
|
| 270 |
+
'<li class="c-step-num"><span class="c-step-num__n">3</span>'
|
| 271 |
+
'<div class="c-step-num__body"><p class="c-step-num__head">Point it at this server</p>'
|
| 272 |
+
f'<code class="c-join__address">{esc(first)}</code>{alternates}{code_note}'
|
| 273 |
f'<pre class="c-join__cmd">{esc(command)}</pre>'
|
| 274 |
+
"<p class=\"c-join__note\">It prints an access code when it pairs. Give that to "
|
| 275 |
+
"the people who should be allowed to use your machine — anyone holding it can "
|
| 276 |
+
"send this worker requests.</p>"
|
| 277 |
+
f"{picture}</div></li>"
|
| 278 |
+
"</ol>"
|
| 279 |
"</div>"
|
| 280 |
)
|
| 281 |
|
distinct_server/ui.py
CHANGED
|
@@ -1342,12 +1342,7 @@ def _identity_from_request(request: Optional[Any]) -> Optional[str]:
|
|
| 1342 |
|
| 1343 |
|
| 1344 |
class DistinctUI:
|
| 1345 |
-
def __init__(
|
| 1346 |
-
self,
|
| 1347 |
-
control_plane: Optional[ControlPlane] = None,
|
| 1348 |
-
*,
|
| 1349 |
-
artifact_dir: Path = Path("agent-artifacts"),
|
| 1350 |
-
) -> None:
|
| 1351 |
self.control = control_plane or ControlPlane()
|
| 1352 |
if not self.control.catalogue:
|
| 1353 |
# The consent handshake needs the server to state what it may ask
|
|
@@ -1355,7 +1350,6 @@ class DistinctUI:
|
|
| 1355 |
self.control.catalogue = _default_server_catalogue()
|
| 1356 |
self.agent_api = AgentApi(self.control)
|
| 1357 |
self.sessions = SessionAuthorizer()
|
| 1358 |
-
self.artifact_dir = artifact_dir.resolve()
|
| 1359 |
# Where claimed run artifacts are stored, per session, so the Outputs
|
| 1360 |
# panel can offer them for download. Session-scoped and deleted with
|
| 1361 |
# the session.
|
|
@@ -3055,7 +3049,15 @@ class DistinctUI:
|
|
| 3055 |
# page, so what the person copies is what their runs will expect.
|
| 3056 |
chosen = _library_defaults(state) if state is not None else ()
|
| 3057 |
tools = ",".join(chosen) if chosen else "local"
|
| 3058 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3059 |
|
| 3060 |
# Pairing/downloads ------------------------------------------------
|
| 3061 |
|
|
@@ -3072,35 +3074,12 @@ class DistinctUI:
|
|
| 3072 |
except (ControlPlaneError, LoginRequired, ValueError) as exc:
|
| 3073 |
raise gr.Error(str(exc)) from exc
|
| 3074 |
|
| 3075 |
-
|
| 3076 |
-
|
| 3077 |
-
|
| 3078 |
-
|
| 3079 |
-
|
| 3080 |
-
|
| 3081 |
-
) -> str:
|
| 3082 |
-
try:
|
| 3083 |
-
self._authorize(state, profile, request)
|
| 3084 |
-
patterns = {
|
| 3085 |
-
"Windows x86-64": "distinct-agent-v*-windows-x86_64.zip",
|
| 3086 |
-
"Linux x86-64": "distinct-agent-v*-linux-x86_64.tar.gz",
|
| 3087 |
-
}
|
| 3088 |
-
pattern = patterns.get(platform_name)
|
| 3089 |
-
if pattern is None:
|
| 3090 |
-
raise ValueError("select a supported agent build")
|
| 3091 |
-
candidates = sorted(self.artifact_dir.glob(pattern), reverse=True)
|
| 3092 |
-
if not candidates:
|
| 3093 |
-
raise ValueError(
|
| 3094 |
-
f"{platform_name} artifact has not been built for this Space revision"
|
| 3095 |
-
)
|
| 3096 |
-
target = candidates[0].resolve()
|
| 3097 |
-
if target.parent != self.artifact_dir or not target.is_file():
|
| 3098 |
-
raise ValueError(
|
| 3099 |
-
f"{platform_name} artifact has not been built for this Space revision"
|
| 3100 |
-
)
|
| 3101 |
-
return str(target)
|
| 3102 |
-
except (ControlPlaneError, LoginRequired, ValueError) as exc:
|
| 3103 |
-
raise gr.Error(str(exc)) from exc
|
| 3104 |
|
| 3105 |
# Component graph --------------------------------------------------
|
| 3106 |
|
|
@@ -3161,16 +3140,9 @@ class DistinctUI:
|
|
| 3161 |
# library is a destination rather than a section, so its
|
| 3162 |
# control is a button that swaps the view; "Run an agent" is
|
| 3163 |
# still a section of this page, so it stays an anchor.
|
| 3164 |
-
|
| 3165 |
-
|
| 3166 |
-
|
| 3167 |
-
elem_classes="c-quicknav__btn", scale=0,
|
| 3168 |
-
)
|
| 3169 |
-
gr.HTML(
|
| 3170 |
-
'<a class="c-quicknav__link" href="#join-agent">Run an agent</a>'
|
| 3171 |
-
)
|
| 3172 |
-
|
| 3173 |
-
# THE SIGN-IN CONTROL, AND WHY IT IS CONDITIONAL.
|
| 3174 |
#
|
| 3175 |
# On a Space, Gradio only wires real OAuth when the interface
|
| 3176 |
# contains a `LoginButton`: `gradio.routes` gates the whole
|
|
@@ -3189,8 +3161,23 @@ class DistinctUI:
|
|
| 3189 |
# self-hosted deployment uses the `/auth/login` route in
|
| 3190 |
# `auth_routes.py` instead, which is a link rather than a
|
| 3191 |
# component because it is an ordinary HTTP redirect.
|
| 3192 |
-
|
| 3193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3194 |
|
| 3195 |
gr.HTML(render.notice())
|
| 3196 |
|
|
@@ -3491,20 +3478,22 @@ class DistinctUI:
|
|
| 3491 |
# which asked the reader to find something this
|
| 3492 |
# process already knew.
|
| 3493 |
join_panel = gr.HTML(self._join_panel())
|
| 3494 |
-
#
|
| 3495 |
-
#
|
| 3496 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3497 |
with gr.Row(elem_classes="c-tail__row"):
|
| 3498 |
-
|
| 3499 |
-
|
| 3500 |
-
value="Windows x86-64",
|
| 3501 |
-
label="Agent build",
|
| 3502 |
)
|
| 3503 |
-
download = gr.DownloadButton(
|
| 3504 |
-
"Download agent", variant="secondary"
|
| 3505 |
-
)
|
| 3506 |
-
create_code = gr.Button("Generate pairing code")
|
| 3507 |
-
gr.Markdown()
|
| 3508 |
|
| 3509 |
with gr.Accordion("Model assessments and their sources", open=False):
|
| 3510 |
gr.HTML(
|
|
@@ -3811,14 +3800,6 @@ class DistinctUI:
|
|
| 3811 |
queue=False,
|
| 3812 |
api_visibility="private",
|
| 3813 |
)
|
| 3814 |
-
download.click(
|
| 3815 |
-
self.download_agent,
|
| 3816 |
-
inputs=[platform_name, session_state],
|
| 3817 |
-
outputs=[download],
|
| 3818 |
-
queue=False,
|
| 3819 |
-
api_visibility="private",
|
| 3820 |
-
)
|
| 3821 |
-
|
| 3822 |
# Two seconds, not five. The activity panel counts elapsed time
|
| 3823 |
# and streams the worker's steps, and a spinner that updates
|
| 3824 |
# every five seconds reads as a stall. The refresh is in-memory
|
|
|
|
| 1342 |
|
| 1343 |
|
| 1344 |
class DistinctUI:
|
| 1345 |
+
def __init__(self, control_plane: Optional[ControlPlane] = None) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
self.control = control_plane or ControlPlane()
|
| 1347 |
if not self.control.catalogue:
|
| 1348 |
# The consent handshake needs the server to state what it may ask
|
|
|
|
| 1350 |
self.control.catalogue = _default_server_catalogue()
|
| 1351 |
self.agent_api = AgentApi(self.control)
|
| 1352 |
self.sessions = SessionAuthorizer()
|
|
|
|
| 1353 |
# Where claimed run artifacts are stored, per session, so the Outputs
|
| 1354 |
# panel can offer them for download. Session-scoped and deleted with
|
| 1355 |
# the session.
|
|
|
|
| 3049 |
# page, so what the person copies is what their runs will expect.
|
| 3050 |
chosen = _library_defaults(state) if state is not None else ()
|
| 3051 |
tools = ",".join(chosen) if chosen else "local"
|
| 3052 |
+
# Where the code lives, taken from the platform rather than written
|
| 3053 |
+
# down here. A hard-coded URL is a URL that goes stale the first time
|
| 3054 |
+
# this is deployed anywhere else, and the panel would then send
|
| 3055 |
+
# volunteers to somebody else's repository.
|
| 3056 |
+
space_id = os.environ.get("SPACE_ID", "").strip()
|
| 3057 |
+
source = f"https://huggingface.co/spaces/{space_id}" if space_id else ""
|
| 3058 |
+
return render.join_this_server(
|
| 3059 |
+
addresses=addresses, code=code, qr=qr, tools=tools, source=source
|
| 3060 |
+
)
|
| 3061 |
|
| 3062 |
# Pairing/downloads ------------------------------------------------
|
| 3063 |
|
|
|
|
| 3074 |
except (ControlPlaneError, LoginRequired, ValueError) as exc:
|
| 3075 |
raise gr.Error(str(exc)) from exc
|
| 3076 |
|
| 3077 |
+
# The worker is Python, distributed as this repository's own source, so
|
| 3078 |
+
# there is no build artifact to serve and no `download_agent` handler any
|
| 3079 |
+
# more. The setup panel says where the code is and how to install it; see
|
| 3080 |
+
# `render.join_this_server`. If frozen builds ever ship, they belong on a
|
| 3081 |
+
# releases page, not behind a control that fails silently when the page
|
| 3082 |
+
# for this revision was never built.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3083 |
|
| 3084 |
# Component graph --------------------------------------------------
|
| 3085 |
|
|
|
|
| 3140 |
# library is a destination rather than a section, so its
|
| 3141 |
# control is a button that swaps the view; "Run an agent" is
|
| 3142 |
# still a section of this page, so it stays an anchor.
|
| 3143 |
+
#
|
| 3144 |
+
# THE SIGN-IN CONTROL SITS IN THIS ROW, AND WHY IT IS
|
| 3145 |
+
# CONDITIONAL.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3146 |
#
|
| 3147 |
# On a Space, Gradio only wires real OAuth when the interface
|
| 3148 |
# contains a `LoginButton`: `gradio.routes` gates the whole
|
|
|
|
| 3161 |
# self-hosted deployment uses the `/auth/login` route in
|
| 3162 |
# `auth_routes.py` instead, which is a link rather than a
|
| 3163 |
# component because it is an ordinary HTTP redirect.
|
| 3164 |
+
#
|
| 3165 |
+
# It is inside the nav row rather than under it because it is
|
| 3166 |
+
# navigation: left on its own line it read as a stray control
|
| 3167 |
+
# sitting between the page's chrome and its content.
|
| 3168 |
+
with gr.Row(elem_classes="c-quicknav"):
|
| 3169 |
+
open_library = gr.Button(
|
| 3170 |
+
"Library", size="sm", variant="secondary",
|
| 3171 |
+
elem_classes="c-quicknav__btn", scale=0,
|
| 3172 |
+
)
|
| 3173 |
+
gr.HTML(
|
| 3174 |
+
'<a class="c-quicknav__link" href="#join-agent">Run an agent</a>'
|
| 3175 |
+
)
|
| 3176 |
+
if gradio_oauth_is_real():
|
| 3177 |
+
gr.LoginButton(
|
| 3178 |
+
"Sign in with Hugging Face", size="sm",
|
| 3179 |
+
elem_classes="c-quicknav__auth", scale=0,
|
| 3180 |
+
)
|
| 3181 |
|
| 3182 |
gr.HTML(render.notice())
|
| 3183 |
|
|
|
|
| 3478 |
# which asked the reader to find something this
|
| 3479 |
# process already knew.
|
| 3480 |
join_panel = gr.HTML(self._join_panel())
|
| 3481 |
+
# ONE CONTROL, BECAUSE THERE IS ONE THING TO DO HERE.
|
| 3482 |
+
#
|
| 3483 |
+
# This row used to offer "Agent build: Windows x86-64"
|
| 3484 |
+
# and a Download button beside it. No such build is
|
| 3485 |
+
# produced for this Space, so the button's only
|
| 3486 |
+
# possible outcome was an error telling the volunteer
|
| 3487 |
+
# that an artifact "has not been built for this Space
|
| 3488 |
+
# revision" — a sentence about our release process,
|
| 3489 |
+
# delivered to somebody who wanted to help. The worker
|
| 3490 |
+
# is Python and always was; the panel above now says
|
| 3491 |
+
# so, and the only control left is the one that mints
|
| 3492 |
+
# the code those instructions need.
|
| 3493 |
with gr.Row(elem_classes="c-tail__row"):
|
| 3494 |
+
create_code = gr.Button(
|
| 3495 |
+
"Generate pairing code", variant="primary", scale=0
|
|
|
|
|
|
|
| 3496 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3497 |
|
| 3498 |
with gr.Accordion("Model assessments and their sources", open=False):
|
| 3499 |
gr.HTML(
|
|
|
|
| 3800 |
queue=False,
|
| 3801 |
api_visibility="private",
|
| 3802 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3803 |
# Two seconds, not five. The activity panel counts elapsed time
|
| 3804 |
# and streams the worker's steps, and a spinner that updates
|
| 3805 |
# every five seconds reads as a stall. The refresh is in-memory
|
pyproject.toml
CHANGED
|
@@ -11,6 +11,11 @@ dependencies = ["gradio[oauth]==6.24.0"]
|
|
| 11 |
|
| 12 |
[project.optional-dependencies]
|
| 13 |
agent = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"dspy[deno]==3.3.0",
|
| 15 |
"gradio_client==2.6.0",
|
| 16 |
"pyRAPL==0.2.3.1; sys_platform == 'linux'",
|
|
|
|
| 11 |
|
| 12 |
[project.optional-dependencies]
|
| 13 |
agent = [
|
| 14 |
+
# Ed25519, for the detached snapshot signature. A direct requirement, not
|
| 15 |
+
# a transitive one: `distinct_protocol.handshake` imports it and refuses
|
| 16 |
+
# to fall back to HMAC, so an agent install that happens to miss it fails
|
| 17 |
+
# at the moment it pairs rather than at install time.
|
| 18 |
+
"cryptography>=42",
|
| 19 |
"dspy[deno]==3.3.0",
|
| 20 |
"gradio_client==2.6.0",
|
| 21 |
"pyRAPL==0.2.3.1; sys_platform == 'linux'",
|
scripts/demo_server.py
CHANGED
|
@@ -25,11 +25,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
| 25 |
|
| 26 |
from distinct_protocol.netpolicy import announce_bind, launch_kwargs # noqa: E402
|
| 27 |
from distinct_server import ControlPlane # noqa: E402
|
| 28 |
-
from distinct_server.presentation import CSS, HEAD # noqa: E402
|
| 29 |
from distinct_server.identity import ( # noqa: E402
|
| 30 |
NotConfigured,
|
| 31 |
refuse_mocked_oauth,
|
| 32 |
)
|
|
|
|
| 33 |
from distinct_server.ui import build_app, distinct_theme # noqa: E402
|
| 34 |
|
| 35 |
|
|
|
|
| 25 |
|
| 26 |
from distinct_protocol.netpolicy import announce_bind, launch_kwargs # noqa: E402
|
| 27 |
from distinct_server import ControlPlane # noqa: E402
|
|
|
|
| 28 |
from distinct_server.identity import ( # noqa: E402
|
| 29 |
NotConfigured,
|
| 30 |
refuse_mocked_oauth,
|
| 31 |
)
|
| 32 |
+
from distinct_server.presentation import CSS, HEAD # noqa: E402
|
| 33 |
from distinct_server.ui import build_app, distinct_theme # noqa: E402
|
| 34 |
|
| 35 |
|
scripts/live_e2e.py
CHANGED
|
@@ -292,10 +292,18 @@ def capture(playwright, server_url: str, shots: Shots, worker_started) -> None:
|
|
| 292 |
|
| 293 |
open_section(other, "Run a community agent")
|
| 294 |
other.wait_for_timeout(2000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
shots.take(
|
| 296 |
other,
|
| 297 |
-
"
|
| 298 |
-
"
|
| 299 |
)
|
| 300 |
|
| 301 |
worker_screen(browser, shots)
|
|
@@ -503,7 +511,7 @@ def main() -> int:
|
|
| 503 |
found = re.search(r"\b([A-Z0-9]{5}(?:-[A-Z0-9]{4,5}){5,8})\b", line)
|
| 504 |
if found:
|
| 505 |
threading.Thread(
|
| 506 |
-
target=lambda: [print(f" worker | {
|
| 507 |
daemon=True,
|
| 508 |
).start()
|
| 509 |
return found.group(1)
|
|
|
|
| 292 |
|
| 293 |
open_section(other, "Run a community agent")
|
| 294 |
other.wait_for_timeout(2000)
|
| 295 |
+
# The panel is now three steps rather than a build to download, so the
|
| 296 |
+
# screenshot is checked for what it must say. A caption promising a
|
| 297 |
+
# Windows build outlived the button that offered one; this asserts
|
| 298 |
+
# instead of describing.
|
| 299 |
+
setup = other.locator(".c-join").inner_text()
|
| 300 |
+
for needle in ("Get the code", "Install it", "pip install -e", "Point it at this server"):
|
| 301 |
+
if needle not in setup:
|
| 302 |
+
raise AssertionError(f"the setup panel no longer says {needle!r}")
|
| 303 |
shots.take(
|
| 304 |
other,
|
| 305 |
+
"set-up-the-agent",
|
| 306 |
+
"How a volunteer gets the worker running: the code, the install, the command.",
|
| 307 |
)
|
| 308 |
|
| 309 |
worker_screen(browser, shots)
|
|
|
|
| 511 |
found = re.search(r"\b([A-Z0-9]{5}(?:-[A-Z0-9]{4,5}){5,8})\b", line)
|
| 512 |
if found:
|
| 513 |
threading.Thread(
|
| 514 |
+
target=lambda: [print(f" worker | {line.rstrip()}") for line in process.stdout],
|
| 515 |
daemon=True,
|
| 516 |
).start()
|
| 517 |
return found.group(1)
|