Update BossAI workflow and workspace experience
This commit is contained in:
@@ -4,10 +4,13 @@ import { useThemeLabelColor } from '../useThemeLabelColor.js';
|
||||
|
||||
interface SiteDistributionData {
|
||||
siteName: string;
|
||||
platform: string;
|
||||
totalBalance: number;
|
||||
totalSpend: number;
|
||||
accountCount: number;
|
||||
platform?: string | null;
|
||||
totalBalance?: number;
|
||||
totalSpend?: number;
|
||||
accountCount?: number;
|
||||
spend?: number;
|
||||
calls?: number;
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
interface SiteDistributionChartProps {
|
||||
@@ -26,6 +29,13 @@ function safeNumber(value: unknown): number {
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatPercentLabel(datum: unknown): string {
|
||||
const item = coerceDatumRecord(datum);
|
||||
const percent = safeNumber(item._percent_);
|
||||
if (percent <= 0) return '';
|
||||
return `${percent.toFixed(percent >= 10 ? 0 : 1)}%`;
|
||||
}
|
||||
|
||||
function SkeletonCircle() {
|
||||
return (
|
||||
<div
|
||||
@@ -98,8 +108,11 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
|
||||
return data.map((item) => ({
|
||||
siteName: item.siteName,
|
||||
platform: item.platform,
|
||||
value: safeNumber(viewMode === 'balance' ? item.totalBalance : item.totalSpend),
|
||||
value: safeNumber(viewMode === 'balance'
|
||||
? item.totalBalance
|
||||
: item.totalSpend ?? item.spend),
|
||||
accountCount: safeNumber(item.accountCount),
|
||||
calls: safeNumber(item.calls),
|
||||
}));
|
||||
}, [data, viewMode]);
|
||||
|
||||
@@ -118,7 +131,12 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.55,
|
||||
pie: { style: { cornerRadius: 4, padAngle: 0.02 } },
|
||||
label: { visible: true, position: 'outside', formatter: '{_percent_}%', style: { fill: labelColor } },
|
||||
label: {
|
||||
visible: true,
|
||||
position: 'outside',
|
||||
formatMethod: (_text: unknown, datum: unknown) => formatPercentLabel(datum),
|
||||
style: { fill: labelColor, fontSize: 12, stroke: 'transparent' },
|
||||
},
|
||||
legends: { visible: false },
|
||||
tooltip: {
|
||||
mark: {
|
||||
@@ -149,6 +167,13 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
|
||||
return String(item.accountCount || 0);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '调用',
|
||||
value: (datum: unknown) => {
|
||||
const item = coerceDatumRecord(datum);
|
||||
return String(item.calls || 0);
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
},
|
||||
},
|
||||
@@ -159,6 +184,7 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
|
||||
}, [chartData, hasData, labelColor]);
|
||||
|
||||
const formatValue = (value: number): string => {
|
||||
if (viewMode === 'balance' && value === 0) return '$0.00';
|
||||
if (value >= 1000) return `$${value.toFixed(2)}`;
|
||||
if (value >= 1) return `$${value.toFixed(3)}`;
|
||||
return `$${value.toFixed(6)}`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { VChart } from '@visactor/react-vchart';
|
||||
import { useThemeLabelColor } from '../useThemeLabelColor.js';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -7,7 +8,10 @@ import { VChart } from '@visactor/react-vchart';
|
||||
|
||||
interface SiteTrendData {
|
||||
date: string;
|
||||
sites: Record<string, { spend: number; calls: number }>;
|
||||
sites?: Record<string, { spend?: number; calls?: number; tokens?: number }>;
|
||||
spend?: number;
|
||||
calls?: number;
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
interface SiteTrendChartProps {
|
||||
@@ -43,20 +47,26 @@ const COLOR_PALETTE = [
|
||||
|
||||
export default function SiteTrendChart({ data, loading }: SiteTrendChartProps) {
|
||||
const [metric, setMetric] = useState<Metric>('spend');
|
||||
const labelColor = useThemeLabelColor();
|
||||
|
||||
/* ---------- data transform ---------- */
|
||||
|
||||
const flatData = useMemo(() => {
|
||||
if (!data || data.length === 0) return [];
|
||||
return data.flatMap((d) =>
|
||||
Object.entries(d.sites).map(([site, v]) => ({
|
||||
return data.flatMap((d) => {
|
||||
const sites = d.sites && Object.keys(d.sites).length > 0
|
||||
? d.sites
|
||||
: { 全部站点: { spend: d.spend, calls: d.calls, tokens: d.tokens } };
|
||||
return Object.entries(sites).map(([site, v]) => ({
|
||||
date: d.date,
|
||||
site,
|
||||
value: metric === 'spend' ? v.spend : v.calls,
|
||||
})),
|
||||
);
|
||||
value: metric === 'spend' ? Number(v.spend ?? 0) : Number(v.calls ?? 0),
|
||||
}));
|
||||
});
|
||||
}, [data, metric]);
|
||||
|
||||
const hasPositiveValue = flatData.some((item) => Number(item.value || 0) > 0);
|
||||
|
||||
/* ---------- loading state ---------- */
|
||||
|
||||
if (loading) {
|
||||
@@ -72,7 +82,7 @@ export default function SiteTrendChart({ data, loading }: SiteTrendChartProps) {
|
||||
|
||||
/* ---------- empty state ---------- */
|
||||
|
||||
if (!data || data.length === 0 || flatData.length === 0) {
|
||||
if (!data || data.length === 0 || flatData.length === 0 || !hasPositiveValue) {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={headerStyle}>
|
||||
@@ -80,7 +90,7 @@ export default function SiteTrendChart({ data, loading }: SiteTrendChartProps) {
|
||||
</div>
|
||||
<div className="empty-state" style={{ padding: 48 }}>
|
||||
<div className="empty-state-title">暂无趋势数据</div>
|
||||
<div className="empty-state-desc">数据加载后将自动展示趋势图表</div>
|
||||
<div className="empty-state-desc">所选时间范围内还没有有效的{metric === 'spend' ? '消耗' : '调用'}记录</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -144,22 +154,38 @@ export default function SiteTrendChart({ data, loading }: SiteTrendChartProps) {
|
||||
axes: [
|
||||
{
|
||||
orient: 'bottom',
|
||||
label: { style: { fontSize: 11, fill: 'var(--color-text-muted)' } },
|
||||
label: {
|
||||
style: { fontSize: 11, fill: labelColor },
|
||||
formatMethod: (value: string) => {
|
||||
const text = String(value || '');
|
||||
if (text.length >= 10) return text.slice(5, 10);
|
||||
return text;
|
||||
},
|
||||
},
|
||||
domainLine: { style: { stroke: 'var(--color-border-light)' } },
|
||||
tick: { style: { stroke: 'var(--color-border-light)' } },
|
||||
},
|
||||
{
|
||||
orient: 'left',
|
||||
label: {
|
||||
style: { fontSize: 11, fill: 'var(--color-text-muted)' },
|
||||
style: { fontSize: 11, fill: labelColor },
|
||||
formatMethod: (value: string) => {
|
||||
const n = Number(value || 0);
|
||||
if (!Number.isFinite(n)) return String(value || '');
|
||||
if (metric === 'spend') {
|
||||
if (n >= 1) return `$${n.toFixed(1)}`;
|
||||
return `$${n.toFixed(4)}`;
|
||||
}
|
||||
return Math.round(n).toLocaleString();
|
||||
},
|
||||
},
|
||||
grid: { style: { stroke: 'var(--color-border-light)', lineDash: [4, 4] } },
|
||||
grid: { style: { stroke: 'var(--color-border-light)', lineDash: [3, 5], lineWidth: 1 } },
|
||||
domainLine: { visible: false },
|
||||
},
|
||||
],
|
||||
color: COLOR_PALETTE,
|
||||
background: 'transparent',
|
||||
padding: { left: 8, right: 16, top: 8, bottom: 8 },
|
||||
padding: { left: 48, right: 18, top: 12, bottom: 36 },
|
||||
};
|
||||
|
||||
/* ---------- render ---------- */
|
||||
|
||||
Reference in New Issue
Block a user