machinaos 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +32 -6
  2. package/bin/cli.js +0 -0
  3. package/client/dist/assets/index-5BWZnM6b.js +703 -0
  4. package/client/dist/index.html +1 -1
  5. package/client/package.json +1 -1
  6. package/client/src/Dashboard.tsx +12 -5
  7. package/client/src/ParameterPanel.tsx +6 -5
  8. package/client/src/components/AIAgentNode.tsx +35 -16
  9. package/client/src/components/CredentialsModal.tsx +450 -5
  10. package/client/src/components/TeamMonitorNode.tsx +269 -0
  11. package/client/src/components/parameterPanel/InputSection.tsx +25 -0
  12. package/client/src/contexts/WebSocketContext.tsx +38 -0
  13. package/client/src/hooks/useApiKeys.ts +44 -0
  14. package/client/src/nodeDefinitions/specializedAgentNodes.ts +59 -3
  15. package/client/src/nodeDefinitions/twitterNodes.ts +441 -0
  16. package/client/src/nodeDefinitions/utilityNodes.ts +45 -1
  17. package/client/src/nodeDefinitions.ts +7 -1
  18. package/client/src/services/executionService.ts +4 -1
  19. package/install.sh +63 -1
  20. package/package.json +5 -2
  21. package/scripts/build.js +0 -0
  22. package/scripts/clean.js +0 -0
  23. package/scripts/daemon.js +0 -0
  24. package/scripts/docker.js +0 -0
  25. package/scripts/install.js +0 -0
  26. package/scripts/postinstall.js +29 -0
  27. package/scripts/preinstall.js +67 -0
  28. package/scripts/serve-client.js +0 -0
  29. package/scripts/start.js +0 -0
  30. package/scripts/stop.js +0 -0
  31. package/scripts/sync-version.js +0 -0
  32. package/server/Dockerfile +10 -15
  33. package/server/constants.py +20 -0
  34. package/server/core/database.py +443 -3
  35. package/server/main.py +9 -1
  36. package/server/models/database.py +112 -2
  37. package/server/pyproject.toml +3 -0
  38. package/server/requirements.txt +3 -0
  39. package/server/routers/twitter.py +390 -0
  40. package/server/routers/websocket.py +320 -0
  41. package/server/services/agent_team.py +266 -0
  42. package/server/services/ai.py +43 -0
  43. package/server/services/compaction.py +39 -4
  44. package/server/services/event_waiter.py +41 -0
  45. package/server/services/handlers/__init__.py +13 -0
  46. package/server/services/handlers/ai.py +66 -2
  47. package/server/services/handlers/tools.py +84 -0
  48. package/server/services/handlers/twitter.py +297 -0
  49. package/server/services/handlers/utility.py +91 -0
  50. package/server/services/node_executor.py +15 -1
  51. package/server/services/pricing.py +270 -0
  52. package/server/services/status_broadcaster.py +79 -0
  53. package/server/services/twitter_oauth.py +410 -0
  54. package/server/skills/social_agent/twitter-search-skill/SKILL.md +146 -0
  55. package/server/skills/social_agent/twitter-send-skill/SKILL.md +142 -0
  56. package/server/skills/social_agent/twitter-user-skill/SKILL.md +165 -0
  57. package/workflows/Zeenie_full.json +459 -0
  58. package/workflows/Zeenie_small.json +459 -0
  59. package/client/dist/assets/index-YVvAiByx.js +0 -703
  60. package/server/requirements-docker.txt +0 -86
@@ -5,17 +5,20 @@
5
5
  */
6
6
 
7
7
  import React, { useState, useEffect, useCallback } from 'react';
8
- import { Button, Tag, Alert, Descriptions, Space, InputNumber, Switch, Input } from 'antd';
8
+ import { Button, Tag, Alert, Descriptions, Space, InputNumber, Switch, Input, Collapse, Statistic, Spin } from 'antd';
9
9
  import {
10
10
  CheckCircleOutlined,
11
11
  SafetyOutlined,
12
+ ReloadOutlined,
13
+ DollarOutlined,
14
+ TwitterOutlined,
12
15
  } from '@ant-design/icons';
