diff --git a/src/components/labs/LabContent.test.tsx b/src/components/labs/LabContent.test.tsx index 795e36b..9cc77ac 100644 --- a/src/components/labs/LabContent.test.tsx +++ b/src/components/labs/LabContent.test.tsx @@ -118,10 +118,12 @@ describe("LabContent", () => { ); 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("title", "http://localhost:8080/"); + expect(link).toHaveClass("lab-service-pill"); }); it("renders inline service URL tokens inside code as plain text", async () => { @@ -193,8 +195,14 @@ describe("LabContent", () => { ); expect( - await screen.findByRole("link", { name: "https://lab.example/openwebui" }), + await screen.findByRole("link", { name: "Open WebUI" }), ).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"); expect(apiMatches.some((node) => node.tagName === "CODE")).toBe(true); expect( diff --git a/src/components/labs/LabContent.tsx b/src/components/labs/LabContent.tsx index 1fbe9ed..e606f88 100644 --- a/src/components/labs/LabContent.tsx +++ b/src/components/labs/LabContent.tsx @@ -53,6 +53,14 @@ const lab1NetronToken = "
"; const tokenizerPlaygroundToken = "
"; const serviceTokenPattern = /\{\{service-(url|address):([a-z0-9-]+)(?::([^}]+))?\}\}/g; +const serviceLabels: Record = { + "chunkviz": "ChunkViz", + "embedding-atlas": "Embedding Atlas", + "open-webui": "Open WebUI", + "promptfoo": "Promptfoo", + "ssh": "SSH", + "unsloth": "Unsloth", +}; function looksLikeCliCommand(commandText: string, className: string) { if (cliLanguagePattern.test(className)) return true; @@ -380,10 +388,25 @@ function replaceServiceTokens( if (tokenType === "url") { 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.rel = "noreferrer"; link.target = "_blank"; - link.textContent = replacement; + link.title = replacement; + link.textContent = visibleLabel; fragment.append(link); } else { fragment.append(replacement); diff --git a/src/styles/globals.css b/src/styles/globals.css index c781d94..453af41 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2051,6 +2051,46 @@ ol { 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, .lab-iframe-embed__status, .lab-iframe-embed__fallback {