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.
- package/README.md +32 -6
- package/bin/cli.js +0 -0
- package/client/dist/assets/index-5BWZnM6b.js +703 -0
- package/client/dist/index.html +1 -1
- package/client/package.json +1 -1
- package/client/src/Dashboard.tsx +12 -5
- package/client/src/ParameterPanel.tsx +6 -5
- package/client/src/components/AIAgentNode.tsx +35 -16
- package/client/src/components/CredentialsModal.tsx +450 -5
- package/client/src/components/TeamMonitorNode.tsx +269 -0
- package/client/src/components/parameterPanel/InputSection.tsx +25 -0
- package/client/src/contexts/WebSocketContext.tsx +38 -0
- package/client/src/hooks/useApiKeys.ts +44 -0
- package/client/src/nodeDefinitions/specializedAgentNodes.ts +59 -3
- package/client/src/nodeDefinitions/twitterNodes.ts +441 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +45 -1
- package/client/src/nodeDefinitions.ts +7 -1
- package/client/src/services/executionService.ts +4 -1
- package/install.sh +63 -1
- package/package.json +5 -2
- package/scripts/build.js +0 -0
- package/scripts/clean.js +0 -0
- package/scripts/daemon.js +0 -0
- package/scripts/docker.js +0 -0
- package/scripts/install.js +0 -0
- package/scripts/postinstall.js +29 -0
- package/scripts/preinstall.js +67 -0
- package/scripts/serve-client.js +0 -0
- package/scripts/start.js +0 -0
- package/scripts/stop.js +0 -0
- package/scripts/sync-version.js +0 -0
- package/server/Dockerfile +10 -15
- package/server/constants.py +20 -0
- package/server/core/database.py +443 -3
- package/server/main.py +9 -1
- package/server/models/database.py +112 -2
- package/server/pyproject.toml +3 -0
- package/server/requirements.txt +3 -0
- package/server/routers/twitter.py +390 -0
- package/server/routers/websocket.py +320 -0
- package/server/services/agent_team.py +266 -0
- package/server/services/ai.py +43 -0
- package/server/services/compaction.py +39 -4
- package/server/services/event_waiter.py +41 -0
- package/server/services/handlers/__init__.py +13 -0
- package/server/services/handlers/ai.py +66 -2
- package/server/services/handlers/tools.py +84 -0
- package/server/services/handlers/twitter.py +297 -0
- package/server/services/handlers/utility.py +91 -0
- package/server/services/node_executor.py +15 -1
- package/server/services/pricing.py +270 -0
- package/server/services/status_broadcaster.py +79 -0
- package/server/services/twitter_oauth.py +410 -0
- package/server/skills/social_agent/twitter-search-skill/SKILL.md +146 -0
- package/server/skills/social_agent/twitter-send-skill/SKILL.md +142 -0
- package/server/skills/social_agent/twitter-user-skill/SKILL.md +165 -0
- package/workflows/Zeenie_full.json +459 -0
- package/workflows/Zeenie_small.json +459 -0
- package/client/dist/assets/index-YVvAiByx.js +0 -703
- 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 }}>
|