feat: Add NanoGPT credit stats UI (#5537)
* Add NanoGPT credit stats UI * fix lint * fix: type check * fix: migrate inline styles to css * feat: add sub active date display * feat: add link to balance page --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@@ -3633,6 +3633,8 @@
|
||||
</div>
|
||||
<div id="nanogpt_form" data-source="nanogpt">
|
||||
<h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
|
||||
<a href="https://nano-gpt.com/balance" target="_blank" rel="noopener noreferrer" class="nanogpt_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a>
|
||||
<span class="nanogpt_credits_display marginLeft5"></span>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_nanogpt" name="api_key_nanogpt" class="text_pole flex1" value="" type="text" autocomplete="off">
|
||||
<div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_nanogpt"></div>
|
||||
|
||||
@@ -1191,5 +1191,103 @@ export async function initSecrets() {
|
||||
toastr.error(t`Could not fetch OpenRouter credits. Please try again.`);
|
||||
}
|
||||
});
|
||||
|
||||
const formatNanoGptNumber = (num, decimals = null) => {
|
||||
const number = Number(num);
|
||||
if (!Number.isFinite(number)) return decimals === null ? '0' : (0).toFixed(decimals);
|
||||
if (decimals !== null) return number.toFixed(decimals);
|
||||
if (number >= 1000000) return (number / 1000000).toFixed(1) + 'M';
|
||||
if (number >= 1000) return (number / 1000).toFixed(1) + 'K';
|
||||
return number.toString();
|
||||
};
|
||||
|
||||
const createNanoGptCreditsPopup = (credits) => {
|
||||
const root = $('<div class="nanogpt-credits-popup"></div>');
|
||||
root.append($('<h3></h3>').text(t`NanoGPT Credits & Usage`));
|
||||
|
||||
const rows = [
|
||||
[t`USD`, `$${formatNanoGptNumber(credits.usdBalance, 2)}`],
|
||||
[t`NANO`, formatNanoGptNumber(credits.nanoBalance, 3)],
|
||||
];
|
||||
|
||||
const addUsage = (label, usage, limit) => {
|
||||
if (usage) {
|
||||
rows.push([label, t`${formatNanoGptNumber(usage.used)} / ${formatNanoGptNumber(limit)} (${formatNanoGptNumber(usage.remaining)} left)`]);
|
||||
}
|
||||
};
|
||||
|
||||
if (credits.subscription?.active) {
|
||||
const sub = credits.subscription;
|
||||
const subEndDate = sub.period?.currentPeriodEnd ? moment(sub.period.currentPeriodEnd).format('LL') : t`Unknown`;
|
||||
rows.push([t`Sub`, t`Active (until ${subEndDate})`]);
|
||||
addUsage(t`Tokens/wk`, sub.weekly_tokens, sub.limits?.weeklyInputTokens);
|
||||
addUsage(t`Tokens/day`, sub.daily_tokens, sub.limits?.dailyInputTokens);
|
||||
addUsage(t`Images/day`, sub.daily_images, sub.limits?.dailyImages);
|
||||
}
|
||||
|
||||
for (const [label, value] of rows) {
|
||||
root.append($('<div></div>').text(label));
|
||||
root.append($('<div></div>').text(value));
|
||||
}
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
$(document).on('click', '.nanogpt_view_credits', async function (event) {
|
||||
event.preventDefault();
|
||||
const display = $(this).siblings('.nanogpt_credits_display').first();
|
||||
display.empty().text(t`Loading…`);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/nanogpt/credits', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const usdBalance = Number(data.usd_balance);
|
||||
const nanoBalance = Number(data.nano_balance);
|
||||
if (!Number.isFinite(usdBalance) || !Number.isFinite(nanoBalance)) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
|
||||
let balances = [`$${formatNanoGptNumber(usdBalance, 2)}`];
|
||||
if (nanoBalance > 0) {
|
||||
balances.push(`${formatNanoGptNumber(nanoBalance, 3)} NANO`);
|
||||
}
|
||||
let shortInlineText = balances.join(' | ');
|
||||
|
||||
if (data.subscription?.active) {
|
||||
shortInlineText += ` | ${t`Sub Active`}`;
|
||||
}
|
||||
|
||||
display.empty().text(shortInlineText + ' ');
|
||||
|
||||
const infoBtn = $('<i class="fa-solid fa-circle-info cursor-pointer nanogpt_info_btn"></i>');
|
||||
infoBtn.attr('title', t`View details`);
|
||||
infoBtn.data('credits', {
|
||||
usdBalance,
|
||||
nanoBalance,
|
||||
subscription: data.subscription,
|
||||
});
|
||||
display.append(infoBtn);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch NanoGPT credits:', error);
|
||||
display.empty().text('');
|
||||
toastr.error(t`Could not fetch NanoGPT credits. Please try again.`);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.nanogpt_info_btn', async function () {
|
||||
const credits = $(this).data('credits');
|
||||
if (credits) {
|
||||
await callGenericPopup(createNanoGptCreditsPopup(credits), POPUP_TYPE.TEXT);
|
||||
}
|
||||
});
|
||||
registerSecretSlashCommands();
|
||||
}
|
||||
|
||||
@@ -6400,6 +6400,38 @@ body:not(.movingUI) .drawer-content.maximized {
|
||||
background-color: rgba(241, 163, 163, 0.2);
|
||||
}
|
||||
|
||||
.nanogpt_info_btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.8em;
|
||||
min-height: 1.8em;
|
||||
margin-left: 0.15em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nanogpt-credits-popup {
|
||||
display: grid;
|
||||
gap: 0.25em 1em;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nanogpt-credits-popup h3 {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0 0 0.25em;
|
||||
}
|
||||
|
||||
.nanogpt-credits-popup div:nth-of-type(odd) {
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nanogpt-credits-popup div:nth-of-type(even) {
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--interactable-outline-color: CanvasText;
|
||||
|
||||
Reference in New Issue
Block a user