|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import type { CSSProperties, ReactNode } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
App, Button, Card, Col, Collapse, DatePicker, Descriptions, Divider, Drawer, Input, InputNumber,
|
|
|
|
@@ -9,8 +9,13 @@ import {
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
import dayjs, { type Dayjs } from 'dayjs';
|
|
|
|
|
import { api, errMsg } from '@/lib/api';
|
|
|
|
|
import { formatWallTime, percentile, yuan } from '@/lib/format';
|
|
|
|
|
import type { ComparisonRecordDetail, ComparisonRecordListItem, CursorPage } from '@/lib/types';
|
|
|
|
|
import { formatWallTime, yuan } from '@/lib/format';
|
|
|
|
|
import type {
|
|
|
|
|
ComparisonRecordDetail,
|
|
|
|
|
ComparisonRecordListItem,
|
|
|
|
|
ComparisonRecordsSummary,
|
|
|
|
|
CursorPage,
|
|
|
|
|
} from '@/lib/types';
|
|
|
|
|
|
|
|
|
|
const { RangePicker } = DatePicker;
|
|
|
|
|
|
|
|
|
@@ -105,8 +110,11 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
date_from: dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
date_to: dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
});
|
|
|
|
|
const [allItems, setAllItems] = useState<ComparisonRecordListItem[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [items, setItems] = useState<ComparisonRecordListItem[]>([]);
|
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
const [overview, setOverview] = useState<ComparisonRecordsSummary | null>(null);
|
|
|
|
|
const [listLoading, setListLoading] = useState(false);
|
|
|
|
|
const [summaryLoading, setSummaryLoading] = useState(false);
|
|
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
|
const [pageSize, setPageSize] = useState(20);
|
|
|
|
|
// LLM 单价(元/百万 token),本地持久化;仅用于前端估算成本,不入库、不影响其它页面
|
|
|
|
@@ -125,7 +133,8 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
const [detail, setDetail] = useState<ComparisonRecordDetail | null>(null);
|
|
|
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
const search = () =>
|
|
|
|
|
const search = () => {
|
|
|
|
|
setPage(1);
|
|
|
|
|
setApplied({
|
|
|
|
|
date_from: range[0].format('YYYY-MM-DD'),
|
|
|
|
|
date_to: range[1].format('YYYY-MM-DD'),
|
|
|
|
@@ -135,6 +144,7 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
store: store.trim() || undefined,
|
|
|
|
|
product: product.trim() || undefined,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
|
setUserId(null);
|
|
|
|
@@ -143,6 +153,7 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
setStore('');
|
|
|
|
|
setProduct('');
|
|
|
|
|
const yesterday = dayjs().subtract(1, 'day');
|
|
|
|
|
setPage(1);
|
|
|
|
|
setRange([yesterday, yesterday]);
|
|
|
|
|
setApplied({
|
|
|
|
|
date_from: yesterday.format('YYYY-MM-DD'),
|
|
|
|
@@ -152,49 +163,50 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let alive = true;
|
|
|
|
|
const loadAll = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const loadPage = async () => {
|
|
|
|
|
setListLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const serverFilters = { ...applied };
|
|
|
|
|
delete serverFilters.date_from;
|
|
|
|
|
delete serverFilters.date_to;
|
|
|
|
|
const rows: ComparisonRecordListItem[] = [];
|
|
|
|
|
let cursor = 0;
|
|
|
|
|
for (let pageIndex = 0; pageIndex < 200; pageIndex += 1) {
|
|
|
|
|
const { data } = await api.get<CursorPage<ComparisonRecordListItem>>(
|
|
|
|
|
'/admin/api/comparison-records',
|
|
|
|
|
{ params: { ...serverFilters, limit: 100, cursor } },
|
|
|
|
|
);
|
|
|
|
|
rows.push(...data.items);
|
|
|
|
|
if (data.next_cursor == null || data.items.length === 0) break;
|
|
|
|
|
cursor = data.next_cursor;
|
|
|
|
|
}
|
|
|
|
|
const { data } = await api.get<CursorPage<ComparisonRecordListItem>>(
|
|
|
|
|
'/admin/api/comparison-records',
|
|
|
|
|
{ params: { ...applied, limit: pageSize, cursor: (page - 1) * pageSize } },
|
|
|
|
|
);
|
|
|
|
|
if (alive) {
|
|
|
|
|
setAllItems(rows);
|
|
|
|
|
setPage(1);
|
|
|
|
|
setItems(data.items);
|
|
|
|
|
setTotal(data.total ?? 0);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (alive) message.error(errMsg(e));
|
|
|
|
|
} finally {
|
|
|
|
|
if (alive) setLoading(false);
|
|
|
|
|
if (alive) setListLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
loadAll();
|
|
|
|
|
loadPage();
|
|
|
|
|
return () => {
|
|
|
|
|
alive = false;
|
|
|
|
|
};
|
|
|
|
|
}, [applied, message, page, pageSize]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let alive = true;
|
|
|
|
|
const loadSummary = async () => {
|
|
|
|
|
setSummaryLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await api.get<ComparisonRecordsSummary>(
|
|
|
|
|
'/admin/api/comparison-records/summary',
|
|
|
|
|
{ params: applied },
|
|
|
|
|
);
|
|
|
|
|
if (alive) setOverview(data);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (alive) message.error(errMsg(e));
|
|
|
|
|
} finally {
|
|
|
|
|
if (alive) setSummaryLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
loadSummary();
|
|
|
|
|
return () => {
|
|
|
|
|
alive = false;
|
|
|
|
|
};
|
|
|
|
|
}, [applied, message]);
|
|
|
|
|
|
|
|
|
|
const filteredItems = useMemo(() => {
|
|
|
|
|
const from = String(applied.date_from ?? '');
|
|
|
|
|
const to = String(applied.date_to ?? '');
|
|
|
|
|
return allItems.filter((item) => {
|
|
|
|
|
const date = dayjs(item.created_at).format('YYYY-MM-DD');
|
|
|
|
|
return (!from || date >= from) && (!to || date <= to);
|
|
|
|
|
});
|
|
|
|
|
}, [allItems, applied]);
|
|
|
|
|
const total = filteredItems.length;
|
|
|
|
|
const items = filteredItems.slice((page - 1) * pageSize, page * pageSize);
|
|
|
|
|
const onChange = (nextPage: number, nextPageSize: number) => {
|
|
|
|
|
setPageSize(nextPageSize);
|
|
|
|
|
setPage(nextPageSize === pageSize ? nextPage : 1);
|
|
|
|
@@ -218,46 +230,6 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
setDetailLoading(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const overview = useMemo(() => {
|
|
|
|
|
const completed = filteredItems.filter(
|
|
|
|
|
(item) => item.status === 'success' || item.status === 'failed',
|
|
|
|
|
);
|
|
|
|
|
const cancelled = filteredItems.filter((item) => item.status === 'cancelled');
|
|
|
|
|
const success = filteredItems.filter((item) => item.status === 'success');
|
|
|
|
|
const successRateDenominator = filteredItems.length - cancelled.length;
|
|
|
|
|
const completedDurations = completed
|
|
|
|
|
.map((item) => item.total_ms)
|
|
|
|
|
.filter((value): value is number => value != null);
|
|
|
|
|
const cancelledDurations = cancelled
|
|
|
|
|
.map((item) => item.total_ms)
|
|
|
|
|
.filter((value): value is number => value != null);
|
|
|
|
|
const costs = filteredItems
|
|
|
|
|
.map((item) => item.llm_cost_yuan)
|
|
|
|
|
.filter((value): value is number => value != null);
|
|
|
|
|
const avg = (values: number[]) =>
|
|
|
|
|
values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
|
|
|
|
|
return {
|
|
|
|
|
started: filteredItems.length,
|
|
|
|
|
completed: completed.length,
|
|
|
|
|
success: success.length,
|
|
|
|
|
successRate: successRateDenominator ? success.length / successRateDenominator : null,
|
|
|
|
|
avgTokenCost: avg(costs),
|
|
|
|
|
lowerPriceRate: success.length
|
|
|
|
|
? success.filter((item) => (item.saved_amount_cents ?? 0) > 0).length / success.length
|
|
|
|
|
: null,
|
|
|
|
|
avgMs: avg(completedDurations),
|
|
|
|
|
p5Ms: percentile(completedDurations, 0.05),
|
|
|
|
|
p50Ms: percentile(completedDurations, 0.5),
|
|
|
|
|
p95Ms: percentile(completedDurations, 0.95),
|
|
|
|
|
p99Ms: percentile(completedDurations, 0.99),
|
|
|
|
|
cancelled: cancelled.length,
|
|
|
|
|
cancelledRate: filteredItems.length ? cancelled.length / filteredItems.length : null,
|
|
|
|
|
cancelledP5Ms: percentile(cancelledDurations, 0.05),
|
|
|
|
|
cancelledP50Ms: percentile(cancelledDurations, 0.5),
|
|
|
|
|
cancelledP95Ms: percentile(cancelledDurations, 0.95),
|
|
|
|
|
};
|
|
|
|
|
}, [filteredItems]);
|
|
|
|
|
|
|
|
|
|
const columns: ColumnsType<ComparisonRecordListItem> = [
|
|
|
|
|
{ title: '时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatWallTime(v) },
|
|
|
|
|
{
|
|
|
|
@@ -424,28 +396,28 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
</Space>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card size="small" style={{ marginBottom: 16 }} loading={loading}>
|
|
|
|
|
<Card size="small" style={{ marginBottom: 16 }} loading={summaryLoading}>
|
|
|
|
|
<Divider orientation="left" plain style={{ marginTop: 0 }}>数据概览</Divider>
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价发起数" value={overview.started} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价完成数" value={overview.completed} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价成功次数" value={overview.success} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价成功率" value={fmtRate(overview.successRate)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="平均 TOKEN 成本" value={overview.avgTokenCost ?? '-'} precision={4} prefix="¥" /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比出更低价率" value={fmtRate(overview.lowerPriceRate)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时平均数" value={fmtMs(overview.avgMs)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 5 分位" value={fmtMs(overview.p5Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 50 分位" value={fmtMs(overview.p50Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 95 分位" value={fmtMs(overview.p95Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 99 分位" value={fmtMs(overview.p99Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价发起数" value={overview?.started ?? 0} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价完成数" value={overview?.completed ?? 0} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价成功次数" value={overview?.success ?? 0} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比价成功率" value={fmtRate(overview?.success_rate ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="平均 TOKEN 成本" value={overview?.avg_token_cost ?? '-'} precision={4} prefix="¥" /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="比出更低价率" value={fmtRate(overview?.lower_price_rate ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时平均数" value={fmtMs(overview?.avg_duration_ms ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 5 分位" value={fmtMs(overview?.p5_duration_ms ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 50 分位" value={fmtMs(overview?.p50_duration_ms ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 95 分位" value={fmtMs(overview?.p95_duration_ms ?? null)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="耗时 99 分位" value={fmtMs(overview?.p99_duration_ms ?? null)} /></Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Divider orientation="left" plain>中途退出</Divider>
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="中途退出次数" value={overview.cancelled} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="中途退出率" value={fmtRate(overview.cancelledRate)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="退出耗时 5 分位" value={fmtMs(overview.cancelledP5Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="退出耗时 50 分位" value={fmtMs(overview.cancelledP50Ms)} /></Col>
|
|
|
|
|
<Col xs={12} md={6}><Statistic title="退出耗时 95 分位" value={fmtMs(overview.cancelledP95Ms)} /></Col>
|
|
|
|
|
<Col flex="1 1 180px"><Statistic title="中途退出次数" value={overview?.cancelled ?? 0} /></Col>
|
|
|
|
|
<Col flex="1 1 180px"><Statistic title="中途退出率" value={fmtRate(overview?.cancelled_rate ?? null)} /></Col>
|
|
|
|
|
<Col flex="1 1 180px"><Statistic title="退出耗时 5 分位" value={fmtMs(overview?.cancelled_p5_ms ?? null)} /></Col>
|
|
|
|
|
<Col flex="1 1 180px"><Statistic title="退出耗时 50 分位" value={fmtMs(overview?.cancelled_p50_ms ?? null)} /></Col>
|
|
|
|
|
<Col flex="1 1 180px"><Statistic title="退出耗时 95 分位" value={fmtMs(overview?.cancelled_p95_ms ?? null)} /></Col>
|
|
|
|
|
</Row>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
@@ -453,7 +425,7 @@ export default function ComparisonRecordsPage() {
|
|
|
|
|
rowKey="id"
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={items}
|
|
|
|
|
loading={loading}
|
|
|
|
|
loading={listLoading}
|
|
|
|
|
pagination={{
|
|
|
|
|
current: page,
|
|
|
|
|
pageSize,
|
|
|
|
|