fix: improve dashboard react doctor score (#215)
This commit is contained in:
@@ -204,7 +204,7 @@ function buildRoomOptions(snapshots: StatusSnapshot[]): RoomOption[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...rooms.values()].sort((a, b) =>
|
return Array.from(rooms.values()).sort((a, b) =>
|
||||||
`${a.name} ${a.folder}`.localeCompare(`${b.name} ${b.folder}`),
|
`${a.name} ${a.folder}`.localeCompare(`${b.name} ${b.folder}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ function DashboardErrorCard({
|
|||||||
<strong>{t.error.api}</strong>
|
<strong>{t.error.api}</strong>
|
||||||
<small>{humanizeError(error, t)}</small>
|
<small>{humanizeError(error, t)}</small>
|
||||||
</span>
|
</span>
|
||||||
<button disabled={refreshing} onClick={onRetry}>
|
<button disabled={refreshing} onClick={onRetry} type="button">
|
||||||
{t.actions.retry}
|
{t.actions.retry}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ export function RoomBoardV2({
|
|||||||
const filtered = allEntries.filter(
|
const filtered = allEntries.filter(
|
||||||
(entry) => filter === 'all' || entry.status === filter,
|
(entry) => filter === 'all' || entry.status === filter,
|
||||||
);
|
);
|
||||||
const sorted = [...filtered].sort((a, b) => {
|
const sorted = Array.from(filtered).sort((a, b) => {
|
||||||
if (sort === 'name') return a.name.localeCompare(b.name);
|
if (sort === 'name') return a.name.localeCompare(b.name);
|
||||||
if (sort === 'queue') {
|
if (sort === 'queue') {
|
||||||
const aQ = a.pendingTasks + (a.pendingMessages ? 1 : 0);
|
const aQ = a.pendingTasks + (a.pendingMessages ? 1 : 0);
|
||||||
@@ -321,10 +321,11 @@ export function RoomBoardV2({
|
|||||||
const nextJid = selectedEntry?.jid ?? null;
|
const nextJid = selectedEntry?.jid ?? null;
|
||||||
if (nextJid !== selectedJid) {
|
if (nextJid !== selectedJid) {
|
||||||
onSelectedJidChange(nextJid);
|
onSelectedJidChange(nextJid);
|
||||||
setMobileDetailOpen(false);
|
|
||||||
}
|
}
|
||||||
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
|
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
|
||||||
|
|
||||||
|
const detailOpen = mobileDetailOpen && selectedEntry?.jid === selectedJid;
|
||||||
|
|
||||||
if (allEntries.length === 0) {
|
if (allEntries.length === 0) {
|
||||||
return <EmptyState>{t.rooms.empty}</EmptyState>;
|
return <EmptyState>{t.rooms.empty}</EmptyState>;
|
||||||
}
|
}
|
||||||
@@ -359,9 +360,7 @@ export function RoomBoardV2({
|
|||||||
{sorted.length === 0 ? (
|
{sorted.length === 0 ? (
|
||||||
<EmptyState>{t.rooms.empty}</EmptyState>
|
<EmptyState>{t.rooms.empty}</EmptyState>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={`rooms-twopane${detailOpen ? ' is-detail-open' : ''}`}>
|
||||||
className={`rooms-twopane${mobileDetailOpen ? ' is-detail-open' : ''}`}
|
|
||||||
>
|
|
||||||
<RoomsList
|
<RoomsList
|
||||||
entries={sorted}
|
entries={sorted}
|
||||||
inbox={inbox}
|
inbox={inbox}
|
||||||
|
|||||||
@@ -91,6 +91,56 @@ interface TaskEditFormProps {
|
|||||||
t: Messages;
|
t: Messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TASK_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
|
||||||
|
en: new Intl.DateTimeFormat(localeTags.en, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}),
|
||||||
|
ja: new Intl.DateTimeFormat(localeTags.ja, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}),
|
||||||
|
ko: new Intl.DateTimeFormat(localeTags.ko, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}),
|
||||||
|
zh: new Intl.DateTimeFormat(localeTags.zh, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const EN_TASK_DATE_TIME_FORMATTER = new Intl.DateTimeFormat(localeTags.en, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const RELATIVE_TIME_FORMATTERS: Record<Locale, Intl.RelativeTimeFormat> = {
|
||||||
|
en: new Intl.RelativeTimeFormat(localeTags.en, {
|
||||||
|
numeric: 'auto',
|
||||||
|
style: 'short',
|
||||||
|
}),
|
||||||
|
ja: new Intl.RelativeTimeFormat(localeTags.ja, {
|
||||||
|
numeric: 'auto',
|
||||||
|
style: 'short',
|
||||||
|
}),
|
||||||
|
ko: new Intl.RelativeTimeFormat(localeTags.ko, {
|
||||||
|
numeric: 'auto',
|
||||||
|
style: 'short',
|
||||||
|
}),
|
||||||
|
zh: new Intl.RelativeTimeFormat(localeTags.zh, {
|
||||||
|
numeric: 'auto',
|
||||||
|
style: 'short',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
function formatTaskDate(
|
function formatTaskDate(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
@@ -98,22 +148,12 @@ function formatTaskDate(
|
|||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
const time = new Intl.DateTimeFormat(localeTags[locale], {
|
const time = TASK_TIME_FORMATTERS[locale].format(date);
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
}).format(date);
|
|
||||||
if (locale === 'ko')
|
if (locale === 'ko')
|
||||||
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
||||||
if (locale === 'ja' || locale === 'zh')
|
if (locale === 'ja' || locale === 'zh')
|
||||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
return EN_TASK_DATE_TIME_FORMATTER.format(date);
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
}).format(date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeDate(
|
function formatRelativeDate(
|
||||||
@@ -135,10 +175,10 @@ function formatRelativeDate(
|
|||||||
];
|
];
|
||||||
const [unit, unitMs] =
|
const [unit, unitMs] =
|
||||||
units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!;
|
units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!;
|
||||||
return new Intl.RelativeTimeFormat(localeTags[locale], {
|
return RELATIVE_TIME_FORMATTERS[locale].format(
|
||||||
numeric: 'auto',
|
Math.round(diffMs / unitMs),
|
||||||
style: 'short',
|
unit,
|
||||||
}).format(Math.round(diffMs / unitMs), unit);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function safePreview(
|
function safePreview(
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function UsageSpeed({ row, t }: { row: UsageRow; t: Messages }) {
|
|||||||
export function UsagePanel({ overview, t }: UsagePanelProps) {
|
export function UsagePanel({ overview, t }: UsagePanelProps) {
|
||||||
const rows = useMemo(
|
const rows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...overview.usage.rows].sort((a, b) => {
|
Array.from(overview.usage.rows).sort((a, b) => {
|
||||||
if (usageActive(a) !== usageActive(b)) return usageActive(a) ? -1 : 1;
|
if (usageActive(a) !== usageActive(b)) return usageActive(a) ? -1 : 1;
|
||||||
return usagePeak(b) - usagePeak(a);
|
return usagePeak(b) - usagePeak(a);
|
||||||
}),
|
}),
|
||||||
@@ -194,24 +194,28 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="usage-matrix" role="table" aria-label={t.panels.usage}>
|
<table className="usage-matrix" aria-label={t.panels.usage}>
|
||||||
<div className="usage-matrix-head" role="row">
|
<thead>
|
||||||
<span>{t.usage.usage}</span>
|
<tr className="usage-matrix-head">
|
||||||
<span>{t.usage.quota.h5}</span>
|
<th scope="col">{t.usage.usage}</th>
|
||||||
<span>{t.usage.quota.d7}</span>
|
<th scope="col">{t.usage.quota.h5}</th>
|
||||||
<span>{t.usage.speed}</span>
|
<th scope="col">{t.usage.quota.d7}</th>
|
||||||
</div>
|
<th scope="col">{t.usage.speed}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<div className="usage-group" key={group.key} role="rowgroup">
|
<tbody className="usage-group" key={group.key}>
|
||||||
<div className="usage-group-label" role="row">
|
<tr className="usage-group-label">
|
||||||
<span>{group.label}</span>
|
<th colSpan={4} scope="colgroup">
|
||||||
</div>
|
{group.label}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
{group.rows.map((row) => {
|
{group.rows.map((row) => {
|
||||||
const risk = usageRiskLevel(row);
|
const risk = usageRiskLevel(row);
|
||||||
const { account, plan } = usageNameParts(row);
|
const { account, plan } = usageNameParts(row);
|
||||||
return (
|
return (
|
||||||
<section className={`usage-row usage-${risk}`} key={row.name}>
|
<tr className={`usage-row usage-${risk}`} key={row.name}>
|
||||||
<div className="usage-account">
|
<th className="usage-account" scope="row">
|
||||||
<strong>{account}</strong>
|
<strong>{account}</strong>
|
||||||
<div>
|
<div>
|
||||||
{usageActive(row) ? (
|
{usageActive(row) ? (
|
||||||
@@ -224,26 +228,32 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</th>
|
||||||
|
<td>
|
||||||
<UsageQuotaMeter
|
<UsageQuotaMeter
|
||||||
row={row}
|
row={row}
|
||||||
rowName={account}
|
rowName={account}
|
||||||
window="h5"
|
window="h5"
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<UsageQuotaMeter
|
<UsageQuotaMeter
|
||||||
row={row}
|
row={row}
|
||||||
rowName={account}
|
rowName={account}
|
||||||
window="d7"
|
window="d7"
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<UsageSpeed row={row} t={t} />
|
<UsageSpeed row={row} t={t} />
|
||||||
</section>
|
</td>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</tbody>
|
||||||
))}
|
))}
|
||||||
</div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
import type { DashboardTask, DashboardTaskAction } from './api';
|
import type { DashboardTask, DashboardTaskAction } from './api';
|
||||||
import { localeTags, type Locale, type Messages } from './i18n';
|
import { localeTags, type Locale, type Messages } from './i18n';
|
||||||
|
|
||||||
|
const SHORT_TIME_FORMAT_OPTIONS = {
|
||||||
|
hour: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
minute: '2-digit',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const MONTH_DAY_TIME_FORMAT_OPTIONS = {
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
minute: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const SHORT_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
|
||||||
|
en: new Intl.DateTimeFormat(localeTags.en, SHORT_TIME_FORMAT_OPTIONS),
|
||||||
|
ja: new Intl.DateTimeFormat(localeTags.ja, SHORT_TIME_FORMAT_OPTIONS),
|
||||||
|
ko: new Intl.DateTimeFormat(localeTags.ko, SHORT_TIME_FORMAT_OPTIONS),
|
||||||
|
zh: new Intl.DateTimeFormat(localeTags.zh, SHORT_TIME_FORMAT_OPTIONS),
|
||||||
|
};
|
||||||
|
|
||||||
|
const MONTH_DAY_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
|
||||||
|
en: new Intl.DateTimeFormat(localeTags.en, MONTH_DAY_TIME_FORMAT_OPTIONS),
|
||||||
|
ja: new Intl.DateTimeFormat(localeTags.ja, MONTH_DAY_TIME_FORMAT_OPTIONS),
|
||||||
|
ko: new Intl.DateTimeFormat(localeTags.ko, MONTH_DAY_TIME_FORMAT_OPTIONS),
|
||||||
|
zh: new Intl.DateTimeFormat(localeTags.zh, MONTH_DAY_TIME_FORMAT_OPTIONS),
|
||||||
|
};
|
||||||
|
|
||||||
export function formatDate(
|
export function formatDate(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
@@ -31,30 +59,16 @@ export function formatDate(
|
|||||||
}
|
}
|
||||||
const sameDay = new Date().toDateString() === date.toDateString();
|
const sameDay = new Date().toDateString() === date.toDateString();
|
||||||
if (sameDay) {
|
if (sameDay) {
|
||||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
return SHORT_TIME_FORMATTERS[locale].format(date);
|
||||||
hour: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
minute: '2-digit',
|
|
||||||
}).format(date);
|
|
||||||
}
|
}
|
||||||
const time = new Intl.DateTimeFormat(localeTags[locale], {
|
const time = SHORT_TIME_FORMATTERS[locale].format(date);
|
||||||
hour: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
minute: '2-digit',
|
|
||||||
}).format(date);
|
|
||||||
if (locale === 'ko')
|
if (locale === 'ko')
|
||||||
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
||||||
if (locale === 'ja')
|
if (locale === 'ja')
|
||||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||||
if (locale === 'zh')
|
if (locale === 'zh')
|
||||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
return MONTH_DAY_TIME_FORMATTERS[locale].format(date);
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
minute: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
}).format(date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function taskActionsFor(task: DashboardTask): DashboardTaskAction[] {
|
export function taskActionsFor(task: DashboardTask): DashboardTaskAction[] {
|
||||||
|
|||||||
@@ -169,6 +169,41 @@ function mergeAdjacentBotChunks(entries: RoomThreadEntry[]): RoomThreadEntry[] {
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectOutputEntries(outputs: RoomOutput[]): RoomThreadEntry[] {
|
||||||
|
const entries: RoomThreadEntry[] = [];
|
||||||
|
for (const output of outputs) {
|
||||||
|
const entry = toOutputEntry(output);
|
||||||
|
if (entry) entries.push(entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectChatEntries(
|
||||||
|
messages: RoomMessage[],
|
||||||
|
outputEntries: RoomThreadEntry[],
|
||||||
|
): RoomThreadEntry[] {
|
||||||
|
const entries: RoomThreadEntry[] = [];
|
||||||
|
for (const message of messages) {
|
||||||
|
if (!isThreadChatMessage(message)) continue;
|
||||||
|
const entry = toMessageEntry(message);
|
||||||
|
if (!isDuplicateOutputMessage(entry, outputEntries)) entries.push(entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectOptimisticPending(
|
||||||
|
pendingMessages: RoomMessage[],
|
||||||
|
confirmedSet: Set<string>,
|
||||||
|
): RoomThreadEntry[] {
|
||||||
|
const entries: RoomThreadEntry[] = [];
|
||||||
|
for (const message of pendingMessages) {
|
||||||
|
if (confirmedSet.has(messageKey(message))) continue;
|
||||||
|
if (isInternalProtocolPayload(message.content)) continue;
|
||||||
|
entries.push(toMessageEntry(message));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildRoomThreadEntries({
|
export function buildRoomThreadEntries({
|
||||||
messages,
|
messages,
|
||||||
outputs,
|
outputs,
|
||||||
@@ -179,20 +214,14 @@ export function buildRoomThreadEntries({
|
|||||||
pendingMessages?: RoomMessage[];
|
pendingMessages?: RoomMessage[];
|
||||||
}): RoomThreadEntry[] {
|
}): RoomThreadEntry[] {
|
||||||
const confirmedSet = new Set(messages.map(messageKey));
|
const confirmedSet = new Set(messages.map(messageKey));
|
||||||
const outputEntries = outputs
|
const outputEntries = collectOutputEntries(outputs);
|
||||||
.map(toOutputEntry)
|
const chatEntries = collectChatEntries(messages, outputEntries);
|
||||||
.filter((entry): entry is RoomThreadEntry => Boolean(entry));
|
const optimisticPending = collectOptimisticPending(
|
||||||
const chatEntries = messages
|
pendingMessages,
|
||||||
.filter(isThreadChatMessage)
|
confirmedSet,
|
||||||
.map(toMessageEntry)
|
|
||||||
.filter((entry) => !isDuplicateOutputMessage(entry, outputEntries));
|
|
||||||
const optimisticPending = pendingMessages
|
|
||||||
.filter((message) => !confirmedSet.has(messageKey(message)))
|
|
||||||
.filter((message) => !isInternalProtocolPayload(message.content))
|
|
||||||
.map(toMessageEntry);
|
|
||||||
|
|
||||||
const entries = [...chatEntries, ...optimisticPending, ...outputEntries].sort(
|
|
||||||
(a, b) => a.timestamp.localeCompare(b.timestamp),
|
|
||||||
);
|
);
|
||||||
|
const entries = chatEntries
|
||||||
|
.concat(optimisticPending, outputEntries)
|
||||||
|
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
||||||
return mergeAdjacentBotChunks(entries);
|
return mergeAdjacentBotChunks(entries);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2227,6 +2227,19 @@ dd,
|
|||||||
.usage-matrix {
|
.usage-matrix {
|
||||||
display: grid;
|
display: grid;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-matrix thead,
|
||||||
|
.usage-matrix tbody {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-matrix th,
|
||||||
|
.usage-matrix td {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
text-align: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-matrix-head,
|
.usage-matrix-head,
|
||||||
@@ -2258,7 +2271,6 @@ dd,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usage-group-label {
|
.usage-group-label {
|
||||||
padding: 7px 10px;
|
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -2267,6 +2279,14 @@ dd,
|
|||||||
background: rgba(191, 95, 44, 0.07);
|
background: rgba(191, 95, 44, 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usage-group-label th {
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
text-transform: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.usage-row {
|
.usage-row {
|
||||||
--meter: var(--green);
|
--meter: var(--green);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function useSelectedRoomActivity({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active || !selectedRoomJid) {
|
if (!active || !selectedRoomJid) {
|
||||||
setRoomActivityLoading(false);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,5 +63,10 @@ export function useSelectedRoomActivity({
|
|||||||
};
|
};
|
||||||
}, [active, pollMs, refreshRoom, selectedRoomJid]);
|
}, [active, pollMs, refreshRoom, selectedRoomJid]);
|
||||||
|
|
||||||
return { refreshRoom, roomActivity, roomActivityLoading };
|
return {
|
||||||
|
refreshRoom,
|
||||||
|
roomActivity,
|
||||||
|
roomActivityLoading:
|
||||||
|
active && selectedRoomJid !== null ? roomActivityLoading : false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,7 @@ async function measure(page: Page, sel: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function evalExpr(page: Page, expr: string) {
|
async function evalExpr(page: Page, expr: string) {
|
||||||
const result = await page.evaluate((e) => {
|
const result = await page.evaluate(expr);
|
||||||
return eval(e);
|
|
||||||
}, expr);
|
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ function normalizeStructuredVisibleContent(value: string): {
|
|||||||
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
|
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
|
||||||
const rows: UsageRowSnapshot[] = [];
|
const rows: UsageRowSnapshot[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const sortedSnapshots = [...snapshots].sort((a, b) =>
|
const sortedSnapshots = Array.from(snapshots).sort((a, b) =>
|
||||||
(b.usageRowsFetchedAt ?? b.updatedAt).localeCompare(
|
(b.usageRowsFetchedAt ?? b.updatedAt).localeCompare(
|
||||||
a.usageRowsFetchedAt ?? a.updatedAt,
|
a.usageRowsFetchedAt ?? a.updatedAt,
|
||||||
),
|
),
|
||||||
@@ -343,7 +343,7 @@ function collectInboxItems(args: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...groupedItems.values()]
|
return Array.from(groupedItems.values())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const severityDelta = severityRank[a.severity] - severityRank[b.severity];
|
const severityDelta = severityRank[a.severity] - severityRank[b.severity];
|
||||||
if (severityDelta !== 0) return severityDelta;
|
if (severityDelta !== 0) return severityDelta;
|
||||||
@@ -567,6 +567,91 @@ function sanitizeRoomTurnOutput(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentRoomTurn(
|
||||||
|
turns: PairedTurnRecord[],
|
||||||
|
): PairedTurnRecord | null {
|
||||||
|
let currentTurn: PairedTurnRecord | null = null;
|
||||||
|
let currentTimestamp = '';
|
||||||
|
for (const turn of turns) {
|
||||||
|
const timestamp = getRoomTurnActivityTimestamp(turn);
|
||||||
|
if (!currentTurn || timestamp.localeCompare(currentTimestamp) > 0) {
|
||||||
|
currentTurn = turn;
|
||||||
|
currentTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentTurn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSanitizedRecentMessages(
|
||||||
|
messages: NewMessage[],
|
||||||
|
): WebDashboardRoomMessage[] {
|
||||||
|
const sanitizedMessages: WebDashboardRoomMessage[] = [];
|
||||||
|
for (const message of messages) {
|
||||||
|
if (isTaskStatusMessage(message)) continue;
|
||||||
|
const sanitized = sanitizeRoomMessage(message);
|
||||||
|
if (sanitized) sanitizedMessages.push(sanitized);
|
||||||
|
}
|
||||||
|
return sanitizedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCanonicalOutboundMessages(
|
||||||
|
outboundItems: WorkItem[] | undefined,
|
||||||
|
sanitizedRecentMessages: WebDashboardRoomMessage[],
|
||||||
|
): WebDashboardRoomMessage[] {
|
||||||
|
const messages: WebDashboardRoomMessage[] = [];
|
||||||
|
for (const item of outboundItems ?? []) {
|
||||||
|
const message = sanitizeCanonicalOutboundMessage(item);
|
||||||
|
if (!message) continue;
|
||||||
|
if (
|
||||||
|
item.delivery_message_id ||
|
||||||
|
hasDiscordEchoForCanonicalOutbound(message, sanitizedRecentMessages)
|
||||||
|
) {
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCanonicalDeliveryMessageIds(
|
||||||
|
outboundItems: WorkItem[] | undefined,
|
||||||
|
): Set<string> {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const item of outboundItems ?? []) {
|
||||||
|
if (item.delivery_message_id) ids.add(item.delivery_message_id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRecentMessages(args: {
|
||||||
|
canonicalDeliveryMessageIds: Set<string>;
|
||||||
|
canonicalOutboundMessages: WebDashboardRoomMessage[];
|
||||||
|
sanitizedRecentMessages: WebDashboardRoomMessage[];
|
||||||
|
}): WebDashboardRoomMessage[] {
|
||||||
|
const messages: WebDashboardRoomMessage[] = [];
|
||||||
|
for (const message of args.sanitizedRecentMessages) {
|
||||||
|
if (args.canonicalDeliveryMessageIds.has(message.id)) continue;
|
||||||
|
if (
|
||||||
|
isDuplicateOfCanonicalOutbound(message, args.canonicalOutboundMessages)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRecentOutputs(
|
||||||
|
outputs: PairedTurnOutput[],
|
||||||
|
outputLimit: number,
|
||||||
|
): WebDashboardRoomTurnOutput[] {
|
||||||
|
const recentOutputs: WebDashboardRoomTurnOutput[] = [];
|
||||||
|
for (const output of outputs.slice(-outputLimit)) {
|
||||||
|
const sanitized = sanitizeRoomTurnOutput(output);
|
||||||
|
if (sanitized) recentOutputs.push(sanitized);
|
||||||
|
}
|
||||||
|
return recentOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildWebDashboardRoomActivity(args: {
|
export function buildWebDashboardRoomActivity(args: {
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
entry: StatusSnapshot['entries'][number];
|
entry: StatusSnapshot['entries'][number];
|
||||||
@@ -585,50 +670,24 @@ export function buildWebDashboardRoomActivity(args: {
|
|||||||
latestAttemptByTurnId.set(attempt.turn_id, attempt);
|
latestAttemptByTurnId.set(attempt.turn_id, attempt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const currentTurn =
|
const currentTurn = getCurrentRoomTurn(args.turns);
|
||||||
[...args.turns].sort((a, b) =>
|
|
||||||
getRoomTurnActivityTimestamp(b).localeCompare(
|
|
||||||
getRoomTurnActivityTimestamp(a),
|
|
||||||
),
|
|
||||||
)[0] ?? null;
|
|
||||||
const currentAttempt = currentTurn
|
const currentAttempt = currentTurn
|
||||||
? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null)
|
? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null)
|
||||||
: null;
|
: null;
|
||||||
const outputLimit = args.outputLimit ?? 4;
|
const outputLimit = args.outputLimit ?? 4;
|
||||||
const sanitizedRecentMessages = args.messages
|
const sanitizedRecentMessages = collectSanitizedRecentMessages(args.messages);
|
||||||
.filter((message) => !isTaskStatusMessage(message))
|
const canonicalOutboundMessages = collectCanonicalOutboundMessages(
|
||||||
.map(sanitizeRoomMessage)
|
args.outboundItems,
|
||||||
.filter((message): message is WebDashboardRoomMessage => Boolean(message));
|
sanitizedRecentMessages,
|
||||||
const canonicalOutboundMessages = (args.outboundItems ?? [])
|
|
||||||
.map((item) => ({
|
|
||||||
item,
|
|
||||||
message: sanitizeCanonicalOutboundMessage(item),
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
(
|
|
||||||
candidate,
|
|
||||||
): candidate is {
|
|
||||||
item: WorkItem;
|
|
||||||
message: WebDashboardRoomMessage;
|
|
||||||
} => Boolean(candidate.message),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
({ item, message }) =>
|
|
||||||
Boolean(item.delivery_message_id) ||
|
|
||||||
hasDiscordEchoForCanonicalOutbound(message, sanitizedRecentMessages),
|
|
||||||
)
|
|
||||||
.map(({ message }) => message);
|
|
||||||
const canonicalDeliveryMessageIds = new Set(
|
|
||||||
(args.outboundItems ?? [])
|
|
||||||
.map((item) => item.delivery_message_id)
|
|
||||||
.filter((id): id is string => Boolean(id)),
|
|
||||||
);
|
);
|
||||||
const recentMessages = sanitizedRecentMessages
|
const canonicalDeliveryMessageIds = collectCanonicalDeliveryMessageIds(
|
||||||
.filter((message) => !canonicalDeliveryMessageIds.has(message.id))
|
args.outboundItems,
|
||||||
.filter(
|
|
||||||
(message) =>
|
|
||||||
!isDuplicateOfCanonicalOutbound(message, canonicalOutboundMessages),
|
|
||||||
);
|
);
|
||||||
|
const recentMessages = collectRecentMessages({
|
||||||
|
canonicalDeliveryMessageIds,
|
||||||
|
canonicalOutboundMessages,
|
||||||
|
sanitizedRecentMessages,
|
||||||
|
});
|
||||||
const shouldExposeExecutionOutputs = args.outboundItems === undefined;
|
const shouldExposeExecutionOutputs = args.outboundItems === undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -641,9 +700,9 @@ export function buildWebDashboardRoomActivity(args: {
|
|||||||
elapsedMs: args.entry.elapsedMs,
|
elapsedMs: args.entry.elapsedMs,
|
||||||
pendingMessages: args.entry.pendingMessages,
|
pendingMessages: args.entry.pendingMessages,
|
||||||
pendingTasks: args.entry.pendingTasks,
|
pendingTasks: args.entry.pendingTasks,
|
||||||
messages: [...recentMessages, ...canonicalOutboundMessages].sort(
|
messages: recentMessages
|
||||||
compareRoomMessagesByTimestamp,
|
.concat(canonicalOutboundMessages)
|
||||||
),
|
.sort(compareRoomMessagesByTimestamp),
|
||||||
pairedTask: args.pairedTask
|
pairedTask: args.pairedTask
|
||||||
? {
|
? {
|
||||||
id: args.pairedTask.id,
|
id: args.pairedTask.id,
|
||||||
@@ -655,12 +714,7 @@ export function buildWebDashboardRoomActivity(args: {
|
|||||||
? sanitizeRoomTurn(currentTurn, currentAttempt)
|
? sanitizeRoomTurn(currentTurn, currentAttempt)
|
||||||
: null,
|
: null,
|
||||||
outputs: shouldExposeExecutionOutputs
|
outputs: shouldExposeExecutionOutputs
|
||||||
? args.outputs
|
? collectRecentOutputs(args.outputs, outputLimit)
|
||||||
.slice(-outputLimit)
|
|
||||||
.map(sanitizeRoomTurnOutput)
|
|
||||||
.filter((output): output is WebDashboardRoomTurnOutput =>
|
|
||||||
Boolean(output),
|
|
||||||
)
|
|
||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user