Compare commits
No commits in common. "6ee09e215ee8a17efc54270ba7b6e0d8b7e94a5c" and "ab481d54d7275517c60e7ae0971d6d117549dd43" have entirely different histories.
6ee09e215e
...
ab481d54d7
13 changed files with 146 additions and 180 deletions
100
README.md
100
README.md
|
|
@ -1,49 +1,65 @@
|
||||||
# token-contribution
|
# dsh-plugin-token-heatmap
|
||||||
|
|
||||||
> GitHub-style daily token usage contribution calendar, model analytics, and timeline trends for [DeepSeek Harness](https://github.com/deepseek-ai/dsh).
|
Visualizes your daily token consumption, prompt intensity, cache hit efficiency, and streaks across all sessions in a 52-week contribution calendar.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Install with a single command via the official DSH CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dsh plugin --profile web add token-contribution
|
|
||||||
```
|
|
||||||
|
|
||||||
*(To remove later: `dsh plugin --profile web remove token-contribution`)*
|
|
||||||
|
|
||||||
### Manual Alternative (pnpm)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/.dsh/profiles/web
|
|
||||||
pnpm add token-contribution
|
|
||||||
```
|
|
||||||
Then add `"token-contribution"` to `bundles` in `~/.dsh/profiles/web/package.json`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="./assets/preview-overview.png" alt="Token Heatmap Overview" width="44%" />
|
|
||||||
|
|
||||||
<img src="./assets/preview-trends.png" alt="Model Usage Breakdown & Trends" width="44%" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **52-Week GitHub Contribution Calendar:** 7 rows × 52 columns tracking daily token activity with a smooth monochromatic gradient anchored on DeepSeek's official brand blue (`#3964FE`).
|
- **52-Week GitHub Contribution Calendar:** 7 rows × 52 columns tracking daily token activity with 4-level color intensity.
|
||||||
- **Interactive Day Inspector:** Click on any day to slide open an inspection drawer showing every session active on that date, exact timestamps, and token breakdown.
|
- **Detailed Summary Cards:**
|
||||||
- **Top Model Usage Breakdown:** Real-time distribution showing tokens, percentages, and request steps for every model used across sessions.
|
- **Total Tokens:** Complete volume of input, output, and cached tokens.
|
||||||
- **Token Usage Trends Timeline:** Stacked bar chart comparing daily Billed Tokens vs. Cache Reads, with **14D / 30D / 90D** range filters and hover tooltips.
|
- **Cache Hit Ratio:** Percentage of tokens served from KV cache reads.
|
||||||
- **Global & Per-Session Filtering:** Toggle between aggregate global history or inspect token activity for individual sessions.
|
- **Daily Average & Active Days:** Real workload metrics.
|
||||||
- **Auto-Expanding Panel:** Smoothly widens the DSH Settings dialog to 1180px so the calendar and charts fit cleanly with zero scrollbars.
|
- **Streak Tracker:** Current and longest active day streaks.
|
||||||
- **Session Token Meter Integration:** Injects a clean action button into the in-session token usage dialog to open the heatmap directly from active sessions.
|
- **Peak Day Callout:** Date and token count for your heaviest day.
|
||||||
- **High-Performance Parser:** Recursively aggregates local `.jsonl.zstd` logs with timestamp and mtime caching for instant loading.
|
- **Dual Color Palette:** Switch between **GitHub Emerald** and **DeepSeek Indigo**.
|
||||||
|
- **Interactive Tooltips:** Hover over any day to see token breakdown (billed input/output vs. cache reads and step counts).
|
||||||
|
- **Session Token Meter Integration:** Integrates into session stats pills with a *"View 12-Month Activity Heatmap"* button that opens the interactive modal dialog anywhere.
|
||||||
|
- **High-Performance Parser:** Aggregates zstandard (`.jsonl.zstd`) session logs with timestamp and mtime caching for near-instant rendering.
|
||||||
|
- **DeepSeek Native Theme:** Automatically adapts to DSH dark and light themes using `--dsw-alias-*` design tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Method 1: Using pnpm in your DSH Web profile
|
||||||
|
|
||||||
|
Inside your DSH profile directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.dsh/profiles/web
|
||||||
|
pnpm add dsh-plugin-token-heatmap
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add `"dsh-plugin-token-heatmap"` to `~/.dsh/profiles/web/package.json` under `dsh.profile.bundles`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dsh": {
|
||||||
|
"profile": {
|
||||||
|
"bundles": [
|
||||||
|
"@deepseek-ai/dsh-base",
|
||||||
|
"@deepseek-ai/dsh-web-app",
|
||||||
|
"dsh-plugin-token-heatmap"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Local Development Link
|
||||||
|
|
||||||
|
Clone the repository locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/oiupoyt/dsh-plugin-token-heatmap.git ~/projects/dsh-plugin-token-heatmap
|
||||||
|
cd ~/projects/dsh-plugin-token-heatmap
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
cd ~/.dsh/profiles/web
|
||||||
|
pnpm add --link ~/projects/dsh-plugin-token-heatmap
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -51,10 +67,10 @@ Then add `"token-contribution"` to `bundles` in `~/.dsh/profiles/web/package.jso
|
||||||
|
|
||||||
1. Open the DeepSeek Harness Web GUI (`http://127.0.0.1:3080`).
|
1. Open the DeepSeek Harness Web GUI (`http://127.0.0.1:3080`).
|
||||||
2. Go to **Settings** (gear icon at the bottom left) → **Token Heatmap**.
|
2. Go to **Settings** (gear icon at the bottom left) → **Token Heatmap**.
|
||||||
3. Or during any session, click on the token stats pill at the bottom of the composer and tap **"View Token Usage Heatmap →"**.
|
3. Or during any session, click on the token stats pill at the bottom of the composer to view token details and tap **"View 12-Month Activity Heatmap →"**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT © [oiupoyt](https://git.oiupoyt.space/oiupoyt)
|
MIT © [oiupoyt](https://github.com/oiupoyt)
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 229 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 178 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
# token-contribution bundle patch
|
# dsh-token-heatmap bundle patch
|
||||||
- insert:
|
- insert:
|
||||||
- id: token-contribution
|
- id: token-heatmap
|
||||||
name: 'token-contribution'
|
name: 'dsh-token-heatmap'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
window.__ModuleLoader__.load({ id: "token-contribution", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
|
window.__ModuleLoader__.load({ id: "dsh-token-heatmap", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
|
||||||
var __create = Object.create;
|
var __create = Object.create;
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
|
@ -72,20 +72,16 @@ function HeatmapInnerView() {
|
||||||
const [hoveredDay, setHoveredDay] = (0, import_react.useState)(null);
|
const [hoveredDay, setHoveredDay] = (0, import_react.useState)(null);
|
||||||
const [hoveredBar, setHoveredBar] = (0, import_react.useState)(null);
|
const [hoveredBar, setHoveredBar] = (0, import_react.useState)(null);
|
||||||
const [tooltipPos, setTooltipPos] = (0, import_react.useState)(null);
|
const [tooltipPos, setTooltipPos] = (0, import_react.useState)(null);
|
||||||
const rootRef = (0, import_react.useRef)(null);
|
|
||||||
(0, import_react.useEffect)(() => {
|
(0, import_react.useEffect)(() => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
const findPanel = () => {
|
const panel = document.querySelector('[class*="_panel"]');
|
||||||
return rootRef.current?.closest('[class*="_panel"]') || document.querySelector(".VOzbGW_panel") || document.querySelector('[class*="_panel"]');
|
|
||||||
};
|
|
||||||
const panel = findPanel();
|
|
||||||
if (panel) {
|
if (panel) {
|
||||||
const prevWidth = panel.style.width;
|
const prevWidth = panel.style.width;
|
||||||
const prevMaxWidth = panel.style.maxWidth;
|
const prevMaxWidth = panel.style.maxWidth;
|
||||||
const prevTransition = panel.style.transition;
|
const prevTransition = panel.style.transition;
|
||||||
panel.style.transition = "width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease";
|
panel.style.transition = "width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease";
|
||||||
panel.style.setProperty("width", "min(1180px, calc(100vw - 32px))", "important");
|
panel.style.width = "min(1080px, calc(100vw - 32px))";
|
||||||
panel.style.setProperty("max-width", "calc(100vw - 32px)", "important");
|
panel.style.maxWidth = "calc(100vw - 32px)";
|
||||||
return () => {
|
return () => {
|
||||||
panel.style.width = prevWidth;
|
panel.style.width = prevWidth;
|
||||||
panel.style.maxWidth = prevMaxWidth;
|
panel.style.maxWidth = prevMaxWidth;
|
||||||
|
|
@ -95,7 +91,7 @@ function HeatmapInnerView() {
|
||||||
}, []);
|
}, []);
|
||||||
const fetchStats = () => {
|
const fetchStats = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch("/api/token-contribution/stats").then((r) => {
|
fetch("/api/dsh-token-heatmap/stats").then((r) => {
|
||||||
if (!r.ok) throw new Error(`HTTP error ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP error ${r.status}`);
|
||||||
return r.json();
|
return r.json();
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
|
@ -298,7 +294,6 @@ function HeatmapInnerView() {
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
"data-dsh-heatmap": "true",
|
"data-dsh-heatmap": "true",
|
||||||
ref: rootRef,
|
|
||||||
style: {
|
style: {
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -452,10 +447,10 @@ function HeatmapInnerView() {
|
||||||
calendarGrid && /* @__PURE__ */ import_react.default.createElement("div", { style: { width: "100%", display: "flex", flexDirection: "column", alignItems: "center" } }, /* @__PURE__ */ import_react.default.createElement(
|
calendarGrid && /* @__PURE__ */ import_react.default.createElement("div", { style: { width: "100%", display: "flex", flexDirection: "column", alignItems: "center" } }, /* @__PURE__ */ import_react.default.createElement(
|
||||||
"svg",
|
"svg",
|
||||||
{
|
{
|
||||||
viewBox: "0 0 880 136",
|
viewBox: "0 0 770 120",
|
||||||
style: {
|
style: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "880px",
|
maxWidth: "770px",
|
||||||
height: "auto",
|
height: "auto",
|
||||||
display: "block",
|
display: "block",
|
||||||
overflow: "visible"
|
overflow: "visible"
|
||||||
|
|
@ -465,35 +460,35 @@ function HeatmapInnerView() {
|
||||||
"text",
|
"text",
|
||||||
{
|
{
|
||||||
key: i,
|
key: i,
|
||||||
x: 32 + m.col * 15.6,
|
x: 28 + m.col * 13.8,
|
||||||
y: 11,
|
y: 10,
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
fill: "var(--dsw-alias-label-tertiary, #888888)",
|
fill: "var(--dsw-alias-label-tertiary, #888888)",
|
||||||
fontFamily: "inherit"
|
fontFamily: "inherit"
|
||||||
},
|
},
|
||||||
m.label
|
m.label
|
||||||
)),
|
)),
|
||||||
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 45, fontSize: 10, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Mon"),
|
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 39, fontSize: 9, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Mon"),
|
||||||
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 76, fontSize: 10, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Wed"),
|
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 67, fontSize: 9, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Wed"),
|
||||||
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 107, fontSize: 10, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Fri"),
|
/* @__PURE__ */ import_react.default.createElement("text", { x: 2, y: 95, fontSize: 9, fill: "var(--dsw-alias-label-tertiary, #888888)", fontFamily: "inherit" }, "Fri"),
|
||||||
calendarGrid.map((week, colIdx) => /* @__PURE__ */ import_react.default.createElement("g", { key: colIdx, transform: `translate(${32 + colIdx * 15.6}, 18)` }, week.map((day, rowIdx) => {
|
calendarGrid.map((week, colIdx) => /* @__PURE__ */ import_react.default.createElement("g", { key: colIdx, transform: `translate(${28 + colIdx * 13.8}, 18)` }, week.map((day, rowIdx) => {
|
||||||
if (!day) return null;
|
if (!day) return null;
|
||||||
const lvl = day.stats.level || 0;
|
const lvl = day.stats.level || 0;
|
||||||
const bg = DEEPSEEK_LEVELS[lvl];
|
const bg = DEEPSEEK_LEVELS[lvl];
|
||||||
const isSelected = selectedDate === day.date;
|
const isSelected = selectedDate === day.date;
|
||||||
const isHovered = hoveredDay?.date === day.date;
|
const isHovered = hoveredDay?.date === day.date;
|
||||||
const strokeColor = isSelected ? "#ffffff" : isHovered ? "#5686fe" : lvl === 0 ? "rgba(255, 255, 255, 0.06)" : "transparent";
|
const strokeColor = isSelected ? "#ffffff" : isHovered ? "#5686fe" : lvl === 0 ? "rgba(255, 255, 255, 0.06)" : "transparent";
|
||||||
const strokeWidth = isSelected ? 1.8 : isHovered ? 1.4 : 1;
|
const strokeWidth = isSelected ? 1.6 : isHovered ? 1.3 : 1;
|
||||||
return /* @__PURE__ */ import_react.default.createElement(
|
return /* @__PURE__ */ import_react.default.createElement(
|
||||||
"rect",
|
"rect",
|
||||||
{
|
{
|
||||||
key: rowIdx,
|
key: rowIdx,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: rowIdx * 15.6,
|
y: rowIdx * 14,
|
||||||
width: 12,
|
width: 11,
|
||||||
height: 12,
|
height: 11,
|
||||||
rx: 2.5,
|
rx: 2,
|
||||||
ry: 2.5,
|
ry: 2,
|
||||||
fill: bg,
|
fill: bg,
|
||||||
stroke: strokeColor,
|
stroke: strokeColor,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
|
|
@ -522,7 +517,7 @@ function HeatmapInnerView() {
|
||||||
{
|
{
|
||||||
style: {
|
style: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "880px",
|
maxWidth: "770px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
|
@ -537,9 +532,9 @@ function HeatmapInnerView() {
|
||||||
{
|
{
|
||||||
key: i,
|
key: i,
|
||||||
style: {
|
style: {
|
||||||
width: 12,
|
width: 11,
|
||||||
height: 12,
|
height: 11,
|
||||||
borderRadius: 2.5,
|
borderRadius: 2,
|
||||||
backgroundColor: c,
|
backgroundColor: c,
|
||||||
border: i === 0 ? "1px solid rgba(255, 255, 255, 0.06)" : "none",
|
border: i === 0 ? "1px solid rgba(255, 255, 255, 0.06)" : "none",
|
||||||
filter: i === 4 ? "drop-shadow(0 0 3px rgba(79, 133, 255, 0.5))" : "none"
|
filter: i === 4 ? "drop-shadow(0 0 3px rgba(79, 133, 255, 0.5))" : "none"
|
||||||
|
|
@ -1117,7 +1112,7 @@ function HeatmapModal({ open, onClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/client/index.tsx
|
// src/client/index.tsx
|
||||||
var name = "token-contribution-client";
|
var name = "dsh-plugin-token-heatmap-client";
|
||||||
var inject = ["slots"];
|
var inject = ["slots"];
|
||||||
function apply(ctx) {
|
function apply(ctx) {
|
||||||
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
||||||
|
|
@ -1125,9 +1120,9 @@ function apply(ctx) {
|
||||||
return ctx.slots.register(
|
return ctx.slots.register(
|
||||||
{
|
{
|
||||||
name: "settings.section",
|
name: "settings.section",
|
||||||
id: "token-contribution",
|
id: "token-heatmap",
|
||||||
order: 14,
|
order: 14,
|
||||||
label: () => "Token Contribution",
|
label: () => "Token Heatmap",
|
||||||
icon: "chart"
|
icon: "chart"
|
||||||
},
|
},
|
||||||
HeatmapView
|
HeatmapView
|
||||||
|
|
@ -1140,34 +1135,18 @@ function apply(ctx) {
|
||||||
(0, import_react3.useEffect)(() => {
|
(0, import_react3.useEffect)(() => {
|
||||||
const handleOpen = () => setIsOpen(true);
|
const handleOpen = () => setIsOpen(true);
|
||||||
const handleClose = () => setIsOpen(false);
|
const handleClose = () => setIsOpen(false);
|
||||||
window.addEventListener("dsh:open-token-contribution", handleOpen);
|
|
||||||
window.addEventListener("dsh:open-token-heatmap", handleOpen);
|
window.addEventListener("dsh:open-token-heatmap", handleOpen);
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") handleClose();
|
if (e.key === "Escape") handleClose();
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("dsh:open-token-contribution", handleOpen);
|
|
||||||
window.removeEventListener("dsh:open-token-heatmap", handleOpen);
|
window.removeEventListener("dsh:open-token-heatmap", handleOpen);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return /* @__PURE__ */ import_react3.default.createElement(HeatmapModal, { open: isOpen, onClose: () => setIsOpen(false) });
|
return /* @__PURE__ */ import_react3.default.createElement(HeatmapModal, { open: isOpen, onClose: () => setIsOpen(false) });
|
||||||
};
|
};
|
||||||
if (!document.getElementById("dsh-token-heatmap-panel-resize")) {
|
|
||||||
const resizeStyle = document.createElement("style");
|
|
||||||
resizeStyle.id = "dsh-token-heatmap-panel-resize";
|
|
||||||
resizeStyle.textContent = `
|
|
||||||
/* Dynamically widen DSH Settings panel when Token Heatmap is active */
|
|
||||||
.VOzbGW_panel:has([data-dsh-heatmap]),
|
|
||||||
[class*="_panel"]:has([data-dsh-heatmap]) {
|
|
||||||
width: min(1180px, calc(100vw - 32px)) !important;
|
|
||||||
max-width: calc(100vw - 32px) !important;
|
|
||||||
transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(resizeStyle);
|
|
||||||
}
|
|
||||||
const modalContainer = document.createElement("div");
|
const modalContainer = document.createElement("div");
|
||||||
modalContainer.id = "token-contribution-modal-root";
|
modalContainer.id = "dsh-token-heatmap-modal-root";
|
||||||
document.body.appendChild(modalContainer);
|
document.body.appendChild(modalContainer);
|
||||||
const root = (0, import_client.createRoot)(modalContainer);
|
const root = (0, import_client.createRoot)(modalContainer);
|
||||||
root.render(/* @__PURE__ */ import_react3.default.createElement(ModalController, null));
|
root.render(/* @__PURE__ */ import_react3.default.createElement(ModalController, null));
|
||||||
|
|
@ -1182,7 +1161,7 @@ function apply(ctx) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.className = "dsh-heatmap-trigger-btn";
|
button.className = "dsh-heatmap-trigger-btn";
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.innerHTML = "<span>View Token Contribution \u2192</span>";
|
button.innerHTML = "<span>View Token Usage Heatmap \u2192</span>";
|
||||||
button.style.cssText = `
|
button.style.cssText = `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1220,16 +1199,16 @@ function apply(ctx) {
|
||||||
settingsTrigger.click();
|
settingsTrigger.click();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const heatmapTab = Array.from(document.querySelectorAll("*")).find(
|
const heatmapTab = Array.from(document.querySelectorAll("*")).find(
|
||||||
(el) => (el.textContent?.trim() === "Token Contribution" || el.textContent?.trim() === "Token Heatmap") && (el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item"))
|
(el) => el.textContent?.trim() === "Token Heatmap" && (el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item"))
|
||||||
);
|
);
|
||||||
if (heatmapTab) {
|
if (heatmapTab) {
|
||||||
heatmapTab.click();
|
heatmapTab.click();
|
||||||
} else {
|
} else {
|
||||||
window.dispatchEvent(new CustomEvent("dsh:open-token-contribution"));
|
window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
|
||||||
}
|
}
|
||||||
}, 120);
|
}, 120);
|
||||||
} else {
|
} else {
|
||||||
window.dispatchEvent(new CustomEvent("dsh:open-token-contribution"));
|
window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
dialogContainer.appendChild(button);
|
dialogContainer.appendChild(button);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,10 +1,10 @@
|
||||||
const name = "token-contribution";
|
const name = "dsh-plugin-token-heatmap";
|
||||||
const inject = ["webServer"];
|
const inject = ["webServer"];
|
||||||
function apply(ctx) {
|
function apply(ctx) {
|
||||||
ctx.effect(
|
ctx.effect(
|
||||||
() => ctx.webServer.register({
|
() => ctx.webServer.register({
|
||||||
kind: "prefix",
|
kind: "prefix",
|
||||||
path: "/api/token-contribution",
|
path: "/api/dsh-token-heatmap",
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
const pathname = url.pathname.replace(/\/+$/, "");
|
const pathname = url.pathname.replace(/\/+$/, "");
|
||||||
|
|
@ -16,7 +16,7 @@ function apply(ctx) {
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (req.method === "GET" && (pathname === "/api/token-contribution" || pathname === "/api/token-contribution/stats" || pathname === "/api/dsh-token-heatmap/stats")) {
|
if (req.method === "GET" && (pathname === "/api/dsh-token-heatmap" || pathname === "/api/dsh-token-heatmap/stats")) {
|
||||||
try {
|
try {
|
||||||
const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);
|
const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);
|
||||||
const data = await aggregateTokens();
|
const data = await aggregateTokens();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../src/server/index.ts"],
|
"sources": ["../src/server/index.ts"],
|
||||||
"sourcesContent": ["export const name = \"token-contribution\";\nexport const inject = [\"webServer\"];\n\nexport function apply(ctx: any) {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: \"prefix\",\n path: \"/api/token-contribution\",\n handler: async (req: any, res: any) => {\n const url = new URL(req.url, \"http://localhost\");\n const pathname = url.pathname.replace(/\\/+$/, \"\");\n\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (req.method === \"GET\" && (pathname === \"/api/token-contribution\" || pathname === \"/api/token-contribution/stats\" || pathname === \"/api/dsh-token-heatmap/stats\")) {\n try {\n const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);\n const data = await aggregateTokens();\n res.writeHead(200, {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-cache\",\n });\n res.end(JSON.stringify(data));\n } catch (err: any) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: err?.message || \"Failed to aggregate token stats\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n },\n })\n );\n}\n"],
|
"sourcesContent": ["export const name = \"dsh-plugin-token-heatmap\";\nexport const inject = [\"webServer\"];\n\nexport function apply(ctx: any) {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: \"prefix\",\n path: \"/api/dsh-token-heatmap\",\n handler: async (req: any, res: any) => {\n const url = new URL(req.url, \"http://localhost\");\n const pathname = url.pathname.replace(/\\/+$/, \"\");\n\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (req.method === \"GET\" && (pathname === \"/api/dsh-token-heatmap\" || pathname === \"/api/dsh-token-heatmap/stats\")) {\n try {\n const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);\n const data = await aggregateTokens();\n res.writeHead(200, {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-cache\",\n });\n res.end(JSON.stringify(data));\n } catch (err: any) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: err?.message || \"Failed to aggregate token stats\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n },\n })\n );\n}\n"],
|
||||||
"mappings": "AAAO,MAAM,OAAO;AACb,MAAM,SAAS,CAAC,WAAW;AAE3B,SAAS,MAAM,KAAU;AAC9B,MAAI;AAAA,IAAO,MACT,IAAI,UAAU,SAAS;AAAA,MACrB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,SAAS,OAAO,KAAU,QAAa;AACrC,cAAM,MAAM,IAAI,IAAI,IAAI,KAAK,kBAAkB;AAC/C,cAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ,EAAE;AAEhD,YAAI,UAAU,+BAA+B,GAAG;AAChD,YAAI,UAAU,gCAAgC,cAAc;AAC5D,YAAI,UAAU,gCAAgC,cAAc;AAE5D,YAAI,IAAI,WAAW,WAAW;AAC5B,cAAI,UAAU,GAAG;AACjB,cAAI,IAAI;AACR;AAAA,QACF;AAEA,YAAI,IAAI,WAAW,UAAU,aAAa,6BAA6B,aAAa,mCAAmC,aAAa,iCAAiC;AACnK,cAAI;AACF,kBAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,qBAAqB,KAAK,IAAI,CAAC;AACxE,kBAAM,OAAO,MAAM,gBAAgB;AACnC,gBAAI,UAAU,KAAK;AAAA,cACjB,gBAAgB;AAAA,cAChB,iBAAiB;AAAA,YACnB,CAAC;AACD,gBAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,UAC9B,SAAS,KAAU;AACjB,gBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,gBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,KAAK,WAAW,kCAAkC,CAAC,CAAC;AAAA,UACtF;AACA;AAAA,QACF;AAEA,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,OAAO,YAAY,CAAC,CAAC;AAAA,MAChD;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
|
"mappings": "AAAO,MAAM,OAAO;AACb,MAAM,SAAS,CAAC,WAAW;AAE3B,SAAS,MAAM,KAAU;AAC9B,MAAI;AAAA,IAAO,MACT,IAAI,UAAU,SAAS;AAAA,MACrB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,SAAS,OAAO,KAAU,QAAa;AACrC,cAAM,MAAM,IAAI,IAAI,IAAI,KAAK,kBAAkB;AAC/C,cAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ,EAAE;AAEhD,YAAI,UAAU,+BAA+B,GAAG;AAChD,YAAI,UAAU,gCAAgC,cAAc;AAC5D,YAAI,UAAU,gCAAgC,cAAc;AAE5D,YAAI,IAAI,WAAW,WAAW;AAC5B,cAAI,UAAU,GAAG;AACjB,cAAI,IAAI;AACR;AAAA,QACF;AAEA,YAAI,IAAI,WAAW,UAAU,aAAa,4BAA4B,aAAa,iCAAiC;AAClH,cAAI;AACF,kBAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,qBAAqB,KAAK,IAAI,CAAC;AACxE,kBAAM,OAAO,MAAM,gBAAgB;AACnC,gBAAI,UAAU,KAAK;AAAA,cACjB,gBAAgB;AAAA,cAChB,iBAAiB;AAAA,YACnB,CAAC;AACD,gBAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,UAC9B,SAAS,KAAU;AACjB,gBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,gBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,KAAK,WAAW,kCAAkC,CAAC,CAAC;AAAA,UACtF;AACA;AAAA,QACF;AAEA,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,OAAO,YAAY,CAAC,CAAC;AAAA,MAChD;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
|
||||||
"names": []
|
"names": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
package.json
15
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "token-contribution",
|
"name": "dsh-token-heatmap",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "GitHub-style token usage contribution calendar and analytics for DeepSeek Harness",
|
"description": "GitHub-style token usage contribution heatmap and analytics for DeepSeek Harness",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -13,8 +13,7 @@
|
||||||
"lib",
|
"lib",
|
||||||
"cordis.patch.yml",
|
"cordis.patch.yml",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE",
|
"LICENSE"
|
||||||
"assets"
|
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
@ -47,10 +46,10 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.oiupoyt.space/oiupoyt/token-contribution.git"
|
"url": "https://git.oiupoyt.space/oiupoyt/dsh-plugin-token-heatmap.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.oiupoyt.space/oiupoyt/token-contribution/issues"
|
"url": "https://git.oiupoyt.space/oiupoyt/dsh-plugin-token-heatmap/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://git.oiupoyt.space/oiupoyt/token-contribution#readme"
|
"homepage": "https://git.oiupoyt.space/oiupoyt/dsh-plugin-token-heatmap#readme"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ console.log("Building server backend...");
|
||||||
execSync("npx esbuild src/server/index.ts src/server/aggregator.ts --outdir=lib --platform=node --format=esm --target=node20 --sourcemap", { stdio: "inherit" });
|
execSync("npx esbuild src/server/index.ts src/server/aggregator.ts --outdir=lib --platform=node --format=esm --target=node20 --sourcemap", { stdio: "inherit" });
|
||||||
|
|
||||||
console.log("Building client frontend bundle...");
|
console.log("Building client frontend bundle...");
|
||||||
const banner = 'window.__ModuleLoader__.load({ id: "token-contribution", factory: (require) => { var module = { exports: {} }; var exports = module.exports;';
|
const banner = 'window.__ModuleLoader__.load({ id: "dsh-token-heatmap", factory: (require) => { var module = { exports: {} }; var exports = module.exports;';
|
||||||
const footer = 'return module.exports; } });';
|
const footer = 'return module.exports; } });';
|
||||||
execSync(`npx esbuild src/client/index.tsx --outfile=lib/client.js --platform=browser --format=cjs --target=es2022 --bundle --external:react --external:react-dom --external:react-dom/client --banner:js='${banner}' --footer:js='${footer}' --sourcemap`, { stdio: "inherit" });
|
execSync(`npx esbuild src/client/index.tsx --outfile=lib/client.js --platform=browser --format=cjs --target=es2022 --bundle --external:react --external:react-dom --external:react-dom/client --banner:js='${banner}' --footer:js='${footer}' --sourcemap`, { stdio: "inherit" });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,28 +116,19 @@ function HeatmapInnerView() {
|
||||||
turns: number;
|
turns: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
|
const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Dynamically enlarge the DSH Settings dialog panel so the calendar fits perfectly without a scrollbar
|
// Auto-resize the settings dialog panel smoothly on mount so it comfortably fits the content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
const findPanel = () => {
|
const panel = document.querySelector('[class*="_panel"]') as HTMLElement | null;
|
||||||
return (
|
|
||||||
rootRef.current?.closest('[class*="_panel"]') ||
|
|
||||||
document.querySelector('.VOzbGW_panel') ||
|
|
||||||
document.querySelector('[class*="_panel"]')
|
|
||||||
) as HTMLElement | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const panel = findPanel();
|
|
||||||
if (panel) {
|
if (panel) {
|
||||||
const prevWidth = panel.style.width;
|
const prevWidth = panel.style.width;
|
||||||
const prevMaxWidth = panel.style.maxWidth;
|
const prevMaxWidth = panel.style.maxWidth;
|
||||||
const prevTransition = panel.style.transition;
|
const prevTransition = panel.style.transition;
|
||||||
|
|
||||||
panel.style.transition = "width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease";
|
panel.style.transition = "width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease";
|
||||||
panel.style.setProperty("width", "min(1180px, calc(100vw - 32px))", "important");
|
panel.style.width = "min(1080px, calc(100vw - 32px))";
|
||||||
panel.style.setProperty("max-width", "calc(100vw - 32px)", "important");
|
panel.style.maxWidth = "calc(100vw - 32px)";
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
panel.style.width = prevWidth;
|
panel.style.width = prevWidth;
|
||||||
|
|
@ -149,7 +140,7 @@ function HeatmapInnerView() {
|
||||||
|
|
||||||
const fetchStats = () => {
|
const fetchStats = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch("/api/token-contribution/stats")
|
fetch("/api/dsh-token-heatmap/stats")
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) throw new Error(`HTTP error ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP error ${r.status}`);
|
||||||
return r.json();
|
return r.json();
|
||||||
|
|
@ -414,7 +405,6 @@ function HeatmapInnerView() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-dsh-heatmap="true"
|
data-dsh-heatmap="true"
|
||||||
ref={rootRef}
|
|
||||||
style={{
|
style={{
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -580,14 +570,14 @@ function HeatmapInnerView() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clean, centered SVG Calendar Grid (Fits 1180px panel with zero scrollbars) */}
|
{/* Clean, centered SVG Calendar Grid (Fits panel smoothly) */}
|
||||||
{calendarGrid && (
|
{calendarGrid && (
|
||||||
<div style={{ width: "100%", display: "flex", flexDirection: "column", alignItems: "center" }}>
|
<div style={{ width: "100%", display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 880 136"
|
viewBox="0 0 770 120"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "880px",
|
maxWidth: "770px",
|
||||||
height: "auto",
|
height: "auto",
|
||||||
display: "block",
|
display: "block",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
|
|
@ -597,9 +587,9 @@ function HeatmapInnerView() {
|
||||||
{monthHeaders.map((m, i) => (
|
{monthHeaders.map((m, i) => (
|
||||||
<text
|
<text
|
||||||
key={i}
|
key={i}
|
||||||
x={32 + m.col * 15.6}
|
x={28 + m.col * 13.8}
|
||||||
y={11}
|
y={10}
|
||||||
fontSize={11}
|
fontSize={10}
|
||||||
fill="var(--dsw-alias-label-tertiary, #888888)"
|
fill="var(--dsw-alias-label-tertiary, #888888)"
|
||||||
fontFamily="inherit"
|
fontFamily="inherit"
|
||||||
>
|
>
|
||||||
|
|
@ -608,19 +598,19 @@ function HeatmapInnerView() {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Day of week labels */}
|
{/* Day of week labels */}
|
||||||
<text x={2} y={45} fontSize={10} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
<text x={2} y={39} fontSize={9} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
||||||
Mon
|
Mon
|
||||||
</text>
|
</text>
|
||||||
<text x={2} y={76} fontSize={10} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
<text x={2} y={67} fontSize={9} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
||||||
Wed
|
Wed
|
||||||
</text>
|
</text>
|
||||||
<text x={2} y={107} fontSize={10} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
<text x={2} y={95} fontSize={9} fill="var(--dsw-alias-label-tertiary, #888888)" fontFamily="inherit">
|
||||||
Fri
|
Fri
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
{/* Day Tiles: comfortable 12px with 15.6px step */}
|
{/* Day Tiles: centered and proportioned */}
|
||||||
{calendarGrid.map((week, colIdx) => (
|
{calendarGrid.map((week, colIdx) => (
|
||||||
<g key={colIdx} transform={`translate(${32 + colIdx * 15.6}, 18)`}>
|
<g key={colIdx} transform={`translate(${28 + colIdx * 13.8}, 18)`}>
|
||||||
{week.map((day, rowIdx) => {
|
{week.map((day, rowIdx) => {
|
||||||
if (!day) return null;
|
if (!day) return null;
|
||||||
|
|
||||||
|
|
@ -638,17 +628,17 @@ function HeatmapInnerView() {
|
||||||
? "rgba(255, 255, 255, 0.06)"
|
? "rgba(255, 255, 255, 0.06)"
|
||||||
: "transparent";
|
: "transparent";
|
||||||
|
|
||||||
const strokeWidth = isSelected ? 1.8 : isHovered ? 1.4 : 1;
|
const strokeWidth = isSelected ? 1.6 : isHovered ? 1.3 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
key={rowIdx}
|
key={rowIdx}
|
||||||
x={0}
|
x={0}
|
||||||
y={rowIdx * 15.6}
|
y={rowIdx * 14}
|
||||||
width={12}
|
width={11}
|
||||||
height={12}
|
height={11}
|
||||||
rx={2.5}
|
rx={2}
|
||||||
ry={2.5}
|
ry={2}
|
||||||
fill={bg}
|
fill={bg}
|
||||||
stroke={strokeColor}
|
stroke={strokeColor}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
|
|
@ -686,7 +676,7 @@ function HeatmapInnerView() {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "880px",
|
maxWidth: "770px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
|
@ -705,9 +695,9 @@ function HeatmapInnerView() {
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
width: 12,
|
width: 11,
|
||||||
height: 12,
|
height: 11,
|
||||||
borderRadius: 2.5,
|
borderRadius: 2,
|
||||||
backgroundColor: c,
|
backgroundColor: c,
|
||||||
border: i === 0 ? "1px solid rgba(255, 255, 255, 0.06)" : "none",
|
border: i === 0 ? "1px solid rgba(255, 255, 255, 0.06)" : "none",
|
||||||
filter: i === 4 ? "drop-shadow(0 0 3px rgba(79, 133, 255, 0.5))" : "none",
|
filter: i === 4 ? "drop-shadow(0 0 3px rgba(79, 133, 255, 0.5))" : "none",
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@ import { createRoot } from "react-dom/client";
|
||||||
import { HeatmapView } from "./HeatmapView.js";
|
import { HeatmapView } from "./HeatmapView.js";
|
||||||
import { HeatmapModal } from "./HeatmapModal.js";
|
import { HeatmapModal } from "./HeatmapModal.js";
|
||||||
|
|
||||||
export const name = "token-contribution-client";
|
export const name = "dsh-plugin-token-heatmap-client";
|
||||||
export const inject = ["slots"];
|
export const inject = ["slots"];
|
||||||
|
|
||||||
export function apply(ctx: any) {
|
export function apply(ctx: any) {
|
||||||
// 1. Register under Settings -> "Token Contribution"
|
// 1. Register under Settings -> "Token Heatmap"
|
||||||
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
||||||
ctx.slots.inject("settings.section", () => {
|
ctx.slots.inject("settings.section", () => {
|
||||||
return ctx.slots.register(
|
return ctx.slots.register(
|
||||||
{
|
{
|
||||||
name: "settings.section",
|
name: "settings.section",
|
||||||
id: "token-contribution",
|
id: "token-heatmap",
|
||||||
order: 14,
|
order: 14,
|
||||||
label: () => "Token Contribution",
|
label: () => "Token Heatmap",
|
||||||
icon: "chart",
|
icon: "chart",
|
||||||
},
|
},
|
||||||
HeatmapView
|
HeatmapView
|
||||||
|
|
@ -25,24 +25,8 @@ export function apply(ctx: any) {
|
||||||
|
|
||||||
// 2. Global Modal Host & In-Session Token Meter Integration
|
// 2. Global Modal Host & In-Session Token Meter Integration
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
// Inject dynamic resizing stylesheet for settings dialog
|
|
||||||
if (!document.getElementById("dsh-token-heatmap-panel-resize")) {
|
|
||||||
const resizeStyle = document.createElement("style");
|
|
||||||
resizeStyle.id = "dsh-token-heatmap-panel-resize";
|
|
||||||
resizeStyle.textContent = `
|
|
||||||
/* Dynamically widen DSH Settings panel when Token Heatmap is active */
|
|
||||||
.VOzbGW_panel:has([data-dsh-heatmap]),
|
|
||||||
[class*="_panel"]:has([data-dsh-heatmap]) {
|
|
||||||
width: min(1180px, calc(100vw - 32px)) !important;
|
|
||||||
max-width: calc(100vw - 32px) !important;
|
|
||||||
transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s ease !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(resizeStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalContainer = document.createElement("div");
|
const modalContainer = document.createElement("div");
|
||||||
modalContainer.id = "token-contribution-modal-root";
|
modalContainer.id = "dsh-token-heatmap-modal-root";
|
||||||
document.body.appendChild(modalContainer);
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
const root = createRoot(modalContainer);
|
const root = createRoot(modalContainer);
|
||||||
|
|
@ -54,14 +38,12 @@ export function apply(ctx: any) {
|
||||||
const handleOpen = () => setIsOpen(true);
|
const handleOpen = () => setIsOpen(true);
|
||||||
const handleClose = () => setIsOpen(false);
|
const handleClose = () => setIsOpen(false);
|
||||||
|
|
||||||
window.addEventListener("dsh:open-token-contribution", handleOpen);
|
|
||||||
window.addEventListener("dsh:open-token-heatmap", handleOpen);
|
window.addEventListener("dsh:open-token-heatmap", handleOpen);
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") handleClose();
|
if (e.key === "Escape") handleClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("dsh:open-token-contribution", handleOpen);
|
|
||||||
window.removeEventListener("dsh:open-token-heatmap", handleOpen);
|
window.removeEventListener("dsh:open-token-heatmap", handleOpen);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -88,7 +70,7 @@ export function apply(ctx: any) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.className = "dsh-heatmap-trigger-btn";
|
button.className = "dsh-heatmap-trigger-btn";
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.innerHTML = "<span>View Token Contribution →</span>";
|
button.innerHTML = "<span>View Token Usage Heatmap →</span>";
|
||||||
button.style.cssText = `
|
button.style.cssText = `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -130,18 +112,18 @@ export function apply(ctx: any) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const heatmapTab = Array.from(document.querySelectorAll("*")).find(
|
const heatmapTab = Array.from(document.querySelectorAll("*")).find(
|
||||||
(el) =>
|
(el) =>
|
||||||
(el.textContent?.trim() === "Token Contribution" || el.textContent?.trim() === "Token Heatmap") &&
|
el.textContent?.trim() === "Token Heatmap" &&
|
||||||
(el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item"))
|
(el.tagName === "BUTTON" || el.tagName === "DIV" || el.getAttribute("role") === "tab" || el.classList.contains("ant-list-item"))
|
||||||
);
|
);
|
||||||
if (heatmapTab) {
|
if (heatmapTab) {
|
||||||
(heatmapTab as HTMLElement).click();
|
(heatmapTab as HTMLElement).click();
|
||||||
} else {
|
} else {
|
||||||
window.dispatchEvent(new CustomEvent("dsh:open-token-contribution"));
|
window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
|
||||||
}
|
}
|
||||||
}, 120);
|
}, 120);
|
||||||
} else {
|
} else {
|
||||||
// Open direct interactive modal
|
// Open direct interactive modal
|
||||||
window.dispatchEvent(new CustomEvent("dsh:open-token-contribution"));
|
window.dispatchEvent(new CustomEvent("dsh:open-token-heatmap"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
export const name = "token-contribution";
|
export const name = "dsh-plugin-token-heatmap";
|
||||||
export const inject = ["webServer"];
|
export const inject = ["webServer"];
|
||||||
|
|
||||||
export function apply(ctx: any) {
|
export function apply(ctx: any) {
|
||||||
ctx.effect(() =>
|
ctx.effect(() =>
|
||||||
ctx.webServer.register({
|
ctx.webServer.register({
|
||||||
kind: "prefix",
|
kind: "prefix",
|
||||||
path: "/api/token-contribution",
|
path: "/api/dsh-token-heatmap",
|
||||||
handler: async (req: any, res: any) => {
|
handler: async (req: any, res: any) => {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
const pathname = url.pathname.replace(/\/+$/, "");
|
const pathname = url.pathname.replace(/\/+$/, "");
|
||||||
|
|
@ -20,7 +20,7 @@ export function apply(ctx: any) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && (pathname === "/api/token-contribution" || pathname === "/api/token-contribution/stats" || pathname === "/api/dsh-token-heatmap/stats")) {
|
if (req.method === "GET" && (pathname === "/api/dsh-token-heatmap" || pathname === "/api/dsh-token-heatmap/stats")) {
|
||||||
try {
|
try {
|
||||||
const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);
|
const { aggregateTokens } = await import(`./aggregator.js?t=${Date.now()}`);
|
||||||
const data = await aggregateTokens();
|
const data = await aggregateTokens();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue