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.
Files changed (78) hide show
  1. package/dist/ActivityPage.kxzzb4yc.js +3 -0
  2. package/dist/ApiDocsPage.zq998hbm.js +4 -0
  3. package/dist/App.55rea8mn.js +61 -0
  4. package/dist/App.5ywb23z4.js +53 -0
  5. package/dist/App.6thds120.js +4 -0
  6. package/dist/{App.jhb45d7r.js → App.9tctxzqm.js} +3 -3
  7. package/dist/App.a8r8ttaz.js +4 -0
  8. package/dist/App.agsv5bje.js +4 -0
  9. package/dist/App.cepapqmx.js +4 -0
  10. package/dist/App.dp041gb3.js +221 -0
  11. package/dist/App.fds72zb5.js +4 -0
  12. package/dist/App.fg9qj2dq.js +4 -0
  13. package/dist/App.ndfejbm9.js +4 -0
  14. package/dist/App.nxmfmq1h.js +13 -0
  15. package/dist/App.qdfyt8ba.js +4 -0
  16. package/dist/{App.9sryp183.js → App.x2d0ygt6.js} +2 -2
  17. package/dist/App.yt9p4nr3.js +20 -0
  18. package/dist/{App.wghtdzsk.js → App.zn4mw16t.js} +1 -1
  19. package/dist/ConnectionsPage.8r96ryw7.js +3 -0
  20. package/dist/McpPage.3cwh0gnd.js +3 -0
  21. package/dist/SettingsPage.ykgdh5ev.js +3 -0
  22. package/dist/SkillsPage.4np1s65b.js +3 -0
  23. package/dist/TasksPage.4g08t7p6.js +3 -0
  24. package/dist/TelemetryPage.72w9pwcp.js +3 -0
  25. package/dist/TestsPage.z4fk3r7r.js +3 -0
  26. package/dist/ThreadsPage.63tcajeh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +2 -2
  31. package/src/crypto.ts +25 -4
  32. package/src/db.ts +24 -1
  33. package/src/mcp-platform.ts +273 -44
  34. package/src/providers.ts +125 -5
  35. package/src/routes/api/agent-utils.ts +105 -8
  36. package/src/routes/api/providers.ts +64 -0
  37. package/src/routes/api/telemetry.ts +0 -7
  38. package/src/routes/share.ts +3 -2
  39. package/src/server.ts +53 -7
  40. package/src/test-runner.ts +1 -1
  41. package/src/web/App.tsx +37 -22
  42. package/src/web/components/agents/AgentCard.tsx +12 -9
  43. package/src/web/components/agents/AgentPanel.tsx +126 -7
  44. package/src/web/components/agents/AgentsView.tsx +30 -8
  45. package/src/web/components/agents/CreateAgentModal.tsx +155 -5
  46. package/src/web/components/dashboard/Dashboard.tsx +9 -7
  47. package/src/web/components/layout/Sidebar.tsx +43 -32
  48. package/src/web/components/meta-agent/MetaAgent.tsx +6 -2
  49. package/src/web/components/settings/SettingsPage.tsx +172 -43
  50. package/src/web/components/telemetry/TelemetryPage.tsx +54 -46
  51. package/src/web/components/tests/TestsPage.tsx +91 -76
  52. package/src/web/context/TelemetryContext.tsx +4 -1
  53. package/src/web/context/UIModeContext.tsx +49 -0
  54. package/src/web/context/index.ts +3 -0
  55. package/src/web/types.ts +67 -3
  56. package/dist/ActivityPage.sw9p594m.js +0 -3
  57. package/dist/ApiDocsPage.90e03bz7.js +0 -4
  58. package/dist/App.3vnrera5.js +0 -4
  59. package/dist/App.94x6mh7f.js +0 -20
  60. package/dist/App.9t1zc5r7.js +0 -53
  61. package/dist/App.p7jjw1zf.js +0 -4
  62. package/dist/App.pfbdzrhh.js +0 -4
  63. package/dist/App.pse0pzar.js +0 -4
  64. package/dist/App.r43t58w6.js +0 -221
  65. package/dist/App.stgng5bx.js +0 -13
  66. package/dist/App.tm3k7h4b.js +0 -4
  67. package/dist/App.vkg121c6.js +0 -4
  68. package/dist/App.xva0tfzh.js +0 -4
  69. package/dist/App.ysxy7akk.js +0 -61
  70. package/dist/App.yzkh4gq2.js +0 -4
  71. package/dist/ConnectionsPage.q5f9fd37.js +0 -3
  72. package/dist/McpPage.f3ccrezb.js +0 -3
  73. package/dist/SettingsPage.zmzm1pp6.js +0 -3
  74. package/dist/SkillsPage.whxnez67.js +0 -3
  75. package/dist/TasksPage.zp4jfevw.js +0 -3
  76. package/dist/TelemetryPage.an0ky78c.js +0 -3
  77. package/dist/TestsPage.18krj0d1.js +0 -3
  78. 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