13
16
  import Modal from './ui/Modal';
14
17
  import QRCodeDisplay from './ui/QRCodeDisplay';
15
18
  import ApiKeyInput from './ui/ApiKeyInput';
16
- import { useApiKeys, ProviderDefaults } from '../hooks/useApiKeys';
19
+ import { useApiKeys, ProviderDefaults, ProviderUsageSummary } from '../hooks/useApiKeys';
17
20
  import { useAppTheme } from '../hooks/useAppTheme';
18
- import { useWhatsAppStatus, useAndroidStatus, useWebSocket, RateLimitConfig, RateLimitStats } from '../contexts/WebSocketContext';
21
+ import { useWhatsAppStatus, useAndroidStatus, useTwitterStatus, useWebSocket, RateLimitConfig, RateLimitStats } from '../contexts/WebSocketContext';
19
22
  import { useWhatsApp } from '../hooks/useWhatsApp';
20
23
  import {
21
24
  OpenAIIcon, ClaudeIcon, GeminiIcon, GroqIcon, OpenRouterIcon, CerebrasIcon,
@@ -44,6 +47,10 @@ const WhatsAppIcon = () => (
44
47
  </svg>
45
48
  );
46
49
 
50
+ const XIcon = () => (
51
+ <TwitterOutlined style={{ fontSize: 20, color: '#000000' }} />
52
+ );
53
+
47
54
  // ============================================================================
48
55
  // TYPES & DATA
49
56
  // ============================================================================
@@ -57,7 +64,7 @@ interface CredentialItem {
57
64
  Icon?: React.FC<{ size?: number }>;
58
65
  CustomIcon?: React.FC;
59
66
  isSpecial?: boolean;
60
- panelType?: 'whatsapp' | 'android';
67
+ panelType?: 'whatsapp' | 'android' | 'twitter';
61
68
  }
62
69
 
63
70
  interface Category {
@@ -84,6 +91,7 @@ const CATEGORIES: Category[] = [
84
91
  label: 'Social Media',
85
92
  items: [
86
93
  { id: 'whatsapp_personal', name: 'WhatsApp Personal', placeholder: '', color: '#25D366', desc: 'Connect via QR code pairing', CustomIcon: WhatsAppIcon, isSpecial: true, panelType: 'whatsapp' },
94
+ { id: 'twitter', name: 'Twitter/X', placeholder: '', color: '#000000', desc: 'Post tweets, search, user lookup', CustomIcon: XIcon, isSpecial: true, panelType: 'twitter' },
87
95
  ],
88
96
  },
89
97
  {
@@ -114,9 +122,10 @@ interface Props {
114
122
 
115
123
  const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
116
124
  const theme = useAppTheme();
117
- const { validateApiKey, saveApiKey, getStoredApiKey, hasStoredKey, removeApiKey, validateGoogleMapsKey, getProviderDefaults, saveProviderDefaults, isConnected } = useApiKeys();
125
+ const { validateApiKey, saveApiKey, getStoredApiKey, hasStoredKey, removeApiKey, validateGoogleMapsKey, getProviderDefaults, saveProviderDefaults, getProviderUsageSummary, isConnected } = useApiKeys();
118
126
  const whatsappStatus = useWhatsAppStatus();
119
127
  const androidStatus = useAndroidStatus();
128
+ const twitterStatus = useTwitterStatus();
120
129
 
121
130
 
122
131
  // Tag style helper - consistent theming for status tags
@@ -143,6 +152,11 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
143
152
  const [defaultsLoading, setDefaultsLoading] = useState<Record<string, boolean>>({});
144
153
  const [defaultsDirty, setDefaultsDirty] = useState<Record<string, boolean>>({});
145
154
 
155
+ // Usage & Costs state
156
+ const [usageSummary, setUsageSummary] = useState<ProviderUsageSummary[]>([]);
157
+ const [usageLoading, setUsageLoading] = useState(false);
158
+ const [usageExpanded, setUsageExpanded] = useState(false);
159
+
146
160
  // Load stored keys and proxy URLs
147
161
  const loadKeys = useCallback(async () => {
148
162
  const allItems = CATEGORIES.flatMap(c => c.items);
@@ -218,6 +232,27 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
218
232
  setDefaultsLoading(l => ({ ...l, [provider]: false }));
219
233
  };
220
234
 
235
+ // Load usage summary when expanded
236
+ const loadUsageSummary = useCallback(async () => {
237
+ if (!isConnected) return;
238
+ setUsageLoading(true);
239
+ try {
240
+ const summary = await getProviderUsageSummary();
241
+ setUsageSummary(summary);
242
+ } catch (error) {
243
+ console.warn('Error loading usage summary:', error);
244
+ } finally {
245
+ setUsageLoading(false);
246
+ }
247
+ }, [getProviderUsageSummary, isConnected]);
248
+
249
+ // Load usage when section is expanded
250
+ useEffect(() => {
251
+ if (usageExpanded && isConnected) {
252
+ loadUsageSummary();
253
+ }
254
+ }, [usageExpanded, isConnected, loadUsageSummary]);
255
+
221
256
  const handleValidate = async (id: string) => {
222
257
  const key = keys[id];
223
258
  if (!key?.trim()) return;
@@ -251,6 +286,9 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
251
286
  if (item.panelType === 'android') {
252
287
  return { connected: androidStatus.paired, label: androidStatus.paired ? 'Paired' : 'Not Paired' };
253
288
  }
289
+ if (item.panelType === 'twitter') {
290
+ return { connected: twitterStatus.connected, label: twitterStatus.connected ? `@${twitterStatus.username}` : 'Not Connected' };
291
+ }
254
292
  return null;
255
293
  };
256
294
 
@@ -374,6 +412,13 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
374
412
  const [whatsappLoading, setWhatsappLoading] = useState<string | null>(null);
375
413
  const [whatsappError, setWhatsappError] = useState<string | null>(null);
376
414
 
415
+ // Twitter state
416
+ const [twitterClientId, setTwitterClientId] = useState('');
417
+ const [twitterClientSecret, setTwitterClientSecret] = useState('');
418
+ const [twitterCredentialsStored, setTwitterCredentialsStored] = useState<boolean | null>(null);
419
+ const [twitterLoading, setTwitterLoading] = useState<string | null>(null);
420
+ const [twitterError, setTwitterError] = useState<string | null>(null);
421
+
377
422
  // Rate limit state
378
423
  const [rateLimitConfig, setRateLimitConfig] = useState<RateLimitConfig | null>(null);
379
424
  const [rateLimitStats, setRateLimitStats] = useState<RateLimitStats | null>(null);
@@ -394,6 +439,85 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
394
439
  }
395
440
  }, [visible, hasStoredKey, getStoredApiKey]);
396
441
 
442
+ // Load Twitter credentials on mount
443
+ useEffect(() => {
444
+ if (visible) {
445
+ // Check if client_id is stored
446
+ hasStoredKey('twitter_client_id').then(async (has) => {
447
+ setTwitterCredentialsStored(has);
448
+ if (has) {
449
+ const clientId = await getStoredApiKey('twitter_client_id');
450
+ const clientSecret = await getStoredApiKey('twitter_client_secret');
451
+ if (clientId) setTwitterClientId(clientId);
452
+ if (clientSecret) setTwitterClientSecret(clientSecret);
453
+ }
454
+ });
455
+ }
456
+ }, [visible, hasStoredKey, getStoredApiKey]);
457
+
458
+ // Twitter handlers
459
+ const handleTwitterSaveCredentials = async () => {
460
+ if (!twitterClientId.trim()) {
461
+ setTwitterError('Client ID is required');
462
+ return;
463
+ }
464
+ setTwitterLoading('save');
465
+ setTwitterError(null);
466
+ try {
467
+ await saveApiKey('twitter_client_id', twitterClientId.trim());
468
+ if (twitterClientSecret.trim()) {
469
+ await saveApiKey('twitter_client_secret', twitterClientSecret.trim());
470
+ }
471
+ setTwitterCredentialsStored(true);
472
+ } catch (err: any) {
473
+ setTwitterError(err.message || 'Failed to save credentials');
474
+ } finally {
475
+ setTwitterLoading(null);
476
+ }
477
+ };
478
+
479
+ const handleTwitterLogin = async () => {
480
+ setTwitterLoading('login');
481
+ setTwitterError(null);
482
+ try {
483
+ const response = await sendRequest('twitter_oauth_login', {});
484
+ if (!response.success) {
485
+ setTwitterError(response.error || 'Failed to start OAuth');
486
+ }
487
+ } catch (err: any) {
488
+ setTwitterError(err.message || 'Failed to start OAuth');
489
+ } finally {
490
+ setTwitterLoading(null);
491
+ }
492
+ };
493
+
494
+ const handleTwitterLogout = async () => {
495
+ setTwitterLoading('logout');
496
+ setTwitterError(null);
497
+ try {
498
+ const response = await sendRequest('twitter_logout', {});
499
+ if (!response.success) {
500
+ setTwitterError(response.error || 'Failed to disconnect');
501
+ }
502
+ } catch (err: any) {
503
+ setTwitterError(err.message || 'Failed to disconnect');
504
+ } finally {
505
+ setTwitterLoading(null);
506
+ }
507
+ };
508
+
509
+ const handleTwitterRefreshStatus = async () => {
510
+ setTwitterLoading('refresh');
511
+ setTwitterError(null);
512
+ try {
513
+ await sendRequest('twitter_oauth_status', {});
514
+ } catch (err: any) {
515
+ setTwitterError(err.message || 'Failed to refresh status');
516
+ } finally {
517
+ setTwitterLoading(null);
518
+ }
519
+ };
520
+
397
521
  // Android handlers
398
522
  const handleAndroidSaveKey = async () => {
399
523
  if (!androidApiKey.trim()) return;
@@ -1077,6 +1201,202 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
1077
1201
  );
1078
1202
  }
1079
1203
 
1204
+ // Twitter panel
1205
+ if (selectedItem.panelType === 'twitter') {
1206
+ return (
1207
+ <div style={{ padding: theme.spacing.xl, display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
1208
+ <Descriptions
1209
+ title={<Space><XIcon /> Twitter/X</Space>}
1210
+ bordered
1211
+ column={1}
1212
+ size="small"
1213
+ style={{
1214
+ marginBottom: theme.spacing.xl,
1215
+ background: theme.colors.backgroundAlt,
1216
+ borderRadius: theme.borderRadius.md,
1217
+ }}
1218
+ styles={{
1219
+ label: {
1220
+ backgroundColor: theme.colors.backgroundPanel,
1221
+ color: theme.colors.textSecondary,
1222
+ fontWeight: theme.fontWeight.medium,
1223
+ },
1224
+ content: {
1225
+ backgroundColor: theme.colors.background,
1226
+ color: theme.colors.text,
1227
+ },
1228
+ }}
1229
+ >
1230
+ <Descriptions.Item label="Status">
1231
+ <Tag style={getTagStyle(twitterStatus.connected ? 'success' : 'error')}>
1232
+ {twitterStatus.connected ? 'Connected' : 'Not Connected'}
1233
+ </Tag>
1234
+ </Descriptions.Item>
1235
+ <Descriptions.Item label="API Credentials">
1236
+ <Tag style={getTagStyle(twitterCredentialsStored ? 'success' : 'error')}>
1237
+ {twitterCredentialsStored === null ? 'Checking...' : twitterCredentialsStored ? 'Configured' : 'Not configured'}
1238
+ </Tag>
1239
+ </Descriptions.Item>
1240
+ {twitterStatus.connected && twitterStatus.username && (
1241
+ <Descriptions.Item label="Account">
1242
+ <Space>
1243
+ {twitterStatus.profile_image_url && (
1244
+ <img
1245
+ src={twitterStatus.profile_image_url}
1246
+ alt={twitterStatus.username}
1247
+ style={{ width: 24, height: 24, borderRadius: '50%' }}
1248
+ />
1249
+ )}
1250
+ <span style={{ fontFamily: 'monospace', fontSize: theme.fontSize.sm }}>@{twitterStatus.username}</span>
1251
+ {twitterStatus.name && <span style={{ color: theme.colors.textSecondary }}>({twitterStatus.name})</span>}
1252
+ </Space>
1253
+ </Descriptions.Item>
1254
+ )}
1255
+ </Descriptions>
1256
+
1257
+ {/* API Credentials Input - Only show if not connected */}
1258
+ {!twitterStatus.connected && (
1259
+ <div style={{ marginBottom: theme.spacing.xl }}>
1260
+ <label style={{
1261
+ display: 'block',
1262
+ fontSize: theme.fontSize.sm,
1263
+ fontWeight: theme.fontWeight.medium,
1264
+ color: theme.colors.text,
1265
+ marginBottom: theme.spacing.sm,
1266
+ }}>
1267
+ Twitter API Credentials
1268
+ </label>
1269
+ <div style={{ marginBottom: theme.spacing.md }}>
1270
+ <Input
1271
+ value={twitterClientId}
1272
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTwitterClientId(e.target.value)}
1273
+ placeholder="Client ID (from X Developer Portal)"
1274
+ style={{
1275
+ marginBottom: theme.spacing.sm,
1276
+ backgroundColor: theme.colors.background,
1277
+ borderColor: theme.colors.border,
1278
+ color: theme.colors.text,
1279
+ fontFamily: 'monospace',
1280
+ fontSize: theme.fontSize.sm,
1281
+ }}
1282
+ />
1283
+ <Input.Password
1284
+ value={twitterClientSecret}
1285
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTwitterClientSecret(e.target.value)}
1286
+ placeholder="Client Secret (optional for PKCE)"
1287
+ style={{
1288
+ backgroundColor: theme.colors.background,
1289
+ borderColor: theme.colors.border,
1290
+ color: theme.colors.text,
1291
+ fontFamily: 'monospace',
1292
+ fontSize: theme.fontSize.sm,
1293
+ }}
1294
+ />
1295
+ </div>
1296
+ <Button
1297
+ onClick={handleTwitterSaveCredentials}
1298
+ loading={twitterLoading === 'save'}
1299
+ disabled={!twitterClientId.trim()}
1300
+ style={{
1301
+ backgroundColor: `${theme.dracula.purple}25`,
1302
+ borderColor: `${theme.dracula.purple}60`,
1303
+ color: theme.dracula.purple,
1304
+ }}
1305
+ >
1306
+ Save Credentials
1307
+ </Button>
1308
+ <div style={{
1309
+ fontSize: theme.fontSize.xs,
1310
+ color: theme.colors.textMuted,
1311
+ marginTop: theme.spacing.sm,
1312
+ lineHeight: 1.5,
1313
+ }}>
1314
+ Get credentials from the X Developer Portal. Create an app with OAuth 2.0 enabled.
1315
+ <br />
1316
+ Callback URL: <code style={{ fontSize: theme.fontSize.xs, color: theme.dracula.cyan }}>http://localhost:3010/api/twitter/callback</code>
1317
+ </div>
1318
+ </div>
1319
+ )}
1320
+
1321
+ {twitterError && (
1322
+ <Alert type="error" message={twitterError} showIcon style={{ marginBottom: theme.spacing.lg }} />
1323
+ )}
1324
+
1325
+ {/* Info box */}
1326
+ <div style={{
1327
+ padding: theme.spacing.md,
1328
+ borderRadius: theme.borderRadius.md,
1329
+ backgroundColor: `${theme.dracula.cyan}10`,
1330
+ border: `1px solid ${theme.dracula.cyan}30`,
1331
+ marginBottom: theme.spacing.xl,
1332
+ flex: 1,
1333
+ }}>
1334
+ <div style={{
1335
+ fontSize: theme.fontSize.sm,
1336
+ color: theme.colors.textSecondary,
1337
+ lineHeight: 1.5,
1338
+ }}>
1339
+ {twitterStatus.connected ? (
1340
+ <>Your Twitter account is connected. You can now use Twitter nodes in your workflows.</>
1341
+ ) : twitterCredentialsStored ? (
1342
+ <>Click Login with Twitter to authorize. A browser window will open for authentication.</>
1343
+ ) : (
1344
+ <>Enter your Twitter API credentials above to get started.</>
1345
+ )}
1346
+ </div>
1347
+ </div>
1348
+
1349
+ {/* Actions */}
1350
+ <div style={{
1351
+ display: 'flex',
1352
+ gap: theme.spacing.sm,
1353
+ justifyContent: 'center',
1354
+ paddingTop: theme.spacing.md,
1355
+ borderTop: `1px solid ${theme.colors.border}`,
1356
+ }}>
1357
+ {!twitterStatus.connected ? (
1358
+ <Button
1359
+ onClick={handleTwitterLogin}
1360
+ loading={twitterLoading === 'login'}
1361
+ disabled={!twitterCredentialsStored}
1362
+ style={{
1363
+ backgroundColor: `${theme.dracula.green}25`,
1364
+ borderColor: `${theme.dracula.green}60`,
1365
+ color: theme.dracula.green,
1366
+ }}
1367
+ >
1368
+ Login with Twitter
1369
+ </Button>
1370
+ ) : (
1371
+ <Button
1372
+ onClick={handleTwitterLogout}
1373
+ loading={twitterLoading === 'logout'}
1374
+ style={{
1375
+ backgroundColor: `${theme.dracula.pink}25`,
1376
+ borderColor: `${theme.dracula.pink}60`,
1377
+ color: theme.dracula.pink,
1378
+ }}
1379
+ >
1380
+ Disconnect
1381
+ </Button>
1382
+ )}
1383
+ <Button
1384
+ onClick={handleTwitterRefreshStatus}
1385
+ loading={twitterLoading === 'refresh'}
1386
+ icon={<ReloadOutlined />}
1387
+ style={{
1388
+ backgroundColor: `${theme.dracula.cyan}25`,
1389
+ borderColor: `${theme.dracula.cyan}60`,
1390
+ color: theme.dracula.cyan,
1391
+ }}
1392
+ >
1393
+ Refresh
1394
+ </Button>
1395
+ </div>
1396
+ </div>
1397
+ );
1398
+ }
1399
+
1080
1400
  const item = selectedItem;
1081
1401
  const isValid = validKeys[item.id];
1082
1402
  const Icon = item.Icon;
@@ -1369,6 +1689,131 @@ const CredentialsModal: React.FC<Props> = ({ visible, onClose }) => {
1369
1689
  </div>
1370
1690
  )}
