mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-01 13:56:53 +08:00
fix(operation-trace): repair pageOf key placement, numbered pager, right column
- pageOf was inserted under the wrong namespace (the locale files already had a hideDetails in toolCalls), so the UI rendered the raw key. Removed it and replaced the dropdown with a numbered pager (first/last always visible, a one-page window around the current page, gaps as an ellipsis). - Restore the right-hand column (duration + record hash) that the previous declutter removed; the digest is still opt-in, but the hash stays visible. - Hide the status code on success: a 200 is the default, so only >=400 shows.
This commit is contained in:
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
@@ -139,6 +140,32 @@ function groupRecords(records: OperationLogRecord[]): GroupedRecord[] {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact page list: the first and last page are always shown, with a
|
||||
* one-page window around the current page and gaps collapsed to an ellipsis.
|
||||
* ``'…'`` entries are rendered as non-clickable separators.
|
||||
*/
|
||||
function pageWindow(
|
||||
current: number,
|
||||
count: number,
|
||||
span = 1,
|
||||
): (number | '…')[] {
|
||||
if (count <= 0) return [];
|
||||
const pages = new Set<number>([1, count]);
|
||||
for (let page = current - span; page <= current + span; page += 1) {
|
||||
if (page >= 1 && page <= count) pages.add(page);
|
||||
}
|
||||
const sorted = [...pages].sort((a, b) => a - b);
|
||||
const result: (number | '…')[] = [];
|
||||
let previous = 0;
|
||||
for (const page of sorted) {
|
||||
if (previous && page - previous > 1) result.push('…');
|
||||
result.push(page);
|
||||
previous = page;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Compact timestamp: time-only for today, date+time otherwise. */
|
||||
function formatTimestamp(value: string | null): string {
|
||||
if (!value) return '';
|
||||
@@ -744,7 +771,10 @@ export default function OperationTracePanel({
|
||||
<span aria-hidden="true">·</span>
|
||||
<span className="font-mono">
|
||||
{record.http_method}
|
||||
{record.status_code !== null
|
||||
{/* A successful 2xx is the default; only surface the
|
||||
status when it carries a signal (an error). */}
|
||||
{record.status_code !== null &&
|
||||
record.status_code >= 400
|
||||
? ` ${record.status_code}`
|
||||
: ''}
|
||||
</span>
|
||||
@@ -793,21 +823,29 @@ export default function OperationTracePanel({
|
||||
redactedLabel={t('operationTrace.redacted')}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center gap-2 pt-1 text-[10px] text-muted-foreground">
|
||||
<span>{record.duration_ms}ms</span>
|
||||
{record.record_hash && (
|
||||
<span
|
||||
className="flex items-center gap-1 font-mono"
|
||||
title={record.record_hash}
|
||||
>
|
||||
<Fingerprint className="size-3" />
|
||||
{record.record_hash.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ItemContent>
|
||||
{/* Restore the always-visible right column: each row is
|
||||
anchored by its duration and tamper-evidence hash, so the
|
||||
digest stays one click away instead of being the only way
|
||||
to see it. */}
|
||||
<ItemActions className="max-sm:hidden">
|
||||
<div className="flex flex-col items-end gap-0.5 text-right">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{record.duration_ms}ms
|
||||
</span>
|
||||
{record.record_hash && (
|
||||
<span
|
||||
className="flex items-center gap-1 font-mono text-[10px] text-muted-foreground"
|
||||
title={record.record_hash}
|
||||
>
|
||||
<Fingerprint className="size-3" />
|
||||
{record.record_hash.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
@@ -836,32 +874,35 @@ export default function OperationTracePanel({
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
{/* Jump straight to a page instead of only stepping one at a
|
||||
time; with hundreds of records the arrows alone are painful. */}
|
||||
<Select
|
||||
value={String(pageIndex)}
|
||||
onValueChange={(value) =>
|
||||
setQuery((prev) => ({
|
||||
...prev,
|
||||
offset: (Number(value) - 1) * PAGE_SIZE,
|
||||
}))
|
||||
}
|
||||
disabled={loading || pageCount <= 1}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: pageCount }, (_, index) => (
|
||||
<SelectItem key={index} value={String(index + 1)}>
|
||||
{t('operationTrace.pageOf', {
|
||||
page: index + 1,
|
||||
total: pageCount,
|
||||
})}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* Numbered pager: the first and last page stay visible with an
|
||||
ellipsis between, so any page is one click away without the
|
||||
arrows filling the bar with "Page x / y" rows. */}
|
||||
{pageWindow(pageIndex, pageCount).map((item, index) =>
|
||||
item === '…' ? (
|
||||
<span
|
||||
key={`gap-${index}`}
|
||||
className="px-1 text-xs text-muted-foreground"
|
||||
>
|
||||
…
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
key={item}
|
||||
size="sm"
|
||||
variant={item === pageIndex ? 'default' : 'outline'}
|
||||
className="h-8 min-w-8 px-2 font-mono text-xs"
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setQuery((prev) => ({
|
||||
...prev,
|
||||
offset: (item - 1) * PAGE_SIZE,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
|
||||
@@ -2548,7 +2548,6 @@ const enUS = {
|
||||
noToolCalls: 'No tool calls recorded',
|
||||
showDetails: 'Show details',
|
||||
hideDetails: 'Hide details',
|
||||
pageOf: 'Page {{page}} / {{total}}',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: 'Total Tokens',
|
||||
|
||||
@@ -2602,7 +2602,6 @@ const esES = {
|
||||
noToolCalls: 'No se registraron llamadas de herramientas',
|
||||
showDetails: 'Mostrar detalles',
|
||||
hideDetails: 'Ocultar detalles',
|
||||
pageOf: 'Página {{page}} / {{total}}',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: 'Tokens totales',
|
||||
|
||||
@@ -2565,7 +2565,6 @@ const jaJP = {
|
||||
noToolCalls: 'ツール呼び出しは記録されていません',
|
||||
showDetails: '詳細を表示',
|
||||
hideDetails: '詳細を非表示',
|
||||
pageOf: '{{page}} / {{total}} ページ',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: '総トークン数',
|
||||
|
||||
@@ -2573,7 +2573,6 @@ const ruRU = {
|
||||
noToolCalls: 'Вызовы инструментов не записаны',
|
||||
showDetails: 'Показать детали',
|
||||
hideDetails: 'Скрыть детали',
|
||||
pageOf: 'Стр. {{page}} / {{total}}',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: 'Всего токенов',
|
||||
|
||||
@@ -2499,7 +2499,6 @@ const thTH = {
|
||||
noToolCalls: 'ยังไม่มีการบันทึกการเรียกใช้เครื่องมือ',
|
||||
showDetails: 'แสดงรายละเอียด',
|
||||
hideDetails: 'ซ่อนรายละเอียด',
|
||||
pageOf: 'หน้า {{page}} / {{total}}',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: 'Token ทั้งหมด',
|
||||
|
||||
@@ -2534,7 +2534,6 @@ const viVN = {
|
||||
noToolCalls: 'Chưa ghi nhận lượt gọi công cụ',
|
||||
showDetails: 'Hiện chi tiết',
|
||||
hideDetails: 'Ẩn chi tiết',
|
||||
pageOf: 'Trang {{page}} / {{total}}',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: 'Tổng số Token',
|
||||
|
||||
@@ -2399,7 +2399,6 @@ const zhHans = {
|
||||
noToolCalls: '未记录工具调用',
|
||||
showDetails: '查看详情',
|
||||
hideDetails: '隐藏详情',
|
||||
pageOf: '第 {{page}} / {{total}} 页',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: '总 Token 数',
|
||||
|
||||
@@ -2400,7 +2400,6 @@ const zhHant = {
|
||||
noToolCalls: '未記錄工具呼叫',
|
||||
showDetails: '查看詳情',
|
||||
hideDetails: '隱藏詳情',
|
||||
pageOf: '第 {{page}} / {{total}} 頁',
|
||||
},
|
||||
tokens: {
|
||||
totalTokens: '總 Token 數',
|
||||
|
||||
Reference in New Issue
Block a user