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
129 changes: 110 additions & 19 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, StrategyBreakdownItem } from '../utils/apiClient';
import type { SessionSavings, SavingsSummary, OptimizationItem, DiffLine, StrategyBreakdownItem, OptimizationTip } from '../utils/apiClient';
import { DateTimePicker } from '../components/DateTimePicker';
import { SessionIdHelp } from '../components/SessionIdHelp';

Expand Down Expand Up @@ -140,26 +140,20 @@ const DiffView: React.FC<{ item: OptimizationItem }> = ({ item }) => {

return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
{/* Optimization explanation panel */}
{item.optimization_reason && (
<div className="px-4 py-3 bg-blue-50 border-b border-blue-100">
<p className="text-sm text-blue-800">
<span className="font-medium">优化说明:</span>{item.optimization_reason}
{/* Explanation banner */}
<div className="px-3 py-2 bg-blue-50 border-b border-blue-100 flex items-start gap-2">
<span className="text-blue-500 mt-0.5">💡</span>
<div>
<p className="text-sm text-blue-800 font-medium">{item.explanation}</p>
<p className="text-xs text-blue-600 mt-0.5">
压缩率 <span className="font-semibold">{item.compression_ratio.toFixed(1)}%</span>
{' · '}
影响后续 <span className="font-semibold">{item.compounding_turns}</span> 轮调用
{' · '}
复合节省 <span className="font-semibold text-green-700">{fmtTokens(item.compounded_saved)}</span> tokens
</p>
{item.compounding_turns > 1 && (
<p className="text-xs text-blue-600 mt-1">
累计效果:此优化在后续 {item.compounding_turns} 轮对话中持续生效,
总计节省 {item.compounded_saved.toLocaleString()} tokens
</p>
)}
<div className="mt-2 h-1.5 rounded bg-blue-100 overflow-hidden">
<div
className="h-full bg-blue-500 rounded"
style={{ width: `${Math.min((item.saved_tokens / Math.max(item.before_tokens, 1)) * 100, 100)}%` }}
/>
</div>
</div>
)}
</div>

{/* Line-level diff body */}
<div className="overflow-x-auto max-h-[400px] overflow-y-auto">
Expand Down Expand Up @@ -210,6 +204,95 @@ const DiffView: React.FC<{ item: OptimizationItem }> = ({ item }) => {
);
};

// ─── Optimization Tips Panel ─────────────────────────────────────────────────

const TIP_STYLE: Record<string, { icon: string; border: string; bg: string; text: string }> = {
success: { icon: '✅', border: 'border-green-200', bg: 'bg-green-50', text: 'text-green-800' },
info: { icon: '💡', border: 'border-blue-200', bg: 'bg-blue-50', text: 'text-blue-800' },
warning: { icon: '⚠️', border: 'border-yellow-200', bg: 'bg-yellow-50', text: 'text-yellow-800' },
};

const OptimizationTipsPanel: React.FC<{ tips: OptimizationTip[] }> = ({ tips }) => {
if (tips.length === 0) return null;
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<span>🎯</span> 优化建议
</h3>
<div className="space-y-2">
{tips.map((tip, idx) => {
const style = TIP_STYLE[tip.level] || TIP_STYLE.info;
return (
<div key={idx} className={`flex items-start gap-2 px-3 py-2 rounded-lg border ${style.border} ${style.bg}`}>
<span className="mt-0.5">{style.icon}</span>
<div>
<p className={`text-sm font-medium ${style.text}`}>{tip.title}</p>
<p className={`text-xs ${style.text} opacity-80 mt-0.5`}>{tip.description}</p>
</div>
</div>
);
})}
</div>
</div>
);
};

// ─── Savings Breakdown Panel ─────────────────────────────────────────────────

