improves rentry leaderboard function

This commit is contained in:
nai-degen 2023-09-10 13:24:39 -05:00
parent 404ce4fc80
commit 437fe1e720
3 changed files with 225 additions and 25 deletions

View File

@ -228,8 +228,42 @@ router.post("/maintenance", (req, res) => {
return res.redirect(`/admin/manage`);
});
router.get("/rentry-stats", (req, res) => {
const users = userStore.getUsers().filter((u) => !u.disabledAt);
router.get("/download-stats", (_req, res) => {
return res.render("admin_download-stats");
});
router.post("/generate-stats", (req, res) => {
const body = req.body;
const valid = z
.object({
anon: z.coerce.boolean().optional().default(false),
sort: z.string().optional().default("prompts"),
maxUsers: z.coerce
.number()
.int()
.min(5)
.max(1000)
.optional()
.default(1000),
tableType: z.enum(["code", "markdown"]).optional().default("markdown"),
format: z
.string()
.optional()
.default("# Stats\n{{header}}\n{{stats}}\n{{time}}"),
})
.strict()
.safeParse(body);
if (!valid.success) {
throw new HttpError(
400,
valid.error.issues.flatMap((issue) => issue.message).join(", ")
);
}
const { anon, sort, format, maxUsers, tableType } = valid.data;
const users = userStore.getUsers();
let totalTokens = 0;
let totalCost = 0;
@ -244,43 +278,63 @@ router.get("/rentry-stats", (req, res) => {
totalPrompts += u.promptCount;
totalIps += u.ip.length;
const id = `...${u.token.slice(-5)}`;
const name =
u.nickname && !req.query.anon
? `${u.nickname.slice(0, 16).padEnd(16)} ${id}`
: `${"Anonymous".padEnd(16)} ${id}`;
const user = name.padEnd(25);
const getName = (u: User) => {
const id = `...${u.token.slice(-5)}`;
const banned = !!u.disabledAt;
let nick = anon || !u.nickname ? "Anonymous" : u.nickname;
if (tableType === "markdown") {
nick = banned ? `~~${nick}~~` : nick;
return `${nick.slice(0, 18)} | ${id}`;
} else {
// Strikethrough doesn't work within code blocks
const dead = !!u.disabledAt ? "[dead] " : "";
nick = `${dead}${nick}`;
return `${nick.slice(0, 18).padEnd(18)} ${id}`.padEnd(27);
}
};
const user = getName(u);
const prompts = `${u.promptCount} proompts`.padEnd(14);
const ips = `${u.ip.length} IPs`.padEnd(8);
const tokens = `${sums.prettyUsage} tokens`.padEnd(30);
return { user, prompts, ips, tokens, sort: u.promptCount };
const sortField = sort === "prompts" ? u.promptCount : sums.sumTokens;
return { user, prompts, ips, tokens, sortField };
})
.sort((a, b) => b.sort - a.sort)
.sort((a, b) => b.sortField - a.sortField)
.map(({ user, prompts, ips, tokens }, i) => {
const pos = (i + 1 + ".").padEnd(4);
return `${pos} | ${user} | ${prompts} | ${ips} | ${tokens}`;
});
const pos = tableType === "markdown" ? (i + 1 + ".").padEnd(4) : "";
return `${pos}${user} | ${prompts} | ${ips} | ${tokens}`;
})
.slice(0, maxUsers);
const strTotalPrompts = `${totalPrompts} proompts`;
const strTotalIps = `${totalIps} IPs`;
const strTotalTokens = `${prettyTokens(totalTokens)} tokens`;
const strTotalCost = `US$${totalCost.toFixed(2)} cost`;
let header = `!!!Note ${users.length} users | ${strTotalPrompts} | ${strTotalIps} | ${strTotalTokens} | ${strTotalCost}`;
const header = `!!!Note ${users.length} users | ${strTotalPrompts} | ${strTotalIps} | ${strTotalTokens} | ${strTotalCost}`;
const time = `\n-> *(as of ${new Date().toISOString()})* <-`;
let table = [];
table.push(lines.join("\n"));
if (valid.data.tableType === "markdown") {
table = ["User||Prompts|IPs|Usage", "---|---|---|---|---", ...table];
} else {
table = ["```text", ...table, "```"];
}
const result = format
.replace("{{header}}", header)
.replace("{{stats}}", table.join("\n"))
.replace("{{time}}", time);
const doc = [];
doc.push("# Stats");
doc.push(header);
doc.push("```");
doc.push(lines.join("\n"));
doc.push("```");
doc.push(` -> *(as of ${new Date().toISOString()})* <-`);
res.setHeader(
"Content-Disposition",
`attachment; filename=proxy-stats-${new Date().toISOString()}.md`
);
res.setHeader("Content-Type", "text/markdown");
res.send(doc.join("\n"));
res.send(result);
});
function getSumsForUser(user: User) {

View File

@ -0,0 +1,147 @@
<%- include("partials/shared_header", { title: "Download Stats - OAI Reverse Proxy Admin" }) %>
<style>
#statsForm {
display: flex;
flex-direction: column;
}
#statsForm div {
display: flex;
flex-direction: row;
margin-bottom: 0.5em;
}
#statsForm div label {
width: 6em;
text-align: right;
margin-right: 1em;
}
#statsForm ul {
margin: 0;
padding-left: 2em;
font-size: 0.8em;
}
#statsForm li {
list-style: none;
}
#statsForm textarea {
font-family: monospace;
flex-grow: 1;
}
</style>
<h1>Download Stats</h1>
<p>
Download usage statistics to a Markdown document. You can paste this into a service like Rentry.org to share it.
</p>
<div>
<h3>Options</h3>
<form id="statsForm" action="/admin/manage/generate-stats" method="post"
style="display: flex; flex-direction: column;">
<input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
<div>
<label for="anon">Anonymize</label>
<input id="anon" type="checkbox" name="anon" value="true" />
</div>
<div>
<label for="sort">Sort</label>
<select id="sort" name="sort">
<option value="tokens" selected>By Token Count</option>
<option value="prompts">By Prompt Count</option>
</select>
</div>
<div>
<label for="maxUsers">Max Users</label>
<input id="maxUsers" type="number" name="maxUsers" value="1000" />
</div>
<div>
<label for="tableType">Table Type</label>
<select id="tableType" name="tableType">
<option value="markdown" selected>Markdown Table</option>
<option value="code">Code Block</option>
</select>
</div>
<div>
<label for="format">Custom Format <ul>
<li><code>{{header}}</code></li>
<li><code>{{stats}}</code></li>
<li><code>{{time}}</code></li>
</ul></label>
<textarea id="format" name="format" rows="10" cols="50" placeholder="{{stats}}" ">
# Stats
{{header}}
{{stats}}
{{time}}
</textarea>
</div>
<div>
<button type=" submit">Download</button>
<button id="copyButton" type="button">Copy to Clipboard</button>
</div>
</form>
</div>
<script>
function loadDefaults() {
const getState = (key) => localStorage.getItem("admin__download-stats__" + key);
const setState = (key, value) => localStorage.setItem("admin__download-stats__" + key, value);
const checkboxes = ["anon"];
const values = ["sort", "format", "tableType", "maxUsers"];
checkboxes.forEach((key) => {
const value = getState(key);
if (value) {
document.getElementById(key).checked = value == "true";
}
document.getElementById(key).addEventListener("change", (e) => {
setState(key, e.target.checked);
});
});
values.forEach((key) => {
const value = getState(key);
if (value) {
document.getElementById(key).value = value;
}
document.getElementById(key).addEventListener("change", (e) => {
setState(key, e.target.value?.trim());
});
});
}
loadDefaults();
async function fetchAndCopy() {
const form = document.getElementById('statsForm');
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'same-origin',
body: new URLSearchParams(formData),
});
if (response.ok) {
const content = await response.text();
copyToClipboard(content);
} else {
console.error('Failed to fetch markdown content');
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard');
}).catch(err => {
alert('Failed to copy to clipboard. Try downloading the file instead.');
});
}
document.getElementById('copyButton').addEventListener('click', fetchAndCopy);
</script>
<%- include("partials/admin-footer") %>

View File

@ -18,8 +18,7 @@
<li><a href="/admin/manage/create-user">Create User</a></li>
<li><a href="/admin/manage/import-users">Import Users</a></li>
<li><a href="/admin/manage/export-users">Export Users</a></li>
<li><a href="/admin/manage/rentry-stats">Download Rentry Stats</a> | <a
href="/admin/manage/rentry-stats?anon=true">Anonymized</a></li>
<li><a href="/admin/manage/download-stats">Download Rentry Stats</a>
</ul>
<h3>Maintenance</h3>
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">