Update BossAI workflow and workspace experience

This commit is contained in:
boos
2026-05-28 10:32:20 +08:00
parent e0bd9b4194
commit 931bcf3fb9
39 changed files with 7791 additions and 1328 deletions
+19 -10
View File
@@ -23,11 +23,11 @@ interface ModelAnalysisPanelProps {
data?: ModelAnalysisData | null;
}
const tabs: Array<{ key: TabKey; label: string; icon: string }> = [
{ key: 'spend', label: '消耗分布', icon: '💰' },
{ key: 'trend', label: '消耗趋势', icon: '📈' },
{ key: 'calls', label: '调用分布', icon: '🔄' },
{ key: 'rank', label: '排行榜', icon: '🏆' },
const tabs: Array<{ key: TabKey; label: string; code: string }> = [
{ key: 'spend', label: '消耗分布', code: 'SP' },
{ key: 'trend', label: '消耗趋势', code: 'TR' },
{ key: 'calls', label: '调用分布', code: 'CL' },
{ key: 'rank', label: '排行榜', code: 'RK' },
];
function toSafeNumber(value: unknown): number {
@@ -46,6 +46,13 @@ function formatPercent(value: number): string {
return `${toSafeNumber(value).toFixed(1)}%`;
}
function formatPiePercent(_text: unknown, datum: unknown): string {
const record = datum && typeof datum === 'object' ? datum as Record<string, unknown> : {};
const percent = toSafeNumber(record._percent_);
if (percent <= 0) return '';
return `${percent.toFixed(percent >= 10 ? 0 : 1)}%`;
}
function EmptyBlock() {
return (
<div className="empty-state" style={{ padding: 28 }}>
@@ -71,15 +78,16 @@ export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
const callRanking = (data?.callRanking || []).slice(0, 10);
const hasData = totals.calls > 0
|| spendDistribution.length > 0
|| spendTrend.some((item) => toSafeNumber(item.spend) > 0);
|| spendDistribution.some((item) => toSafeNumber(item.spend) > 0)
|| spendTrend.some((item) => toSafeNumber(item.spend) > 0)
|| callsDistribution.some((item) => toSafeNumber(item.calls) > 0);
const spendBarSpec = useMemo(() => ({
type: 'bar' as const,
data: [{ id: 'data', values: spendDistribution.map(d => ({ model: d.model.length > 25 ? d.model.slice(0, 25) + '...' : d.model, value: toSafeNumber(d.spend) })).reverse() }],
xField: 'value', yField: 'model', direction: 'horizontal' as const,
bar: { style: { cornerRadius: [0, 6, 6, 0], fill: { gradient: 'linear' as const, x0: 0, y0: 0, x1: 1, y1: 0, stops: [{ offset: 0, color: '#4f46e5' }, { offset: 1, color: '#818cf8' }] } } },
label: { visible: true, position: 'right', formatter: '{value}', style: { fontSize: 11, fill: labelColor, stroke: 'transparent' } },
label: { visible: true, position: 'right', formatMethod: (text: unknown) => formatCurrency(Number(text || 0)), style: { fontSize: 11, fill: labelColor, stroke: 'transparent' } },
axes: [{ orient: 'left', label: { style: { fontSize: 11, fill: labelColor } } }, { orient: 'bottom', visible: false }],
animation: true, background: 'transparent',
}), [spendDistribution, labelColor]);
@@ -102,7 +110,7 @@ export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
valueField: 'calls', categoryField: 'model',
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: formatPiePercent, style: { fill: labelColor, fontSize: 12, stroke: 'transparent' } },
legends: { visible: false },
animation: true,
color: ['#4f46e5', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'],
@@ -138,7 +146,8 @@ export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
className={`pill-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.icon} {tab.label}
<span className="pill-tab-code">{tab.code}</span>
{tab.label}
</button>
))}
</div>
@@ -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)}`;
+38 -12
View File
@@ -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 ---------- */