@bradsjm/logprob-visualizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
2
+ <title id="title">Logprob Visualizer favicon</title>
3
+ <desc id="desc">Rounded square with probability bars transitioning from low to high and an accent probability curve</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0%" x2="100%" y1="100%" y2="0%">
6
+ <stop offset="0%" stop-color="#0f291c" />
7
+ <stop offset="100%" stop-color="#153524" />
8
+ </linearGradient>
9
+ <filter id="shadow" x="-15%" y="-15%" width="130%" height="130%">
10
+ <feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#05100a" flood-opacity="0.35" />
11
+ </filter>
12
+ </defs>
13
+ <rect x="4" y="4" width="56" height="56" rx="14" fill="url(#bg)" />
14
+ <g transform="translate(12 14)">
15
+ <rect x="4" y="18" width="8" height="22" rx="3" fill="#e04848" />
16
+ <rect x="17" y="12" width="8" height="28" rx="3" fill="#ffc95c" />
17
+ <rect x="30" y="8" width="8" height="32" rx="3" fill="#abd382" />
18
+ <rect x="43" y="4" width="8" height="36" rx="3" fill="#1ba74a" />
19
+ <path d="M4 28 C13 20.5 22 14 34 9 C40.5 6 47.5 5 51 4" fill="none" stroke="#7a7ddc" stroke-width="3" stroke-linecap="round" />
20
+ <circle cx="4" cy="28" r="3" fill="#7a7ddc" filter="url(#shadow)" />
21
+ <circle cx="22" cy="16" r="3" fill="#7a7ddc" filter="url(#shadow)" />
22
+ <circle cx="40" cy="8" r="3" fill="#7a7ddc" filter="url(#shadow)" />
23
+ <circle cx="51" cy="4" r="3" fill="#7a7ddc" filter="url(#shadow)" />
24
+ </g>
25
+ </svg>
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Logprob Visualizer - See Your Completion Token by Token</title>
7
+ <meta
8
+ name="description"
9
+ content="Explore token-level probabilities from OpenAI Chat Completions with interactive visualizations and branching"
10
+ />
11
+ <meta name="color-scheme" content="light" />
12
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13
+ <link rel="alternate icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
14
+ <link rel="alternate icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
15
+ <link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png" />
16
+ <script type="module" crossorigin src="/assets/index-DEhFxOHf.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-AS3_kbXM.css">
18
+ </head>
19
+
20
+ <body>
21
+ <div id="root"></div>
22
+ </body>
23
+ </html>
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@bradsjm/logprob-visualizer",
3
+ "version": "1.0.0",
4
+ "description": "Local web UI for exploring token-level log probabilities from OpenAI-compatible chat models.",
5
+ "keywords": [
6
+ "logprobs",
7
+ "openai",
8
+ "llm",
9
+ "visualizer",
10
+ "tokens"
11
+ ],
12
+ "homepage": "https://github.com/bradsjm/logprob-visualizer#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/bradsjm/logprob-visualizer/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Jonathan Bradshaw",
19
+ "email": "jb@nrgup.net"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/bradsjm/logprob-visualizer.git"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
29
+ "type": "module",
30
+ "bin": {
31
+ "logprob-visualizer": "./bin/logprob-visualizer.js"
32
+ },
33
+ "files": [
34
+ "bin/logprob-visualizer.js",
35
+ "runtime/server.js",
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ },
42
+ "scripts": {
43
+ "build:dev": "vite build --mode development",
44
+ "build": "vite build",
45
+ "dev": "vite",
46
+ "lint:fix": "eslint . --fix",
47
+ "lint": "eslint .",
48
+ "prepack": "pnpm run build",
49
+ "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"",
50
+ "preview": "vite preview",
51
+ "start": "node ./bin/logprob-visualizer.js",
52
+ "test": "vitest run",
53
+ "typecheck": "tsc -p tsconfig.app.json --noEmit"
54
+ },
55
+ "dependencies": {
56
+ "@radix-ui/react-collapsible": "^1.1.12",
57
+ "@radix-ui/react-dialog": "^1.1.15",
58
+ "@radix-ui/react-label": "^2.1.8",
59
+ "@radix-ui/react-select": "^2.2.6",
60
+ "@radix-ui/react-slider": "^1.3.6",
61
+ "@radix-ui/react-slot": "^1.2.4",
62
+ "@radix-ui/react-switch": "^1.2.6",
63
+ "@radix-ui/react-tooltip": "^1.2.8",
64
+ "@tanstack/react-query": "^5.96.2",
65
+ "class-variance-authority": "^0.7.1",
66
+ "clsx": "^2.1.1",
67
+ "lucide-react": "^0.462.0",
68
+ "react": "^18.3.1",
69
+ "react-dom": "^18.3.1",
70
+ "react-router-dom": "^6.30.3",
71
+ "sonner": "^1.7.4",
72
+ "tailwind-merge": "^2.6.1",
73
+ "tailwindcss-animate": "^1.0.7",
74
+ "use-stick-to-bottom": "^1.1.3"
75
+ },
76
+ "devDependencies": {
77
+ "@eslint/js": "^9.39.4",
78
+ "@tailwindcss/typography": "^0.5.19",
79
+ "@types/node": "^22.19.17",
80
+ "@types/react": "^18.3.28",
81
+ "@types/react-dom": "^18.3.7",
82
+ "@vitejs/plugin-react-swc": "^3.11.0",
83
+ "autoprefixer": "^10.4.27",
84
+ "eslint": "^9.39.4",
85
+ "eslint-plugin-import": "^2.32.0",
86
+ "eslint-plugin-react": "^7.37.5",
87
+ "eslint-plugin-react-hooks": "^5.2.0",
88
+ "eslint-plugin-react-refresh": "^0.4.26",
89
+ "globals": "^17.4.0",
90
+ "jsdom": "^26.1.0",
91
+ "postcss": "^8.5.8",
92
+ "tailwindcss": "^3.4.19",
93
+ "typescript": "^5.9.3",
94
+ "typescript-eslint": "^8.58.0",
95
+ "vite": "^5.4.21",
96
+ "vitest": "^2.1.9"
97
+ }
98
+ }
@@ -0,0 +1,247 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createServer } from "node:http";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const DEFAULT_HOST = "127.0.0.1";
8
+ const DEFAULT_PORT = 8080;
9
+ const DIST_DIR = path.resolve(
10
+ path.dirname(fileURLToPath(import.meta.url)),
11
+ "../dist",
12
+ );
13
+
14
+ const CONTENT_TYPES = {
15
+ ".css": "text/css; charset=utf-8",
16
+ ".html": "text/html; charset=utf-8",
17
+ ".ico": "image/x-icon",
18
+ ".js": "text/javascript; charset=utf-8",
19
+ ".json": "application/json; charset=utf-8",
20
+ ".png": "image/png",
21
+ ".svg": "image/svg+xml",
22
+ ".txt": "text/plain; charset=utf-8",
23
+ };
24
+
25
+ const HELP_TEXT = `Usage: npx @bradsjm/logprob-visualizer [options]
26
+
27
+ Options:
28
+ --host <address> Host to bind the static server (default: 127.0.0.1)
29
+ --port <number> Port to bind the static server (default: 8080)
30
+ --no-open Do not open the browser automatically
31
+ --help Show this help text
32
+ `;
33
+
34
+ function getContentType(filePath) {
35
+ return CONTENT_TYPES[path.extname(filePath)] ?? "application/octet-stream";
36
+ }
37
+
38
+ function getCacheControl(requestPath) {
39
+ if (requestPath === "/" || requestPath.endsWith(".html")) {
40
+ return "no-cache";
41
+ }
42
+
43
+ if (requestPath.startsWith("/assets/")) {
44
+ return "public, max-age=31536000, immutable";
45
+ }
46
+
47
+ return "public, max-age=3600";
48
+ }
49
+
50
+ function isPathInside(parentPath, childPath) {
51
+ const relativePath = path.relative(parentPath, childPath);
52
+ return (
53
+ relativePath !== "" &&
54
+ !relativePath.startsWith("..") &&
55
+ !path.isAbsolute(relativePath)
56
+ );
57
+ }
58
+
59
+ async function loadResponseBody(filePath) {
60
+ return readFile(filePath);
61
+ }
62
+
63
+ async function resolveRequestFile(distDir, requestPath) {
64
+ const requestFilePath = requestPath === "/" ? "/index.html" : requestPath;
65
+ const normalizedPath = path.normalize(requestFilePath).replace(/^(\.\.[/\\])+/, "");
66
+ const candidatePath = path.resolve(distDir, `.${normalizedPath}`);
67
+
68
+ if (candidatePath === distDir || isPathInside(distDir, candidatePath)) {
69
+ try {
70
+ return {
71
+ body: await loadResponseBody(candidatePath),
72
+ filePath: candidatePath,
73
+ statusCode: 200,
74
+ };
75
+ } catch (error) {
76
+ if (error && typeof error === "object" && "code" in error && error.code !== "ENOENT") {
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+
82
+ if (path.extname(requestPath) !== "") {
83
+ return {
84
+ body: Buffer.from("Not Found"),
85
+ filePath: null,
86
+ statusCode: 404,
87
+ };
88
+ }
89
+
90
+ const indexFilePath = path.join(distDir, "index.html");
91
+ return {
92
+ body: await loadResponseBody(indexFilePath),
93
+ filePath: indexFilePath,
94
+ statusCode: 200,
95
+ };
96
+ }
97
+
98
+ export function parseCliArgs(argv) {
99
+ const options = {
100
+ host: DEFAULT_HOST,
101
+ openBrowser: true,
102
+ port: DEFAULT_PORT,
103
+ showHelp: false,
104
+ };
105
+
106
+ for (let index = 0; index < argv.length; index += 1) {
107
+ const arg = argv[index];
108
+
109
+ if (arg === "--help") {
110
+ options.showHelp = true;
111
+ continue;
112
+ }
113
+
114
+ if (arg === "--no-open") {
115
+ options.openBrowser = false;
116
+ continue;
117
+ }
118
+
119
+ if (arg === "--host") {
120
+ const nextArg = argv[index + 1];
121
+ if (!nextArg) {
122
+ throw new Error("Missing value for --host.");
123
+ }
124
+
125
+ options.host = nextArg;
126
+ index += 1;
127
+ continue;
128
+ }
129
+
130
+ if (arg === "--port") {
131
+ const nextArg = argv[index + 1];
132
+ if (!nextArg) {
133
+ throw new Error("Missing value for --port.");
134
+ }
135
+
136
+ const port = Number.parseInt(nextArg, 10);
137
+ if (Number.isNaN(port) || port < 0 || port > 65535) {
138
+ throw new Error(`Invalid port: ${nextArg}`);
139
+ }
140
+
141
+ options.port = port;
142
+ index += 1;
143
+ continue;
144
+ }
145
+
146
+ throw new Error(`Unknown argument: ${arg}`);
147
+ }
148
+
149
+ return options;
150
+ }
151
+
152
+ export function formatHelpText() {
153
+ return HELP_TEXT;
154
+ }
155
+
156
+ export function maybeOpenBrowser(url) {
157
+ const openCommand =
158
+ process.platform === "darwin"
159
+ ? ["open", url]
160
+ : process.platform === "win32"
161
+ ? ["cmd", "/c", "start", "", url]
162
+ : ["xdg-open", url];
163
+
164
+ try {
165
+ const child = spawn(openCommand[0], openCommand.slice(1), {
166
+ detached: true,
167
+ stdio: "ignore",
168
+ });
169
+ child.unref();
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ export function createRequestHandler(distDir = DIST_DIR) {
177
+ return async (request, response) => {
178
+ if (request.method !== "GET" && request.method !== "HEAD") {
179
+ response.writeHead(405, { Allow: "GET, HEAD" });
180
+ response.end();
181
+ return;
182
+ }
183
+
184
+ const requestUrl = new URL(request.url ?? "/", "http://localhost");
185
+
186
+ try {
187
+ const result = await resolveRequestFile(distDir, requestUrl.pathname);
188
+ const contentType =
189
+ result.filePath === null ? "text/plain; charset=utf-8" : getContentType(result.filePath);
190
+ const headers = {
191
+ "Cache-Control": getCacheControl(requestUrl.pathname),
192
+ "Content-Type": contentType,
193
+ };
194
+
195
+ response.writeHead(result.statusCode, headers);
196
+ if (request.method === "HEAD") {
197
+ response.end();
198
+ return;
199
+ }
200
+
201
+ response.end(result.body);
202
+ } catch (error) {
203
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
204
+ response.end(
205
+ error instanceof Error ? error.message : "Unexpected static server error.",
206
+ );
207
+ }
208
+ };
209
+ }
210
+
211
+ export async function startServer({
212
+ distDir = DIST_DIR,
213
+ host = DEFAULT_HOST,
214
+ openBrowser = true,
215
+ port = DEFAULT_PORT,
216
+ } = {}) {
217
+ const server = createServer(createRequestHandler(distDir));
218
+
219
+ await new Promise((resolve, reject) => {
220
+ server.once("error", reject);
221
+ server.listen(port, host, () => {
222
+ server.off("error", reject);
223
+ resolve();
224
+ });
225
+ });
226
+
227
+ const address = server.address();
228
+ if (!address || typeof address === "string") {
229
+ throw new Error("Unable to determine the listening address.");
230
+ }
231
+
232
+ const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
233
+ const url = `http://${displayHost}:${address.port}`;
234
+
235
+ let browserOpened = false;
236
+ if (openBrowser) {
237
+ browserOpened = maybeOpenBrowser(url);
238
+ }
239
+
240
+ return {
241
+ browserOpened,
242
+ host,
243
+ port: address.port,
244
+ server,
245
+ url,
246
+ };
247
+ }