1371
1691
 
1692
+ {/* Usage & Costs Section - Only for AI providers */}
1693
+ {CATEGORIES.find(c => c.key === 'ai')?.items.some(i => i.id === item.id) && (
1694
+ <div style={{ marginBottom: theme.spacing.xl }}>
1695
+ <Collapse
1696
+ ghost
1697
+ onChange={(keys) => setUsageExpanded(keys.includes('usage'))}
1698
+ items={[{
1699
+ key: 'usage',
1700
+ label: (
1701
+ <span style={{ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, color: theme.colors.text }}>
1702
+ <DollarOutlined style={{ marginRight: theme.spacing.sm }} />
1703
+ Usage & Costs
1704
+ </span>
1705
+ ),
1706
+ children: usageLoading ? (
1707
+ <div style={{ textAlign: 'center', padding: theme.spacing.lg }}>
1708
+ <Spin size="small" />
1709
+ </div>
1710
+ ) : (() => {
1711
+ const providerData = usageSummary.find(p => p.provider === item.id);
1712
+ if (!providerData || providerData.execution_count === 0) {
1713
+ return (
1714
+ <Alert
1715
+ message={`No usage data yet for ${item.name}`}
1716
+ type="info"
1717
+ showIcon
1718
+ style={{ marginBottom: theme.spacing.md }}
1719
+ />
1720
+ );
1721
+ }
1722
+ return (
1723
+ <Space direction="vertical" size="middle" style={{ width: '100%' }}>
1724
+ {/* Summary Stats */}
1725
+ <div style={{ display: 'flex', gap: theme.spacing.lg, flexWrap: 'wrap' }}>
1726
+ <Statistic
1727
+ title="Total Tokens"
1728
+ value={providerData.total_tokens}
1729
+ valueStyle={{ color: theme.dracula.cyan, fontSize: theme.fontSize.lg }}
1730
+ />
1731
+ <Statistic
1732
+ title="Total Cost"
1733
+ value={providerData.total_cost}
1734
+ precision={4}
1735
+ prefix="$"
1736
+ valueStyle={{ color: theme.dracula.green, fontSize: theme.fontSize.lg }}
1737
+ />
1738
+ <Statistic
1739
+ title="Executions"
1740
+ value={providerData.execution_count}
1741
+ valueStyle={{ color: theme.dracula.purple, fontSize: theme.fontSize.lg }}
1742
+ />
1743
+ </div>
1744
+
1745
+ {/* Token Breakdown */}
1746
+ <Descriptions size="small" column={2} bordered>
1747
+ <Descriptions.Item label="Input Tokens">
1748
+ {providerData.total_input_tokens.toLocaleString()}
1749
+ <span style={{ color: theme.dracula.green, marginLeft: 8 }}>
1750
+ (${providerData.total_input_cost.toFixed(4)})
1751
+ </span>
1752
+ </Descriptions.Item>
1753
+ <Descriptions.Item label="Output Tokens">
1754
+ {providerData.total_output_tokens.toLocaleString()}
1755
+ <span style={{ color: theme.dracula.green, marginLeft: 8 }}>
1756
+ (${providerData.total_output_cost.toFixed(4)})
1757
+ </span>
1758
+ </Descriptions.Item>
1759
+ {providerData.total_cache_cost > 0 && (
1760
+ <Descriptions.Item label="Cache Cost" span={2}>
1761
+ <span style={{ color: theme.dracula.green }}>
1762
+ ${providerData.total_cache_cost.toFixed(4)}
1763
+ </span>
1764
+ </Descriptions.Item>
1765
+ )}
1766
+ </Descriptions>
1767
+
1768
+ {/* Model Breakdown */}
1769
+ {providerData.models.length > 1 && (
1770
+ <div>
1771
+ <div style={{ fontSize: theme.fontSize.xs, color: theme.colors.textMuted, marginBottom: theme.spacing.sm }}>
1772
+ By Model
1773
+ </div>
1774
+ <div style={{
1775
+ maxHeight: 120,
1776
+ overflow: 'auto',
1777
+ backgroundColor: theme.colors.backgroundAlt,
1778
+ borderRadius: theme.borderRadius.sm,
1779
+ padding: theme.spacing.sm,
1780
+ }}>
1781
+ {providerData.models.map((model, idx) => (
1782
+ <div key={idx} style={{
1783
+ display: 'flex',
1784
+ justifyContent: 'space-between',
1785
+ padding: `${theme.spacing.xs} 0`,
1786
+ fontSize: theme.fontSize.sm,
1787
+ borderBottom: idx < providerData.models.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
1788
+ }}>
1789
+ <span style={{ fontFamily: 'monospace', fontSize: theme.fontSize.xs, color: theme.colors.text }}>
1790
+ {model.model}
1791
+ </span>
1792
+ <span style={{ color: theme.dracula.green }}>
1793
+ ${model.total_cost.toFixed(4)}
1794
+ </span>
1795
+ </div>
1796
+ ))}
1797
+ </div>
1798
+ </div>
1799
+ )}
1800
+
1801
+ <Button
1802
+ size="small"
1803
+ icon={<ReloadOutlined />}
1804
+ onClick={loadUsageSummary}
1805
+ loading={usageLoading}
1806
+ >
1807
+ Refresh
1808
+ </Button>
1809
+ </Space>
1810
+ );
1811
+ })()
1812
+ }]}
1813
+ />
1814
+ </div>
1815
+ )}
1816
+
1372
1817
  {/* Models list */}
1373
1818
  {models[item.id]?.length > 0 && (
1374
1819
  <div style={{ marginBottom: theme.spacing.xl }}>