Styling & mobile fixes
This commit is contained in:
parent
1b366f8fc5
commit
2b7a73e818
|
|
@ -1,5 +1,4 @@
|
|||
.modal {
|
||||
max-width: 480px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +60,12 @@
|
|||
color: var(--accent-alt) !important;
|
||||
}
|
||||
|
||||
.nextPlace {
|
||||
.timeCol {
|
||||
color: var(--yellow) !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
|
||||
|
|
@ -69,9 +73,3 @@
|
|||
color: var(--yellow);
|
||||
}
|
||||
}
|
||||
|
||||
.nextPlaceMeta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
.panel {
|
||||
width: 420px;
|
||||
width: 520px;
|
||||
min-width: 320px;
|
||||
max-width: 50dvw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
@media (max-width: 768px) {
|
||||
.panel {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.panel {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
height: fit-content;
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useBool } from "@common/hooks/useBool";
|
|||
import { useHordeState } from "../../contexts/state";
|
||||
import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api";
|
||||
import { formatNumber, formatTime } from "@common/utils";
|
||||
import { calculateTotalKudosPerHour } from "../../utils/calculations";
|
||||
import { Modal } from "@common/components/Modal";
|
||||
import styles from "../../assets/leaderboard-modal.module.css";
|
||||
|
||||
|
|
@ -19,9 +20,7 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
|
|||
|
||||
const ownWorkerIds = new Set(user?.worker_ids ?? []);
|
||||
const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id));
|
||||
const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0);
|
||||
const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0);
|
||||
const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0;
|
||||
const kudosPerHour = calculateTotalKudosPerHour(ownWorkers);
|
||||
|
||||
const [rows, setRows] = useState<(LeaderboardEntry & { rank: number; diff?: number })[]>([]);
|
||||
const loading = useBool(false);
|
||||
|
|
@ -93,17 +92,8 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
|
|||
return () => { cancelled = true; };
|
||||
}, [open, user?.username]);
|
||||
|
||||
const nextRow = rows.length >= 2 ? rows[rows.length - 2] : null;
|
||||
const footer = user && nextRow?.diff && nextRow.diff > 0 && kudosPerHour > 0
|
||||
? (() => {
|
||||
const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600);
|
||||
return (
|
||||
<span class={styles.nextPlace}>
|
||||
Time to #{nextRow.rank}: <strong>{formatTime(secs)}</strong>
|
||||
<span class={styles.nextPlaceMeta}> ({formatNumber(Math.round(kudosPerHour))} kudos/h)</span>
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
const footer = user && kudosPerHour > 0
|
||||
? <span class={styles.footer}>Kudos gathering speed: <strong>{formatNumber(Math.round(kudosPerHour))} kudos/h</strong></span>
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
|
|
@ -118,21 +108,36 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
|
|||
<th>Username</th>
|
||||
<th>Kudos</th>
|
||||
{user && <th>Diff</th>}
|
||||
{user && kudosPerHour > 0 && <th>Time</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.rank} class={r.username === user?.username ? styles.selfRow : undefined}>
|
||||
<td>{r.rank}</td>
|
||||
<td>{r.username}</td>
|
||||
<td>{formatNumber(r.kudos)}</td>
|
||||
{user && (
|
||||
<td class={r.diff !== undefined ? (r.diff > 0 ? styles.above : styles.below) : undefined}>
|
||||
{r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
{rows.map(r => {
|
||||
const remainingSecs = r.diff !== undefined && r.diff > 0 && kudosPerHour > 0
|
||||
? Math.ceil((r.diff / kudosPerHour) * 3600)
|
||||
: undefined;
|
||||
return (
|
||||
<tr key={r.rank} class={r.username === user?.username ? styles.selfRow : undefined}>
|
||||
<td>{r.rank}</td>
|
||||
<td>{r.username}</td>
|
||||
<td>{formatNumber(r.kudos)}</td>
|
||||
{user && (
|
||||
<td class={r.diff !== undefined ? (r.diff > 0 ? styles.above : styles.below) : undefined}>
|
||||
{r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
|
||||
</td>
|
||||
)}
|
||||
{user && kudosPerHour > 0 && (
|
||||
<td class={styles.timeCol}>
|
||||
{r.username === user?.username
|
||||
? '—'
|
||||
: remainingSecs !== undefined
|
||||
? formatTime(remainingSecs)
|
||||
: '—'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from "preact/hooks";
|
||||
import { useHordeState } from "../contexts/state";
|
||||
import { formatNumber, formatTime } from "@common/utils";
|
||||
import { calculateTotalKudosPerHour } from "../utils/calculations";
|
||||
import styles from "../assets/stats-panel.module.css";
|
||||
|
||||
type ModelSortKey = 'name' | 'count' | 'queued' | 'jobs' | 'performance';
|
||||
|
|
@ -16,10 +17,9 @@ export const StatsPanel = () => {
|
|||
const ownWorkerIds = new Set(user?.worker_ids ?? []);
|
||||
const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id));
|
||||
|
||||
// Kudos/hour: sum generated kudos across own workers / total uptime hours
|
||||
// Kudos/hour: sum of per-worker kudos/hour rates
|
||||
const kudosPerHour = calculateTotalKudosPerHour(ownWorkers);
|
||||
const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0);
|
||||
const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0);
|
||||
const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0;
|
||||
|
||||
// Own model names set
|
||||
const ownModelNames = new Set(ownWorkers.flatMap(w => w.models));
|
||||
|
|
@ -27,11 +27,12 @@ export const StatsPanel = () => {
|
|||
// Sort models
|
||||
const sortedModels = [...models].sort((a, b) => {
|
||||
let cmp = 0;
|
||||
if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
|
||||
else if (sortKey === 'count') cmp = a.count - b.count;
|
||||
if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
|
||||
else if (sortKey === 'count') cmp = a.count - b.count;
|
||||
else if (sortKey === 'queued') cmp = a.queued - b.queued;
|
||||
else if (sortKey === 'jobs') cmp = a.jobs - b.jobs;
|
||||
else cmp = a.performance - b.performance;
|
||||
else if (sortKey === 'jobs') cmp = a.jobs - b.jobs;
|
||||
else cmp = a.performance - b.performance;
|
||||
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
|
||||
|
|
@ -45,14 +46,13 @@ export const StatsPanel = () => {
|
|||
|
||||
return (
|
||||
<aside class={styles.panel}>
|
||||
{user ? (
|
||||
<section class={styles.userStats}>
|
||||
<div class={styles.username}>{user.username}</div>
|
||||
<dl class={styles.stats}>
|
||||
<div class={styles.stat}>
|
||||
<dt>Kudos</dt>
|
||||
<dd>{formatNumber(user.kudos)}</dd>
|
||||
</div>
|
||||
<section class={styles.userStats}>
|
||||
{user && (<div class={styles.username}>{user.username}</div>)}
|
||||
<dl class={styles.stats}>
|
||||
{user && <><div class={styles.stat}>
|
||||
<dt>Kudos</dt>
|
||||
<dd>{formatNumber(user.kudos)}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Kudos/h</dt>
|
||||
<dd>{formatNumber(Math.round(kudosPerHour))}</dd>
|
||||
|
|
@ -73,31 +73,21 @@ export const StatsPanel = () => {
|
|||
<dt>Total uptime</dt>
|
||||
<dd>{formatTime(totalUptime)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
) : (
|
||||
<section class={styles.networkStats}>
|
||||
<div class={styles.sectionTitle}>Network</div>
|
||||
<dl class={styles.stats}>
|
||||
<div class={styles.stat}>
|
||||
<dt>Active workers</dt>
|
||||
<dd>{performance?.text_worker_count ?? '—'}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Queued requests</dt>
|
||||
<dd>{performance ? formatNumber(performance.queued_text_requests) : '—'}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Queued tokens</dt>
|
||||
<dd>{performance ? formatNumber(performance.queued_tokens) : '—'}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Throughput (1m)</dt>
|
||||
<dd>{performance ? `${formatNumber(performance.past_minute_tokens)} tok` : '—'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
)}
|
||||
</>}
|
||||
<div class={styles.stat}>
|
||||
<dt>Queued requests</dt>
|
||||
<dd>{performance ? formatNumber(performance.queued_text_requests) : '—'}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Queued tokens</dt>
|
||||
<dd>{performance ? formatNumber(performance.queued_tokens) : '—'}</dd>
|
||||
</div>
|
||||
<div class={styles.stat}>
|
||||
<dt>Throughput (1m)</dt>
|
||||
<dd>{performance ? `${formatNumber(performance.past_minute_tokens)} tok` : '—'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class={styles.modelsSection}>
|
||||
<div class={styles.sectionTitle}>Models</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { WorkerData } from "../utils/api";
|
||||
|
||||
/**
|
||||
* Calculate total kudos/hour across a set of workers.
|
||||
* Computes kudos/hour for each worker individually, then sums them.
|
||||
*/
|
||||
export const calculateTotalKudosPerHour = (workers: WorkerData[]): number => {
|
||||
return workers.reduce((sum, w) => {
|
||||
const uptime = w.uptime;
|
||||
const generated = w.kudos_details?.generated ?? 0;
|
||||
if (uptime <= 0) return sum;
|
||||
return sum + (generated / uptime) * 3600;
|
||||
}, 0);
|
||||
};
|
||||
Loading…
Reference in New Issue