improves rentry leaderboard function
This commit is contained in:
parent
404ce4fc80
commit
437fe1e720
|
@ -228,8 +228,42 @@ router.post("/maintenance", (req, res) => {
|
||||||
return res.redirect(`/admin/manage`);
|
return res.redirect(`/admin/manage`);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/rentry-stats", (req, res) => {
|
router.get("/download-stats", (_req, res) => {
|
||||||
const users = userStore.getUsers().filter((u) => !u.disabledAt);
|
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 totalTokens = 0;
|
||||||
let totalCost = 0;
|
let totalCost = 0;
|
||||||
|
@ -244,43 +278,63 @@ router.get("/rentry-stats", (req, res) => {
|
||||||
totalPrompts += u.promptCount;
|
totalPrompts += u.promptCount;
|
||||||
totalIps += u.ip.length;
|
totalIps += u.ip.length;
|
||||||
|
|
||||||
|
const getName = (u: User) => {
|
||||||
const id = `...${u.token.slice(-5)}`;
|
const id = `...${u.token.slice(-5)}`;
|
||||||
const name =
|
const banned = !!u.disabledAt;
|
||||||
u.nickname && !req.query.anon
|
let nick = anon || !u.nickname ? "Anonymous" : u.nickname;
|
||||||
? `${u.nickname.slice(0, 16).padEnd(16)} ${id}`
|
|
||||||
: `${"Anonymous".padEnd(16)} ${id}`;
|
if (tableType === "markdown") {
|
||||||
const user = name.padEnd(25);
|
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 prompts = `${u.promptCount} proompts`.padEnd(14);
|
||||||
const ips = `${u.ip.length} IPs`.padEnd(8);
|
const ips = `${u.ip.length} IPs`.padEnd(8);
|
||||||
const tokens = `${sums.prettyUsage} tokens`.padEnd(30);
|
const tokens = `${sums.prettyUsage} tokens`.padEnd(30);
|
||||||
|
const sortField = sort === "prompts" ? u.promptCount : sums.sumTokens;
|
||||||
return { user, prompts, ips, tokens, sort: u.promptCount };
|
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) => {
|
.map(({ user, prompts, ips, tokens }, i) => {
|
||||||
const pos = (i + 1 + ".").padEnd(4);
|
const pos = tableType === "markdown" ? (i + 1 + ".").padEnd(4) : "";
|
||||||
return `${pos} | ${user} | ${prompts} | ${ips} | ${tokens}`;
|
return `${pos}${user} | ${prompts} | ${ips} | ${tokens}`;
|
||||||
});
|
})
|
||||||
|
.slice(0, maxUsers);
|
||||||
|
|
||||||
const strTotalPrompts = `${totalPrompts} proompts`;
|
const strTotalPrompts = `${totalPrompts} proompts`;
|
||||||
const strTotalIps = `${totalIps} IPs`;
|
const strTotalIps = `${totalIps} IPs`;
|
||||||
const strTotalTokens = `${prettyTokens(totalTokens)} tokens`;
|
const strTotalTokens = `${prettyTokens(totalTokens)} tokens`;
|
||||||
const strTotalCost = `US$${totalCost.toFixed(2)} cost`;
|
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(
|
res.setHeader(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
`attachment; filename=proxy-stats-${new Date().toISOString()}.md`
|
`attachment; filename=proxy-stats-${new Date().toISOString()}.md`
|
||||||
);
|
);
|
||||||
res.setHeader("Content-Type", "text/markdown");
|
res.setHeader("Content-Type", "text/markdown");
|
||||||
res.send(doc.join("\n"));
|
res.send(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getSumsForUser(user: User) {
|
function getSumsForUser(user: User) {
|
||||||
|
|
|
@ -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") %>
|
|
@ -18,8 +18,7 @@
|
||||||
<li><a href="/admin/manage/create-user">Create User</a></li>
|
<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/import-users">Import Users</a></li>
|
||||||
<li><a href="/admin/manage/export-users">Export 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
|
<li><a href="/admin/manage/download-stats">Download Rentry Stats</a>
|
||||||
href="/admin/manage/rentry-stats?anon=true">Anonymized</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Maintenance</h3>
|
<h3>Maintenance</h3>
|
||||||
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
|
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
|
||||||
|
|
Loading…
Reference in New Issue