mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
add working demo files
This commit is contained in:
parent
0649720f79
commit
03ba307618
2 changed files with 459 additions and 127 deletions
|
|
@ -41,12 +41,116 @@ interface PerUserAnalyticsResponse {
|
|||
interface PerUserUsageProps {
|
||||
accessToken: string | null;
|
||||
selectedTags: string[];
|
||||
selectedTeam?: string; // DEMO: Team filter
|
||||
selectedAgents?: string[]; // DEMO: Agent filter
|
||||
formatAbbreviatedNumber: (value: number, decimalPlaces?: number) => string;
|
||||
}
|
||||
|
||||
const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => {
|
||||
// DEMO: Clean agent name mapping for display
|
||||
const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
||||
"claude-code": "Claude Code",
|
||||
"claude-code-max": "Claude Code Max",
|
||||
"codex-cli": "Codex CLI",
|
||||
"GithubCopilot": "GitHub Copilot IDE",
|
||||
"go-gh": "GitHub Copilot CLI",
|
||||
};
|
||||
|
||||
// DEMO: Helper function to clean agent names for display
|
||||
const cleanAgentName = (raw: string | null): string => {
|
||||
if (!raw) return "Unknown";
|
||||
|
||||
// Strip "User-Agent:" prefix
|
||||
let name = raw.replace(/^User-Agent:\s*/i, "");
|
||||
|
||||
// Handle versioned strings (e.g., "GithubCopilot/1.155.0")
|
||||
if (name.startsWith("GithubCopilot")) return "GitHub Copilot IDE";
|
||||
if (name === "go-gh") return "GitHub Copilot CLI";
|
||||
if (name.startsWith("Mozilla/")) return "Browser";
|
||||
|
||||
return AGENT_DISPLAY_NAMES[name] || name;
|
||||
};
|
||||
|
||||
// DEMO: Helper function to get auth method based on agent name
|
||||
const getAuthMethod = (agentName: string | null): string => {
|
||||
const clean = cleanAgentName(agentName);
|
||||
|
||||
if (clean === "Claude Code Max") return "Client OAuth (Anthropic)";
|
||||
if (clean === "GitHub Copilot IDE") return "Client OAuth (GitHub)";
|
||||
if (clean === "GitHub Copilot CLI") return "Client OAuth (GitHub)";
|
||||
|
||||
// Claude Code and Codex CLI use proxy-managed keys
|
||||
return "AI Gateway Key";
|
||||
};
|
||||
|
||||
// DEMO: Auth badge component with colored styling and clear distinction
|
||||
const AuthBadge: React.FC<{ method: string }> = ({ method }) => {
|
||||
const isClientOAuth = method.startsWith("Client OAuth");
|
||||
|
||||
// Client OAuth = green (user's own credentials, client-side)
|
||||
// AI Gateway Key = blue (proxy-managed, server-side)
|
||||
const style: React.CSSProperties = {
|
||||
display: "inline-block",
|
||||
padding: "3px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
backgroundColor: isClientOAuth ? "#dcfce7" : "#dbeafe", // green-100 or blue-100
|
||||
color: isClientOAuth ? "#166534" : "#1e40af", // green-800 or blue-800
|
||||
border: isClientOAuth ? "1px solid #86efac" : "1px solid #93c5fd", // green-300 or blue-300
|
||||
};
|
||||
|
||||
return <span style={style}>{method}</span>;
|
||||
};
|
||||
|
||||
// DEMO: Extended interface for demo users with auth method and team
|
||||
interface DemoUserMetrics extends PerUserMetrics {
|
||||
auth_method: string;
|
||||
team: string;
|
||||
}
|
||||
|
||||
// DEMO: User to team mapping
|
||||
const USER_TEAM_MAP: Record<string, string> = {
|
||||
"alice@company.com": "Operations AI",
|
||||
"bob@company.com": "Dispatch",
|
||||
"charlie@company.com": "Platform",
|
||||
"diana@company.com": "Voice",
|
||||
"eve@company.com": "Platform",
|
||||
"frank@company.com": "Dispatch",
|
||||
"grace@company.com": "Voice",
|
||||
};
|
||||
|
||||
// DEMO: Hardcoded per-user data for demo purposes with auth method and team
|
||||
// - "AI Gateway Key" = credentials managed by LiteLLM proxy (server-side)
|
||||
// - "Client OAuth (Provider)" = user's own OAuth credentials (client-side, BYOK)
|
||||
const DEMO_USERS: DemoUserMetrics[] = [
|
||||
{ user_id: "user-1", user_email: "alice@company.com", team: "Operations AI", auth_method: "AI Gateway Key", user_agent: "Claude Code", successful_requests: 2841, failed_requests: 8, total_requests: 2849, total_tokens: 3800000, spend: 176.20 },
|
||||
{ user_id: "user-2", user_email: "bob@company.com", team: "Dispatch", auth_method: "AI Gateway Key", user_agent: "Codex CLI", successful_requests: 2103, failed_requests: 7, total_requests: 2110, total_tokens: 2900000, spend: 129.45 },
|
||||
{ user_id: "user-3", user_email: "charlie@company.com", team: "Platform", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot IDE", successful_requests: 5612, failed_requests: 3, total_requests: 5615, total_tokens: 6800000, spend: 203.12 },
|
||||
{ user_id: "user-4", user_email: "diana@company.com", team: "Voice", auth_method: "Client OAuth (Anthropic)", user_agent: "Claude Code Max", successful_requests: 1987, failed_requests: 8, total_requests: 1995, total_tokens: 2400000, spend: 112.80 },
|
||||
{ user_id: "user-5", user_email: "eve@company.com", team: "Platform", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot IDE", successful_requests: 4231, failed_requests: 2, total_requests: 4233, total_tokens: 5100000, spend: 152.67 },
|
||||
{ user_id: "user-6", user_email: "frank@company.com", team: "Dispatch", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot CLI", successful_requests: 1876, failed_requests: 3, total_requests: 1879, total_tokens: 2600000, spend: 98.34 },
|
||||
{ user_id: "user-7", user_email: "grace@company.com", team: "Voice", auth_method: "AI Gateway Key", user_agent: "Claude Code", successful_requests: 1542, failed_requests: 2, total_requests: 1544, total_tokens: 2100000, spend: 89.50 },
|
||||
];
|
||||
|
||||
const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags, selectedTeam, selectedAgents, formatAbbreviatedNumber }) => {
|
||||
// Maximum number of user agent categories to show in charts to prevent color palette overflow
|
||||
const MAX_USER_AGENTS = 8;
|
||||
|
||||
// DEMO: Filter users by selected team and selected agents
|
||||
const filteredUsers = DEMO_USERS.filter((user) => {
|
||||
// Filter by team
|
||||
if (selectedTeam && selectedTeam !== "All Teams" && user.team !== selectedTeam) {
|
||||
return false;
|
||||
}
|
||||
// Filter by agents
|
||||
if (selectedAgents && selectedAgents.length > 0) {
|
||||
const userAgentClean = cleanAgentName(user.user_agent);
|
||||
if (!selectedAgents.includes(userAgentClean)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const [perUserData, setPerUserData] = useState<PerUserAnalyticsResponse>({
|
||||
results: [],
|
||||
total_count: 0,
|
||||
|
|
@ -105,13 +209,15 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* Tab 1: Existing User Details Table */}
|
||||
{/* DEMO: Tab 1 - User Details Table with hardcoded demo data, Team, and Auth Method columns */}
|
||||
<TabPanel>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>User ID</TableHeaderCell>
|
||||
<TableHeaderCell>User Email</TableHeaderCell>
|
||||
<TableHeaderCell>Team</TableHeaderCell>
|
||||
<TableHeaderCell>Auth Method</TableHeaderCell>
|
||||
<TableHeaderCell>User Agent</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Success Generations</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Tokens</TableHeaderCell>
|
||||
|
|
@ -120,7 +226,8 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{perUserData.results.slice(0, 10).map((item: PerUserMetrics, index: number) => (
|
||||
{/* DEMO: Using filtered demo users data with team and auth method */}
|
||||
{filteredUsers.map((item: DemoUserMetrics, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Text className="font-medium">{item.user_id}</Text>
|
||||
|
|
@ -129,7 +236,13 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
<Text>{item.user_email || "N/A"}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{item.user_agent || "Unknown"}</Text>
|
||||
<Text className="text-gray-600">{item.team}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<AuthBadge method={item.auth_method} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{cleanAgentName(item.user_agent)}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>{formatAbbreviatedNumber(item.successful_requests)}</Text>
|
||||
|
|
@ -141,34 +254,15 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
<Text>{formatAbbreviatedNumber(item.failed_requests)}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>${formatAbbreviatedNumber(item.spend, 4)}</Text>
|
||||
<Text>${formatAbbreviatedNumber(item.spend, 2)}</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{perUserData.results.length > 10 && (
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<Text className="text-sm text-gray-500">Showing 10 of {perUserData.total_count} results</Text>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={handlePrevPage} disabled={currentPage === 1}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={handleNextPage}
|
||||
disabled={currentPage >= perUserData.total_pages}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Tab 2: Usage Distribution Histogram */}
|
||||
{/* DEMO: Tab 2 - Usage Distribution Histogram with filtered demo data */}
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">User Usage Distribution</Title>
|
||||
|
|
@ -177,10 +271,10 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
|
||||
<BarChart
|
||||
data={(() => {
|
||||
// Get top user agents by frequency first
|
||||
// DEMO: Get unique clean agent names from filtered demo data
|
||||
const userAgentCounts = new Map<string, number>();
|
||||
perUserData.results.forEach((item: PerUserMetrics) => {
|
||||
const agent = item.user_agent || "Unknown";
|
||||
filteredUsers.forEach((item: PerUserMetrics) => {
|
||||
const agent = cleanAgentName(item.user_agent);
|
||||
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
|
||||
});
|
||||
|
||||
|
|
@ -199,10 +293,10 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
"100K+ requests": { range: [100000, Infinity], agents: {} as Record<string, number> },
|
||||
};
|
||||
|
||||
// Count users in each category by user agent (only for top user agents)
|
||||
perUserData.results.forEach((item: PerUserMetrics) => {
|
||||
// DEMO: Count users in each category by user agent (using filtered data)
|
||||
filteredUsers.forEach((item: PerUserMetrics) => {
|
||||
const successCount = item.successful_requests;
|
||||
const userAgent = item.user_agent || "Unknown";
|
||||
const userAgent = cleanAgentName(item.user_agent);
|
||||
|
||||
// Only process if this is one of the top user agents
|
||||
if (topUserAgents.includes(userAgent)) {
|
||||
|
|
@ -231,10 +325,10 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
|
|||
})()}
|
||||
index="category"
|
||||
categories={(() => {
|
||||
// Count user agents by frequency and get top ones
|
||||
// DEMO: Count user agents by frequency and get top ones with clean names (using filtered data)
|
||||
const userAgentCounts = new Map<string, number>();
|
||||
perUserData.results.forEach((item: PerUserMetrics) => {
|
||||
const agent = item.user_agent || "Unknown";
|
||||
filteredUsers.forEach((item: PerUserMetrics) => {
|
||||
const agent = cleanAgentName(item.user_agent);
|
||||
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,14 @@ import {
|
|||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@tremor/react";
|
||||
import { Select, Tooltip } from "antd";
|
||||
import { Select } from "antd";
|
||||
import { userAgentSummaryCall, tagDauCall, tagWauCall, tagMauCall, tagDistinctCall } from "./networking";
|
||||
import PerUserUsage from "./per_user_usage";
|
||||
import { DateRangePickerValue } from "@tremor/react";
|
||||
|
|
@ -61,6 +67,200 @@ interface UserAgentActivityProps {
|
|||
onDateChange?: (value: DateRangePickerValue) => void; // Optional - not used anymore
|
||||
}
|
||||
|
||||
// DEMO: Clean agent name mapping for display
|
||||
const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
||||
"claude-code": "Claude Code",
|
||||
"claude-code-max": "Claude Code Max",
|
||||
"codex-cli": "Codex CLI",
|
||||
"GithubCopilot": "GitHub Copilot IDE",
|
||||
"go-gh": "GitHub Copilot CLI",
|
||||
};
|
||||
|
||||
// DEMO: Helper function to clean agent names for display
|
||||
const cleanAgentName = (raw: string): string => {
|
||||
// Strip "User-Agent:" prefix
|
||||
let name = raw.replace(/^User-Agent:\s*/i, "");
|
||||
|
||||
// Handle versioned strings (e.g., "GithubCopilot/1.155.0")
|
||||
if (name.startsWith("GithubCopilot")) return "GitHub Copilot IDE";
|
||||
if (name === "go-gh") return "GitHub Copilot CLI";
|
||||
if (name.startsWith("Mozilla/")) return "Browser";
|
||||
|
||||
return AGENT_DISPLAY_NAMES[name] || name;
|
||||
};
|
||||
|
||||
// DEMO: Hardcoded agent summary data for demo purposes
|
||||
const DEMO_AGENTS = [
|
||||
{
|
||||
name: "GitHub Copilot IDE",
|
||||
raw_ua: "GithubCopilot/1.155.0",
|
||||
success_requests: 34219,
|
||||
total_tokens: 42100000,
|
||||
total_cost: 1247.92,
|
||||
},
|
||||
{
|
||||
name: "Claude Code",
|
||||
raw_ua: "claude-code",
|
||||
success_requests: 12847,
|
||||
total_tokens: 18200000,
|
||||
total_cost: 842.30,
|
||||
},
|
||||
{
|
||||
name: "Claude Code Max",
|
||||
raw_ua: "claude-code-max",
|
||||
success_requests: 8421,
|
||||
total_tokens: 11700000,
|
||||
total_cost: 523.18,
|
||||
},
|
||||
{
|
||||
name: "Codex CLI",
|
||||
raw_ua: "codex-cli",
|
||||
success_requests: 5102,
|
||||
total_tokens: 7100000,
|
||||
total_cost: 312.45,
|
||||
},
|
||||
{
|
||||
name: "GitHub Copilot CLI",
|
||||
raw_ua: "go-gh",
|
||||
success_requests: 2340,
|
||||
total_tokens: 3200000,
|
||||
total_cost: 98.67,
|
||||
},
|
||||
];
|
||||
|
||||
// DEMO: Hardcoded DAU data for demo purposes (7 days with realistic weekday/weekend pattern)
|
||||
const DEMO_DAU = [
|
||||
{ date: "2026-01-30", "GitHub Copilot IDE": 18, "Claude Code": 12, "Claude Code Max": 6, "Codex CLI": 4, "GitHub Copilot CLI": 3 },
|
||||
{ date: "2026-01-31", "GitHub Copilot IDE": 20, "Claude Code": 14, "Claude Code Max": 7, "Codex CLI": 5, "GitHub Copilot CLI": 3 },
|
||||
{ date: "2026-02-01", "GitHub Copilot IDE": 15, "Claude Code": 10, "Claude Code Max": 5, "Codex CLI": 3, "GitHub Copilot CLI": 2 },
|
||||
{ date: "2026-02-02", "GitHub Copilot IDE": 14, "Claude Code": 9, "Claude Code Max": 4, "Codex CLI": 3, "GitHub Copilot CLI": 2 },
|
||||
{ date: "2026-02-03", "GitHub Copilot IDE": 22, "Claude Code": 16, "Claude Code Max": 8, "Codex CLI": 6, "GitHub Copilot CLI": 4 },
|
||||
{ date: "2026-02-04", "GitHub Copilot IDE": 24, "Claude Code": 18, "Claude Code Max": 9, "Codex CLI": 7, "GitHub Copilot CLI": 5 },
|
||||
{ date: "2026-02-05", "GitHub Copilot IDE": 26, "Claude Code": 19, "Claude Code Max": 10, "Codex CLI": 8, "GitHub Copilot CLI": 5 },
|
||||
];
|
||||
|
||||
// DEMO: Agent names for chart categories (excludes Browser/Mozilla)
|
||||
const DEMO_AGENT_CATEGORIES = ["GitHub Copilot IDE", "Claude Code", "Claude Code Max", "Codex CLI", "GitHub Copilot CLI"];
|
||||
|
||||
// DEMO: Available teams for filtering
|
||||
const DEMO_TEAMS_LIST = ["All Teams", "Operations AI", "Platform", "Voice", "Dispatch"];
|
||||
|
||||
// DEMO: Budget data per agent (monthly budget and current usage)
|
||||
const DEMO_BUDGETS: Record<string, { budget: number; used: number }> = {
|
||||
"GitHub Copilot IDE": { budget: 2000, used: 1248 },
|
||||
"Claude Code": { budget: 1500, used: 842 },
|
||||
"Claude Code Max": { budget: 1000, used: 523 },
|
||||
"Codex CLI": { budget: 500, used: 312 },
|
||||
"GitHub Copilot CLI": { budget: 200, used: 99 },
|
||||
};
|
||||
|
||||
// DEMO: Guardrails enabled per agent
|
||||
const DEMO_GUARDRAILS: Record<string, string[]> = {
|
||||
"GitHub Copilot IDE": ["PII Redaction", "IP Protection"],
|
||||
"Claude Code": ["PII Redaction", "Prompt Injection"],
|
||||
"Claude Code Max": ["PII Redaction", "Prompt Injection"],
|
||||
"Codex CLI": ["PII Redaction"],
|
||||
"GitHub Copilot CLI": ["PII Redaction"],
|
||||
};
|
||||
|
||||
// DEMO: User data for computing team stats (mirrors per_user_usage.tsx)
|
||||
const DEMO_USERS_FOR_TEAMS = [
|
||||
{ user_email: "alice@company.com", team: "Operations AI", user_agent: "Claude Code", successful_requests: 2841, total_tokens: 3800000, spend: 176.20 },
|
||||
{ user_email: "bob@company.com", team: "Dispatch", user_agent: "Codex CLI", successful_requests: 2103, total_tokens: 2900000, spend: 129.45 },
|
||||
{ user_email: "charlie@company.com", team: "Platform", user_agent: "GitHub Copilot IDE", successful_requests: 5612, total_tokens: 6800000, spend: 203.12 },
|
||||
{ user_email: "diana@company.com", team: "Voice", user_agent: "Claude Code Max", successful_requests: 1987, total_tokens: 2400000, spend: 112.80 },
|
||||
{ user_email: "eve@company.com", team: "Platform", user_agent: "GitHub Copilot IDE", successful_requests: 4231, total_tokens: 5100000, spend: 152.67 },
|
||||
{ user_email: "frank@company.com", team: "Dispatch", user_agent: "GitHub Copilot CLI", successful_requests: 1876, total_tokens: 2600000, spend: 98.34 },
|
||||
{ user_email: "grace@company.com", team: "Voice", user_agent: "Claude Code", successful_requests: 1542, total_tokens: 2100000, spend: 89.50 },
|
||||
];
|
||||
|
||||
// DEMO: Function to compute team stats from filtered user data
|
||||
const computeTeamStats = (users: typeof DEMO_USERS_FOR_TEAMS, selectedAgents: string[]) => {
|
||||
// Filter users by selected agents
|
||||
const filteredUsers = users.filter((user) => {
|
||||
if (selectedAgents.length === 0) return true;
|
||||
return selectedAgents.includes(user.user_agent);
|
||||
});
|
||||
|
||||
// Group by team
|
||||
const teamMap = new Map<string, { active_users: number; total_requests: number; total_tokens: number; total_cost: number }>();
|
||||
|
||||
filteredUsers.forEach((user) => {
|
||||
const existing = teamMap.get(user.team) || { active_users: 0, total_requests: 0, total_tokens: 0, total_cost: 0 };
|
||||
teamMap.set(user.team, {
|
||||
active_users: existing.active_users + 1,
|
||||
total_requests: existing.total_requests + user.successful_requests,
|
||||
total_tokens: existing.total_tokens + user.total_tokens,
|
||||
total_cost: existing.total_cost + user.spend,
|
||||
});
|
||||
});
|
||||
|
||||
// Convert to array and sort by total_requests
|
||||
return Array.from(teamMap.entries())
|
||||
.map(([team, stats]) => ({ team, ...stats }))
|
||||
.sort((a, b) => b.total_requests - a.total_requests);
|
||||
};
|
||||
|
||||
// DEMO: Budget progress bar component
|
||||
const BudgetProgressBar: React.FC<{ budget: number; used: number }> = ({ budget, used }) => {
|
||||
const percentage = Math.round((used / budget) * 100);
|
||||
|
||||
// Color based on percentage: green < 60%, amber 60-80%, red > 80%
|
||||
let fillColor = "#22c55e"; // green
|
||||
if (percentage >= 80) {
|
||||
fillColor = "#ef4444"; // red
|
||||
} else if (percentage >= 60) {
|
||||
fillColor = "#eab308"; // amber
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Text className="text-xs text-gray-500">Budget: ${budget.toLocaleString()}/mo</Text>
|
||||
<Text className="text-xs text-gray-500">{percentage}%</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: "6px",
|
||||
borderRadius: "3px",
|
||||
backgroundColor: "#e5e7eb",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: fillColor,
|
||||
borderRadius: "3px",
|
||||
transition: "width 0.3s ease"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// DEMO: Guardrails line component
|
||||
const GuardrailsLine: React.FC<{ guardrails: string[] }> = ({ guardrails }) => {
|
||||
if (!guardrails || guardrails.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderTop: "1px solid #e5e7eb",
|
||||
paddingTop: "8px",
|
||||
marginTop: "8px"
|
||||
}}
|
||||
>
|
||||
<Text className="text-xs text-gray-500">
|
||||
<span style={{ color: "#22c55e", marginRight: "4px" }}>✓</span>
|
||||
{guardrails.join(", ")}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, userRole, dateValue, onDateChange }) => {
|
||||
// Maximum number of categories to show in charts to prevent color palette overflow
|
||||
const MAX_CATEGORIES = 10;
|
||||
|
|
@ -78,6 +278,12 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [tagsLoading, setTagsLoading] = useState(false);
|
||||
|
||||
// DEMO: Team filter state
|
||||
const [selectedTeam, setSelectedTeam] = useState<string>("All Teams");
|
||||
|
||||
// DEMO: Agent filter state (for filtering by demo agent names)
|
||||
const [selectedAgents, setSelectedAgents] = useState<string[]>([]);
|
||||
|
||||
// Separate loading states for each endpoint
|
||||
const [dauLoading, setDauLoading] = useState(false);
|
||||
const [wauLoading, setWauLoading] = useState(false);
|
||||
|
|
@ -206,20 +412,14 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
return () => clearTimeout(timeoutId);
|
||||
}, [accessToken, dateValue, selectedTags]);
|
||||
|
||||
// Helper function to extract user agent from tag
|
||||
// Helper function to extract user agent from tag and clean it for display
|
||||
const extractUserAgent = (tag: string): string => {
|
||||
let name = tag;
|
||||
if (tag.startsWith("User-Agent: ")) {
|
||||
return tag.replace("User-Agent: ", "");
|
||||
name = tag.replace("User-Agent: ", "");
|
||||
}
|
||||
return tag;
|
||||
};
|
||||
|
||||
// Helper function to truncate user agent name (used with Ant Design Tooltip)
|
||||
const truncateUserAgent = (userAgent: string): string => {
|
||||
if (userAgent.length > 15) {
|
||||
return userAgent.substring(0, 15) + "...";
|
||||
}
|
||||
return userAgent;
|
||||
// Apply clean agent name mapping
|
||||
return cleanAgentName(name);
|
||||
};
|
||||
|
||||
// Get all user agents for each chart type based on their specific data
|
||||
|
|
@ -379,102 +579,151 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
<Subtitle>Performance metrics for different user agents</Subtitle>
|
||||
</div>
|
||||
|
||||
{/* User Agent Filter */}
|
||||
<div className="w-96">
|
||||
<Text className="text-sm font-medium block mb-2">Filter by User Agents</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="All User Agents"
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
allowClear={true}
|
||||
loading={tagsLoading}
|
||||
optionFilterProp="label"
|
||||
className="rounded-md"
|
||||
maxTagCount="responsive"
|
||||
>
|
||||
{availableTags.map((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
const displayName = userAgent.length > 50 ? `${userAgent.substring(0, 50)}...` : userAgent;
|
||||
return (
|
||||
<Select.Option key={tag} value={tag} label={displayName} title={userAgent}>
|
||||
{displayName}
|
||||
{/* DEMO: Filter dropdowns - Team and User Agent */}
|
||||
<div className="flex gap-4">
|
||||
{/* Team Filter */}
|
||||
<div className="w-48">
|
||||
<Text className="text-sm font-medium block mb-2">Filter by Team</Text>
|
||||
<Select
|
||||
placeholder="All Teams"
|
||||
value={selectedTeam}
|
||||
onChange={setSelectedTeam}
|
||||
style={{ width: "100%" }}
|
||||
className="rounded-md"
|
||||
>
|
||||
{DEMO_TEAMS_LIST.map((team) => (
|
||||
<Select.Option key={team} value={team}>
|
||||
{team}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* DEMO: User Agent Filter - Using hardcoded demo agents */}
|
||||
<div className="w-64">
|
||||
<Text className="text-sm font-medium block mb-2">Filter by User Agents</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="All User Agents"
|
||||
value={selectedAgents}
|
||||
onChange={setSelectedAgents}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
allowClear={true}
|
||||
optionFilterProp="label"
|
||||
className="rounded-md"
|
||||
maxTagCount="responsive"
|
||||
>
|
||||
{DEMO_AGENT_CATEGORIES.map((agent) => (
|
||||
<Select.Option key={agent} value={agent} label={agent}>
|
||||
{agent}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Range Picker is controlled by parent component */}
|
||||
|
||||
{/* Top 4 User Agents Cards */}
|
||||
{/* DEMO: Top 5 User Agents Cards - Using hardcoded demo data */}
|
||||
{summaryLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<Grid numItems={4} className="gap-4">
|
||||
{(summaryData.results || []).slice(0, 4).map((tag, index) => {
|
||||
const userAgent = extractUserAgent(tag.tag);
|
||||
const displayName = truncateUserAgent(userAgent);
|
||||
<Grid numItems={5} className="gap-4">
|
||||
{DEMO_AGENTS.map((agent, index) => {
|
||||
return (
|
||||
<Card key={index}>
|
||||
<Tooltip title={userAgent} placement="top">
|
||||
<Title className="truncate">{displayName}</Title>
|
||||
</Tooltip>
|
||||
<Title>{agent.name}</Title>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.successful_requests)}</Metric>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(agent.success_requests)}</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Tokens</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.total_tokens)}</Metric>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(agent.total_tokens)}</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">${formatAbbreviatedNumber(tag.total_spend, 4)}</Metric>
|
||||
<Metric className="text-lg">${formatAbbreviatedNumber(agent.total_cost, 2)}</Metric>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Fill remaining slots if less than 4 agents */}
|
||||
{Array.from({ length: Math.max(0, 4 - (summaryData.results || []).length) }).map((_, index) => (
|
||||
<Card key={`empty-${index}`}>
|
||||
<Title>No Data</Title>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Tokens</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */}
|
||||
{/* DEMO: Main TabGroup - Per User Usage first, Per Team Usage second, then DAU/WAU/MAU */}
|
||||
<Card>
|
||||
<TabGroup>
|
||||
<TabList className="mb-6">
|
||||
<Tab>Per User Usage</Tab>
|
||||
<Tab>Per Team Usage</Tab>
|
||||
<Tab>DAU/WAU/MAU</Tab>
|
||||
<Tab>Per User Usage (Last 30 Days)</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* DAU/WAU/MAU Tab Panel */}
|
||||
{/* Per User Usage Tab Panel - First */}
|
||||
<TabPanel>
|
||||
<PerUserUsage
|
||||
accessToken={accessToken}
|
||||
selectedTags={selectedTags}
|
||||
selectedTeam={selectedTeam}
|
||||
selectedAgents={selectedAgents}
|
||||
formatAbbreviatedNumber={formatAbbreviatedNumber}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* DEMO: Per Team Usage Tab Panel - Second (filtered by selected agents) */}
|
||||
<TabPanel>
|
||||
<div className="mb-6">
|
||||
<Title>Per Team Usage</Title>
|
||||
<Subtitle>Usage metrics grouped by team</Subtitle>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Active Users</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Requests</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Tokens</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Total Cost</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{computeTeamStats(DEMO_USERS_FOR_TEAMS, selectedAgents).map((team, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
className="cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => setSelectedTeam(team.team)}
|
||||
>
|
||||
<TableCell>
|
||||
<Text className="font-medium">{team.team}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>{team.active_users}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>{formatAbbreviatedNumber(team.total_requests)}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>{formatAbbreviatedNumber(team.total_tokens)}</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>${formatAbbreviatedNumber(team.total_cost, 2)}</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Text className="text-xs text-gray-500 mt-4">Click a team row to filter the page by that team</Text>
|
||||
</TabPanel>
|
||||
|
||||
{/* DAU/WAU/MAU Tab Panel - Third */}
|
||||
<TabPanel>
|
||||
<div className="mb-6">
|
||||
<Title>DAU, WAU & MAU per Agent</Title>
|
||||
|
|
@ -493,19 +742,17 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
<div className="mb-4">
|
||||
<Title className="text-lg">Daily Active Users - Last 7 Days</Title>
|
||||
</div>
|
||||
{dauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={dailyChartData}
|
||||
index="date"
|
||||
categories={allDauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
{/* DEMO: Using hardcoded DAU data instead of API data */}
|
||||
<BarChart
|
||||
data={DEMO_DAU}
|
||||
index="date"
|
||||
categories={DEMO_AGENT_CATEGORIES}
|
||||
colors={["blue", "purple", "violet", "cyan", "green"]}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
|
|
@ -548,15 +795,6 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabPanel>
|
||||
|
||||
{/* Per User Usage Tab Panel */}
|
||||
<TabPanel>
|
||||
<PerUserUsage
|
||||
accessToken={accessToken}
|
||||
selectedTags={selectedTags}
|
||||
formatAbbreviatedNumber={formatAbbreviatedNumber}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue