Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 84 additions & 4 deletions src/agentsight/dashboard/src/pages/AtifViewerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import type {
AtifDocument, AtifStep, AtifToolCall, AtifObservation, AtifStepMetrics,
} from '../types';
import { fetchAtifBySession, fetchAtifByConversation } from '../utils/apiClient';
import { fetchAtifBySession, fetchAtifByConversation, fetchSessionSavings } from '../utils/apiClient';
import type { SessionSavingsDetail, OptimizationItem } from '../utils/apiClient';

// ─── Helpers ──────────────────────────────────────────────────────────────────

Expand All @@ -27,6 +28,15 @@ function shortId(id: string, len = 20): string {
return id.length > len ? id.slice(0, len) + '\u2026' : id;
}

// ─── Strategy label config (shared with TokenSavingsPage) ────────────────────

const STRATEGY_LABELS: Record<string, { label: string; color: string; bg: string }> = {
'compress-schema': { label: 'Schema 压缩', color: 'text-blue-700', bg: 'bg-blue-100' },
'compress-response': { label: '响应压缩', color: 'text-violet-700', bg: 'bg-violet-100' },
'rewrite-command': { label: '命令重写', color: 'text-orange-700', bg: 'bg-orange-100' },
'compress-toon': { label: 'TOON 编码', color: 'text-teal-700', bg: 'bg-teal-100' },
};

// ─── Source styling ───────────────────────────────────────────────────────────

