So someone can try a model without setting up a client, and — more often — tell whether a problem is the gateway or their code. The page carries a session, and the server turns that into the identity an API key would have produced, resolved from the person's project. The request then goes down the ordinary path by being handed to the same handler /v1 uses: admission, model allowlist, rate limits, budget, accounting. There is exactly one request path, so there is no second place for its rules to drift — and without that, a playground is a hole in every limit the rest of the gateway enforces. The allowlist test posts a forbidden model and expects 403 for that reason. The body is rebuilt rather than forwarded, so the page cannot set fields it has no business setting: a stream:false that would hang the reader, a max_tokens large enough to occupy a replica for an hour. One turn is capped at 512 tokens. The picker offers what is actually served and allowed, not what is catalogued, because offering a model with no replica produces a 503 the reader cannot interpret. Inline vanilla JavaScript, for the same reason the charts are hand-drawn SVG. It streams, because on CPU inference ten seconds is normal and a chat that hangs and then dumps a paragraph feels broken. Replies are written with textContent throughout: model output and user input have no business being parsed as markup. Its POSTs carry the CSRF token in a header, which a cross-site form cannot set at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
5.5 KiB
CSS
102 lines
5.5 KiB
CSS
/* Deliberately one small stylesheet with no build step. The single-binary goal
|
|
rules out a JS toolchain, and nothing here needs one. */
|
|
:root {
|
|
color-scheme: light dark;
|
|
--bg: #fbfbfa;
|
|
--fg: #1c1c1a;
|
|
--muted: #6b6b66;
|
|
--line: #e3e3df;
|
|
--card: #ffffff;
|
|
--accent: #2f5d50;
|
|
--ok: #2f7a4f;
|
|
--warn: #9a6b1f;
|
|
--bad: #a33a32;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #17181a; --fg: #e8e8e4; --muted: #9a9a93; --line: #2c2e31;
|
|
--card: #1e2022; --accent: #7fb3a2; --ok: #6fbd8c; --warn: #d6a44e; --bad: #e07b72;
|
|
}
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; background: var(--bg); color: var(--fg);
|
|
font: 15px/1.55 ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
.wrap { max-width: 1100px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
|
|
header { border-bottom: 1px solid var(--line); margin-bottom: 2rem; padding-bottom: 1.25rem; }
|
|
h1 { font-size: 1.5rem; margin: 0 0 .25rem; letter-spacing: -.01em; }
|
|
h2 { font-size: 1.05rem; margin: 2.25rem 0 .75rem; letter-spacing: -.005em; }
|
|
.sub { color: var(--muted); margin: 0; }
|
|
nav { margin-top: .75rem; display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
nav a { color: var(--accent); text-decoration: none; font-size: .9rem; }
|
|
nav a:hover { text-decoration: underline; }
|
|
table { width: 100%; border-collapse: collapse; font-size: .875rem; background: var(--card);
|
|
border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--line); }
|
|
th { font-weight: 600; color: var(--muted); font-size: .78rem; text-transform: uppercase;
|
|
letter-spacing: .04em; }
|
|
tr:last-child td { border-bottom: none; }
|
|
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; }
|
|
pre { background: var(--card); border: 1px solid var(--line); border-radius: 6px;
|
|
padding: .9rem 1rem; overflow-x: auto; }
|
|
.state { font-weight: 600; font-size: .8rem; }
|
|
.state-ready, .state-active { color: var(--ok); }
|
|
.state-pending, .state-starting, .state-draining, .state-cordoned { color: var(--warn); }
|
|
.state-failed, .state-lost, .state-unreachable, .state-revoked { color: var(--bad); }
|
|
.empty { color: var(--muted); font-style: italic; padding: .9rem 0; }
|
|
.note { color: var(--muted); font-size: .85rem; margin: .4rem 0 0; }
|
|
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
|
|
color: var(--muted); font-size: .8rem; display: flex; justify-content: space-between; gap: 1rem; }
|
|
button {
|
|
font: inherit; font-weight: 600; cursor: pointer;
|
|
background: var(--accent); color: var(--bg); border: none;
|
|
border-radius: 6px; padding: .55rem 1.1rem;
|
|
}
|
|
button:hover { filter: brightness(1.1); }
|
|
ul { padding-left: 1.2rem; }
|
|
li { margin: .25rem 0; }
|
|
a { color: var(--accent); }
|
|
.wrap.narrow { max-width: 30rem; }
|
|
label { display: block; margin: .9rem 0; font-size: .9rem; font-weight: 600; }
|
|
input[type=text], input[type=email], input[type=password] {
|
|
display: block; width: 100%; margin-top: .3rem; font: inherit; font-weight: 400;
|
|
padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px;
|
|
background: var(--card); color: var(--fg);
|
|
}
|
|
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
.error { color: var(--bad); border: 1px solid var(--bad); border-radius: 6px;
|
|
padding: .6rem .8rem; background: var(--card); }
|
|
form.inline { display: inline; }
|
|
button.link { background: none; color: var(--accent); padding: 0;
|
|
text-decoration: underline; font-weight: 400; font-size: .9rem; }
|
|
button.link.danger { color: var(--bad); }
|
|
.ok { color: var(--ok); border: 1px solid var(--ok); border-radius: 6px;
|
|
padding: .6rem .8rem; background: var(--card); }
|
|
select { font: inherit; padding: .35rem .4rem; border: 1px solid var(--line);
|
|
border-radius: 6px; background: var(--card); color: var(--fg); }
|
|
.chart-wrap { position: relative; margin: .5rem 0 1.5rem; }
|
|
.chart { width: 100%; height: 120px; display: block;
|
|
background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
|
|
.chart rect { transition: opacity .1s; }
|
|
.chart rect:hover { opacity: .65; }
|
|
.chart .axis { stroke: var(--line); stroke-width: 1; }
|
|
.chart-peak { position: absolute; top: .4rem; right: .6rem;
|
|
font-size: .72rem; color: var(--muted); }
|
|
.chart-row { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; }
|
|
@media (max-width: 720px) { .chart-row { grid-template-columns: 1fr; } }
|
|
.chat-bar { display: flex; align-items: center; gap: 1rem; margin-bottom: .75rem; }
|
|
.inline-label { font-size: .85rem; font-weight: 600; margin: 0; }
|
|
.inline-label select { margin-left: .4rem; font-weight: 400; }
|
|
.chat-log { min-height: 16rem; max-height: 55vh; overflow-y: auto; padding: .5rem;
|
|
background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
|
|
.bubble { white-space: pre-wrap; word-wrap: break-word; padding: .55rem .75rem;
|
|
margin: .4rem 0; border-radius: 8px; max-width: 46rem; }
|
|
.bubble.you { background: var(--accent); color: var(--bg); margin-left: auto; }
|
|
.bubble.assistant { background: var(--bg); border: 1px solid var(--line); }
|
|
.bubble.failed { color: var(--bad); border-color: var(--bad); }
|
|
.chat-composer { display: flex; gap: .6rem; margin-top: .75rem; align-items: flex-end; }
|
|
.chat-composer textarea { flex: 1; font: inherit; padding: .5rem .6rem; resize: vertical;
|
|
border: 1px solid var(--line); border-radius: 6px; background: var(--card); color: var(--fg); }
|