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 {