const SavingsBreakdownPanel: React.FC<{ sessions: SessionSavings[] }> = ({ sessions }) => {
// Get top 5 optimization items across all sessions by compounded_saved
const allItems = sessions.flatMap(s =>
s.optimization_items.map(item => ({
...item,
session_id: s.session_id,
agent_name: s.agent_name,
}))
);
const topItems = [...allItems]
.sort((a, b) => b.compounded_saved - a.compounded_saved)
.slice(0, 5);

if (topItems.length === 0) return null;

const maxSaved = topItems[0]?.compounded_saved || 1;

return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<span>📊</span> 节省排行 Top 5(按复合节省量)
</h3>
<div className="space-y-2">
{topItems.map((item, idx) => {
const cfg = CATEGORY_CONFIG[item.category];
const pct = (item.compounded_saved / maxSaved) * 100;
return (
<div key={item.id || idx} className="flex items-center gap-3">
<span className="text-xs text-gray-400 w-4 text-right">#{idx + 1}</span>
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${cfg.bg} ${cfg.color} flex-shrink-0`}>
{cfg.label}
</span>
<div className="flex-1 min-w-0">
<div className="h-5 bg-gray-100 rounded-full overflow-hidden relative">
<div
className="h-full bg-gradient-to-r from-green-400 to-green-600 rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
<span className="absolute inset-0 flex items-center px-2 text-xs font-medium text-gray-700">
{fmtTokens(item.compounded_saved)} tokens
</span>
</div>
</div>
<span className="text-xs text-gray-400 flex-shrink-0 truncate max-w-[100px]" title={item.agent_name}>
{item.agent_name}
</span>
</div>
);
})}
</div>
</div>
);
};

// ─── Optimization table row ───────────────────────────────────────────────────

const OptimizationTableRow: React.FC<{ item: OptimizationItem }> = ({ item }) => {
Expand Down Expand Up @@ -402,6 +485,7 @@ export const TokenSavingsPage: React.FC = () => {
const [sessions, setSessions] = useState<SessionSavings[]>([]);
const [summary, setSummary] = useState<SavingsSummary | null>(null);
const [statsAvailable, setStatsAvailable] = useState(true);
const [tips, setTips] = useState<OptimizationTip[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [agentNames, setAgentNames] = useState<string[]>([]);
Expand All @@ -427,6 +511,7 @@ export const TokenSavingsPage: React.FC = () => {
setSessions(resp.sessions);
setSummary(resp.summary);
setStatsAvailable(resp.stats_available);
setTips(resp.optimization_tips ?? []);
} catch (e: any) {
setError(e.message || 'Failed to fetch token savings');
} finally {
Expand Down Expand Up @@ -744,6 +829,12 @@ export const TokenSavingsPage: React.FC = () => {
);
})()}

{/* ── Optimization tips + Savings breakdown ── */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<OptimizationTipsPanel tips={tips} />
<SavingsBreakdownPanel sessions={sessions} />
</div>

{/* ── Session table ── */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
Expand Down
100 changes: 96 additions & 4 deletions src/agentsight/dashboard/src/test/TokenSavingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe('TokenSavingsPage', () => {
sessions: [],
summary: { total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, total_compounded_saved: 0, total_compounded_tool_saved: 0, total_compounded_mcp_saved: 0, compounded_savings_rate: 0 },
stats_available: false,
optimization_tips: [{ level: 'warning', title: '未检测到 Tokenless 组件', description: '未发现 stats.db,请确认 tokenless 组件已安装并启用。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
Expand All @@ -116,6 +117,7 @@ describe('TokenSavingsPage', () => {
],
},
stats_available: true,
optimization_tips: [{ level: 'success', title: '节省效果良好', description: '当前复合节省率 25.0%,已达到良好水平。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
Expand Down Expand Up @@ -153,6 +155,7 @@ describe('TokenSavingsPage', () => {
strategy_breakdown: [],
},
stats_available: true,
optimization_tips: [],
});
await act(async () => { renderPage(); });
await act(async () => {
Expand Down Expand Up @@ -180,6 +183,9 @@ describe('TokenSavingsPage', () => {
before_tokens: 400,
after_tokens: 100,
compounded_saved: 300,
compression_ratio: 75.0,
explanation: '工具输出优化: 原始 400 tokens → 100 tokens,压缩率 75.0%。后续 1 轮LLM调用均受益,复合节省 300 tokens。',
compounding_turns: 1,
before_text: 'long original text',
after_text: 'short text',
diff_lines: [],
Expand All @@ -198,19 +204,30 @@ describe('TokenSavingsPage', () => {
],
},
stats_available: true,
optimization_tips: [],
});
await act(async () => { renderPage(); });
await act(async () => {
fireEvent.click(screen.getByText('查询'));
});
// Click session row to expand
const row = screen.getByText('Expander').closest('tr');
// Click session row to expand - find the one inside the table (has <tr> ancestor)
const allExpander = screen.getAllByText('Expander');
const row = allExpander.map(el => el.closest('tr')).find(tr => tr !== null);
await act(async () => {
fireEvent.click(row!);
});
expect(screen.getByText('工具输出')).toBeInTheDocument();
expect(screen.getByText('Schema 压缩')).toBeInTheDocument();
expect(screen.getAllByText('工具输出').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('Schema 压缩').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('详情').length).toBeGreaterThanOrEqual(1);
// Click "详情" to expand DiffView and verify compression_ratio renders
const detailBtns = screen.getAllByText('详情');
await act(async () => {
fireEvent.click(detailBtns[0]);
});
// The explanation banner should show compression ratio text
expect(screen.getByText(/75\.0%/)).toBeInTheDocument();
// The explanation text itself
expect(screen.getByText(/压缩率/)).toBeInTheDocument();
});

it('should show savings rate badge as 优秀 when >= 30%', async () => {
Expand All @@ -227,11 +244,86 @@ describe('TokenSavingsPage', () => {
strategy_breakdown: [],
},
stats_available: true,
optimization_tips: [{ level: 'success', title: '节省效果优秀', description: '当前复合节省率 50.0%,表现优秀!继续保持当前配置。' }],
});
await act(async () => { renderPage(); });
await act(async () => {
fireEvent.click(screen.getByText('查询'));
});
expect(screen.getByText('优秀')).toBeInTheDocument();
});

it('should render optimization tips panel', async () => {
mockFetchTokenSavings.mockResolvedValue({
sessions: [],
summary: {
total_input_tokens: 5000,
total_output_tokens: 3000,
total_tokens: 8000,
total_compounded_saved: 200,
total_compounded_tool_saved: 200,
total_compounded_mcp_saved: 0,
compounded_savings_rate: 2.5,
},
stats_available: true,
optimization_tips: [
{ level: 'warning', title: '节省率较低', description: '当前复合节省率不足 5%,建议检查 tokenless 配置。' },
{ level: 'info', title: '建议开启 MCP 响应压缩', description: '当前仅有工具输出优化,未检测到 MCP 响应压缩。开启后可进一步降低 Token 消耗。' },
],
});
await act(async () => { renderPage(); });
await act(async () => {
fireEvent.click(screen.getByText('查询'));
});
expect(screen.getByText('优化建议')).toBeInTheDocument();
expect(screen.getByText('节省率较低')).toBeInTheDocument();
expect(screen.getByText('建议开启 MCP 响应压缩')).toBeInTheDocument();
});

it('should render savings breakdown panel with top items', async () => {
mockFetchTokenSavings.mockResolvedValue({
sessions: [{
session_id: 'sess-breakdown',
agent_name: 'BreakdownAgent',
total_input_tokens: 3000,
total_output_tokens: 1500,
saved_tokens: 800,
compounded_saved: 800,
compounded_savings_rate: 17.8,
optimization_items: [
{
id: 'item-1',
category: 'tool_output',
before_tokens: 500,
after_tokens: 100,
saved_tokens: 400,
compounded_saved: 800,
compounding_turns: 2,
compression_ratio: 80.0,
explanation: '工具输出优化: 原始 500 tokens → 100 tokens,压缩率 80.0%。',
before_text: 'original',
after_text: 'compressed',
diff_lines: [],
},
],
}],
summary: {
total_input_tokens: 3000,
total_output_tokens: 1500,
total_tokens: 4500,
total_compounded_saved: 800,
total_compounded_tool_saved: 800,
total_compounded_mcp_saved: 0,
compounded_savings_rate: 17.8,
},
stats_available: true,
optimization_tips: [],
});
await act(async () => { renderPage(); });
await act(async () => {
fireEvent.click(screen.getByText('查询'));
});
expect(screen.getByText('节省排行 Top 5(按复合节省量)')).toBeInTheDocument();
expect(screen.getAllByText('BreakdownAgent').length).toBeGreaterThanOrEqual(1);
});
});
9 changes: 9 additions & 0 deletions src/agentsight/dashboard/src/utils/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export interface OptimizationItem {
saved_tokens: number;
compounded_saved: number;
compounding_turns: number;
compression_ratio: number;
explanation: string;
before_summary: string;
after_summary: string;
optimization_reason: string;
Expand Down Expand Up @@ -259,10 +261,17 @@ export interface SavingsSummary {
strategy_breakdown: StrategyBreakdownItem[];
}

export interface OptimizationTip {
level: 'success' | 'info' | 'warning';
title: string;
description: string;
}

export interface TokenSavingsResponse {
stats_available: boolean;
summary: SavingsSummary;
sessions: SessionSavings[];
optimization_tips: OptimizationTip[];
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/agentsight/src/bin/cli/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ mod tests {
args: Some(args.into()),
exit_code: None,
},
session_id: None,
}
}

Expand Down
Loading
Loading