mirror of
https://github.com/mfakbar127/Claude-Samurai
synced 2026-06-21 13:57:42 +00:00
monitor chart
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
# Claude Code Usage Monitor Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of the Claude Code usage monitoring feature that reads and analyzes usage data from JSONL files stored in `~/.claude/projects/**/*.jsonl`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Rust/Tauri)
|
||||
|
||||
#### Data Structures
|
||||
|
||||
```rust
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct UsageData {
|
||||
pub input_tokens: Option<u64>,
|
||||
pub cache_read_input_tokens: Option<u64>,
|
||||
pub output_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct ProjectUsageRecord {
|
||||
pub uuid: String,
|
||||
pub timestamp: String,
|
||||
pub model: Option<String>,
|
||||
pub usage: Option<UsageData>,
|
||||
}
|
||||
```
|
||||
|
||||
#### Core Function: `read_project_usage_files()`
|
||||
|
||||
1. **Directory Discovery**
|
||||
- Base path: `~/.claude/projects/`
|
||||
- Recursive search through all subdirectories
|
||||
- Finds all files with `.jsonl` extension
|
||||
|
||||
2. **File Processing**
|
||||
- Reads each JSONL file line by line
|
||||
- Parses each line as JSON object
|
||||
- Extracts required fields from each record
|
||||
|
||||
3. **Field Extraction Logic**
|
||||
|
||||
**UUID & Timestamp (Required)**
|
||||
```rust
|
||||
let uuid = json_value.get("uuid")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let timestamp = json_value.get("timestamp")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
```
|
||||
|
||||
**Model (Optional)**
|
||||
```rust
|
||||
let model = if let Some(model_str) = json_value.get("model")
|
||||
.and_then(|v| v.as_str()) {
|
||||
Some(model_str.to_string())
|
||||
} else if let Some(message_obj) = json_value.get("message") {
|
||||
message_obj.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
```
|
||||
|
||||
**Usage Data (Optional)**
|
||||
```rust
|
||||
let usage = if let Some(usage_obj) = json_value.get("usage") {
|
||||
Some(UsageData {
|
||||
input_tokens: usage_obj.get("input_tokens").and_then(|v| v.as_u64()),
|
||||
cache_read_input_tokens: usage_obj.get("cache_read_input_tokens").and_then(|v| v.as_u64()),
|
||||
output_tokens: usage_obj.get("output_tokens").and_then(|v| v.as_u64()),
|
||||
})
|
||||
} else if let Some(message_obj) = json_value.get("message") {
|
||||
if let Some(usage_obj) = message_obj.get("usage") {
|
||||
Some(UsageData {
|
||||
input_tokens: usage_obj.get("input_tokens").and_then(|v| v.as_u64()),
|
||||
cache_read_input_tokens: usage_obj.get("cache_read_input_tokens").and_then(|v| v.as_u64()),
|
||||
output_tokens: usage_obj.get("output_tokens").and_then(|v| v.as_u64()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
```
|
||||
|
||||
4. **Filtering Logic**
|
||||
- Records must have valid UUID and timestamp
|
||||
- Records must have usage data (not None)
|
||||
- Records must have meaningful token usage: `input_tokens + output_tokens > 0`
|
||||
|
||||
### Frontend (TypeScript/React)
|
||||
|
||||
#### TypeScript Interfaces
|
||||
|
||||
```typescript
|
||||
export interface UsageData {
|
||||
input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
}
|
||||
|
||||
export interface ProjectUsageRecord {
|
||||
uuid: string;
|
||||
timestamp: string;
|
||||
model?: string;
|
||||
usage?: UsageData;
|
||||
}
|
||||
```
|
||||
|
||||
#### React Query Hook
|
||||
|
||||
```typescript
|
||||
export const useProjectUsageFiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ["project-usage-files"],
|
||||
queryFn: () => invoke<ProjectUsageRecord[]>("read_project_usage_files"),
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### Monitor Page Implementation
|
||||
|
||||
The Monitor page (`src/pages/MonitorPage.tsx`) displays aggregated token usage:
|
||||
|
||||
```typescript
|
||||
<ul>
|
||||
<li>Total Input Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.input_tokens || 0), 0)}</li>
|
||||
<li>Total Output Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.output_tokens || 0), 0)}</li>
|
||||
<li>Total Cache Read Input Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.cache_read_input_tokens || 0), 0)}</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## JSONL File Structure
|
||||
|
||||
The system processes JSONL files that contain various types of records. Usage data is typically found in records with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"parentUuid": "...",
|
||||
"userType": "external",
|
||||
"cwd": "...",
|
||||
"sessionId": "...",
|
||||
"version": "...",
|
||||
"gitBranch": "...",
|
||||
"message": {
|
||||
"id": "...",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "glm-4.6",
|
||||
"content": [...],
|
||||
"usage": {
|
||||
"input_tokens": 3505,
|
||||
"output_tokens": 60,
|
||||
"cache_read_input_tokens": 9888
|
||||
}
|
||||
},
|
||||
"type": "assistant",
|
||||
"uuid": "...",
|
||||
"timestamp": "2025-09-30T09:06:50.519Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Flow
|
||||
|
||||
1. **Directory Scan**: Recursively scan `~/.claude/projects/` for `.jsonl` files
|
||||
2. **File Read**: Read each JSONL file completely
|
||||
3. **Line Processing**: Process each line as a separate JSON object
|
||||
4. **Field Extraction**: Extract UUID, timestamp, model, and usage fields
|
||||
5. **Filtering**: Filter out records without valid usage data
|
||||
6. **Aggregation**: Sum tokens across all valid records
|
||||
7. **Display**: Show aggregated totals in the Monitor page
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Missing Directory**: Returns empty array if projects directory doesn't exist
|
||||
- **Invalid JSON**: Skips malformed JSON lines with error logging
|
||||
- **Missing Fields**: Gracefully handles missing optional fields (usage, model)
|
||||
- **File Read Errors**: Returns error message if files can't be read
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- All JSONL files are read into memory during processing
|
||||
- Large project directories may result in longer initial load times
|
||||
- No pagination implemented - processes all available data at once
|
||||
- Caching handled by React Query to avoid repeated reads
|
||||
|
||||
## Debug Features
|
||||
|
||||
The implementation includes debug logging that outputs:
|
||||
- Projects directory path
|
||||
- Number of files processed
|
||||
- Number of lines processed
|
||||
- Number of valid records found
|
||||
|
||||
This helps troubleshoot issues with data discovery and processing.
|
||||
@@ -35,6 +35,7 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -42,6 +43,7 @@
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
|
||||
Generated
+42
-19
@@ -16,7 +16,7 @@ importers:
|
||||
version: 5.2.2(react-hook-form@7.64.0(react@19.2.0))
|
||||
'@lobehub/icons':
|
||||
specifier: ^2.38.0
|
||||
version: 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: ^1.2.12
|
||||
version: 1.2.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -83,6 +83,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
i18next:
|
||||
specifier: ^25.5.3
|
||||
version: 25.5.3(typescript@5.8.3)
|
||||
@@ -104,6 +107,9 @@ importers:
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.0
|
||||
react-chartjs-2:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(chart.js@4.5.0)(react@19.2.0)
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.0(react@19.2.0)
|
||||
@@ -2198,6 +2204,9 @@ packages:
|
||||
dagre-d3-es@7.0.11:
|
||||
resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dayjs@1.11.18:
|
||||
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
|
||||
|
||||
@@ -3280,6 +3289,12 @@ packages:
|
||||
react: ^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react-chartjs-2@5.3.0:
|
||||
resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
|
||||
peerDependencies:
|
||||
chart.js: ^4.1.1
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-colorful@5.6.1:
|
||||
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
|
||||
peerDependencies:
|
||||
@@ -4469,12 +4484,12 @@ snapshots:
|
||||
|
||||
'@lobehub/emojilib@1.0.0': {}
|
||||
|
||||
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@lobehub/emojilib': 1.0.0
|
||||
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
emoji-regex: 10.5.0
|
||||
lodash-es: 4.17.21
|
||||
lucide-react: 0.469.0(react@19.2.0)
|
||||
@@ -4491,11 +4506,11 @@ snapshots:
|
||||
- micromark-util-types
|
||||
- supports-color
|
||||
|
||||
'@lobehub/icons@2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/icons@2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
lucide-react: 0.469.0(react@19.2.0)
|
||||
polished: 4.3.1
|
||||
react: 19.2.0
|
||||
@@ -4510,7 +4525,7 @@ snapshots:
|
||||
- micromark-util-types
|
||||
- supports-color
|
||||
|
||||
'@lobehub/ui@2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/ui@2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -4521,16 +4536,16 @@ snapshots:
|
||||
'@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0)
|
||||
'@floating-ui/react': 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/icons': 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/icons': 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@mdx-js/mdx': 3.1.1
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.2.0)(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.2.0)
|
||||
'@shikijs/transformers': 3.13.0
|
||||
'@splinetool/runtime': 0.9.526
|
||||
ahooks: 3.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
chroma-js: 3.1.2
|
||||
class-variance-authority: 0.7.1
|
||||
dayjs: 1.11.18
|
||||
@@ -5805,7 +5820,7 @@ snapshots:
|
||||
screenfull: 5.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
antd-style@3.7.1(@types/react@19.2.0)(antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
antd-style@3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@babel/runtime': 7.28.4
|
||||
@@ -5814,7 +5829,7 @@ snapshots:
|
||||
'@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0)
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/utils': 1.4.2
|
||||
antd: 5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
use-merge-value: 1.2.0(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
@@ -5822,7 +5837,7 @@ snapshots:
|
||||
- react-dom
|
||||
- supports-color
|
||||
|
||||
antd@5.27.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@ant-design/colors': 7.2.1
|
||||
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -5854,7 +5869,7 @@ snapshots:
|
||||
rc-motion: 2.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-notification: 5.6.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-pagination: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-picker: 4.11.3(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-picker: 4.11.3(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-progress: 4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-rate: 2.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
rc-resize-observer: 1.4.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -6222,6 +6237,8 @@ snapshots:
|
||||
d3: 7.9.0
|
||||
lodash-es: 4.17.21
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.18: {}
|
||||
|
||||
debug@4.4.3:
|
||||
@@ -7598,7 +7615,7 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
rc-picker@4.11.3(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
rc-picker@4.11.3(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -7609,6 +7626,7 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
optionalDependencies:
|
||||
date-fns: 4.1.0
|
||||
dayjs: 1.11.18
|
||||
|
||||
rc-progress@4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
@@ -7783,6 +7801,11 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- supports-color
|
||||
|
||||
react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.2.0):
|
||||
dependencies:
|
||||
chart.js: 4.5.0
|
||||
react: 19.2.0
|
||||
|
||||
react-colorful@5.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ChartOptions,
|
||||
} from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { format, startOfDay, startOfWeek, startOfMonth, subHours, subDays, getYear, getMonth, getDaysInMonth } from "date-fns";
|
||||
import { ProjectUsageRecord } from "@/lib/query";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
interface TokenUsageChartProps {
|
||||
data: ProjectUsageRecord[];
|
||||
onFilteredDataChange?: (filteredData: ProjectUsageRecord[]) => void;
|
||||
}
|
||||
|
||||
type TimeRange = "5h" | "1h" | "30min" | "today" | "7d" | "30d" | "month" | "week" | "all";
|
||||
|
||||
export function TokenUsageChart({ data, onFilteredDataChange }: TokenUsageChartProps) {
|
||||
const [selectedModel, setSelectedModel] = useState<string>("all");
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("5h");
|
||||
|
||||
// Get unique models from data
|
||||
const availableModels = useMemo(() => {
|
||||
const models = new Set<string>();
|
||||
data.forEach((record) => {
|
||||
if (record.model) {
|
||||
models.add(record.model);
|
||||
}
|
||||
});
|
||||
return Array.from(models).sort();
|
||||
}, [data]);
|
||||
|
||||
// Filter data based on selected model and time range
|
||||
const filteredData = useMemo(() => {
|
||||
let filtered = data;
|
||||
|
||||
// Filter by model
|
||||
if (selectedModel !== "all") {
|
||||
filtered = filtered.filter((record) => record.model === selectedModel);
|
||||
}
|
||||
|
||||
// Filter by time range
|
||||
const now = new Date();
|
||||
let startTime: Date;
|
||||
|
||||
switch (timeRange) {
|
||||
case "30min":
|
||||
startTime = subHours(now, 0.5);
|
||||
break;
|
||||
case "1h":
|
||||
startTime = subHours(now, 1);
|
||||
break;
|
||||
case "5h":
|
||||
startTime = subHours(now, 5);
|
||||
break;
|
||||
case "today":
|
||||
startTime = startOfDay(now);
|
||||
break;
|
||||
case "7d":
|
||||
startTime = subDays(now, 6);
|
||||
break;
|
||||
case "30d":
|
||||
startTime = subDays(now, 29);
|
||||
break;
|
||||
case "week":
|
||||
startTime = startOfWeek(now);
|
||||
break;
|
||||
case "month":
|
||||
startTime = startOfMonth(now);
|
||||
break;
|
||||
case "all":
|
||||
// For "all time", find the earliest timestamp in the data
|
||||
if (filtered.length > 0) {
|
||||
const earliestTime = new Date(
|
||||
Math.min(...filtered.map((record) => new Date(record.timestamp).getTime()))
|
||||
);
|
||||
startTime = earliestTime;
|
||||
} else {
|
||||
startTime = new Date(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
startTime = subHours(now, 5);
|
||||
}
|
||||
|
||||
filtered = filtered.filter((record) => new Date(record.timestamp) >= startTime);
|
||||
|
||||
return filtered;
|
||||
}, [data, selectedModel, timeRange]);
|
||||
|
||||
// Notify parent component when filtered data changes
|
||||
useEffect(() => {
|
||||
if (onFilteredDataChange) {
|
||||
onFilteredDataChange(filteredData);
|
||||
}
|
||||
}, [filteredData, onFilteredDataChange]);
|
||||
// Group data based on time range
|
||||
const groupDataByInterval = (records: ProjectUsageRecord[]) => {
|
||||
const intervals: { [key: string]: { input: number; output: number; cache: number } } = {};
|
||||
const now = new Date();
|
||||
|
||||
if (timeRange === "all") {
|
||||
// For all time, group by month
|
||||
const earliestTime = records.length > 0
|
||||
? new Date(Math.min(...records.map((record) => new Date(record.timestamp).getTime())))
|
||||
: new Date();
|
||||
|
||||
const startOfEarliestMonth = new Date(getYear(earliestTime), getMonth(earliestTime), 1);
|
||||
const startOfCurrentMonth = new Date(getYear(now), getMonth(now), 1);
|
||||
|
||||
// Generate monthly intervals from earliest month to current month
|
||||
for (let monthTime = startOfEarliestMonth.getTime(); monthTime <= startOfCurrentMonth.getTime(); monthTime += getDaysInMonth(new Date(monthTime)) * 24 * 60 * 60 * 1000) {
|
||||
intervals[monthTime] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
// Group records into monthly intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const monthStart = new Date(getYear(recordTime), getMonth(recordTime), 1);
|
||||
const monthKey = monthStart.getTime();
|
||||
|
||||
if (intervals[monthKey]) {
|
||||
intervals[monthKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[monthKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[monthKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else if (timeRange === "30min" || timeRange === "1h" || timeRange === "5h") {
|
||||
if (timeRange === "30min" || timeRange === "1h") {
|
||||
// Group by 5-minute intervals for 30 minutes and 1 hour
|
||||
const minutes = timeRange === "30min" ? 30 : 60;
|
||||
const currentInterval = Math.floor(now.getMinutes() / 5) * 5;
|
||||
now.setMinutes(currentInterval, 0, 0);
|
||||
|
||||
// Generate intervals
|
||||
const intervalCount = Math.ceil(minutes / 5);
|
||||
for (let i = intervalCount - 1; i >= 0; i--) {
|
||||
const intervalTime = new Date(now.getTime() - i * 5 * 60 * 1000);
|
||||
const intervalKey = intervalTime.getTime();
|
||||
intervals[intervalKey] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
// Group records into 5-minute intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const recordIntervalKey = Math.floor(recordTime.getTime() / (5 * 60 * 1000)) * (5 * 60 * 1000);
|
||||
|
||||
if (intervals[recordIntervalKey]) {
|
||||
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[recordIntervalKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[recordIntervalKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Group by 30-minute intervals for 5h time range
|
||||
const currentInterval = Math.floor(now.getMinutes() / 30) * 30;
|
||||
now.setMinutes(currentInterval, 0, 0);
|
||||
|
||||
// Generate intervals (10 intervals for 5 hours)
|
||||
for (let i = 9; i >= 0; i--) {
|
||||
const intervalTime = new Date(now.getTime() - i * 30 * 60 * 1000);
|
||||
const intervalKey = intervalTime.getTime();
|
||||
intervals[intervalKey] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
// Group records into 30-minute intervals
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const recordIntervalKey = Math.floor(recordTime.getTime() / (30 * 60 * 1000)) * (30 * 60 * 1000);
|
||||
|
||||
if (intervals[recordIntervalKey]) {
|
||||
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[recordIntervalKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[recordIntervalKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (timeRange === "today") {
|
||||
// Group by hour for today
|
||||
const startOfToday = startOfDay(now);
|
||||
const currentHour = now.getHours();
|
||||
|
||||
for (let i = 0; i <= currentHour; i++) {
|
||||
const hourTime = new Date(startOfToday.getTime() + i * 60 * 60 * 1000);
|
||||
intervals[hourTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const hourStart = new Date(recordTime);
|
||||
hourStart.setMinutes(0, 0, 0);
|
||||
const hourKey = hourStart.getTime();
|
||||
|
||||
if (intervals[hourKey]) {
|
||||
intervals[hourKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[hourKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[hourKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Group by day for longer periods (7d, 30d, week, month)
|
||||
const days = timeRange === "7d" ? 7 : timeRange === "30d" ? 30 : timeRange === "week" ? 7 : 30;
|
||||
let startDate: Date;
|
||||
|
||||
if (timeRange === "week") {
|
||||
startDate = startOfWeek(now);
|
||||
} else if (timeRange === "month") {
|
||||
startDate = startOfMonth(now);
|
||||
} else {
|
||||
// For 7d and 30d, start from (days-1) days ago to include today
|
||||
startDate = subDays(now, days - 1);
|
||||
}
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const dayTime = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000);
|
||||
intervals[dayTime.getTime()] = { input: 0, output: 0, cache: 0 };
|
||||
}
|
||||
|
||||
records.forEach((record) => {
|
||||
const recordTime = new Date(record.timestamp);
|
||||
const dayStart = new Date(recordTime);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayKey = dayStart.getTime();
|
||||
|
||||
if (intervals[dayKey]) {
|
||||
intervals[dayKey].input += record.usage?.input_tokens || 0;
|
||||
intervals[dayKey].output += record.usage?.output_tokens || 0;
|
||||
intervals[dayKey].cache += record.usage?.cache_read_input_tokens || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return intervals;
|
||||
};
|
||||
|
||||
const groupedData = groupDataByInterval(filteredData);
|
||||
|
||||
// Prepare chart data
|
||||
const labels = Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
if (timeRange === "all") {
|
||||
return format(date, "MMM yyyy");
|
||||
} else if (timeRange === "today") {
|
||||
return format(date, "HH:mm");
|
||||
} else if (timeRange === "30min" || timeRange === "1h" || timeRange === "5h") {
|
||||
return format(date, "HH:mm");
|
||||
} else {
|
||||
return format(date, "MMM dd");
|
||||
}
|
||||
});
|
||||
|
||||
const chartData = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Input Tokens",
|
||||
data: Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((key) => groupedData[key].input),
|
||||
borderColor: "rgb(59, 130, 246)",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
tension: 0.4,
|
||||
},
|
||||
{
|
||||
label: "Output Tokens",
|
||||
data: Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((key) => groupedData[key].output),
|
||||
borderColor: "rgb(16, 185, 129)",
|
||||
backgroundColor: "rgba(16, 185, 129, 0.1)",
|
||||
tension: 0.4,
|
||||
},
|
||||
{
|
||||
label: "Cache Read Tokens",
|
||||
data: Object.keys(groupedData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map((key) => groupedData[key].cache),
|
||||
borderColor: "rgb(245, 158, 11)",
|
||||
backgroundColor: "rgba(245, 158, 11, 0.1)",
|
||||
tension: 0.4,
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Get chart title based on time range
|
||||
const getChartTitle = () => {
|
||||
const modelFilter = selectedModel !== "all" ? ` - Model: ${selectedModel}` : "";
|
||||
|
||||
switch (timeRange) {
|
||||
case "30min":
|
||||
return `Token Usage (Last 30 Minutes - 5 min intervals)${modelFilter}`;
|
||||
case "1h":
|
||||
return `Token Usage (Last Hour - 5 min intervals)${modelFilter}`;
|
||||
case "5h":
|
||||
return `Token Usage (Last 5 Hours)${modelFilter}`;
|
||||
case "today":
|
||||
return `Token Usage (Today)${modelFilter}`;
|
||||
case "7d":
|
||||
return `Token Usage (Last 7 Days)${modelFilter}`;
|
||||
case "30d":
|
||||
return `Token Usage (Last 30 Days)${modelFilter}`;
|
||||
case "week":
|
||||
return `Token Usage (This Week)${modelFilter}`;
|
||||
case "month":
|
||||
return `Token Usage (This Month)${modelFilter}`;
|
||||
case "all":
|
||||
return `Token Usage (All Time - Monthly)${modelFilter}`;
|
||||
default:
|
||||
return `Token Usage${modelFilter}`;
|
||||
}
|
||||
};
|
||||
|
||||
const options: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top" as const,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: getChartTitle(),
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
let label = context.dataset.label || "";
|
||||
if (label) {
|
||||
label += ": ";
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat().format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Time",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Tokens",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: "nearest",
|
||||
axis: "x",
|
||||
intersect: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="h-96 flex items-center justify-center border rounded-lg bg-muted/20">
|
||||
<p className="text-muted-foreground">No usage data available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="model-filter" className="text-sm font-medium">
|
||||
Model:
|
||||
</label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="model-filter" className="w-48">
|
||||
<SelectValue placeholder="All models" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All models</SelectItem>
|
||||
{availableModels.map((model) => (
|
||||
<SelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="time-range" className="text-sm font-medium">
|
||||
Time Range:
|
||||
</label>
|
||||
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
|
||||
<SelectTrigger id="time-range" className="w-48">
|
||||
<SelectValue placeholder="Select time range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30min">Last 30 minutes</SelectItem>
|
||||
<SelectItem value="1h">Last 1 hour</SelectItem>
|
||||
<SelectItem value="5h">Last 5 hours</SelectItem>
|
||||
<SelectItem value="today">Start of today</SelectItem>
|
||||
<SelectItem value="7d">Last 7 days</SelectItem>
|
||||
<SelectItem value="30d">Last 30 days</SelectItem>
|
||||
<SelectItem value="week">Start of this week</SelectItem>
|
||||
<SelectItem value="month">Start of this month</SelectItem>
|
||||
<SelectItem value="all">All time</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="h-96 w-full">
|
||||
<Line data={chartData} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
import { useProjectUsageFiles } from "@/lib/query";
|
||||
import { TokenUsageChart } from "@/components/TokenUsageChart";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ProjectUsageRecord } from "@/lib/query";
|
||||
|
||||
export function MonitorPage() {
|
||||
const { data: usageData, isLoading, error } = useProjectUsageFiles();
|
||||
const [filteredUsageData, setFilteredUsageData] = useState<ProjectUsageRecord[]>([]);
|
||||
|
||||
// Initialize filtered data with full data
|
||||
useEffect(() => {
|
||||
if (usageData) {
|
||||
setFilteredUsageData(usageData);
|
||||
}
|
||||
}, [usageData]);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
@@ -10,19 +21,38 @@ export function MonitorPage() {
|
||||
<h3 className="font-bold" data-tauri-drag-region>Monitor</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<div className="px-4 space-y-6">
|
||||
{isLoading ? (
|
||||
<p>Loading usage data...</p>
|
||||
) : error ? (
|
||||
<p>Error loading usage data: {error.message}</p>
|
||||
) : usageData && usageData.length > 0 ? (
|
||||
<div>
|
||||
<ul>
|
||||
<li>Total Input Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.input_tokens || 0), 0)}</li>
|
||||
<li>Total Output Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.output_tokens || 0), 0)}</li>
|
||||
<li>Total Cache Read Input Tokens: {usageData.reduce((sum, record) => sum + (record.usage?.cache_read_input_tokens || 0), 0)}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 border rounded-lg bg-blue-50 dark:bg-blue-950/20">
|
||||
<h3 className="font-semibold text-blue-800 dark:text-blue-200">Input Tokens</h3>
|
||||
<p className="text-2xl font-bold text-blue-600 dark:text-blue-300">
|
||||
{new Intl.NumberFormat().format(filteredUsageData.reduce((sum, record) => sum + (record.usage?.input_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg bg-green-50 dark:bg-green-950/20">
|
||||
<h3 className="font-semibold text-green-800 dark:text-green-200">Output Tokens</h3>
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-300">
|
||||
{new Intl.NumberFormat().format(filteredUsageData.reduce((sum, record) => sum + (record.usage?.output_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg bg-amber-50 dark:bg-amber-950/20">
|
||||
<h3 className="font-semibold text-amber-800 dark:text-amber-200">Cache Read Tokens</h3>
|
||||
<p className="text-2xl font-bold text-amber-600 dark:text-amber-300">
|
||||
{new Intl.NumberFormat().format(filteredUsageData.reduce((sum, record) => sum + (record.usage?.cache_read_input_tokens || 0), 0))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<TokenUsageChart data={usageData} onFilteredDataChange={setFilteredUsageData} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>No usage data found.</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user