diff --git a/README.md b/README.md index 1df2798..aa168b9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode ## TODO -- total cost tracker - settings - auto-retry on edit - ctrl+enter vs enter for sending diff --git a/static/css/chat.css b/static/css/chat.css index 8045261..03d7840 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -73,29 +73,26 @@ body.resizing * { cursor: grabbing !important; } +#total, #version { position: absolute; font-size: 12px; font-style: italic; top: 3px; - right: 6px; + right: 4px; color: #a5adcb; } +#total { + right: unset; + left: 4px; +} + #version a { color: #a5adcb; 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 { position: absolute; top: 0; diff --git a/static/index.html b/static/index.html index 7162e51..c229a66 100644 --- a/static/index.html +++ b/static/index.html @@ -16,6 +16,7 @@ whiskr +
diff --git a/static/js/chat.js b/static/js/chat.js index 219fd3f..1ce5215 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1,5 +1,6 @@ (() => { const $version = document.getElementById("version"), + $total = document.getElementById("total"), $messages = document.getElementById("messages"), $chat = document.getElementById("chat"), $message = document.getElementById("message"), @@ -41,7 +42,14 @@ let searchAvailable = false, activeMessage = null, isResizing = false, - scrollResize = false; + scrollResize = false, + totalCost = 0; + + function updateTotalCost() { + storeValue("total-cost", totalCost); + + $total.textContent = formatMoney(totalCost); + } function updateScrollButton() { const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight); @@ -448,8 +456,7 @@ if (this.#statistics) { const { provider, model, ttft, time, input, output, cost } = this.#statistics; - const tps = output / (time / 1000), - price = cost < 1 ? `${fixed(cost * 100, 1)}ct` : `$${fixed(cost, 2)}`; + const tps = output / (time / 1000); html = [ provider ? `
${provider} (${model.split("/").pop()})
` : "", @@ -462,7 +469,7 @@ =
${input + output}t
`, - `
${price}
`, + `
${formatMoney(cost)}
`, ].join(""); } @@ -587,6 +594,10 @@ } this.setStatistics(data); + + totalCost += data.cost; + + updateTotalCost(); } catch (err) { console.error(err); @@ -1030,6 +1041,11 @@ // start icon preload preloadIcons(data.icons); + // render total cost + totalCost = loadValue("total-cost", 0); + + updateTotalCost(); + // render version if (data.version === "dev") { $version.remove(); @@ -1223,6 +1239,16 @@ return message; } + $total.addEventListener("auxclick", (event) => { + if (event.button !== 1) { + return; + } + + totalCost = 0; + + updateTotalCost(); + }); + $messages.addEventListener("scroll", () => { updateScrollButton(); }); @@ -1244,7 +1270,7 @@ $resizeBar.addEventListener("mousedown", event => { const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight) <= 10; - if (event.buttons === 4) { + if (event.button === 1) { $chat.style.height = ""; storeValue("resized", false); @@ -1252,7 +1278,7 @@ scroll(isAtBottom, true); return; - } else if (event.buttons !== 1) { + } else if (event.button !== 0) { return; } diff --git a/static/js/lib.js b/static/js/lib.js index 9a7c52e..884b032 100644 --- a/static/js/lib.js +++ b/static/js/lib.js @@ -84,6 +84,26 @@ function fixed(num, decimals = 0) { 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) { return Math.min(Math.max(num, min), max); }