1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-08 17:06:42 +00:00

total cost tracker

This commit is contained in:
Laura
2025-08-23 18:07:53 +02:00
parent 9bf526fd01
commit 480e955910
5 changed files with 60 additions and 17 deletions

View File

@@ -30,7 +30,6 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode
## TODO ## TODO
- total cost tracker
- settings - settings
- auto-retry on edit - auto-retry on edit
- ctrl+enter vs enter for sending - ctrl+enter vs enter for sending

View File

@@ -73,29 +73,26 @@ body.resizing * {
cursor: grabbing !important; cursor: grabbing !important;
} }
#total,
#version { #version {
position: absolute; position: absolute;
font-size: 12px; font-size: 12px;
font-style: italic; font-style: italic;
top: 3px; top: 3px;
right: 6px; right: 4px;
color: #a5adcb; color: #a5adcb;
} }
#total {
right: unset;
left: 4px;
}
#version a { #version a {
color: #a5adcb; color: #a5adcb;
text-decoration: none; text-decoration: none;
} }
body.loading #version {
font-size: 0;
animation: rotating 1.2s linear infinite;
background-image: url(icons/spinner.svg);
width: 16px;
height: 16px;
top: 6px;
}
#loading { #loading {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@@ -16,6 +16,7 @@
<title>whiskr</title> <title>whiskr</title>
</head> </head>
<body class="loading"> <body class="loading">
<div id="total" title="Accumulated total cost, middle-click to reset"></div>
<div id="version"></div> <div id="version"></div>
<div id="loading"> <div id="loading">

View File

@@ -1,5 +1,6 @@
(() => { (() => {
const $version = document.getElementById("version"), const $version = document.getElementById("version"),
$total = document.getElementById("total"),
$messages = document.getElementById("messages"), $messages = document.getElementById("messages"),
$chat = document.getElementById("chat"), $chat = document.getElementById("chat"),
$message = document.getElementById("message"), $message = document.getElementById("message"),
@@ -41,7 +42,14 @@
let searchAvailable = false, let searchAvailable = false,
activeMessage = null, activeMessage = null,
isResizing = false, isResizing = false,
scrollResize = false; scrollResize = false,
totalCost = 0;
function updateTotalCost() {
storeValue("total-cost", totalCost);
$total.textContent = formatMoney(totalCost);
}
function updateScrollButton() { function updateScrollButton() {
const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight); const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
@@ -448,8 +456,7 @@
if (this.#statistics) { if (this.#statistics) {
const { provider, model, ttft, time, input, output, cost } = this.#statistics; const { provider, model, ttft, time, input, output, cost } = this.#statistics;
const tps = output / (time / 1000), const tps = output / (time / 1000);
price = cost < 1 ? `${fixed(cost * 100, 1)}ct` : `$${fixed(cost, 2)}`;
html = [ html = [
provider ? `<div class="provider">${provider} (${model.split("/").pop()})</div>` : "", provider ? `<div class="provider">${provider} (${model.split("/").pop()})</div>` : "",
@@ -462,7 +469,7 @@
= =
<div class="total">${input + output}t</div> <div class="total">${input + output}t</div>
</div>`, </div>`,
`<div class="cost">${price}</div>`, `<div class="cost">${formatMoney(cost)}</div>`,
].join(""); ].join("");
} }
@@ -587,6 +594,10 @@
} }
this.setStatistics(data); this.setStatistics(data);
totalCost += data.cost;
updateTotalCost();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -1030,6 +1041,11 @@
// start icon preload // start icon preload
preloadIcons(data.icons); preloadIcons(data.icons);
// render total cost
totalCost = loadValue("total-cost", 0);
updateTotalCost();
// render version // render version
if (data.version === "dev") { if (data.version === "dev") {
$version.remove(); $version.remove();
@@ -1223,6 +1239,16 @@
return message; return message;
} }
$total.addEventListener("auxclick", (event) => {
if (event.button !== 1) {
return;
}
totalCost = 0;
updateTotalCost();
});
$messages.addEventListener("scroll", () => { $messages.addEventListener("scroll", () => {
updateScrollButton(); updateScrollButton();
}); });
@@ -1244,7 +1270,7 @@
$resizeBar.addEventListener("mousedown", event => { $resizeBar.addEventListener("mousedown", event => {
const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight) <= 10; const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight) <= 10;
if (event.buttons === 4) { if (event.button === 1) {
$chat.style.height = ""; $chat.style.height = "";
storeValue("resized", false); storeValue("resized", false);
@@ -1252,7 +1278,7 @@
scroll(isAtBottom, true); scroll(isAtBottom, true);
return; return;
} else if (event.buttons !== 1) { } else if (event.button !== 0) {
return; return;
} }

View File

@@ -84,6 +84,26 @@ function fixed(num, decimals = 0) {
return num.toFixed(decimals).replace(/\.?0+$/m, ""); return num.toFixed(decimals).replace(/\.?0+$/m, "");
} }
function formatMoney(num) {
if (num === 0) {
return "0ct";
}
if (num < 1) {
let decimals = 1;
if (num < 0.0001) {
decimals = 3;
} else if (num < 0.001) {
decimals = 2;
}
return `${fixed(num * 100, decimals)}ct`;
}
return `$${fixed(num, 2)}`;
}
function clamp(num, min, max) { function clamp(num, min, max) {
return Math.min(Math.max(num, min), max); return Math.min(Math.max(num, min), max);
} }