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`);
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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/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">
|
||||
|
|
Loading…
Reference in New Issue