tokviz 0.1.0 → 0.3.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.
@@ -0,0 +1 @@
1
+ :root{--bg:#fff;--bg-secondary:#f5f5f5;--text:#1a1a1a;--text-secondary:#666;--border:#e0e0e0;--accent:#2563eb;--accent-hover:#1d4ed8;--card-bg:#fff;--card-shadow:0 1px 3px #0000001a;--chart-input:#3b82f6;--chart-output:#f59e0b;--chart-cost:#8b5cf6}@media (prefers-color-scheme:dark){:root{--bg:#0a0a0a;--bg-secondary:#1a1a1a;--text:#e5e5e5;--text-secondary:#999;--border:#333;--accent:#3b82f6;--accent-hover:#60a5fa;--card-bg:#1a1a1a;--card-shadow:0 1px 3px #0000004d;--chart-input:#60a5fa;--chart-output:#fbbf24;--chart-cost:#a78bfa}}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;line-height:1.5}.app{max-width:1200px;margin:0 auto;padding:24px}.header{justify-content:space-between;align-items:center;margin-bottom:24px;display:flex}.header h1{align-items:center;gap:8px;font-size:24px;font-weight:600;display:flex}.logo{width:28px;height:28px}.connection-status{color:var(--text-secondary);align-items:center;gap:6px;font-size:13px;display:flex}.status-dot{background:#ef4444;border-radius:50%;width:8px;height:8px}.status-dot.connected{background:#22c55e}.tabs{border-bottom:1px solid var(--border);gap:4px;margin-bottom:24px;display:flex}.tab{color:var(--text-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:8px 16px;font-size:14px;font-weight:500;transition:color .15s,border-color .15s}.tab:hover{color:var(--text)}.tab.active{color:var(--accent);border-bottom-color:var(--accent)}.cards{grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px;display:grid}.card{background:var(--card-bg);border:1px solid var(--border);box-shadow:var(--card-shadow);border-radius:8px;padding:16px}.card-label{text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);margin-bottom:4px;font-size:12px}.card-value{font-size:24px;font-weight:600}.chart-container{background:var(--card-bg);border:1px solid var(--border);box-shadow:var(--card-shadow);border-radius:8px;margin-bottom:24px;padding:20px}.chart-title{color:var(--text-secondary);margin-bottom:16px;font-size:14px;font-weight:500}.chart-controls{justify-content:flex-end;margin-bottom:12px;display:flex}.toggle{cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:8px;display:flex}.toggle-label{color:var(--text-secondary);font-size:13px}.toggle input{display:none}.toggle-track{background:var(--border);border-radius:10px;width:36px;height:20px;transition:background .2s;position:relative}.toggle-track:after{content:"";background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .2s;position:absolute;top:2px;left:2px}.toggle input:checked+.toggle-track{background:var(--accent)}.toggle input:checked+.toggle-track:after{transform:translate(16px)}table{border-collapse:collapse;width:100%;font-size:13px}th{text-align:left;border-bottom:2px solid var(--border);color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;padding:10px 12px;font-size:12px;font-weight:600}td{border-bottom:1px solid var(--border);padding:10px 12px}tr.clickable{cursor:pointer}tr.clickable:hover{background:var(--bg-secondary)}.empty-state{text-align:center;min-height:400px;color:var(--text-secondary);flex-direction:column;justify-content:center;align-items:center;display:flex}.empty-state h2{color:var(--text);margin-bottom:12px;font-size:20px}.empty-state p{max-width:500px;margin-bottom:8px}.empty-state code{background:var(--bg-secondary);border-radius:4px;padding:2px 6px;font-size:13px}.detail-overlay{z-index:100;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.detail-panel{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;width:90%;max-width:900px;max-height:80vh;padding:24px;overflow-y:auto;box-shadow:0 20px 60px #0000004d}.detail-panel h3{margin-bottom:16px;font-size:16px}.detail-panel .close-btn{float:right;cursor:pointer;color:var(--text-secondary);background:0 0;border:none;padding:4px 8px;font-size:20px}.detail-panel .close-btn:hover{color:var(--text)}.attr-table{width:100%}.attr-table td{vertical-align:top;padding:6px 8px}.attr-table td:first-child{white-space:nowrap;width:40%;color:var(--text-secondary);font-weight:500}.attr-table td:last-child{word-break:break-all}.mono{font-family:SF Mono,Fira Code,monospace;font-size:12px}
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect x="2" y="18" width="6" height="12" rx="1.5" fill="#6366f1"/>
3
+ <rect x="10" y="10" width="6" height="20" rx="1.5" fill="#8b5cf6"/>
4
+ <rect x="18" y="4" width="6" height="26" rx="1.5" fill="#a78bfa"/>
5
+ <rect x="26" y="14" width="4" height="16" rx="1.5" fill="#c4b5fd"/>
6
+ <circle cx="5" cy="14" r="2" fill="#6366f1"/>
7
+ <circle cx="13" cy="6" r="2" fill="#8b5cf6"/>
8
+ <circle cx="21" cy="2" r="2" fill="#a78bfa"/>
9
+ <circle cx="28" cy="10" r="2" fill="#c4b5fd"/>
10
+ <line x1="5" y1="14" x2="13" y2="6" stroke="#7c3aed" stroke-width="1.5" stroke-linecap="round"/>
11
+ <line x1="13" y1="6" x2="21" y2="2" stroke="#7c3aed" stroke-width="1.5" stroke-linecap="round"/>
12
+ <line x1="21" y1="2" x2="28" y2="10" stroke="#7c3aed" stroke-width="1.5" stroke-linecap="round"/>
13
+ </svg>
@@ -3,9 +3,10 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
7
  <title>tokviz</title>
7
- <script type="module" crossorigin src="/assets/index-SNGqhtM5.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-R0O4p4a7.css">
8
+ <script type="module" crossorigin src="/assets/index-DjPze0rR.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-YQMK5nkl.css">
9
10
  </head>
10
11
  <body>
11
12
  <div id="root"></div>
@@ -343,17 +343,87 @@ function toFloat(value) {
343
343
  return isNaN(n) ? 0 : n;
344
344
  }
345
345
 
346
+ // src/server/demo.ts
347
+ import { randomBytes } from "crypto";
348
+ var models = [
349
+ { name: "gpt-4o", inputCostPer1k: 25e-4, outputCostPer1k: 0.01 },
350
+ { name: "gpt-4o-mini", inputCostPer1k: 15e-5, outputCostPer1k: 6e-4 },
351
+ { name: "claude-sonnet-4-20250514", inputCostPer1k: 3e-3, outputCostPer1k: 0.015 },
352
+ { name: "claude-3-5-haiku-20241022", inputCostPer1k: 8e-4, outputCostPer1k: 4e-3 },
353
+ { name: "gemini-2.0-flash", inputCostPer1k: 1e-4, outputCostPer1k: 4e-4 }
354
+ ];
355
+ var operations = ["chat", "chat", "chat", "embeddings", "completions"];
356
+ function hexId(bytes) {
357
+ return randomBytes(bytes).toString("hex");
358
+ }
359
+ function randomInt(min, max) {
360
+ return Math.floor(Math.random() * (max - min + 1)) + min;
361
+ }
362
+ function pick(arr) {
363
+ return arr[Math.floor(Math.random() * arr.length)];
364
+ }
365
+ function generateSpan(baseTime) {
366
+ const model = pick(models);
367
+ const operation = pick(operations);
368
+ const now = baseTime ?? Date.now();
369
+ const duration = randomInt(200, 8e3);
370
+ const startTime = now - duration;
371
+ const inputTokens = randomInt(50, 12e3);
372
+ const outputTokens = operation === "embeddings" ? 0 : randomInt(20, 4e3);
373
+ const totalTokens = inputTokens + outputTokens;
374
+ const cost = inputTokens / 1e3 * model.inputCostPer1k + outputTokens / 1e3 * model.outputCostPer1k;
375
+ const spanId = hexId(8);
376
+ const traceId = hexId(16);
377
+ return {
378
+ id: spanId,
379
+ traceId,
380
+ name: `${operation} ${model.name}`,
381
+ startTime,
382
+ endTime: now,
383
+ duration,
384
+ model: model.name,
385
+ requestModel: model.name,
386
+ responseModel: model.name,
387
+ inputTokens,
388
+ outputTokens,
389
+ totalTokens,
390
+ cost,
391
+ operationName: operation,
392
+ attributes: {
393
+ "gen_ai.operation.name": operation,
394
+ "gen_ai.request.model": model.name,
395
+ "gen_ai.response.model": model.name,
396
+ "gen_ai.usage.input_tokens": inputTokens,
397
+ "gen_ai.usage.output_tokens": outputTokens,
398
+ "gen_ai.usage.total_tokens": totalTokens,
399
+ "gen_ai.usage.cost": parseFloat(cost.toFixed(6)),
400
+ "gen_ai.system": model.name.startsWith("gpt") ? "openai" : model.name.startsWith("claude") ? "anthropic" : "google_genai"
401
+ }
402
+ };
403
+ }
404
+ function generateInitialSpans(count = 25) {
405
+ const spans = [];
406
+ const now = Date.now();
407
+ for (let i = 0; i < count; i++) {
408
+ const time = now - randomInt(0, 10 * 6e4);
409
+ spans.push(generateSpan(time));
410
+ }
411
+ return spans.sort((a, b) => a.startTime - b.startTime);
412
+ }
413
+
346
414
  // src/server/index.ts
347
415
  var __filename = fileURLToPath(import.meta.url);
348
416
  var __dirname = dirname(__filename);
349
417
  var { values } = parseArgs({
350
418
  options: {
351
419
  port: { type: "string", default: "4318" },
352
- "no-open": { type: "boolean", default: false }
420
+ "no-open": { type: "boolean", default: false },
421
+ demo: { type: "boolean", default: false }
353
422
  },
354
423
  strict: false
355
424
  });
356
425
  var port = parseInt(values.port || "4318", 10);
426
+ var isDemo = values.demo ?? false;
357
427
  var app = express();
358
428
  var server = createServer(app);
359
429
  var wss = new WebSocketServer({ server, path: "/ws" });
@@ -410,6 +480,24 @@ server.listen(port, () => {
410
480
  console.log(` OTLP endpoint: http://localhost:${port}/v1/traces`);
411
481
  console.log(` Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:${port}
412
482
  `);
483
+ if (isDemo) {
484
+ let scheduleNext2 = function() {
485
+ const delay = 3e3 + Math.random() * 5e3;
486
+ setTimeout(() => {
487
+ const span = generateSpan();
488
+ store.add(span);
489
+ broadcast({ type: "spans", data: [span] });
490
+ scheduleNext2();
491
+ }, delay);
492
+ };
493
+ var scheduleNext = scheduleNext2;
494
+ console.log(" Demo mode: generating synthetic data\n");
495
+ const initialSpans = generateInitialSpans(25);
496
+ for (const span of initialSpans) {
497
+ store.add(span);
498
+ }
499
+ scheduleNext2();
500
+ }
413
501
  if (!values["no-open"]) {
414
502
  open(`http://localhost:${port}`);
415
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokviz",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time, local-only web dashboard that visualizes LLM token usage and costs from standard OpenTelemetry data",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "dev": "concurrently -n server,client \"tsx watch src/server/index.ts --no-open\" \"vite\"",
14
+ "demo": "concurrently -n server,client \"tsx watch src/server/index.ts --no-open --demo\" \"vite\"",
14
15
  "dev:server": "tsx watch src/server/index.ts --no-open",
15
16
  "dev:client": "vite",
16
17
  "build": "npm run build:client && npm run build:server",
@@ -33,14 +34,14 @@
33
34
  "license": "MIT",
34
35
  "repository": {
35
36
  "type": "git",
36
- "url": "https://github.com/waldekmastykarz/tokviz.git"
37
+ "url": "git+https://github.com/waldekmastykarz/tokviz.git"
37
38
  },
38
39
  "engines": {
39
40
  "node": ">=18"
40
41
  },
41
42
  "dependencies": {
42
43
  "express": "^4.21.2",
43
- "open": "^10.1.0",
44
+ "open": "^11.0.0",
44
45
  "protobufjs": "^7.4.0",
45
46
  "ws": "^8.18.0"
46
47
  },
@@ -49,14 +50,14 @@
49
50
  "@types/react": "^19.0.0",
50
51
  "@types/react-dom": "^19.0.0",
51
52
  "@types/ws": "^8.5.13",
52
- "@vitejs/plugin-react": "^4.3.4",
53
+ "@vitejs/plugin-react": "^6.0.1",
53
54
  "concurrently": "^9.1.0",
54
55
  "react": "^19.0.0",
55
56
  "react-dom": "^19.0.0",
56
- "recharts": "^2.15.0",
57
+ "recharts": "^3.8.1",
57
58
  "tsup": "^8.4.0",
58
59
  "tsx": "^4.19.0",
59
- "typescript": "^5.7.0",
60
- "vite": "^6.0.0"
60
+ "typescript": "^6.0.3",
61
+ "vite": "^8.0.0"
61
62
  }
62
63
  }
@@ -1 +0,0 @@
1
- :root{--bg: #ffffff;--bg-secondary: #f5f5f5;--text: #1a1a1a;--text-secondary: #666666;--border: #e0e0e0;--accent: #2563eb;--accent-hover: #1d4ed8;--card-bg: #ffffff;--card-shadow: 0 1px 3px rgba(0, 0, 0, .1);--chart-input: #3b82f6;--chart-output: #f59e0b;--chart-cost: #8b5cf6}@media(prefers-color-scheme:dark){:root{--bg: #0a0a0a;--bg-secondary: #1a1a1a;--text: #e5e5e5;--text-secondary: #999999;--border: #333333;--accent: #3b82f6;--accent-hover: #60a5fa;--card-bg: #1a1a1a;--card-shadow: 0 1px 3px rgba(0, 0, 0, .3);--chart-input: #60a5fa;--chart-output: #fbbf24;--chart-cost: #a78bfa}}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}.app{max-width:1200px;margin:0 auto;padding:24px}.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}.header h1{font-size:24px;font-weight:600}.connection-status{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-secondary)}.status-dot{width:8px;height:8px;border-radius:50%;background:#ef4444}.status-dot.connected{background:#22c55e}.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:24px}.tab{padding:8px 16px;border:none;background:none;color:var(--text-secondary);font-size:14px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}.tab:hover{color:var(--text)}.tab.active{color:var(--accent);border-bottom-color:var(--accent)}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}.card{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:16px;box-shadow:var(--card-shadow)}.card-label{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);margin-bottom:4px}.card-value{font-size:24px;font-weight:600}.chart-container{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:24px;box-shadow:var(--card-shadow)}.chart-title{font-size:14px;font-weight:500;margin-bottom:16px;color:var(--text-secondary)}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:10px 12px;border-bottom:2px solid var(--border);font-weight:600;color:var(--text-secondary);font-size:12px;text-transform:uppercase;letter-spacing:.5px}td{padding:10px 12px;border-bottom:1px solid var(--border)}tr.clickable{cursor:pointer}tr.clickable:hover{background:var(--bg-secondary)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:400px;text-align:center;color:var(--text-secondary)}.empty-state h2{font-size:20px;margin-bottom:12px;color:var(--text)}.empty-state p{max-width:500px;margin-bottom:8px}.empty-state code{background:var(--bg-secondary);padding:2px 6px;border-radius:4px;font-size:13px}.detail-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.detail-panel{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d}.detail-panel h3{margin-bottom:16px;font-size:16px}.detail-panel .close-btn{float:right;border:none;background:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:4px 8px}.detail-panel .close-btn:hover{color:var(--text)}.attr-table{width:100%}.attr-table td{padding:6px 8px;vertical-align:top}.attr-table td:first-child{font-weight:500;white-space:nowrap;width:40%;color:var(--text-secondary)}.attr-table td:last-child{word-break:break-all}.mono{font-family:SF Mono,Fira Code,monospace;font-size:12px}