Polish lab service links as action pills

This commit is contained in:
2026-04-24 08:06:17 -06:00
parent ea55178f9c
commit fcb2dcb36d
3 changed files with 74 additions and 3 deletions
+10 -2
View File
@@ -118,10 +118,12 @@ describe("LabContent", () => {
); );
const link = await screen.findByRole("link", { const link = await screen.findByRole("link", {
name: "http://localhost:8080/", name: "Open WebUI on port 8080",
}); });
expect(link).toHaveAttribute("href", "http://localhost:8080/"); expect(link).toHaveAttribute("href", "http://localhost:8080/");
expect(link).toHaveAttribute("title", "http://localhost:8080/");
expect(link).toHaveClass("lab-service-pill");
}); });
it("renders inline service URL tokens inside code as plain text", async () => { it("renders inline service URL tokens inside code as plain text", async () => {
@@ -193,8 +195,14 @@ describe("LabContent", () => {
); );
expect( expect(
await screen.findByRole("link", { name: "https://lab.example/openwebui" }), await screen.findByRole("link", { name: "Open WebUI" }),
).toHaveAttribute("href", "https://lab.example/openwebui"); ).toHaveAttribute("href", "https://lab.example/openwebui");
expect(
screen.getByRole("link", { name: "Open WebUI" }),
).toHaveClass("lab-service-pill");
expect(
screen.getByRole("link", { name: "Open WebUI" }),
).toHaveAttribute("title", "https://lab.example/openwebui");
const apiMatches = await screen.findAllByText("https://lab.example/openwebui/api"); const apiMatches = await screen.findAllByText("https://lab.example/openwebui/api");
expect(apiMatches.some((node) => node.tagName === "CODE")).toBe(true); expect(apiMatches.some((node) => node.tagName === "CODE")).toBe(true);
expect( expect(
+24 -1
View File
@@ -53,6 +53,14 @@ const lab1NetronToken = "<div data-lab1-netron-panel></div>";
const tokenizerPlaygroundToken = "<div data-tokenizer-playground></div>"; const tokenizerPlaygroundToken = "<div data-tokenizer-playground></div>";
const serviceTokenPattern = const serviceTokenPattern =
/\{\{service-(url|address):([a-z0-9-]+)(?::([^}]+))?\}\}/g; /\{\{service-(url|address):([a-z0-9-]+)(?::([^}]+))?\}\}/g;
const serviceLabels: Record<string, string> = {
"chunkviz": "ChunkViz",
"embedding-atlas": "Embedding Atlas",
"open-webui": "Open WebUI",
"promptfoo": "Promptfoo",
"ssh": "SSH",
"unsloth": "Unsloth",
};
function looksLikeCliCommand(commandText: string, className: string) { function looksLikeCliCommand(commandText: string, className: string) {
if (cliLanguagePattern.test(className)) return true; if (cliLanguagePattern.test(className)) return true;
@@ -380,10 +388,25 @@ function replaceServiceTokens(
if (tokenType === "url") { if (tokenType === "url") {
const link = document.createElement("a"); const link = document.createElement("a");
const serviceLabel = serviceLabels[serviceId] ?? replacement;
let visibleLabel = serviceLabel;
try {
const resolvedUrl = new URL(replacement);
if (resolvedUrl.port) {
visibleLabel = `${serviceLabel} on port ${resolvedUrl.port}`;
}
} catch {
visibleLabel = serviceLabel;
}
link.className = "lab-service-pill";
link.dataset.serviceId = serviceId;
link.href = replacement; link.href = replacement;
link.rel = "noreferrer"; link.rel = "noreferrer";
link.target = "_blank"; link.target = "_blank";
link.textContent = replacement; link.title = replacement;
link.textContent = visibleLabel;
fragment.append(link); fragment.append(link);
} else { } else {
fragment.append(replacement); fragment.append(replacement);
+40
View File
@@ -2051,6 +2051,46 @@ ol {
box-shadow: 0 12px 28px -22px rgba(15, 92, 139, 0.85); box-shadow: 0 12px 28px -22px rgba(15, 92, 139, 0.85);
} }
.lab-content a.lab-service-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin: 0.1rem 0.15rem;
border-radius: 999px;
border: 1px solid #0f5c8b;
background: #0f5c8b;
color: #fff;
font-size: 0.88rem;
font-weight: 600;
line-height: 1.2;
padding: 0.48rem 0.9rem;
text-decoration: none;
vertical-align: middle;
transition:
transform 120ms ease,
box-shadow 120ms ease,
background-color 120ms ease;
}
.lab-content a.lab-service-pill::before {
content: "Open";
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
padding: 0.16rem 0.48rem;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.lab-content a.lab-service-pill:hover {
transform: translateY(-1px);
box-shadow: 0 12px 28px -22px rgba(15, 92, 139, 0.85);
}
.lab1-netron-panel__note, .lab1-netron-panel__note,
.lab-iframe-embed__status, .lab-iframe-embed__status,
.lab-iframe-embed__fallback { .lab-iframe-embed__fallback {