Refactor lab 1 for Netron and local confidence views

This commit is contained in:
2026-04-16 11:15:39 -06:00
parent a97c8a7694
commit e4621ca65b
20 changed files with 1634 additions and 280 deletions
+163
View File
@@ -0,0 +1,163 @@
import { NextResponse } from "next/server";
import { normalizeUpstreamChatEndpoint } from "~/lib/lab2-chat";
import {
clampLab1Messages,
extractLab1AssistantContent,
extractLab1ResponseTokens,
getLab1SystemPrompt,
LAB1_CONFIDENCE_MODEL_ALIAS,
LAB1_DEFAULT_MAX_TOKENS,
LAB1_DEFAULT_TEMPERATURE,
type Lab1ConfidenceMessage,
} from "~/lib/lab1-confidence";
type ChatRouteRequestBody = {
messages?: Lab1ConfidenceMessage[];
};
const LOCAL_OLLAMA_TIMEOUT_MS = 90000;
function getLocalOllamaEndpoint() {
const configuredBaseUrl =
process.env.COURSEWARE_OLLAMA_BASE_URL?.trim() || "http://127.0.0.1:11434";
return normalizeUpstreamChatEndpoint(configuredBaseUrl);
}
function getLab1ModelAlias() {
return (
process.env.COURSEWARE_LAB1_OLLAMA_MODEL_ALIAS?.trim() ||
LAB1_CONFIDENCE_MODEL_ALIAS
);
}
export async function POST(request: Request) {
let body: ChatRouteRequestBody;
try {
body = (await request.json()) as ChatRouteRequestBody;
} catch {
return NextResponse.json(
{
error: "The request body must be valid JSON.",
},
{ status: 400 },
);
}
if (!Array.isArray(body.messages) || body.messages.length === 0) {
return NextResponse.json(
{
error: "At least one chat message is required.",
},
{ status: 400 },
);
}
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
LOCAL_OLLAMA_TIMEOUT_MS,
);
try {
const upstreamResponse = await fetch(getLocalOllamaEndpoint(), {
body: JSON.stringify({
logprobs: true,
max_tokens: LAB1_DEFAULT_MAX_TOKENS,
messages: [
{
content: getLab1SystemPrompt(),
role: "system",
},
...clampLab1Messages(body.messages),
],
model: getLab1ModelAlias(),
stream: false,
temperature: LAB1_DEFAULT_TEMPERATURE,
top_logprobs: 5,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
signal: controller.signal,
});
const responseText = await upstreamResponse.text();
const parsedBody = JSON.parse(responseText) as unknown;
if (!upstreamResponse.ok) {
const message =
typeof parsedBody === "object" &&
parsedBody !== null &&
"error" in parsedBody &&
typeof parsedBody.error === "object" &&
parsedBody.error !== null &&
"message" in parsedBody.error &&
typeof parsedBody.error.message === "string"
? parsedBody.error.message
: `The local Ollama endpoint returned ${upstreamResponse.status}.`;
return NextResponse.json(
{
error: message,
},
{ status: upstreamResponse.status },
);
}
if (!parsedBody || typeof parsedBody !== "object") {
return NextResponse.json(
{
error: "The local Ollama endpoint returned an unreadable response.",
},
{ status: 502 },
);
}
const tokens = extractLab1ResponseTokens(parsedBody);
if (tokens.length === 0) {
return NextResponse.json(
{
error:
"The local Ollama response did not include token logprobs. Confirm the installed Ollama version supports logprobs.",
},
{ status: 502 },
);
}
const content =
extractLab1AssistantContent(parsedBody) ||
tokens.map((token) => token.token).join("");
return NextResponse.json({
content,
model:
("model" in parsedBody && typeof parsedBody.model === "string"
? parsedBody.model
: getLab1ModelAlias()),
role: "assistant",
tokens,
});
} catch (caughtError) {
if (caughtError instanceof Error && caughtError.name === "AbortError") {
return NextResponse.json(
{
error: `The local Ollama endpoint timed out after ${Math.floor(LOCAL_OLLAMA_TIMEOUT_MS / 1000)} seconds.`,
},
{ status: 504 },
);
}
return NextResponse.json(
{
error: "The Lab 1 confidence route could not reach the local Ollama endpoint.",
},
{ status: 502 },
);
} finally {
clearTimeout(timeoutId);
}
}
@@ -0,0 +1,75 @@
import { createReadStream, statSync } from "fs";
import path from "path";
import { Readable } from "stream";
import { NextResponse } from "next/server";
const modelFileMap = {
"llama-3.2-1b-q4_k_m.gguf": {
envKey: "COURSEWARE_LAB1_LLAMA_MODEL_PATH",
fileName: "Llama-3.2-1B.Q4_K_M.gguf",
},
"qwen3-0.6b-q8_0.gguf": {
envKey: "COURSEWARE_LAB1_QWEN_MODEL_PATH",
fileName: "Qwen3-0.6B-Q8_0.gguf",
},
} as const;
type ModelSlug = keyof typeof modelFileMap;
function resolveModelPath(slug: string) {
const config = modelFileMap[slug as ModelSlug];
if (!config) {
return null;
}
const configuredPath = process.env[config.envKey]?.trim();
if (!configuredPath) {
return null;
}
return {
absolutePath: path.resolve(configuredPath),
fileName: config.fileName,
};
}
export async function GET(
_request: Request,
context: { params: Promise<{ filename: string }> },
) {
const { filename } = await context.params;
const resolvedFile = resolveModelPath(filename.toLowerCase());
if (!resolvedFile) {
return NextResponse.json(
{
error: "The requested Lab 1 model file was not found.",
},
{ status: 404 },
);
}
try {
const fileStats = statSync(resolvedFile.absolutePath);
const stream = Readable.toWeb(
createReadStream(resolvedFile.absolutePath),
) as ReadableStream;
return new NextResponse(stream, {
headers: {
"Cache-Control": "private, max-age=0, must-revalidate",
"Content-Disposition": `attachment; filename="${resolvedFile.fileName}"`,
"Content-Length": String(fileStats.size),
"Content-Type": "application/octet-stream",
},
});
} catch {
return NextResponse.json(
{
error: "The requested Lab 1 model file could not be opened.",
},
{ status: 404 },
);
}
}
@@ -0,0 +1,84 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { Lab1ConfidenceChat } from "~/components/labs/Lab1ConfidenceChat";
describe("Lab1ConfidenceChat", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("renders colorized tokens and tooltip data from the Lab 1 chat route", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => {
return {
json: async () => ({
content: "often works",
model: "lab1-qwen3-0.6b-q8_0",
role: "assistant",
tokens: [
{
logprob: Math.log(0.4),
probability: 40,
token: "often",
topAlternatives: [
{ probability: 14, token: "commonly" },
{ probability: 10, token: "also" },
],
},
{
logprob: Math.log(0.8),
probability: 80,
token: " works",
topAlternatives: [],
},
],
}),
ok: true,
};
}),
);
render(<Lab1ConfidenceChat />);
fireEvent.change(screen.getByLabelText("Prompt"), {
target: { value: "Explain how often phishing succeeds." },
});
fireEvent.submit(
screen.getByRole("button", { name: "Generate Output" }).closest("form")!,
);
expect(await screen.findByLabelText("often 40.0%")).toBeInTheDocument();
expect(screen.getByText("14.0%:")).toBeInTheDocument();
expect(screen.getByText("commonly")).toBeInTheDocument();
expect(screen.getByText("lab1-qwen3-0.6b-q8_0")).toBeInTheDocument();
});
it("shows an inline error when the local route fails", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => {
return {
json: async () => ({
error: "The local Ollama request failed.",
}),
ok: false,
};
}),
);
render(<Lab1ConfidenceChat />);
fireEvent.change(screen.getByLabelText("Prompt"), {
target: { value: "Trigger an error." },
});
fireEvent.submit(
screen.getByRole("button", { name: "Generate Output" }).closest("form")!,
);
expect(
await screen.findByText("The local Ollama request failed."),
).toBeInTheDocument();
});
});
+244
View File
@@ -0,0 +1,244 @@
"use client";
import { FormEvent, useState } from "react";
import {
formatProbabilityPercent,
getConfidenceBand,
type Lab1ConfidenceMessage,
type Lab1ConfidenceResponse,
type Lab1ResponseToken,
} from "~/lib/lab1-confidence";
type UserTurn = {
content: string;
id: string;
role: "user";
};
type AssistantTurn = Lab1ConfidenceResponse & {
error?: string;
id: string;
};
type ChatTurn = AssistantTurn | UserTurn;
const starterPrompts = [
"The quick brown fox",
"Write one sentence explaining what a firewall does.",
"List three words that describe a phishing email.",
] as const;
function buildTurnId() {
return `lab1-turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function toConversation(messages: ChatTurn[]) {
return messages.map(({ content, role }) => ({ content, role }));
}
function renderTooltip(token: Lab1ResponseToken) {
return (
<span className="lab1-confidence__tooltip">
<strong>{formatProbabilityPercent(token.probability)}</strong>
{token.topAlternatives.length > 0 ? (
<span className="lab1-confidence__tooltip-list">
{token.topAlternatives.map((candidate) => (
<span key={`${token.token}-${candidate.token}`}>
{formatProbabilityPercent(candidate.probability)}:{" "}
<code>{candidate.token}</code>
</span>
))}
</span>
) : (
<span className="lab1-confidence__tooltip-list">
<span>No alternate tokens returned for this position.</span>
</span>
)}
</span>
);
}
export function Lab1ConfidenceChat() {
const [draft, setDraft] = useState<string>(starterPrompts[0]);
const [messages, setMessages] = useState<ChatTurn[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const prompt = draft.trim();
if (!prompt) {
setError("Enter a prompt to inspect the model output.");
return;
}
const nextUserTurn: UserTurn = {
content: prompt,
id: buildTurnId(),
role: "user",
};
const nextConversation = [...messages, nextUserTurn];
setMessages(nextConversation);
setDraft("");
setError(null);
setIsSubmitting(true);
try {
const response = await fetch("/api/lab1/chat", {
body: JSON.stringify({
messages: toConversation(nextConversation),
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const payload = (await response.json()) as Lab1ConfidenceResponse & {
error?: string;
};
if (!response.ok) {
throw new Error(payload.error || "The local Ollama request failed.");
}
setMessages((currentMessages) => [
...currentMessages,
{
...payload,
id: buildTurnId(),
},
]);
} catch (caughtError) {
setError(
caughtError instanceof Error
? caughtError.message
: "The local Ollama request failed.",
);
} finally {
setIsSubmitting(false);
}
}
return (
<section className="lab1-confidence" data-widget-enhanced="true">
<div className="lab1-confidence__header">
<p className="lab1-confidence__eyebrow">Lab 1 Confidence View</p>
<h3>Visualize token confidence locally</h3>
<p className="lab1-confidence__lede">
This widget uses the preloaded local Lab 1 Qwen model. Hover over any
output token to inspect its probability and the strongest alternate
predictions returned for that position.
</p>
</div>
<div className="lab1-confidence__prompt-row">
{starterPrompts.map((prompt) => (
<button
className="lab1-confidence__prompt-chip"
key={prompt}
onClick={() => setDraft(prompt)}
type="button"
>
{prompt}
</button>
))}
</div>
<div className="lab1-confidence__transcript" aria-live="polite">
{messages.length === 0 ? (
<div className="lab1-confidence__empty">
<strong>Try a short prompt first.</strong>
<p>
Start with one of the suggested prompts, then hover across the
model output to compare high-confidence and low-confidence tokens.
</p>
</div>
) : (
messages.map((message) => {
if (message.role === "user") {
return (
<article
className="lab1-confidence__message lab1-confidence__message--user"
key={message.id}
>
<div className="lab1-confidence__message-meta">
<span>You</span>
</div>
<pre className="lab1-confidence__message-body">
<code>{message.content}</code>
</pre>
</article>
);
}
return (
<article className="lab1-confidence__message" key={message.id}>
<div className="lab1-confidence__message-meta">
<span>Assistant</span>
<code>{message.model}</code>
</div>
<div className="lab1-confidence__token-stream" role="list">
{message.tokens.map((token, index) => (
<span
aria-label={`${token.token} ${formatProbabilityPercent(
token.probability,
)}`}
className={`lab1-confidence__token lab1-confidence__token--${getConfidenceBand(
token.probability,
)}`}
key={`${message.id}-${index}-${token.token}`}
role="listitem"
>
{token.token}
{renderTooltip(token)}
</span>
))}
</div>
{message.error ? (
<p className="lab1-confidence__message-warning">
{message.error}
</p>
) : null}
</article>
);
})
)}
</div>
<form className="lab1-confidence__composer" onSubmit={handleSubmit}>
<label
className="lab1-confidence__composer-label"
htmlFor="lab1-confidence-draft"
>
Prompt
</label>
<textarea
id="lab1-confidence-draft"
name="draft"
onChange={(event) => setDraft(event.target.value)}
placeholder="Ask a question or start a phrase and inspect the output."
rows={5}
value={draft}
/>
<div className="lab1-confidence__composer-actions">
<div className="lab1-confidence__composer-state">
<span>Inference target</span>
<strong>Local Lab 1 Qwen model</strong>
</div>
<button disabled={isSubmitting} type="submit">
{isSubmitting ? "Generating..." : "Generate Output"}
</button>
</div>
{error ? <p className="lab1-confidence__error">{error}</p> : null}
</form>
</section>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { useEffect, useState } from "react";
import {
fetchCoursewareRuntimeConfig,
normalizeCoursewareRuntimeConfig,
} from "~/lib/courseware-runtime";
export function Lab1NetronPanel() {
const [runtimeConfig, setRuntimeConfig] = useState(() =>
normalizeCoursewareRuntimeConfig(),
);
const [isResolved, setIsResolved] = useState(false);
useEffect(() => {
let isCancelled = false;
void fetchCoursewareRuntimeConfig()
.then((nextConfig) => {
if (isCancelled) return;
setRuntimeConfig(nextConfig);
})
.catch(() => {
if (isCancelled) return;
setRuntimeConfig(normalizeCoursewareRuntimeConfig());
})
.finally(() => {
if (isCancelled) return;
setIsResolved(true);
});
return () => {
isCancelled = true;
};
}, []);
return (
<section className="lab1-netron-panel" data-widget-enhanced="true">
<div>
<p className="lab1-netron-panel__eyebrow">Lab 1 Model Structure</p>
<h3>Open Netron on port 8338</h3>
<p className="lab1-netron-panel__lede">
Netron runs as a local browser tool for this lab. Open it, then use
the download links in this section to load each GGUF file manually.
</p>
</div>
<div className="lab1-netron-panel__actions">
<a
className="lab1-netron-panel__primary"
href={runtimeConfig.lab1NetronUrl}
rel="noreferrer"
target="_blank"
>
Open Netron
</a>
<span className="lab1-netron-panel__note" aria-live="polite">
{isResolved
? `Netron URL: ${runtimeConfig.lab1NetronUrl}`
: "Resolving the Netron URL..."}
</span>
</div>
</section>
);
}
@@ -51,7 +51,7 @@ describe("fetchLab3RuntimeConfig", () => {
);
await expect(fetchLab3RuntimeConfig()).resolves.toEqual({
terminalPath: "http://127.0.0.1:7681/wetty",
terminalPath: "http://localhost:7681/wetty",
});
expect(fetchMock).toHaveBeenCalledWith(LAB3_RUNTIME_CONFIG_PATH, {
@@ -95,7 +95,7 @@ describe("Lab3TerminalFrame", () => {
await waitFor(() => {
expect(screen.getByTitle("Lab 3 terminal session")).toHaveAttribute(
"src",
"http://127.0.0.1:7681/wetty",
"http://localhost:7681/wetty",
);
});
expect(
+11 -6
View File
@@ -2,7 +2,10 @@
import { useEffect, useRef, useState } from "react";
import { fetchLab3RuntimeConfig, normalizeLab3RuntimeConfig } from "~/lib/lab3-runtime";
import {
fetchCoursewareRuntimeConfig,
normalizeCoursewareRuntimeConfig,
} from "~/lib/courseware-runtime";
type Lab3TerminalFrameProps = {
srcPath?: string;
@@ -14,18 +17,20 @@ export function Lab3TerminalFrame({ srcPath }: Lab3TerminalFrameProps) {
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
const [isConfigResolved, setIsConfigResolved] = useState(Boolean(srcPath));
const [runtimeConfig, setRuntimeConfig] = useState(() => {
return normalizeLab3RuntimeConfig(
return normalizeCoursewareRuntimeConfig(
srcPath ? { lab3TerminalUrl: srcPath } : undefined,
);
});
const terminalPath = runtimeConfig.terminalPath;
const terminalPath = runtimeConfig.lab3TerminalUrl;
useEffect(() => {
let isCancelled = false;
if (srcPath) {
setRuntimeConfig(normalizeLab3RuntimeConfig({ lab3TerminalUrl: srcPath }));
setRuntimeConfig(
normalizeCoursewareRuntimeConfig({ lab3TerminalUrl: srcPath }),
);
setIsConfigResolved(true);
return;
}
@@ -33,14 +38,14 @@ export function Lab3TerminalFrame({ srcPath }: Lab3TerminalFrameProps) {
setIsConfigResolved(false);
setStatus("loading");
void fetchLab3RuntimeConfig()
void fetchCoursewareRuntimeConfig()
.then((nextConfig) => {
if (isCancelled) return;
setRuntimeConfig(nextConfig);
})
.catch(() => {
if (isCancelled) return;
setRuntimeConfig(normalizeLab3RuntimeConfig());
setRuntimeConfig(normalizeCoursewareRuntimeConfig());
})
.finally(() => {
if (isCancelled) return;
+39
View File
@@ -0,0 +1,39 @@
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { LabContent } from "~/components/labs/LabContent";
describe("LabContent", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("renders the Lab 1 widget tokens into interactive components", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({
lab1NetronUrl: "http://127.0.0.1:8338",
lab3TerminalUrl: "http://127.0.0.1:7681/wetty",
}),
{ status: 200 },
),
);
render(
<LabContent
className="lab-content"
html={[
"<div data-lab1-netron-panel></div>",
"<div data-tokenizer-playground></div>",
"<div data-lab1-confidence></div>",
].join("")}
/>,
);
await waitFor(() => {
expect(screen.getByRole("link", { name: "Open Netron" })).toBeInTheDocument();
});
expect(screen.getByText("Tokenizer Playground")).toBeInTheDocument();
expect(screen.getByText("Visualize token confidence locally")).toBeInTheDocument();
});
});
+19 -1
View File
@@ -1,10 +1,13 @@
"use client";
import { Fragment, useEffect, useRef, useState } from "react";
import { Lab1ConfidenceChat } from "~/components/labs/Lab1ConfidenceChat";
import { Lab1NetronPanel } from "~/components/labs/Lab1NetronPanel";
import { Lab3TerminalFrame } from "~/components/labs/Lab3TerminalFrame";
import { Objective5Chat } from "~/components/labs/Objective5Chat";
import { QuantizationGridExplorer } from "~/components/labs/QuantizationGridExplorer";
import { QuantizationExplorer } from "~/components/labs/QuantizationExplorer";
import { TokenizerPlaygroundEmbed } from "~/components/labs/TokenizerPlaygroundEmbed";
type LabContentProps = {
className: string;
@@ -35,6 +38,9 @@ const quantizationGridExplorerToken =
"<div data-quantization-grid-explorer></div>";
const objective5ChatToken = "<div data-objective5-chat></div>";
const lab3TerminalToken = "<div data-lab3-terminal></div>";
const lab1ConfidenceToken = "<div data-lab1-confidence></div>";
const lab1NetronToken = "<div data-lab1-netron-panel></div>";
const tokenizerPlaygroundToken = "<div data-tokenizer-playground></div>";
function looksLikeCliCommand(commandText: string, className: string) {
if (cliLanguagePattern.test(className)) return true;
@@ -199,7 +205,7 @@ export function LabContent({ className, html }: LabContentProps) {
const renderedContent = html
.split(
new RegExp(
`(${escapeRegex(quantizationExplorerToken)}|${escapeRegex(quantizationGridExplorerToken)}|${escapeRegex(objective5ChatToken)}|${escapeRegex(lab3TerminalToken)})`,
`(${escapeRegex(quantizationExplorerToken)}|${escapeRegex(quantizationGridExplorerToken)}|${escapeRegex(objective5ChatToken)}|${escapeRegex(lab3TerminalToken)}|${escapeRegex(lab1ConfidenceToken)}|${escapeRegex(lab1NetronToken)}|${escapeRegex(tokenizerPlaygroundToken)})`,
"g",
),
)
@@ -225,6 +231,18 @@ export function LabContent({ className, html }: LabContentProps) {
return <Lab3TerminalFrame key={`lab3-terminal-${index}`} />;
}
if (part === lab1ConfidenceToken) {
return <Lab1ConfidenceChat key={`lab1-confidence-${index}`} />;
}
if (part === lab1NetronToken) {
return <Lab1NetronPanel key={`lab1-netron-${index}`} />;
}
if (part === tokenizerPlaygroundToken) {
return <TokenizerPlaygroundEmbed key={`tokenizer-playground-${index}`} />;
}
return (
<Fragment key={`html-segment-${index}`}>
<div dangerouslySetInnerHTML={{ __html: part }} />
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
type LabIFrameEmbedProps = {
eyebrow: string;
heading: string;
lede: string;
src: string;
title: string;
};
export function LabIFrameEmbed({
eyebrow,
heading,
lede,
src,
title,
}: LabIFrameEmbedProps) {
const [hasLoaded, setHasLoaded] = useState(false);
const [hasErrored, setHasErrored] = useState(false);
return (
<section className="lab-iframe-embed" data-widget-enhanced="true">
<div className="lab-iframe-embed__header">
<p className="lab-iframe-embed__eyebrow">{eyebrow}</p>
<h3>{heading}</h3>
<p className="lab-iframe-embed__lede">{lede}</p>
</div>
<div className="lab-iframe-embed__actions">
<a
className="lab-iframe-embed__link"
href={src}
rel="noreferrer"
target="_blank"
>
Open in New Tab
</a>
<span className="lab-iframe-embed__status" aria-live="polite">
{hasErrored
? "The embedded view is unavailable right now."
: hasLoaded
? "The embedded tool is ready below."
: "Loading the embedded tool..."}
</span>
</div>
<div className="lab-iframe-embed__frame-shell">
<iframe
className="lab-iframe-embed__frame"
loading="lazy"
onError={() => setHasErrored(true)}
onLoad={() => setHasLoaded(true)}
referrerPolicy="no-referrer"
src={src}
title={title}
/>
</div>
<p className="lab-iframe-embed__fallback">
If the embedded tool does not appear, open it in a new tab and continue
from there.
</p>
</section>
);
}
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TokenizerPlaygroundEmbed } from "~/components/labs/TokenizerPlaygroundEmbed";
describe("TokenizerPlaygroundEmbed", () => {
it("renders the iframe wrapper and fallback link", () => {
render(<TokenizerPlaygroundEmbed />);
expect(screen.getByTitle("Tokenizer playground")).toHaveAttribute(
"src",
"https://xenova-the-tokenizer-playground.static.hf.space",
);
expect(screen.getByRole("link", { name: "Open in New Tab" })).toHaveAttribute(
"href",
"https://xenova-the-tokenizer-playground.static.hf.space",
);
});
});
@@ -0,0 +1,18 @@
"use client";
import { LabIFrameEmbed } from "~/components/labs/LabIFrameEmbed";
const TOKENIZER_PLAYGROUND_URL =
"https://xenova-the-tokenizer-playground.static.hf.space";
export function TokenizerPlaygroundEmbed() {
return (
<LabIFrameEmbed
eyebrow="Lab 1 Tokenization"
heading="Tokenizer Playground"
lede="Try several prompts, compare how the text is segmented, and then inspect the token IDs that the model will actually consume."
src={TOKENIZER_PLAYGROUND_URL}
title="Tokenizer playground"
/>
);
}
+94
View File
@@ -0,0 +1,94 @@
export const COURSEWARE_RUNTIME_CONFIG_PATH = "/courseware-runtime.json";
export const LAB1_DEFAULT_NETRON_URL = "http://127.0.0.1:8338";
export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty";
export type CoursewareRuntimeConfig = {
lab1NetronUrl?: string;
lab3TerminalUrl?: string;
};
export type ResolvedCoursewareRuntimeConfig = {
lab1NetronUrl: string;
lab3TerminalUrl: string;
};
const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1"]);
function rewriteLoopbackHost(urlValue: string, currentHostname?: string) {
try {
const url = new URL(urlValue);
if (!currentHostname || !loopbackHosts.has(url.hostname)) {
return url.toString();
}
url.hostname = currentHostname;
return url.toString();
} catch {
return urlValue;
}
}
function getCurrentHostname() {
if (typeof window === "undefined") {
return undefined;
}
const hostname = window.location.hostname?.trim();
return hostname || undefined;
}
export function getLab1NetronUrl(
envValue?: string,
currentHostname = getCurrentHostname(),
) {
const trimmedValue = envValue?.trim();
if (!trimmedValue) {
return rewriteLoopbackHost(LAB1_DEFAULT_NETRON_URL, currentHostname);
}
return rewriteLoopbackHost(trimmedValue, currentHostname);
}
export function getLab3TerminalPath(
envValue?: string,
currentHostname = getCurrentHostname(),
) {
const trimmedValue = envValue?.trim();
if (!trimmedValue) {
return LAB3_DEFAULT_TERMINAL_PATH;
}
if (/^https?:\/\//i.test(trimmedValue)) {
return rewriteLoopbackHost(trimmedValue, currentHostname);
}
return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`;
}
export function normalizeCoursewareRuntimeConfig(
config?: CoursewareRuntimeConfig,
currentHostname = getCurrentHostname(),
): ResolvedCoursewareRuntimeConfig {
return {
lab1NetronUrl: getLab1NetronUrl(config?.lab1NetronUrl, currentHostname),
lab3TerminalUrl: getLab3TerminalPath(
config?.lab3TerminalUrl,
currentHostname,
),
};
}
export async function fetchCoursewareRuntimeConfig() {
const response = await fetch(COURSEWARE_RUNTIME_CONFIG_PATH, {
cache: "no-store",
});
if (!response.ok) {
throw new Error(`Runtime config request failed: ${response.status}`);
}
const config = (await response.json()) as CoursewareRuntimeConfig;
return normalizeCoursewareRuntimeConfig(config);
}
+85
View File
@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import {
extractLab1AssistantContent,
extractLab1ResponseTokens,
formatProbabilityPercent,
getConfidenceBand,
logprobToProbabilityPercent,
} from "~/lib/lab1-confidence";
describe("logprobToProbabilityPercent", () => {
it("converts a logprob into a rounded percentage", () => {
expect(logprobToProbabilityPercent(Math.log(0.4))).toBe(40);
});
});
describe("extractLab1AssistantContent", () => {
it("reads assistant content from an OpenAI-compatible response", () => {
expect(
extractLab1AssistantContent({
choices: [
{
message: {
content: "hello from the local model",
},
},
],
}),
).toBe("hello from the local model");
});
});
describe("extractLab1ResponseTokens", () => {
it("maps token logprobs and alternate candidates into display data", () => {
expect(
extractLab1ResponseTokens({
choices: [
{
logprobs: {
content: [
{
logprob: Math.log(0.4),
token: "often",
top_logprobs: [
{ logprob: Math.log(0.4), token: "often" },
{ logprob: Math.log(0.14), token: "commonly" },
{ logprob: Math.log(0.1), token: "also" },
],
},
],
},
},
],
}),
).toEqual([
{
logprob: Math.log(0.4),
probability: 40,
token: "often",
topAlternatives: [
{ probability: 14, token: "commonly" },
{ probability: 10, token: "also" },
],
},
]);
});
});
describe("getConfidenceBand", () => {
it("assigns a stable band for each probability range", () => {
expect(getConfidenceBand(75)).toBe("very-high");
expect(getConfidenceBand(45)).toBe("high");
expect(getConfidenceBand(20)).toBe("medium");
expect(getConfidenceBand(7)).toBe("low");
expect(getConfidenceBand(1)).toBe("very-low");
});
});
describe("formatProbabilityPercent", () => {
it("formats probability values for tooltip display", () => {
expect(formatProbabilityPercent(40)).toBe("40.0%");
expect(formatProbabilityPercent(4.2)).toBe("4.20%");
expect(formatProbabilityPercent(0.456)).toBe("0.456%");
});
});
+172
View File
@@ -0,0 +1,172 @@
export const LAB1_CONFIDENCE_MODEL_ALIAS = "lab1-qwen3-0.6b-q8_0";
export const LAB1_DEFAULT_MAX_TOKENS = 64;
export const LAB1_DEFAULT_TEMPERATURE = 0.7;
export const LAB1_MAX_CONTEXT_MESSAGES = 10;
export const LAB1_MAX_MESSAGE_LENGTH = 4000;
export type Lab1ConfidenceRole = "assistant" | "user";
export type Lab1ConfidenceMessage = {
content: string;
role: Lab1ConfidenceRole;
};
export type Lab1TopAlternative = {
probability: number;
token: string;
};
export type Lab1ResponseToken = {
logprob: number;
probability: number;
token: string;
topAlternatives: Lab1TopAlternative[];
};
export type Lab1ConfidenceResponse = {
content: string;
model: string;
role: "assistant";
tokens: Lab1ResponseToken[];
};
type OpenAiLogprobAlternative = {
logprob?: number;
token?: string;
};
type OpenAiLogprobToken = {
logprob?: number;
token?: string;
top_logprobs?: OpenAiLogprobAlternative[];
};
type OpenAiCompatibilityPayload = {
choices?: Array<{
logprobs?: {
content?: OpenAiLogprobToken[];
};
message?: {
content?: string;
};
}>;
model?: string;
};
export function getLab1SystemPrompt() {
return [
"You are helping students inspect token-level confidence in a local language model.",
"Reply clearly and concisely.",
"Prefer one compact paragraph unless the user explicitly asks for a list.",
].join(" ");
}
export function clampLab1Messages(messages: Lab1ConfidenceMessage[]) {
return messages
.filter((message) => {
return (
(message.role === "assistant" || message.role === "user") &&
typeof message.content === "string"
);
})
.map((message) => {
return {
content: message.content.slice(0, LAB1_MAX_MESSAGE_LENGTH),
role: message.role,
} satisfies Lab1ConfidenceMessage;
})
.slice(-LAB1_MAX_CONTEXT_MESSAGES);
}
export function logprobToProbabilityPercent(logprob: number) {
if (!Number.isFinite(logprob)) {
return 0;
}
return roundProbability(Math.exp(logprob) * 100);
}
export function formatProbabilityPercent(probability: number) {
if (probability >= 10) {
return `${probability.toFixed(1)}%`;
}
if (probability >= 1) {
return `${probability.toFixed(2)}%`;
}
if (probability > 0) {
return `${probability.toFixed(3)}%`;
}
return "0%";
}
export function getConfidenceBand(probability: number) {
if (probability >= 70) return "very-high";
if (probability >= 40) return "high";
if (probability >= 15) return "medium";
if (probability >= 5) return "low";
return "very-low";
}
export function extractLab1AssistantContent(payload: OpenAiCompatibilityPayload) {
const content = payload.choices?.[0]?.message?.content?.trim();
return content || null;
}
export function extractLab1ResponseTokens(
payload: OpenAiCompatibilityPayload,
): Lab1ResponseToken[] {
const rawTokens = payload.choices?.[0]?.logprobs?.content;
if (!Array.isArray(rawTokens)) {
return [];
}
return rawTokens.flatMap((rawToken) => {
const token = rawToken.token ?? "";
const logprob = rawToken.logprob;
if (!token || typeof logprob !== "number" || !Number.isFinite(logprob)) {
return [];
}
const topAlternatives = Array.isArray(rawToken.top_logprobs)
? rawToken.top_logprobs
.flatMap((candidate) => {
const candidateToken = candidate.token ?? "";
const candidateLogprob = candidate.logprob;
if (
!candidateToken ||
candidateToken === token ||
typeof candidateLogprob !== "number" ||
!Number.isFinite(candidateLogprob)
) {
return [];
}
return [
{
probability: logprobToProbabilityPercent(candidateLogprob),
token: candidateToken,
} satisfies Lab1TopAlternative,
];
})
.slice(0, 5)
: [];
return [
{
logprob,
probability: logprobToProbabilityPercent(logprob),
token,
topAlternatives,
} satisfies Lab1ResponseToken,
];
});
}
function roundProbability(value: number) {
return Math.max(0, Math.min(100, Math.round(value * 100) / 100));
}
+20 -24
View File
@@ -1,43 +1,39 @@
export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty";
export const LAB3_RUNTIME_CONFIG_PATH = "/courseware-runtime.json";
import {
COURSEWARE_RUNTIME_CONFIG_PATH,
LAB3_DEFAULT_TERMINAL_PATH,
fetchCoursewareRuntimeConfig,
getLab3TerminalPath as getSharedLab3TerminalPath,
normalizeCoursewareRuntimeConfig,
type CoursewareRuntimeConfig,
} from "~/lib/courseware-runtime";
export type Lab3RuntimeConfig = {
lab3TerminalUrl?: string;
};
export const LAB3_RUNTIME_CONFIG_PATH = COURSEWARE_RUNTIME_CONFIG_PATH;
export { LAB3_DEFAULT_TERMINAL_PATH };
export type Lab3RuntimeConfig = Pick<CoursewareRuntimeConfig, "lab3TerminalUrl">;
export type ResolvedLab3RuntimeConfig = {
terminalPath: string;
};
export function getLab3TerminalPath(envValue?: string) {
const trimmedValue = envValue?.trim();
if (!trimmedValue) {
return LAB3_DEFAULT_TERMINAL_PATH;
}
if (/^https?:\/\//i.test(trimmedValue)) {
return trimmedValue;
}
return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`;
return getSharedLab3TerminalPath(envValue);
}
export function normalizeLab3RuntimeConfig(
config?: Lab3RuntimeConfig,
): ResolvedLab3RuntimeConfig {
const runtimeConfig = normalizeCoursewareRuntimeConfig(config);
return {
terminalPath: getLab3TerminalPath(config?.lab3TerminalUrl),
terminalPath: runtimeConfig.lab3TerminalUrl,
};
}
export async function fetchLab3RuntimeConfig() {
const response = await fetch(LAB3_RUNTIME_CONFIG_PATH, { cache: "no-store" });
const runtimeConfig = await fetchCoursewareRuntimeConfig();
if (!response.ok) {
throw new Error(`Runtime config request failed: ${response.status}`);
}
const config = (await response.json()) as Lab3RuntimeConfig;
return normalizeLab3RuntimeConfig(config);
return {
terminalPath: runtimeConfig.lab3TerminalUrl,
};
}
+315
View File
@@ -1747,3 +1747,318 @@ ol {
height: 18rem;
}
}
.lab-content [data-lab1-confidence],
.lab-content [data-lab1-netron-panel],
.lab-content [data-tokenizer-playground] {
display: block;
margin: 1.75rem 0;
}
.lab-content .lab-screenshot-placeholder {
margin: 1.25rem 0;
border: 1px dashed #b8c6d6;
border-radius: 1rem;
background:
linear-gradient(135deg, rgba(0, 78, 120, 0.04), rgba(248, 156, 39, 0.08));
padding: 1rem 1.1rem;
color: #345;
}
.lab-content .lab-screenshot-placeholder strong {
display: block;
margin-bottom: 0.35rem;
color: #004e78;
}
.lab1-netron-panel,
.lab-iframe-embed,
.lab1-confidence {
border: 1px solid #d7e2ee;
border-radius: 1.25rem;
background: linear-gradient(180deg, #fff 0%, #f7fafc 100%);
box-shadow: 0 18px 40px -34px rgba(0, 78, 120, 0.55);
padding: 1.35rem;
}
.lab1-netron-panel h3,
.lab-iframe-embed h3,
.lab1-confidence h3 {
margin: 0;
color: #12344d;
}
.lab1-netron-panel__eyebrow,
.lab-iframe-embed__eyebrow,
.lab1-confidence__eyebrow {
margin: 0 0 0.35rem;
color: #0f5c8b;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.lab1-netron-panel__lede,
.lab-iframe-embed__lede,
.lab1-confidence__lede {
margin: 0.55rem 0 0;
color: #53687b;
}
.lab1-netron-panel__actions,
.lab-iframe-embed__actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.lab1-netron-panel__primary,
.lab-iframe-embed__link,
.lab1-confidence__composer button,
.lab1-confidence__prompt-chip {
border-radius: 999px;
border: 1px solid #0f5c8b;
background: #0f5c8b;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 600;
padding: 0.68rem 1.05rem;
text-decoration: none;
transition:
transform 120ms ease,
box-shadow 120ms ease,
background-color 120ms ease;
}
.lab1-netron-panel__primary:hover,
.lab-iframe-embed__link:hover,
.lab1-confidence__composer button:hover,
.lab1-confidence__prompt-chip: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 {
color: #63788d;
font-size: 0.92rem;
}
.lab-iframe-embed__frame-shell {
margin-top: 1rem;
overflow: hidden;
border: 1px solid #d7e2ee;
border-radius: 1rem;
background: #fff;
}
.lab-iframe-embed__frame {
display: block;
width: 100%;
min-height: 560px;
border: 0;
background: #f8fafc;
}
.lab1-confidence__header {
display: grid;
gap: 0.2rem;
}
.lab1-confidence__prompt-row {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
margin-top: 1rem;
}
.lab1-confidence__prompt-chip {
background: #f3f8fc;
border-color: #d1dfeb;
color: #0f5c8b;
}
.lab1-confidence__transcript {
display: grid;
gap: 1rem;
margin-top: 1rem;
}
.lab1-confidence__empty,
.lab1-confidence__message {
border: 1px solid #d7e2ee;
border-radius: 1rem;
background: #fff;
padding: 1rem;
}
.lab1-confidence__message--user {
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.lab1-confidence__message-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
color: #456;
font-size: 0.92rem;
font-weight: 600;
}
.lab1-confidence__message-meta code {
color: #0f5c8b;
}
.lab1-confidence__message-body {
margin: 0.75rem 0 0;
white-space: pre-wrap;
}
.lab1-confidence__token-stream {
margin-top: 0.85rem;
padding: 0.9rem;
border-radius: 0.95rem;
background: #f8fbfd;
border: 1px solid #e0e8f0;
line-height: 2;
white-space: pre-wrap;
}
.lab1-confidence__token {
position: relative;
border-radius: 0.42rem;
padding: 0.12rem 0.08rem;
transition: filter 120ms ease;
}
.lab1-confidence__token:hover {
filter: saturate(1.05);
}
.lab1-confidence__token--very-high {
background: rgba(88, 185, 102, 0.3);
}
.lab1-confidence__token--high {
background: rgba(149, 209, 102, 0.26);
}
.lab1-confidence__token--medium {
background: rgba(242, 220, 96, 0.26);
}
.lab1-confidence__token--low {
background: rgba(246, 171, 82, 0.24);
}
.lab1-confidence__token--very-low {
background: rgba(233, 117, 89, 0.24);
}
.lab1-confidence__tooltip {
position: absolute;
left: 0;
top: calc(100% + 0.45rem);
z-index: 5;
display: none;
min-width: 180px;
max-width: 260px;
border: 1px solid #d7e2ee;
border-radius: 0.85rem;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 18px 38px -26px rgba(17, 44, 73, 0.7);
color: #24384c;
padding: 0.7rem 0.8rem;
white-space: normal;
}
.lab1-confidence__token:hover .lab1-confidence__tooltip,
.lab1-confidence__token:focus-visible .lab1-confidence__tooltip {
display: block;
}
.lab1-confidence__tooltip strong {
display: block;
color: #0f5c8b;
}
.lab1-confidence__tooltip-list {
display: grid;
gap: 0.22rem;
margin-top: 0.35rem;
font-size: 0.88rem;
}
.lab1-confidence__composer {
display: grid;
gap: 0.7rem;
margin-top: 1rem;
}
.lab1-confidence__composer-label {
color: #12344d;
font-weight: 700;
}
.lab1-confidence__composer textarea {
width: 100%;
min-height: 110px;
border: 1px solid #cedbe8;
border-radius: 1rem;
padding: 0.95rem 1rem;
font: inherit;
resize: vertical;
}
.lab1-confidence__composer textarea:focus {
outline: 2px solid rgba(15, 92, 139, 0.18);
outline-offset: 1px;
}
.lab1-confidence__composer-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.lab1-confidence__composer-state {
display: grid;
gap: 0.18rem;
}
.lab1-confidence__composer-state span {
color: #63788d;
font-size: 0.88rem;
}
.lab1-confidence__composer-state strong {
color: #12344d;
}
.lab1-confidence__message-warning,
.lab1-confidence__error {
color: #b54731;
}
@media (max-width: 768px) {
.lab-iframe-embed__actions,
.lab1-confidence__composer-actions,
.lab1-netron-panel__actions,
.lab1-confidence__message-meta {
align-items: stretch;
flex-direction: column;
}
.lab-iframe-embed__frame {
min-height: 440px;
}
}