apteva 0.4.53 → 0.4.56
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/dist/ActivityPage.kxzzb4yc.js +3 -0
- package/dist/ApiDocsPage.zq998hbm.js +4 -0
- package/dist/App.55rea8mn.js +61 -0
- package/dist/App.5ywb23z4.js +53 -0
- package/dist/App.6thds120.js +4 -0
- package/dist/{App.jhb45d7r.js → App.9tctxzqm.js} +3 -3
- package/dist/App.a8r8ttaz.js +4 -0
- package/dist/App.agsv5bje.js +4 -0
- package/dist/App.cepapqmx.js +4 -0
- package/dist/App.dp041gb3.js +221 -0
- package/dist/App.fds72zb5.js +4 -0
- package/dist/App.fg9qj2dq.js +4 -0
- package/dist/App.ndfejbm9.js +4 -0
- package/dist/App.nxmfmq1h.js +13 -0
- package/dist/App.qdfyt8ba.js +4 -0
- package/dist/{App.9sryp183.js → App.x2d0ygt6.js} +2 -2
- package/dist/App.yt9p4nr3.js +20 -0
- package/dist/{App.wghtdzsk.js → App.zn4mw16t.js} +1 -1
- package/dist/ConnectionsPage.8r96ryw7.js +3 -0
- package/dist/McpPage.3cwh0gnd.js +3 -0
- package/dist/SettingsPage.ykgdh5ev.js +3 -0
- package/dist/SkillsPage.4np1s65b.js +3 -0
- package/dist/TasksPage.4g08t7p6.js +3 -0
- package/dist/TelemetryPage.72w9pwcp.js +3 -0
- package/dist/TestsPage.z4fk3r7r.js +3 -0
- package/dist/ThreadsPage.63tcajeh.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/crypto.ts +25 -4
- package/src/db.ts +24 -1
- package/src/mcp-platform.ts +273 -44
- package/src/providers.ts +125 -5
- package/src/routes/api/agent-utils.ts +105 -8
- package/src/routes/api/providers.ts +64 -0
- package/src/routes/api/telemetry.ts +0 -7
- package/src/routes/share.ts +3 -2
- package/src/server.ts +53 -7
- package/src/test-runner.ts +1 -1
- package/src/web/App.tsx +37 -22
- package/src/web/components/agents/AgentCard.tsx +12 -9
- package/src/web/components/agents/AgentPanel.tsx +126 -7
- package/src/web/components/agents/AgentsView.tsx +30 -8
- package/src/web/components/agents/CreateAgentModal.tsx +155 -5
- package/src/web/components/dashboard/Dashboard.tsx +9 -7
- package/src/web/components/layout/Sidebar.tsx +43 -32
- package/src/web/components/meta-agent/MetaAgent.tsx +6 -2
- package/src/web/components/settings/SettingsPage.tsx +172 -43
- package/src/web/components/telemetry/TelemetryPage.tsx +54 -46
- package/src/web/components/tests/TestsPage.tsx +91 -76
- package/src/web/context/TelemetryContext.tsx +4 -1
- package/src/web/context/UIModeContext.tsx +49 -0
- package/src/web/context/index.ts +3 -0
- package/src/web/types.ts +67 -3
- package/dist/ActivityPage.sw9p594m.js +0 -3
- package/dist/ApiDocsPage.90e03bz7.js +0 -4
- package/dist/App.3vnrera5.js +0 -4
- package/dist/App.94x6mh7f.js +0 -20
- package/dist/App.9t1zc5r7.js +0 -53
- package/dist/App.p7jjw1zf.js +0 -4
- package/dist/App.pfbdzrhh.js +0 -4
- package/dist/App.pse0pzar.js +0 -4
- package/dist/App.r43t58w6.js +0 -221
- package/dist/App.stgng5bx.js +0 -13
- package/dist/App.tm3k7h4b.js +0 -4
- package/dist/App.vkg121c6.js +0 -4
- package/dist/App.xva0tfzh.js +0 -4
- package/dist/App.ysxy7akk.js +0 -61
- package/dist/App.yzkh4gq2.js +0 -4
- package/dist/ConnectionsPage.q5f9fd37.js +0 -3
- package/dist/McpPage.f3ccrezb.js +0 -3
- package/dist/SettingsPage.zmzm1pp6.js +0 -3
- package/dist/SkillsPage.whxnez67.js +0 -3
- package/dist/TasksPage.zp4jfevw.js +0 -3
- package/dist/TelemetryPage.an0ky78c.js +0 -3
- package/dist/TestsPage.18krj0d1.js +0 -3
- package/dist/ThreadsPage.nnphgy98.js +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useCallback, createContext, useContext, type ReactNode } from "react";
|
|
2
2
|
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
-
import { useAuth, useProjects, useTriggerRefresh, useTheme } from "../../context";
|
|
3
|
+
import { useAuth, useProjects, useTriggerRefresh, useTheme, useUIMode } from "../../context";
|
|
4
4
|
|
|
5
5
|
interface MetaAgentStatus {
|
|
6
6
|
enabled: boolean;
|
|
@@ -129,6 +129,7 @@ export function MetaAgentButton() {
|
|
|
129
129
|
// Chat panel component - renders as a right-side drawer
|
|
130
130
|
export function MetaAgentPanel() {
|
|
131
131
|
const { theme } = useTheme();
|
|
132
|
+
const { t } = useUIMode();
|
|
132
133
|
const ctx = useMetaAgent();
|
|
133
134
|
const { currentProjectId, currentProject } = useProjects();
|
|
134
135
|
const triggerRefresh = useTriggerRefresh();
|
|
@@ -191,7 +192,10 @@ export function MetaAgentPanel() {
|
|
|
191
192
|
<AssistantIcon className="w-12 h-12 text-[var(--color-border-light)] mb-4" />
|
|
192
193
|
<h3 className="font-medium mb-2">Apteva Assistant</h3>
|
|
193
194
|
<p className="text-sm text-[var(--color-text-muted)] mb-6">
|
|
194
|
-
|
|
195
|
+
{t(
|
|
196
|
+
"I can help you navigate Apteva, create agents, set up MCP servers, and more.",
|
|
197
|
+
"I can help you hire employees, assign them work, and manage your team."
|
|
198
|
+
)}
|
|
195
199
|
</p>
|
|
196
200
|
{error && (
|
|
197
201
|
<p className="text-sm text-red-400 mb-4">{error}</p>
|
|
@@ -2,16 +2,20 @@ import React, { useState, useEffect } from "react";
|
|
|
2
2
|
import { CheckIcon, CloseIcon, PlusIcon } from "../common/Icons";
|
|
3
3
|
import { Modal, useConfirm } from "../common/Modal";
|
|
4
4
|
import { Select } from "../common/Select";
|
|
5
|
-
import { useProjects, useAuth, useTheme, type Project } from "../../context";
|
|
5
|
+
import { useProjects, useAuth, useTheme, useUIMode, type Project } from "../../context";
|
|
6
6
|
import type { ThemeMode, ThemeStyle } from "../../themes";
|
|
7
7
|
import type { Provider } from "../../types";
|
|
8
|
+
import type { UIMode } from "../../context";
|
|
8
9
|
|
|
9
10
|
type SettingsTab = "general" | "providers" | "projects" | "channels" | "api-keys" | "account" | "updates" | "data" | "assistant";
|
|
10
11
|
|
|
11
12
|
export function SettingsPage() {
|
|
12
13
|
const { projectsEnabled, metaAgentEnabled } = useProjects();
|
|
14
|
+
const { isBusiness } = useUIMode();
|
|
13
15
|
const [activeTab, setActiveTab] = useState<SettingsTab>("general");
|
|
14
16
|
|
|
17
|
+
const hiddenInBusiness: SettingsTab[] = ["providers", "api-keys", "data"];
|
|
18
|
+
|
|
15
19
|
const tabs: { key: SettingsTab; label: string }[] = [
|
|
16
20
|
{ key: "general", label: "General" },
|
|
17
21
|
{ key: "providers", label: "Providers" },
|
|
@@ -22,7 +26,7 @@ export function SettingsPage() {
|
|
|
22
26
|
{ key: "account", label: "Account" },
|
|
23
27
|
{ key: "updates", label: "Updates" },
|
|
24
28
|
{ key: "data", label: "Data" },
|
|
25
|
-
];
|
|
29
|
+
].filter(tab => !isBusiness || !hiddenInBusiness.includes(tab.key));
|
|
26
30
|
|
|
27
31
|
return (
|
|
28
32
|
<div className="flex-1 flex flex-col md:flex-row overflow-hidden">
|
|
@@ -102,6 +106,7 @@ function SettingsNavItem({
|
|
|
102
106
|
function GeneralSettings() {
|
|
103
107
|
const { authFetch } = useAuth();
|
|
104
108
|
const { mode, style, setMode, setStyle } = useTheme();
|
|
109
|
+
const { mode: uiMode, setMode: setUIMode } = useUIMode();
|
|
105
110
|
const [instanceUrl, setInstanceUrl] = useState("");
|
|
106
111
|
const [loading, setLoading] = useState(true);
|
|
107
112
|
const [saving, setSaving] = useState(false);
|
|
@@ -220,6 +225,39 @@ function GeneralSettings() {
|
|
|
220
225
|
</div>
|
|
221
226
|
</div>
|
|
222
227
|
|
|
228
|
+
{/* UI Mode */}
|
|
229
|
+
<div className="bg-[var(--color-surface)] card p-4 mb-4">
|
|
230
|
+
<h3 className="font-medium mb-2">UI Mode</h3>
|
|
231
|
+
<p className="text-sm text-[var(--color-text-muted)] mb-4">Switch between developer and business views.</p>
|
|
232
|
+
<div className="flex gap-3">
|
|
233
|
+
{([
|
|
234
|
+
{ value: "developer" as UIMode, label: "Developer", description: "Full control over agents, providers, MCP, and configuration" },
|
|
235
|
+
{ value: "business" as UIMode, label: "Business", description: "Simplified view focused on employees and conversations" },
|
|
236
|
+
]).map(opt => (
|
|
237
|
+
<button
|
|
238
|
+
key={opt.value}
|
|
239
|
+
onClick={() => setUIMode(opt.value)}
|
|
240
|
+
className={`flex-1 max-w-[240px] px-4 py-3 border text-left transition ${
|
|
241
|
+
uiMode === opt.value
|
|
242
|
+
? "border-[var(--color-accent)] bg-[var(--color-accent-10)]"
|
|
243
|
+
: "border-[var(--color-border-light)] bg-[var(--color-bg)] hover:border-[var(--color-scrollbar)]"
|
|
244
|
+
}`}
|
|
245
|
+
style={{ borderRadius: "var(--radius-card)" }}
|
|
246
|
+
>
|
|
247
|
+
<div className="flex items-center gap-2 mb-1">
|
|
248
|
+
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
|
|
249
|
+
uiMode === opt.value ? "border-[var(--color-accent)]" : "border-[var(--color-scrollbar)]"
|
|
250
|
+
}`}>
|
|
251
|
+
{uiMode === opt.value && <div className="w-2 h-2 rounded-full bg-[var(--color-accent)]" />}
|
|
252
|
+
</div>
|
|
253
|
+
<span className="text-sm font-medium">{opt.label}</span>
|
|
254
|
+
</div>
|
|
255
|
+
<p className="text-xs text-[var(--color-text-muted)] ml-6">{opt.description}</p>
|
|
256
|
+
</button>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
223
261
|
<div className="bg-[var(--color-surface)] card p-4">
|
|
224
262
|
<h3 className="font-medium mb-2">Instance URL</h3>
|
|
225
263
|
<p className="text-sm text-[var(--color-text-muted)] mb-4">
|
|
@@ -354,9 +392,13 @@ function ProvidersSettings() {
|
|
|
354
392
|
};
|
|
355
393
|
|
|
356
394
|
const llmProviders = providers.filter(p => p.type === "llm");
|
|
395
|
+
const cloudVoiceProviders = providers.filter(p => p.type === "voice" && !p.isLocal);
|
|
396
|
+
const localVoiceProviders = providers.filter(p => p.type === "voice" && p.isLocal);
|
|
397
|
+
const voiceProviders = providers.filter(p => p.type === "voice");
|
|
357
398
|
const integrations = providers.filter(p => p.type === "integration");
|
|
358
399
|
const browserProviders = providers.filter(p => p.type === "browser");
|
|
359
400
|
const llmConfiguredCount = llmProviders.filter(p => p.hasKey).length;
|
|
401
|
+
const voiceConfiguredCount = voiceProviders.filter(p => p.hasKey).length;
|
|
360
402
|
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
361
403
|
const browserConfiguredCount = browserProviders.filter(p => p.hasKey).length;
|
|
362
404
|
|
|
@@ -426,6 +468,79 @@ function ProvidersSettings() {
|
|
|
426
468
|
</div>
|
|
427
469
|
</div>
|
|
428
470
|
|
|
471
|
+
{/* Voice Providers Section */}
|
|
472
|
+
<div>
|
|
473
|
+
<div className="mb-6">
|
|
474
|
+
<h2 className="text-xl font-semibold mb-1">Voice Providers</h2>
|
|
475
|
+
<p className="text-[var(--color-text-muted)]">
|
|
476
|
+
Configure voice providers for real-time voice conversations. {voiceConfiguredCount} of {voiceProviders.length} configured.
|
|
477
|
+
</p>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{/* Cloud Voice Providers */}
|
|
481
|
+
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 uppercase tracking-wider">Cloud</h3>
|
|
482
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mb-6">
|
|
483
|
+
{cloudVoiceProviders.map(provider => (
|
|
484
|
+
<IntegrationKeyCard
|
|
485
|
+
key={provider.id}
|
|
486
|
+
provider={provider}
|
|
487
|
+
isEditing={selectedProvider === provider.id}
|
|
488
|
+
apiKey={apiKey}
|
|
489
|
+
saving={saving}
|
|
490
|
+
testing={testing}
|
|
491
|
+
error={selectedProvider === provider.id ? error : null}
|
|
492
|
+
success={selectedProvider === provider.id ? success : null}
|
|
493
|
+
onStartEdit={() => {
|
|
494
|
+
setSelectedProvider(provider.id);
|
|
495
|
+
setError(null);
|
|
496
|
+
setSuccess(null);
|
|
497
|
+
}}
|
|
498
|
+
onCancelEdit={() => {
|
|
499
|
+
setSelectedProvider(null);
|
|
500
|
+
setApiKey("");
|
|
501
|
+
setError(null);
|
|
502
|
+
}}
|
|
503
|
+
onApiKeyChange={setApiKey}
|
|
504
|
+
onSave={saveKey}
|
|
505
|
+
onDelete={() => deleteKey(provider.id)}
|
|
506
|
+
projectsEnabled={projectsEnabled}
|
|
507
|
+
projects={projects}
|
|
508
|
+
onRefresh={fetchProviders}
|
|
509
|
+
/>
|
|
510
|
+
))}
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
{/* Local Voice Providers */}
|
|
514
|
+
<h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 uppercase tracking-wider">Local</h3>
|
|
515
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
516
|
+
{localVoiceProviders.map(provider => (
|
|
517
|
+
<ProviderKeyCard
|
|
518
|
+
key={provider.id}
|
|
519
|
+
provider={provider}
|
|
520
|
+
isEditing={selectedProvider === provider.id}
|
|
521
|
+
apiKey={apiKey}
|
|
522
|
+
saving={saving}
|
|
523
|
+
testing={testing}
|
|
524
|
+
error={selectedProvider === provider.id ? error : null}
|
|
525
|
+
success={selectedProvider === provider.id ? success : null}
|
|
526
|
+
onStartEdit={() => {
|
|
527
|
+
setSelectedProvider(provider.id);
|
|
528
|
+
setError(null);
|
|
529
|
+
setSuccess(null);
|
|
530
|
+
}}
|
|
531
|
+
onCancelEdit={() => {
|
|
532
|
+
setSelectedProvider(null);
|
|
533
|
+
setApiKey("");
|
|
534
|
+
setError(null);
|
|
535
|
+
}}
|
|
536
|
+
onApiKeyChange={setApiKey}
|
|
537
|
+
onSave={saveKey}
|
|
538
|
+
onDelete={() => deleteKey(provider.id)}
|
|
539
|
+
/>
|
|
540
|
+
))}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
429
544
|
{/* MCP Integrations Section */}
|
|
430
545
|
<div>
|
|
431
546
|
<div className="mb-6">
|
|
@@ -1030,26 +1145,28 @@ function ProviderKeyCard({
|
|
|
1030
1145
|
const { authFetch: providerAuthFetch } = useAuth();
|
|
1031
1146
|
const isOllama = provider.id === "ollama";
|
|
1032
1147
|
const isCDP = provider.id === "cdp";
|
|
1033
|
-
const
|
|
1148
|
+
const isLocal = provider.isLocal || false;
|
|
1149
|
+
const isUrlBased = isLocal || isCDP;
|
|
1034
1150
|
const isBrowser = provider.type === "browser";
|
|
1035
1151
|
const isMultiField = provider.id === "browserbase";
|
|
1036
|
-
const
|
|
1152
|
+
const voiceSubtype = (provider as any).voiceSubtype as string | undefined;
|
|
1153
|
+
const [localStatus, setLocalStatus] = React.useState<{ connected: boolean; modelCount?: number; isDocker?: boolean } | null>(null);
|
|
1037
1154
|
const [installing, setInstalling] = React.useState(false);
|
|
1038
1155
|
const [installResult, setInstallResult] = React.useState<{ success: boolean; message: string } | null>(null);
|
|
1039
1156
|
|
|
1040
|
-
// Check
|
|
1041
|
-
const
|
|
1042
|
-
providerAuthFetch(
|
|
1157
|
+
// Check status for local providers (Ollama + local voice providers)
|
|
1158
|
+
const checkLocalStatus = React.useCallback(() => {
|
|
1159
|
+
providerAuthFetch(`/api/providers/${provider.id}/status`)
|
|
1043
1160
|
.then(res => res.json())
|
|
1044
|
-
.then(data =>
|
|
1045
|
-
.catch(() =>
|
|
1046
|
-
}, [providerAuthFetch]);
|
|
1161
|
+
.then(data => setLocalStatus({ connected: data.connected, modelCount: data.modelCount, isDocker: data.isDocker }))
|
|
1162
|
+
.catch(() => setLocalStatus({ connected: false }));
|
|
1163
|
+
}, [providerAuthFetch, provider.id]);
|
|
1047
1164
|
|
|
1048
1165
|
React.useEffect(() => {
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1166
|
+
if (isLocal) {
|
|
1167
|
+
checkLocalStatus();
|
|
1051
1168
|
}
|
|
1052
|
-
}, [
|
|
1169
|
+
}, [isLocal, provider.hasKey, checkLocalStatus]);
|
|
1053
1170
|
|
|
1054
1171
|
const handleInstallOllama = async () => {
|
|
1055
1172
|
setInstalling(true);
|
|
@@ -1059,8 +1176,7 @@ function ProviderKeyCard({
|
|
|
1059
1176
|
const data = await res.json();
|
|
1060
1177
|
if (data.success) {
|
|
1061
1178
|
setInstallResult({ success: true, message: data.message });
|
|
1062
|
-
|
|
1063
|
-
checkOllamaStatus();
|
|
1179
|
+
checkLocalStatus();
|
|
1064
1180
|
} else {
|
|
1065
1181
|
setInstallResult({ success: false, message: data.error || "Installation failed" });
|
|
1066
1182
|
}
|
|
@@ -1079,26 +1195,33 @@ function ProviderKeyCard({
|
|
|
1079
1195
|
<div className="min-w-0">
|
|
1080
1196
|
<h3 className="font-medium">{provider.name}</h3>
|
|
1081
1197
|
<p className="text-sm text-[var(--color-text-muted)] truncate">
|
|
1082
|
-
{
|
|
1083
|
-
?
|
|
1084
|
-
:
|
|
1085
|
-
?
|
|
1086
|
-
:
|
|
1087
|
-
? "
|
|
1088
|
-
:
|
|
1198
|
+
{provider.description
|
|
1199
|
+
? provider.description
|
|
1200
|
+
: isBrowser
|
|
1201
|
+
? "Browser automation"
|
|
1202
|
+
: provider.type === "integration"
|
|
1203
|
+
? "MCP integration"
|
|
1204
|
+
: isLocal
|
|
1205
|
+
? "Run locally"
|
|
1206
|
+
: `${provider.models.length} models`}
|
|
1089
1207
|
</p>
|
|
1208
|
+
{voiceSubtype && (
|
|
1209
|
+
<span className="text-[10px] uppercase tracking-wider text-[var(--color-text-muted)] bg-[var(--color-surface-raised)] px-1.5 py-0.5 rounded mt-1 inline-block">
|
|
1210
|
+
{voiceSubtype === "both" ? "STT + TTS" : voiceSubtype === "stt" ? "STT" : "TTS"}
|
|
1211
|
+
</span>
|
|
1212
|
+
)}
|
|
1090
1213
|
</div>
|
|
1091
1214
|
{provider.hasKey ? (
|
|
1092
1215
|
<span className={`text-xs flex items-center gap-1 px-2 py-1 rounded whitespace-nowrap flex-shrink-0 ${
|
|
1093
|
-
|
|
1094
|
-
?
|
|
1216
|
+
isLocal && localStatus
|
|
1217
|
+
? localStatus.connected
|
|
1095
1218
|
? "text-green-400 bg-green-500/10"
|
|
1096
1219
|
: "text-yellow-400 bg-yellow-500/10"
|
|
1097
1220
|
: "text-green-400 bg-green-500/10"
|
|
1098
1221
|
}`}>
|
|
1099
|
-
{
|
|
1100
|
-
|
|
1101
|
-
<><CheckIcon className="w-3 h-3" />{
|
|
1222
|
+
{isLocal && localStatus ? (
|
|
1223
|
+
localStatus.connected ? (
|
|
1224
|
+
<><CheckIcon className="w-3 h-3" />{localStatus.modelCount ? `${localStatus.modelCount} models` : "Connected"}</>
|
|
1102
1225
|
) : (
|
|
1103
1226
|
<>Not running</>
|
|
1104
1227
|
)
|
|
@@ -1147,8 +1270,8 @@ function ProviderKeyCard({
|
|
|
1147
1270
|
type={isUrlBased ? "text" : "password"}
|
|
1148
1271
|
value={apiKey}
|
|
1149
1272
|
onChange={e => onApiKeyChange(e.target.value)}
|
|
1150
|
-
placeholder={
|
|
1151
|
-
? "http://localhost:
|
|
1273
|
+
placeholder={isLocal
|
|
1274
|
+
? (provider.defaultBaseUrl || "http://localhost:8080")
|
|
1152
1275
|
: isCDP ? "ws://localhost:9222"
|
|
1153
1276
|
: provider.hasKey ? "Enter new API key..." : "Enter API key..."}
|
|
1154
1277
|
autoFocus
|
|
@@ -1159,7 +1282,7 @@ function ProviderKeyCard({
|
|
|
1159
1282
|
<p className="text-xs text-[var(--color-text-muted)]">
|
|
1160
1283
|
{isCDP
|
|
1161
1284
|
? "Enter the CDP URL of your browser (e.g., ws://localhost:9222)"
|
|
1162
|
-
:
|
|
1285
|
+
: `Enter the server URL. Default is ${provider.defaultBaseUrl || "http://localhost:8080"}`}
|
|
1163
1286
|
</p>
|
|
1164
1287
|
)}
|
|
1165
1288
|
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
@@ -1182,15 +1305,21 @@ function ProviderKeyCard({
|
|
|
1182
1305
|
</div>
|
|
1183
1306
|
) : provider.hasKey ? (
|
|
1184
1307
|
<div>
|
|
1185
|
-
{
|
|
1308
|
+
{isLocal && localStatus && !localStatus.connected && (
|
|
1186
1309
|
<div className="mb-3">
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1310
|
+
{isOllama && !localStatus.isDocker ? (
|
|
1311
|
+
<button
|
|
1312
|
+
onClick={handleInstallOllama}
|
|
1313
|
+
disabled={installing}
|
|
1314
|
+
className="w-full px-3 py-1.5 bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 rounded text-sm font-medium transition disabled:opacity-50 disabled:cursor-wait"
|
|
1315
|
+
>
|
|
1316
|
+
{installing ? "Starting Ollama..." : "Start Ollama"}
|
|
1317
|
+
</button>
|
|
1318
|
+
) : (
|
|
1319
|
+
<p className="text-xs text-yellow-400/80">
|
|
1320
|
+
Service not reachable. Make sure it's running at the configured URL.
|
|
1321
|
+
</p>
|
|
1322
|
+
)}
|
|
1194
1323
|
{installResult && (
|
|
1195
1324
|
<p className={`text-xs mt-1.5 ${installResult.success ? "text-green-400" : "text-red-400"}`}>
|
|
1196
1325
|
{installResult.message}
|
|
@@ -1206,7 +1335,7 @@ function ProviderKeyCard({
|
|
|
1206
1335
|
rel="noopener noreferrer"
|
|
1207
1336
|
className="text-sm text-[#3b82f6] hover:underline"
|
|
1208
1337
|
>
|
|
1209
|
-
{
|
|
1338
|
+
{isLocal ? "Setup guide" : "View docs"}
|
|
1210
1339
|
</a>
|
|
1211
1340
|
) : (
|
|
1212
1341
|
<span />
|
|
@@ -1229,14 +1358,14 @@ function ProviderKeyCard({
|
|
|
1229
1358
|
</div>
|
|
1230
1359
|
) : (
|
|
1231
1360
|
<div>
|
|
1232
|
-
{isOllama && !
|
|
1361
|
+
{isOllama && !localStatus?.isDocker && (
|
|
1233
1362
|
<div className="mb-3">
|
|
1234
1363
|
<button
|
|
1235
1364
|
onClick={handleInstallOllama}
|
|
1236
1365
|
disabled={installing}
|
|
1237
1366
|
className="w-full px-3 py-2 bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30 rounded text-sm font-medium transition disabled:opacity-50 disabled:cursor-wait"
|
|
1238
1367
|
>
|
|
1239
|
-
{installing ? "Installing Ollama..." :
|
|
1368
|
+
{installing ? "Installing Ollama..." : localStatus?.connected ? "Ollama Running" : "Install Ollama"}
|
|
1240
1369
|
</button>
|
|
1241
1370
|
{installResult && (
|
|
1242
1371
|
<p className={`text-xs mt-1.5 ${installResult.success ? "text-green-400" : "text-red-400"}`}>
|
|
@@ -1253,7 +1382,7 @@ function ProviderKeyCard({
|
|
|
1253
1382
|
rel="noopener noreferrer"
|
|
1254
1383
|
className="text-sm text-[#3b82f6] hover:underline"
|
|
1255
1384
|
>
|
|
1256
|
-
{
|
|
1385
|
+
{isLocal ? "Setup guide" : isBrowser ? "View docs" : "Get API key"}
|
|
1257
1386
|
</a>
|
|
1258
1387
|
) : (
|
|
1259
1388
|
<span />
|
|
@@ -1506,7 +1635,7 @@ function IntegrationKeyCard({
|
|
|
1506
1635
|
const isUrlBased = provider.isLocal;
|
|
1507
1636
|
const inputType = isUrlBased ? "text" : "password";
|
|
1508
1637
|
const inputPlaceholder = isUrlBased
|
|
1509
|
-
? (provider.
|
|
1638
|
+
? (provider.defaultBaseUrl || "http://localhost:8080")
|
|
1510
1639
|
: "Enter API key...";
|
|
1511
1640
|
|
|
1512
1641
|
// Enhanced view with project support
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
2
2
|
import { Select } from "../common/Select";
|
|
3
|
-
import { useTelemetryContext, useProjects, useAuth, type TelemetryEvent } from "../../context";
|
|
3
|
+
import { useTelemetryContext, useProjects, useAuth, useUIMode, type TelemetryEvent } from "../../context";
|
|
4
4
|
import {
|
|
5
5
|
AreaChart, Area, BarChart, Bar,
|
|
6
6
|
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
|
|
@@ -97,6 +97,7 @@ export function TelemetryPage() {
|
|
|
97
97
|
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
98
98
|
const { currentProjectId, currentProject, costTrackingEnabled, projectsEnabled, projects } = useProjects();
|
|
99
99
|
const { authFetch } = useAuth();
|
|
100
|
+
const { isDev, isBusiness, t } = useUIMode();
|
|
100
101
|
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
101
102
|
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
102
103
|
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
@@ -143,8 +144,8 @@ export function TelemetryPage() {
|
|
|
143
144
|
}
|
|
144
145
|
};
|
|
145
146
|
|
|
146
|
-
// Track
|
|
147
|
-
const
|
|
147
|
+
// Track the timestamp of the last fetch — only count realtime events arriving after this
|
|
148
|
+
const fetchTimestampRef = useRef<number>(0);
|
|
148
149
|
|
|
149
150
|
// Track which events are "new" (for animation) - stores event IDs with their arrival time
|
|
150
151
|
const [newEventIds, setNewEventIds] = useState<Set<string>>(new Set());
|
|
@@ -220,8 +221,8 @@ export function TelemetryPage() {
|
|
|
220
221
|
const events = eventsData.events || [];
|
|
221
222
|
setHistoricalEvents(events);
|
|
222
223
|
|
|
223
|
-
//
|
|
224
|
-
|
|
224
|
+
// Record fetch time — realtime events before this are already in DB aggregations
|
|
225
|
+
fetchTimestampRef.current = Date.now();
|
|
225
226
|
|
|
226
227
|
// Fetch usage by agent
|
|
227
228
|
const usageParams = new URLSearchParams();
|
|
@@ -283,7 +284,8 @@ export function TelemetryPage() {
|
|
|
283
284
|
let deltaCost = 0;
|
|
284
285
|
|
|
285
286
|
for (const event of realtimeEvents) {
|
|
286
|
-
|
|
287
|
+
// Only count events received via SSE AFTER the last fetch (DB already has everything before)
|
|
288
|
+
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
287
289
|
deltaEvents++;
|
|
288
290
|
const eventStats = extractEventStats(event);
|
|
289
291
|
deltaLlmCalls += eventStats.llm_calls;
|
|
@@ -320,9 +322,9 @@ export function TelemetryPage() {
|
|
|
320
322
|
usageMap.set(u.agent_id, { ...u });
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
// Add deltas from real-time events
|
|
325
|
+
// Add deltas from real-time events received after the last fetch
|
|
324
326
|
for (const event of realtimeEvents) {
|
|
325
|
-
if (
|
|
327
|
+
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
326
328
|
const eventStats = extractEventStats(event);
|
|
327
329
|
const existing = usageMap.get(event.agent_id);
|
|
328
330
|
if (existing) {
|
|
@@ -397,9 +399,9 @@ export function TelemetryPage() {
|
|
|
397
399
|
buckets.set(d.date, { ...d });
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
// Add deltas from real-time events
|
|
402
|
+
// Add deltas from real-time events received after the last fetch
|
|
401
403
|
for (const event of realtimeEvents) {
|
|
402
|
-
if (
|
|
404
|
+
if (event._receivedAt && event._receivedAt > fetchTimestampRef.current) {
|
|
403
405
|
const ts = new Date(event.timestamp);
|
|
404
406
|
const key = useDaily
|
|
405
407
|
? `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")}`
|
|
@@ -617,19 +619,23 @@ export function TelemetryPage() {
|
|
|
617
619
|
<div className="flex flex-wrap gap-4 mb-6">
|
|
618
620
|
<StatCard label="Events" value={formatNumber(stats.total_events)} />
|
|
619
621
|
<StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
|
|
620
|
-
<StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />
|
|
622
|
+
{isDev && <StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />}
|
|
621
623
|
<StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
|
|
622
|
-
|
|
623
|
-
<StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
|
|
624
|
-
{(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
|
|
624
|
+
{isDev && (
|
|
625
625
|
<>
|
|
626
|
-
<StatCard label="
|
|
627
|
-
<StatCard label="
|
|
626
|
+
<StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
|
|
627
|
+
<StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
|
|
628
|
+
{(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
|
|
629
|
+
<>
|
|
630
|
+
<StatCard label="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
|
|
631
|
+
<StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
|
|
632
|
+
</>
|
|
633
|
+
)}
|
|
634
|
+
{stats.total_reasoning_tokens > 0 && (
|
|
635
|
+
<StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
|
|
636
|
+
)}
|
|
628
637
|
</>
|
|
629
638
|
)}
|
|
630
|
-
{stats.total_reasoning_tokens > 0 && (
|
|
631
|
-
<StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
|
|
632
|
-
)}
|
|
633
639
|
{costTrackingEnabled && (
|
|
634
640
|
<StatCard label="Total Cost" value={`$${stats.total_cost.toFixed(4)}`} color="orange" />
|
|
635
641
|
)}
|
|
@@ -720,7 +726,9 @@ export function TelemetryPage() {
|
|
|
720
726
|
const stackedData = chartData.map(d => {
|
|
721
727
|
const cacheRead = d.cache_read_tokens || 0;
|
|
722
728
|
const cacheWrite = d.cache_creation_tokens || 0;
|
|
723
|
-
|
|
729
|
+
// Anthropic: input_tokens includes cache_read but NOT cache_creation
|
|
730
|
+
// So regular = input_tokens - cache_read (don't subtract cache_write)
|
|
731
|
+
const regularInput = Math.max(0, d.input_tokens - cacheRead);
|
|
724
732
|
const reasoning = d.reasoning_tokens || 0;
|
|
725
733
|
const regularOutput = Math.max(0, d.output_tokens - reasoning);
|
|
726
734
|
return {
|
|
@@ -827,19 +835,19 @@ export function TelemetryPage() {
|
|
|
827
835
|
|
|
828
836
|
return (
|
|
829
837
|
<div className="mb-6">
|
|
830
|
-
<h2 className="text-lg font-medium mb-3">Usage by Agent</h2>
|
|
838
|
+
<h2 className="text-lg font-medium mb-3">{t("Usage by Agent", "Usage by Employee")}</h2>
|
|
831
839
|
<div className="bg-[var(--color-surface)] card overflow-hidden">
|
|
832
840
|
<table className="w-full text-sm">
|
|
833
841
|
<thead>
|
|
834
842
|
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
835
|
-
<SortHeader label="Agent" field="agent" align="left" />
|
|
843
|
+
<SortHeader label={t("Agent", "Employee")} field="agent" align="left" />
|
|
836
844
|
<SortHeader label="LLM Calls" field="llm_calls" />
|
|
837
|
-
<SortHeader label="Tool Calls" field="tool_calls" />
|
|
838
|
-
<SortHeader label="Input Tokens" field="input_tokens" />
|
|
839
|
-
<SortHeader label="Output Tokens" field="output_tokens" />
|
|
840
|
-
{hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
841
|
-
{hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
842
|
-
{hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
845
|
+
{isDev && <SortHeader label="Tool Calls" field="tool_calls" />}
|
|
846
|
+
{isDev && <SortHeader label="Input Tokens" field="input_tokens" />}
|
|
847
|
+
{isDev && <SortHeader label="Output Tokens" field="output_tokens" />}
|
|
848
|
+
{isDev && hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
849
|
+
{isDev && hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
850
|
+
{isDev && hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
843
851
|
<SortHeader label="Errors" field="errors" />
|
|
844
852
|
{costTrackingEnabled && <SortHeader label="Est. Cost" field="cost" />}
|
|
845
853
|
</tr>
|
|
@@ -849,16 +857,16 @@ export function TelemetryPage() {
|
|
|
849
857
|
<tr key={u.agent_id} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
|
|
850
858
|
<td className="p-3 font-medium">{getAgentName(u.agent_id)}</td>
|
|
851
859
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
|
|
852
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
|
|
853
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
|
|
854
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
|
|
855
|
-
{hasCacheTokens && (
|
|
860
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>}
|
|
861
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>}
|
|
862
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>}
|
|
863
|
+
{isDev && hasCacheTokens && (
|
|
856
864
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
857
865
|
)}
|
|
858
|
-
{hasCacheTokens && (
|
|
866
|
+
{isDev && hasCacheTokens && (
|
|
859
867
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
860
868
|
)}
|
|
861
|
-
{hasReasoningTokens && (
|
|
869
|
+
{isDev && hasReasoningTokens && (
|
|
862
870
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
863
871
|
)}
|
|
864
872
|
<td className="p-3 text-right">
|
|
@@ -921,12 +929,12 @@ export function TelemetryPage() {
|
|
|
921
929
|
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
|
|
922
930
|
<PSortHeader label="Project" field="project" align="left" />
|
|
923
931
|
<PSortHeader label="LLM Calls" field="llm_calls" />
|
|
924
|
-
<PSortHeader label="Tool Calls" field="tool_calls" />
|
|
925
|
-
<PSortHeader label="Input Tokens" field="input_tokens" />
|
|
926
|
-
<PSortHeader label="Output Tokens" field="output_tokens" />
|
|
927
|
-
{hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
928
|
-
{hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
929
|
-
{hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
932
|
+
{isDev && <PSortHeader label="Tool Calls" field="tool_calls" />}
|
|
933
|
+
{isDev && <PSortHeader label="Input Tokens" field="input_tokens" />}
|
|
934
|
+
{isDev && <PSortHeader label="Output Tokens" field="output_tokens" />}
|
|
935
|
+
{isDev && hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
|
|
936
|
+
{isDev && hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
|
|
937
|
+
{isDev && hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
|
|
930
938
|
<PSortHeader label="Errors" field="errors" />
|
|
931
939
|
{costTrackingEnabled && <PSortHeader label="Est. Cost" field="cost" />}
|
|
932
940
|
</tr>
|
|
@@ -943,16 +951,16 @@ export function TelemetryPage() {
|
|
|
943
951
|
</span>
|
|
944
952
|
</td>
|
|
945
953
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
|
|
946
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
|
|
947
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
|
|
948
|
-
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
|
|
949
|
-
{hasProjCacheTokens && (
|
|
954
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>}
|
|
955
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>}
|
|
956
|
+
{isDev && <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>}
|
|
957
|
+
{isDev && hasProjCacheTokens && (
|
|
950
958
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
|
|
951
959
|
)}
|
|
952
|
-
{hasProjCacheTokens && (
|
|
960
|
+
{isDev && hasProjCacheTokens && (
|
|
953
961
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
|
|
954
962
|
)}
|
|
955
|
-
{hasProjReasoningTokens && (
|
|
963
|
+
{isDev && hasProjReasoningTokens && (
|
|
956
964
|
<td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
|
|
957
965
|
)}
|
|
958
966
|
<td className="p-3 text-right">
|