const SOURCE_STYLES: Record<string, { dot: string; badge: string; border: string; label: string }> = {
Expand Down Expand Up @@ -121,9 +131,10 @@ interface StepCardProps {
step: AtifStep;
expandedSections: Set<string>;
onToggleSection: (key: string) => void;
savingsMap?: Map<string, OptimizationItem>;
}

const StepCard: React.FC<StepCardProps> = ({ step, expandedSections, onToggleSection }) => {
const StepCard: React.FC<StepCardProps> = ({ step, expandedSections, onToggleSection, savingsMap }) => {
const style = getSourceStyle(step.source);
const sectionKey = (name: string) => `${step.step_id}-${name}`;
const isOpen = (name: string) => expandedSections.has(sectionKey(name));
Expand Down Expand Up @@ -197,7 +208,7 @@ const StepCard: React.FC<StepCardProps> = ({ step, expandedSections, onToggleSec
>
<div className="space-y-2">
{step.tool_calls!.map((tc, i) => (
<ToolCallItem key={tc.tool_call_id || i} tc={tc} />
<ToolCallItem key={tc.tool_call_id || i} tc={tc} savingsMap={savingsMap} />
))}
</div>
</Collapsible>
Expand Down Expand Up @@ -263,12 +274,14 @@ const StepCard: React.FC<StepCardProps> = ({ step, expandedSections, onToggleSec

// ─── ToolCallItem ─────────────────────────────────────────────────────────────

const ToolCallItem: React.FC<{ tc: AtifToolCall }> = ({ tc }) => {
const ToolCallItem: React.FC<{ tc: AtifToolCall; savingsMap?: Map<string, OptimizationItem> }> = ({ tc, savingsMap }) => {
const [showArgs, setShowArgs] = useState(false);
const argsStr = typeof tc.arguments === 'string'
? tc.arguments
: JSON.stringify(tc.arguments, null, 2);
const isLongArgs = argsStr.length > 200;
const savings = savingsMap?.get(tc.tool_call_id);
const stratStyle = savings ? (STRATEGY_LABELS[savings.strategy] ?? { label: savings.strategy_label, color: 'text-gray-700', bg: 'bg-gray-100' }) : null;

return (
<div className="border border-orange-100 rounded-lg overflow-hidden">
Expand All @@ -277,6 +290,11 @@ const ToolCallItem: React.FC<{ tc: AtifToolCall }> = ({ tc }) => {
{tc.function_name}
</span>
<span className="text-xs text-gray-400 font-mono">{shortId(tc.tool_call_id, 16)}</span>
{savings && stratStyle && (
<span className={`px-2 py-0.5 rounded text-xs font-medium ${stratStyle.bg} ${stratStyle.color}`}>
已优化 -{fmtTokens(savings.compounded_saved)} tokens ({stratStyle.label})
</span>
)}
{isLongArgs && (
<button
onClick={() => setShowArgs(!showArgs)}
Expand Down Expand Up @@ -347,6 +365,13 @@ export const AtifViewerPage: React.FC = () => {
const [doc, setDoc] = useState<AtifDocument | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [savingsDetail, setSavingsDetail] = useState<SessionSavingsDetail | null>(null);

// Build tool_call_id → OptimizationItem map for StepCard badges
const savingsMap = React.useMemo(() => {
if (!savingsDetail?.items?.length) return new Map<string, OptimizationItem>();
return new Map(savingsDetail.items.map(item => [item.id, item]));
}, [savingsDetail]);

// UI state
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -381,6 +406,12 @@ export const AtifViewerPage: React.FC = () => {
data = await fetchAtifBySession(i.trim());
}
setDoc(data);
// Fetch savings data for the session
if (data.session_id) {
fetchSessionSavings(data.session_id)
.then(setSavingsDetail)
.catch(() => setSavingsDetail(null));
}
} catch (e: any) {
setError(e.message ?? '加载失败');
} finally {
Expand Down Expand Up @@ -599,6 +630,54 @@ export const AtifViewerPage: React.FC = () => {
)}
</div>

{/* Token Savings Comparison Card */}
{savingsDetail && savingsDetail.total_compounded_saved > 0 && (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Token 节省对比</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div>
<span className="text-xs text-gray-500">原始 Token(未优化)</span>
<p className="text-xl font-bold text-gray-700">{fmtTokens(savingsDetail.total_original_tokens)}</p>
</div>
<div>
<span className="text-xs text-gray-500">实际 Token(优化后)</span>
<p className="text-xl font-bold text-blue-600">{fmtTokens(savingsDetail.total_actual_tokens)}</p>
</div>
<div>
<span className="text-xs text-gray-500">节省</span>
<p className="text-xl font-bold text-green-600">
-{fmtTokens(savingsDetail.total_compounded_saved)}
<span className="text-sm font-normal text-gray-400 ml-1">
({savingsDetail.savings_rate.toFixed(1)}%)
</span>
</p>
</div>
</div>
{/* Comparison bar */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 w-10">原始</span>
<div className="flex-1 h-3 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full bg-gray-400 rounded-full" style={{ width: '100%' }} />
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 w-10">实际</span>
<div className="flex-1 h-3 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-green-500 rounded-full"
style={{
width: savingsDetail.total_original_tokens > 0
? `${(savingsDetail.total_actual_tokens / savingsDetail.total_original_tokens) * 100}%`
: '100%',
}}
/>
</div>
</div>
</div>
</div>
)}

{/* Step Timeline */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Expand All @@ -624,6 +703,7 @@ export const AtifViewerPage: React.FC = () => {
step={step}
expandedSections={expandedSections}
onToggleSection={toggleSection}
savingsMap={savingsMap}
/>
))}
</div>
Expand Down
146 changes: 111 additions & 35 deletions src/agentsight/dashboard/src/pages/TokenSavingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
PieChart, Pie, Cell, ResponsiveContainer,
} from 'recharts';
import { fetchTokenSavings, fetchAgentNames } from '../utils/apiClient';
import type { SessionSavings, SavingsSummary, OptimizationItem, DiffLine } from '../utils/apiClient';
import type { SessionSavings, SavingsSummary, OptimizationItem, DiffLine, StrategyBreakdownItem } from '../utils/apiClient';
import { DateTimePicker } from '../components/DateTimePicker';
import { SessionIdHelp } from '../components/SessionIdHelp';

Expand Down Expand Up @@ -74,6 +74,15 @@ const CATEGORY_CONFIG: Record<OptimizationCategory, { label: string; color: stri
mcp_response: { label: 'MCP输出', color: 'text-violet-700', bg: 'bg-violet-100' },
};

// ─── Strategy config ─────────────────────────────────────────────────────────

const STRATEGY_CONFIG: Record<string, { label: string; color: string; bg: string; pie: string; tooltip: string }> = {
'compress-schema': { label: 'Schema 压缩', color: 'text-blue-700', bg: 'bg-blue-100', pie: '#3b82f6', tooltip: '精简工具/MCP 接口定义,减少上下文体积' },
'compress-response': { label: '响应压缩', color: 'text-violet-700', bg: 'bg-violet-100', pie: '#8b5cf6', tooltip: '清理响应冗余字段,保留语义关键内容' },
'rewrite-command': { label: '命令重写', color: 'text-orange-700', bg: 'bg-orange-100', pie: '#f59e0b', tooltip: '将工具命令重写为更精简的等价形式' },
'compress-toon': { label: 'TOON 编码', color: 'text-teal-700', bg: 'bg-teal-100', pie: '#14b8a6', tooltip: '将 JSON 输出转换为紧凑 TOON 表格文本' },
};

// ─── Pie chart data ───────────────────────────────────────────────────────────

const PIE_COLORS = ['#3b82f6', '#10b981']; // 输入蓝, 输出绿
Expand Down Expand Up @@ -113,6 +122,11 @@ const DiffView: React.FC<{ item: OptimizationItem }> = ({ item }) => {
const OptimizationTableRow: React.FC<{ item: OptimizationItem }> = ({ item }) => {
const [expanded, setExpanded] = useState(false);
const cfg = CATEGORY_CONFIG[item.category];
const stratCfg = STRATEGY_CONFIG[item.strategy] ?? {
label: item.strategy_label || item.strategy,
color: 'text-gray-700', bg: 'bg-gray-100', pie: '#9ca3af',
tooltip: '',
};

return (
<>
Expand All @@ -122,6 +136,17 @@ const OptimizationTableRow: React.FC<{ item: OptimizationItem }> = ({ item }) =>
{cfg.label}
</span>
</td>
<td className="px-4 py-3">
<span className={`relative group px-2 py-0.5 rounded text-xs font-medium ${stratCfg.bg} ${stratCfg.color} cursor-default`}>
{stratCfg.label}
{stratCfg.tooltip && (
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1.5 hidden group-hover:block px-2 py-1.5 rounded bg-gray-800 text-white text-xs whitespace-nowrap shadow-lg z-50 pointer-events-none">
{stratCfg.tooltip}
<span className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-800" />
</span>
)}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-600 text-right">
{fmtTokens(item.before_tokens)}
</td>
Expand All @@ -142,7 +167,7 @@ const OptimizationTableRow: React.FC<{ item: OptimizationItem }> = ({ item }) =>
</tr>
{expanded && (
<tr className="bg-gray-50">
<td colSpan={5} className="px-4 py-3">
<td colSpan={6} className="px-4 py-3">
<DiffView item={item} />
</td>
</tr>
Expand Down Expand Up @@ -218,12 +243,15 @@ const SessionRow: React.FC<{
<td colSpan={6} className="px-4 lg:px-8 py-4">
{/* Optimization items table */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table className="w-full min-w-[600px]">
<table className="w-full min-w-[700px]">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wide w-[90px]">
分类
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wide w-[110px]">
节省策略
</th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-600 uppercase tracking-wide w-[100px]">
优化前
</th>
Expand Down Expand Up @@ -446,44 +474,92 @@ export const TokenSavingsPage: React.FC = () => {
</div>
</div>

{/* Card 2: Saved tokens */}
{/* Card 2: Saved tokens — strategy breakdown pie */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<p className="text-sm text-gray-500">已降低 Token</p>
<p className="text-3xl font-bold text-green-600 mt-1">
{fmtTokens(totalCompoundedSaved)}
</p>
<div className="mt-3">
<ResponsiveContainer width="100%" height={60}>
<PieChart>
<Pie
data={[
{ name: '工具', value: totalCompoundedToolSaved },
{ name: 'MCP', value: totalCompoundedMcpSaved },
]}
cx="50%"
cy="50%"
innerRadius={14}
outerRadius={26}
paddingAngle={2}
dataKey="value"
stroke="none"
>
{SAVED_PIE_COLORS.map((c, i) => (
<Cell key={i} fill={c} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
<div className="flex justify-center gap-4 -mt-1">
<span className="flex items-center gap-1 text-xs text-gray-500">
<span className="w-2 h-2 rounded-full bg-orange-500" />
工具 {fmtTokens(totalCompoundedToolSaved)}
</span>
<span className="flex items-center gap-1 text-xs text-gray-500">
<span className="w-2 h-2 rounded-full bg-violet-500" />
MCP {fmtTokens(totalCompoundedMcpSaved)}
</span>
</div>
{(() => {
const breakdown = summary?.strategy_breakdown ?? [];
const hasStrategy = breakdown.length > 0 && breakdown.some(b => b.compounded_saved > 0);
if (hasStrategy) {
const pieData = breakdown
.filter(b => b.compounded_saved > 0)
.map(b => ({
name: (STRATEGY_CONFIG[b.strategy]?.label ?? b.label),
value: b.compounded_saved,
color: (STRATEGY_CONFIG[b.strategy]?.pie ?? '#9ca3af'),
}));
return (
<>
<ResponsiveContainer width="100%" height={60}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius={14}
outerRadius={26}
paddingAngle={2}
dataKey="value"
stroke="none"
>
{pieData.map((d, i) => (
<Cell key={i} fill={d.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
<div className="flex flex-wrap justify-center gap-3 -mt-1">
{pieData.map((d, i) => (
<span key={i} className="flex items-center gap-1 text-xs text-gray-500">
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: d.color }} />
{d.name} {fmtTokens(d.value)}
</span>
))}
</div>
</>
);
}
// Fallback to category-level 2-slice pie
return (
<>
<ResponsiveContainer width="100%" height={60}>
<PieChart>
<Pie
data={[
{ name: '工具', value: totalCompoundedToolSaved },
{ name: 'MCP', value: totalCompoundedMcpSaved },
]}
cx="50%"
cy="50%"
innerRadius={14}
outerRadius={26}
paddingAngle={2}
dataKey="value"
stroke="none"
>
{SAVED_PIE_COLORS.map((c, i) => (
<Cell key={i} fill={c} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
<div className="flex justify-center gap-4 -mt-1">
<span className="flex items-center gap-1 text-xs text-gray-500">
<span className="w-2 h-2 rounded-full bg-orange-500" />
工具 {fmtTokens(totalCompoundedToolSaved)}
</span>
<span className="flex items-center gap-1 text-xs text-gray-500">
<span className="w-2 h-2 rounded-full bg-violet-500" />
MCP {fmtTokens(totalCompoundedMcpSaved)}
</span>
</div>
</>
);
})()}
</div>
</div>

Expand Down
Loading
Loading