- I can help you navigate Apteva, create agents, set up MCP servers, and more.
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 isUrlBased = isOllama || isCDP;
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 [ollamaStatus, setOllamaStatus] = React.useState<{ connected: boolean; modelCount?: number; isDocker?: boolean } | null>(null);
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 Ollama status when configured or after install
1041
- const checkOllamaStatus = React.useCallback(() => {
1042
- providerAuthFetch("/api/providers/ollama/status")
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 => setOllamaStatus({ connected: data.connected, modelCount: data.modelCount, isDocker: data.isDocker }))
1045
- .catch(() => setOllamaStatus({ connected: false }));
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 (isOllama) {
1050
- checkOllamaStatus();
1166
+ if (isLocal) {
1167
+ checkLocalStatus();
1051
1168
  }
1052
- }, [isOllama, provider.hasKey, checkOllamaStatus]);
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
- // Auto-save the default URL and refresh status
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
- {isBrowser
1083
- ? (provider.description || "Browser automation")
1084
- : provider.type === "integration"
1085
- ? (provider.description || "MCP integration")
1086
- : isOllama
1087
- ? "Run models locally"
1088
- : `${provider.models.length} models`}
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
- isOllama && ollamaStatus
1094
- ? ollamaStatus.connected
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
- {isOllama && ollamaStatus ? (
1100
- ollamaStatus.connected ? (
1101
- <><CheckIcon className="w-3 h-3" />{ollamaStatus.modelCount} models</>
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={isOllama
1151
- ? "http://localhost:11434"
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
- : "Enter your Ollama server URL. Default is http://localhost:11434"}
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
- {isOllama && ollamaStatus && !ollamaStatus.connected && !ollamaStatus.isDocker && (
1308
+ {isLocal && localStatus && !localStatus.connected && (
1186
1309
  <div className="mb-3">
1187
- <button
1188
- onClick={handleInstallOllama}
1189
- disabled={installing}
1190
- 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"
1191
- >
1192
- {installing ? "Starting Ollama..." : "Start Ollama"}
1193
- </button>
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&apos;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
- {isOllama ? "Ollama docs" : "View docs"}
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 && !ollamaStatus?.isDocker && (
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..." : ollamaStatus?.connected ? "Ollama Running" : "Install 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
- {isOllama ? "Manual install" : isBrowser ? "View docs" : "Get API key"}
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.id === "cdp" ? "ws://localhost:9222" : "http://localhost:11434")
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 IDs that were in the fetched stats to avoid double-counting
147
- const countedEventIdsRef = useRef<Set<string>>(new Set());
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
- // Mark all fetched event IDs as counted (stats already include them)
224
- countedEventIdsRef.current = new Set(events.map((e: TelemetryEvent) => e.id));
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
- if (!countedEventIdsRef.current.has(event.id)) {
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 (!countedEventIdsRef.current.has(event.id)) {
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 not already counted
402
+ // Add deltas from real-time events received after the last fetch
401
403
  for (const event of realtimeEvents) {
402
- if (!countedEventIdsRef.current.has(event.id)) {
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
- <StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
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="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
627
- <StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
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
- const regularInput = Math.max(0, d.input_tokens - cacheRead - cacheWrite);
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">