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
@@ -4,10 +4,10 @@ import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, Re
4
4
  import { formatCron, formatRelativeTime, TrajectoryView } from "../tasks/TasksPage";
5
5
  import { Select } from "../common/Select";
6
6
  import { useConfirm } from "../common/Modal";
7
- import { useTelemetry, useTheme } from "../../context";
7
+ import { useTelemetry, useTheme, useUIMode } from "../../context";
8
8
  import { useAuth } from "../../context";
9
- import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, MultiAgentConfig, OperatorConfig, Task } from "../../types";
10
- import { getMultiAgentConfig, getOperatorConfig } from "../../types";
9
+ import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, MultiAgentConfig, OperatorConfig, RealtimeConfig, Task } from "../../types";
10
+ import { getMultiAgentConfig, getOperatorConfig, getRealtimeConfig, isRealtimeEnabled, REALTIME_PROVIDERS } from "../../types";
11
11
 
12
12
  type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
13
13
 
@@ -33,6 +33,7 @@ const FEATURE_CONFIG = [
33
33
 
34
34
  export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAgent, onDeleteAgent }: AgentPanelProps) {
35
35
  const [activeTab, setActiveTab] = useState<Tab>("chat");
36
+ const { isDev } = useUIMode();
36
37
 
37
38
  return (
38
39
  <div className="w-full h-full flex flex-col overflow-hidden bg-[var(--color-bg)] border-l border-[var(--color-border)]">
@@ -56,9 +57,11 @@ export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAg
56
57
  <TabButton active={activeTab === "files"} onClick={() => setActiveTab("files")}>
57
58
  Files
58
59
  </TabButton>
59
- <TabButton active={activeTab === "settings"} onClick={() => setActiveTab("settings")}>
60
- Settings
61
- </TabButton>
60
+ {isDev && (
61
+ <TabButton active={activeTab === "settings"} onClick={() => setActiveTab("settings")}>
62
+ Settings
63
+ </TabButton>
64
+ )}
62
65
  </div>
63
66
  </div>
64
67
 
@@ -113,17 +116,20 @@ function TabButton({ active, onClick, children }: { active: boolean; onClick: ()
113
116
 
114
117
  function ChatTab({ agent, onStartAgent }: { agent: Agent; onStartAgent: (e?: React.MouseEvent) => void }) {
115
118
  const { theme } = useTheme();
119
+ const { accessToken } = useAuth();
116
120
  if (agent.status === "running" && agent.port) {
117
121
  return (
118
122
  <Chat
119
123
  agentId="default"
120
124
  apiUrl={`/api/agents/${agent.id}`}
125
+ apiKey={accessToken || undefined}
121
126
  placeholder="Message this agent..."
122
127
  context={agent.systemPrompt}
123
128
  variant="terminal"
124
129
  theme={theme.id as "light" | "dark"}
125
130
  headerTitle={agent.name}
126
- enableVoice={!!agent.features.realtime}
131
+ enableVoice={isRealtimeEnabled(agent.features)}
132
+ voiceProvider={getRealtimeConfig(agent.features).ttsProvider}
127
133
  />
128
134
  );
129
135
  }
@@ -1626,6 +1632,21 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1626
1632
  };
1627
1633
  }
1628
1634
  });
1635
+ } else if (key === "realtime") {
1636
+ // Special handling for realtime feature - convert to RealtimeConfig
1637
+ setForm(prev => {
1638
+ if (isRealtimeEnabled(prev.features)) {
1639
+ return { ...prev, features: { ...prev.features, realtime: false } };
1640
+ } else {
1641
+ return {
1642
+ ...prev,
1643
+ features: {
1644
+ ...prev.features,
1645
+ realtime: { enabled: true },
1646
+ },
1647
+ };
1648
+ }
1649
+ });
1629
1650
  } else {
1630
1651
  setForm(prev => ({
1631
1652
  ...prev,
@@ -1758,6 +1779,7 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1758
1779
  // For agents/operator features, check the enabled property of the config
1759
1780
  const isEnabled = key === "agents" ? isAgentsEnabled()
1760
1781
  : key === "operator" ? isOperatorEnabled()
1782
+ : key === "realtime" ? isRealtimeEnabled(form.features)
1761
1783
  : !!form.features[key];
1762
1784
  return (
1763
1785
  <button
@@ -1806,6 +1828,103 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1806
1828
  </FormField>
1807
1829
  )}
1808
1830
 
1831
+ {/* Voice Configuration - shown when Realtime is enabled */}
1832
+ {isRealtimeEnabled(form.features) && (() => {
1833
+ const rtConfig = getRealtimeConfig(form.features);
1834
+ const hasOpenAI = providers.some(p => p.id === "openai" && p.hasKey);
1835
+ const hasGemini = providers.some(p => p.id === "gemini" && p.hasKey);
1836
+ const voiceProviders = providers.filter(p => p.type === "voice" && p.hasKey);
1837
+ const hasStandard = voiceProviders.length > 0;
1838
+ const currentMode = rtConfig.provider || "standard";
1839
+
1840
+ const updateRt = (updates: Partial<RealtimeConfig>) => {
1841
+ setForm(prev => ({
1842
+ ...prev,
1843
+ features: {
1844
+ ...prev.features,
1845
+ realtime: { ...getRealtimeConfig(prev.features), ...updates },
1846
+ },
1847
+ }));
1848
+ };
1849
+
1850
+ const sttOptions = voiceProviders
1851
+ .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "stt" || (p as any).voiceSubtype === "both")
1852
+ .map(p => ({ value: p.id, label: p.name }));
1853
+ const ttsOptions = voiceProviders
1854
+ .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "tts" || (p as any).voiceSubtype === "both")
1855
+ .map(p => ({ value: p.id, label: p.name }));
1856
+
1857
+ return (
1858
+ <div className="p-3 bg-[var(--color-surface)] rounded border border-[var(--color-border-light)] space-y-3">
1859
+ <p className="text-xs text-[var(--color-text-muted)] font-medium uppercase tracking-wider">Voice Configuration</p>
1860
+ <FormField label="Voice Mode">
1861
+ <div className="flex gap-2 flex-wrap">
1862
+ {hasOpenAI && (
1863
+ <button type="button" onClick={() => updateRt({ provider: "openai", model: "gpt-realtime", voice: "alloy" })}
1864
+ className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "openai" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1865
+ OpenAI Realtime
1866
+ </button>
1867
+ )}
1868
+ {hasGemini && (
1869
+ <button type="button" onClick={() => updateRt({ provider: "gemini", geminiVoice: "Kore" })}
1870
+ className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "gemini" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1871
+ Gemini Live
1872
+ </button>
1873
+ )}
1874
+ {hasStandard && (
1875
+ <button type="button" onClick={() => updateRt({ provider: "standard" })}
1876
+ className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "standard" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1877
+ Standard (STT+LLM+TTS)
1878
+ </button>
1879
+ )}
1880
+ </div>
1881
+ </FormField>
1882
+ {currentMode === "openai" && (
1883
+ <>
1884
+ <FormField label="Realtime Model">
1885
+ <Select value={rtConfig.model || "gpt-realtime"} options={REALTIME_PROVIDERS.openai.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))} onChange={(value) => updateRt({ model: value })} placeholder="Select model..." />
1886
+ </FormField>
1887
+ <FormField label="Voice">
1888
+ <Select value={rtConfig.voice || "alloy"} options={REALTIME_PROVIDERS.openai.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))} onChange={(value) => updateRt({ voice: value })} placeholder="Select voice..." />
1889
+ </FormField>
1890
+ </>
1891
+ )}
1892
+ {currentMode === "gemini" && (
1893
+ <>
1894
+ <FormField label="Realtime Model">
1895
+ <Select value={rtConfig.geminiModel || REALTIME_PROVIDERS.gemini.models[0].value} options={REALTIME_PROVIDERS.gemini.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))} onChange={(value) => updateRt({ geminiModel: value })} placeholder="Select model..." />
1896
+ </FormField>
1897
+ <FormField label="Voice">
1898
+ <Select value={rtConfig.geminiVoice || "Kore"} options={REALTIME_PROVIDERS.gemini.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))} onChange={(value) => updateRt({ geminiVoice: value })} placeholder="Select voice..." />
1899
+ </FormField>
1900
+ <div className="flex items-center gap-2">
1901
+ <button type="button" onClick={() => updateRt({ googleSearch: !rtConfig.googleSearch })}
1902
+ className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded border transition ${rtConfig.googleSearch ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1903
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
1904
+ Google Search
1905
+ </button>
1906
+ <span className="text-xs text-[var(--color-text-faint)]">Enable search grounding</span>
1907
+ </div>
1908
+ </>
1909
+ )}
1910
+ {currentMode === "standard" && (
1911
+ <>
1912
+ {sttOptions.length > 0 && (
1913
+ <FormField label="STT Provider">
1914
+ <Select value={rtConfig.sttProvider || (sttOptions[0]?.value ?? "")} options={sttOptions} onChange={(value) => updateRt({ sttProvider: value })} placeholder="Select STT provider..." />
1915
+ </FormField>
1916
+ )}
1917
+ {ttsOptions.length > 0 && (
1918
+ <FormField label="TTS Provider">
1919
+ <Select value={rtConfig.ttsProvider || (ttsOptions[0]?.value ?? "")} options={ttsOptions} onChange={(value) => updateRt({ ttsProvider: value })} placeholder="Select TTS provider..." />
1920
+ </FormField>
1921
+ )}
1922
+ </>
1923
+ )}
1924
+ </div>
1925
+ );
1926
+ })()}
1927
+
1809
1928
  {/* Agent Built-in Tools - Anthropic only */}
1810
1929
  {form.provider === "anthropic" && (
1811
1930
  <FormField label="Agent Built-in Tools">
@@ -2,7 +2,8 @@ import React, { useMemo } from "react";
2
2
  import { AgentCard } from "./AgentCard";
3
3
  import { AgentPanel } from "./AgentPanel";
4
4
  import { LoadingSpinner } from "../common/LoadingSpinner";
5
- import { useProjects } from "../../context";
5
+ import { useProjects, useUIMode } from "../../context";
6
+ import { useMetaAgent } from "../meta-agent/MetaAgent";
6
7
  import type { Agent, Provider } from "../../types";
7
8
 
8
9
  interface AgentsViewProps {
@@ -33,6 +34,7 @@ export function AgentsView({
33
34
  canCreateAgent = true,
34
35
  }: AgentsViewProps) {
35
36
  const { currentProjectId, currentProject } = useProjects();
37
+ const { isBusiness, t } = useUIMode();
36
38
 
37
39
  // Filter agents by current project
38
40
  const filteredAgents = useMemo(() => {
@@ -49,10 +51,10 @@ export function AgentsView({
49
51
  }, [agents, currentProjectId]);
50
52
 
51
53
  const headerTitle = currentProjectId === null
52
- ? "Agents"
54
+ ? t("Agents", "Employees")
53
55
  : currentProjectId === "unassigned"
54
- ? "Unassigned Agents"
55
- : currentProject?.name || "Agents";
56
+ ? t("Unassigned Agents", "Unassigned Employees")
57
+ : currentProject?.name || t("Agents", "Employees");
56
58
 
57
59
  return (
58
60
  <div className="flex-1 flex overflow-hidden relative">
@@ -70,11 +72,11 @@ export function AgentsView({
70
72
  <h1 className="text-xl font-semibold">{headerTitle}</h1>
71
73
  {currentProjectId !== null && (
72
74
  <span className="text-sm text-[var(--color-text-muted)]">
73
- ({filteredAgents.length} agent{filteredAgents.length !== 1 ? "s" : ""})
75
+ ({filteredAgents.length} {t("agent", "employee")}{filteredAgents.length !== 1 ? "s" : ""})
74
76
  </span>
75
77
  )}
76
78
  </div>
77
- {onNewAgent && (
79
+ {!isBusiness && onNewAgent && (
78
80
  <button
79
81
  onClick={onNewAgent}
80
82
  disabled={!canCreateAgent}
@@ -86,9 +88,13 @@ export function AgentsView({
86
88
  </div>
87
89
 
88
90
  {loading ? (
89
- <LoadingSpinner message="Loading agents..." />
91
+ <LoadingSpinner message={t("Loading agents...", "Loading employees...")} />
90
92
  ) : filteredAgents.length === 0 ? (
91
- <EmptyState onNewAgent={onNewAgent} canCreateAgent={canCreateAgent} hasProjectFilter={currentProjectId !== null} />
93
+ isBusiness ? (
94
+ <BusinessEmptyState />
95
+ ) : (
96
+ <EmptyState onNewAgent={onNewAgent} canCreateAgent={canCreateAgent} hasProjectFilter={currentProjectId !== null} />
97
+ )
92
98
  ) : (
93
99
  <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3 auto-rows-fr">
94
100
  {filteredAgents.map((agent) => (
@@ -156,3 +162,19 @@ function EmptyState({ onNewAgent, canCreateAgent, hasProjectFilter }: { onNewAge
156
162
  </div>
157
163
  );
158
164
  }
165
+
166
+ function BusinessEmptyState() {
167
+ const { toggle } = useMetaAgent();
168
+ return (
169
+ <div className="text-center py-20 text-[var(--color-text-muted)]">
170
+ <p className="text-lg">No employees yet</p>
171
+ <p className="text-sm mt-1">Ask the Assistant to create one for you</p>
172
+ <button
173
+ onClick={toggle}
174
+ className="mt-4 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-4 py-2 rounded font-medium transition"
175
+ >
176
+ Open Assistant
177
+ </button>
178
+ </div>
179
+ );
180
+ }
@@ -3,8 +3,8 @@ import { Modal } from "../common/Modal";
3
3
  import { Select } from "../common/Select";
4
4
  import { MemoryIcon, TasksIcon, FilesIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, MultiAgentIcon } from "../common/Icons";
5
5
  import { useProjects, useAuth } from "../../context";
6
- import type { Provider, NewAgentForm, AgentFeatures, MultiAgentConfig } from "../../types";
7
- import { getMultiAgentConfig } from "../../types";
6
+ import type { Provider, NewAgentForm, AgentFeatures, MultiAgentConfig, RealtimeConfig } from "../../types";
7
+ import { getMultiAgentConfig, getRealtimeConfig, isRealtimeEnabled, REALTIME_PROVIDERS } from "../../types";
8
8
 
9
9
  interface CreateAgentModalProps {
10
10
  form: NewAgentForm;
@@ -99,10 +99,8 @@ export function CreateAgentModal({
99
99
  ? form.features.agents
100
100
  : (form.features.agents as MultiAgentConfig)?.enabled ?? false;
101
101
  if (isEnabled) {
102
- // Turning off
103
102
  onFormChange({ ...form, features: { ...form.features, agents: false } });
104
103
  } else {
105
- // Turning on with defaults - use project as group
106
104
  onFormChange({
107
105
  ...form,
108
106
  features: {
@@ -111,6 +109,19 @@ export function CreateAgentModal({
111
109
  },
112
110
  });
113
111
  }
112
+ } else if (key === "realtime") {
113
+ // Special handling for realtime feature
114
+ if (isRealtimeEnabled(form.features)) {
115
+ onFormChange({ ...form, features: { ...form.features, realtime: false } });
116
+ } else {
117
+ onFormChange({
118
+ ...form,
119
+ features: {
120
+ ...form.features,
121
+ realtime: { enabled: true },
122
+ },
123
+ });
124
+ }
114
125
  } else {
115
126
  onFormChange({
116
127
  ...form,
@@ -197,7 +208,9 @@ export function CreateAgentModal({
197
208
  <FormField label="Features">
198
209
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
199
210
  {FEATURE_CONFIG.map(({ key, label, description, icon: Icon }) => {
200
- const isEnabled = key === "agents" ? isAgentsEnabled() : !!form.features[key];
211
+ const isEnabled = key === "agents" ? isAgentsEnabled()
212
+ : key === "realtime" ? isRealtimeEnabled(form.features)
213
+ : !!form.features[key];
201
214
  return (
202
215
  <button
203
216
  key={key}
@@ -222,6 +235,143 @@ export function CreateAgentModal({
222
235
  </div>
223
236
  </FormField>
224
237
 
238
+ {/* Voice Configuration - shown when Realtime is enabled */}
239
+ {isRealtimeEnabled(form.features) && (() => {
240
+ const rtConfig = getRealtimeConfig(form.features);
241
+ const hasOpenAI = providers.some(p => p.id === "openai" && p.hasKey);
242
+ const hasGemini = providers.some(p => p.id === "gemini" && p.hasKey);
243
+ const voiceProviders = providers.filter(p => p.type === "voice" && p.hasKey);
244
+ const hasStandard = voiceProviders.length > 0;
245
+ const currentMode = rtConfig.provider || "standard";
246
+
247
+ const updateRt = (updates: Partial<RealtimeConfig>) => {
248
+ onFormChange({
249
+ ...form,
250
+ features: {
251
+ ...form.features,
252
+ realtime: { ...rtConfig, ...updates },
253
+ },
254
+ });
255
+ };
256
+
257
+ // STT/TTS options for standard mode
258
+ const sttOptions = voiceProviders
259
+ .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "stt" || (p as any).voiceSubtype === "both")
260
+ .map(p => ({ value: p.id, label: p.name }));
261
+ const ttsOptions = voiceProviders
262
+ .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "tts" || (p as any).voiceSubtype === "both")
263
+ .map(p => ({ value: p.id, label: p.name }));
264
+
265
+ return (
266
+ <div className="p-3 bg-[var(--color-surface)] rounded border border-[var(--color-border-light)] space-y-3">
267
+ <p className="text-xs text-[var(--color-text-muted)] font-medium uppercase tracking-wider">Voice Configuration</p>
268
+
269
+ {/* Voice Mode Selection */}
270
+ <FormField label="Voice Mode">
271
+ <div className="flex gap-2 flex-wrap">
272
+ {hasOpenAI && (
273
+ <button type="button" onClick={() => updateRt({ provider: "openai", model: "gpt-realtime", voice: "alloy" })}
274
+ className={`px-3 py-1.5 text-sm btn border transition ${currentMode === "openai" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
275
+ OpenAI Realtime
276
+ </button>
277
+ )}
278
+ {hasGemini && (
279
+ <button type="button" onClick={() => updateRt({ provider: "gemini", geminiVoice: "Kore" })}
280
+ className={`px-3 py-1.5 text-sm btn border transition ${currentMode === "gemini" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
281
+ Gemini Live
282
+ </button>
283
+ )}
284
+ {hasStandard && (
285
+ <button type="button" onClick={() => updateRt({ provider: "standard" })}
286
+ className={`px-3 py-1.5 text-sm btn border transition ${currentMode === "standard" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
287
+ Standard (STT+LLM+TTS)
288
+ </button>
289
+ )}
290
+ </div>
291
+ </FormField>
292
+
293
+ {/* OpenAI Realtime options */}
294
+ {currentMode === "openai" && (
295
+ <>
296
+ <FormField label="Realtime Model">
297
+ <Select
298
+ value={rtConfig.model || "gpt-realtime"}
299
+ options={REALTIME_PROVIDERS.openai.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))}
300
+ onChange={(value) => updateRt({ model: value })}
301
+ placeholder="Select model..."
302
+ />
303
+ </FormField>
304
+ <FormField label="Voice">
305
+ <Select
306
+ value={rtConfig.voice || "alloy"}
307
+ options={REALTIME_PROVIDERS.openai.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))}
308
+ onChange={(value) => updateRt({ voice: value })}
309
+ placeholder="Select voice..."
310
+ />
311
+ </FormField>
312
+ </>
313
+ )}
314
+
315
+ {/* Gemini Live options */}
316
+ {currentMode === "gemini" && (
317
+ <>
318
+ <FormField label="Realtime Model">
319
+ <Select
320
+ value={rtConfig.geminiModel || REALTIME_PROVIDERS.gemini.models[0].value}
321
+ options={REALTIME_PROVIDERS.gemini.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))}
322
+ onChange={(value) => updateRt({ geminiModel: value })}
323
+ placeholder="Select model..."
324
+ />
325
+ </FormField>
326
+ <FormField label="Voice">
327
+ <Select
328
+ value={rtConfig.geminiVoice || "Kore"}
329
+ options={REALTIME_PROVIDERS.gemini.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))}
330
+ onChange={(value) => updateRt({ geminiVoice: value })}
331
+ placeholder="Select voice..."
332
+ />
333
+ </FormField>
334
+ <div className="flex items-center gap-2">
335
+ <button type="button"
336
+ onClick={() => updateRt({ googleSearch: !rtConfig.googleSearch })}
337
+ className={`flex items-center gap-2 px-3 py-1.5 text-sm btn border transition ${rtConfig.googleSearch ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
338
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
339
+ Google Search
340
+ </button>
341
+ <span className="text-xs text-[var(--color-text-faint)]">Enable search grounding</span>
342
+ </div>
343
+ </>
344
+ )}
345
+
346
+ {/* Standard mode: STT + TTS provider selection */}
347
+ {currentMode === "standard" && (
348
+ <>
349
+ {sttOptions.length > 0 && (
350
+ <FormField label="STT Provider">
351
+ <Select
352
+ value={rtConfig.sttProvider || (sttOptions[0]?.value ?? "")}
353
+ options={sttOptions}
354
+ onChange={(value) => updateRt({ sttProvider: value })}
355
+ placeholder="Select STT provider..."
356
+ />
357
+ </FormField>
358
+ )}
359
+ {ttsOptions.length > 0 && (
360
+ <FormField label="TTS Provider">
361
+ <Select
362
+ value={rtConfig.ttsProvider || (ttsOptions[0]?.value ?? "")}
363
+ options={ttsOptions}
364
+ onChange={(value) => updateRt({ ttsProvider: value })}
365
+ placeholder="Select TTS provider..."
366
+ />
367
+ </FormField>
368
+ )}
369
+ </>
370
+ )}
371
+ </div>
372
+ );
373
+ })()}
374
+
225
375
  {/* Agent Built-in Tools - Anthropic only */}
226
376
  {form.provider === "anthropic" && (
227
377
  <FormField label="Agent Built-in Tools">
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
2
- import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
2
+ import { useAgentActivity, useAuth, useProjects, useTelemetryContext, useUIMode } from "../../context";
3
3
  import { useTelemetry } from "../../context/TelemetryContext";
4
4
  import type { TelemetryEvent } from "../../context";
5
5
  import type { Agent, Provider, Route, DashboardStats, Task } from "../../types";
@@ -24,6 +24,7 @@ export function Dashboard({
24
24
  }: DashboardProps) {
25
25
  const { authFetch } = useAuth();
26
26
  const { currentProjectId } = useProjects();
27
+ const { isDev, t } = useUIMode();
27
28
  const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
28
29
  const { events: taskTelemetryEvents } = useTelemetry({ category: "TASK" });
29
30
  const lastProcessedTaskEventRef = useRef<string | null>(null);
@@ -145,24 +146,24 @@ export function Dashboard({
145
146
  return (
146
147
  <div className="flex-1 overflow-auto p-6">
147
148
  {/* Stats Cards */}
148
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
149
- <StatCard label="Agents" value={filteredAgents.length} subValue={`${filteredRunningCount} running`} />
149
+ <div className={`grid grid-cols-2 ${isDev ? 'sm:grid-cols-4' : 'sm:grid-cols-3'} gap-4 mb-6`}>
150
+ <StatCard label={t("Agents", "Employees")} value={filteredAgents.length} subValue={`${filteredRunningCount} running`} />
150
151
  <StatCard label="Tasks" value={taskStats.total} subValue={`${taskStats.pending} pending`} />
151
152
  <StatCard label="Completed" value={taskStats.completed} color="text-green-400" />
152
- <StatCard label="Providers" value={configuredProviders.length} color="text-[var(--color-accent)]" />
153
+ {isDev && <StatCard label="Providers" value={configuredProviders.length} color="text-[var(--color-accent)]" />}
153
154
  </div>
154
155
 
155
156
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
156
157
  {/* Agents List */}
157
158
  <DashboardCard
158
- title="Agents"
159
+ title={t("Agents", "Employees")}
159
160
  actionLabel="View All"
160
161
  onAction={() => onNavigate("agents")}
161
162
  >
162
163
  {loading ? (
163
164
  <div className="p-4 text-center text-[var(--color-text-muted)]">Loading...</div>
164
165
  ) : filteredAgents.length === 0 ? (
165
- <div className="p-4 text-center text-[var(--color-text-muted)]">No agents yet</div>
166
+ <div className="p-4 text-center text-[var(--color-text-muted)]">{t("No agents yet", "No employees yet")}</div>
166
167
  ) : (
167
168
  <div className="divide-y divide-[var(--color-border)]">
168
169
  {filteredAgents.slice(0, 5).map((agent) => (
@@ -299,6 +300,7 @@ function DashboardCard({ title, actionLabel, onAction, children }: DashboardCard
299
300
  function AgentListItem({ agent, onSelect, onMessage, showProject }: { agent: Agent; onSelect: () => void; onMessage?: () => void; showProject?: boolean }) {
300
301
  const { isActive, label } = useAgentActivity(agent.id);
301
302
  const { projects } = useProjects();
303
+ const { isDev } = useUIMode();
302
304
  const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
303
305
 
304
306
  return (
@@ -322,7 +324,7 @@ function AgentListItem({ agent, onSelect, onMessage, showProject }: { agent: Age
322
324
  {isActive && label ? (
323
325
  <span className="text-green-400 truncate">{label}</span>
324
326
  ) : (
325
- <span>{agent.provider} · {agent.status === "running" ? "idle" : "stopped"}</span>
327
+ <span>{isDev ? `${agent.provider} · ` : ""}{agent.status === "running" ? "idle" : "stopped"}</span>
326
328
  )}
327
329
  {showProject && project && (
328
330
  <>
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { DashboardIcon, ThreadsIcon, AgentsIcon, ActivityIcon, TasksIcon, ConnectionsIcon, McpIcon, SkillsIcon, TestsIcon, TelemetryIcon, ApiIcon, SettingsIcon, CloseIcon } from "../common/Icons";
3
- import { useAuth } from "../../context";
3
+ import { useAuth, useUIMode } from "../../context";
4
4
  import type { Route } from "../../types";
5
5
 
6
6
  interface SidebarProps {
@@ -14,6 +14,7 @@ interface SidebarProps {
14
14
 
15
15
  export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onClose }: SidebarProps) {
16
16
  const { user, logout } = useAuth();
17
+ const { isDev, t } = useUIMode();
17
18
  const [showUserMenu, setShowUserMenu] = useState(false);
18
19
 
19
20
  const handleNavigate = (newRoute: Route) => {
@@ -69,7 +70,7 @@ export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onCl
69
70
  />
70
71
  <NavButton
71
72
  icon={<AgentsIcon />}
72
- label="Agents"
73
+ label={t("Agents", "Employees")}
73
74
  active={route === "agents"}
74
75
  onClick={() => handleNavigate("agents")}
75
76
  badge={agentCount > 0 ? String(agentCount) : undefined}
@@ -93,42 +94,52 @@ export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onCl
93
94
  onClick={() => handleNavigate("tasks")}
94
95
  badge={taskCount && taskCount > 0 ? String(taskCount) : undefined}
95
96
  />
96
- <NavButton
97
- icon={<McpIcon />}
98
- label="MCP"
99
- active={route === "mcp"}
100
- onClick={() => handleNavigate("mcp")}
101
- />
102
- <NavButton
103
- icon={<SkillsIcon />}
104
- label="Skills"
105
- active={route === "skills"}
106
- onClick={() => handleNavigate("skills")}
107
- />
108
- <NavButton
109
- icon={<ConnectionsIcon />}
110
- label="Connections"
111
- active={route === "connections"}
112
- onClick={() => handleNavigate("connections")}
113
- />
114
- <NavButton
115
- icon={<TestsIcon />}
116
- label="Tests"
117
- active={route === "tests"}
118
- onClick={() => handleNavigate("tests")}
119
- />
97
+ {isDev && (
98
+ <NavButton
99
+ icon={<McpIcon />}
100
+ label="MCP"
101
+ active={route === "mcp"}
102
+ onClick={() => handleNavigate("mcp")}
103
+ />
104
+ )}
105
+ {isDev && (
106
+ <NavButton
107
+ icon={<SkillsIcon />}
108
+ label="Skills"
109
+ active={route === "skills"}
110
+ onClick={() => handleNavigate("skills")}
111
+ />
112
+ )}
113
+ {isDev && (
114
+ <NavButton
115
+ icon={<ConnectionsIcon />}
116
+ label="Connections"
117
+ active={route === "connections"}
118
+ onClick={() => handleNavigate("connections")}
119
+ />
120
+ )}
121
+ {isDev && (
122
+ <NavButton
123
+ icon={<TestsIcon />}
124
+ label="Tests"
125
+ active={route === "tests"}
126
+ onClick={() => handleNavigate("tests")}
127
+ />
128
+ )}
120
129
  <NavButton
121
130
  icon={<TelemetryIcon />}
122
131
  label="Analytics"
123
132
  active={route === "analytics"}
124
133
  onClick={() => handleNavigate("analytics")}
125
134
  />
126
- <NavButton
127
- icon={<ApiIcon />}
128
- label="API"
129
- active={route === "api"}
130
- onClick={() => handleNavigate("api")}
131
- />
135
+ {isDev && (
136
+ <NavButton
137
+ icon={<ApiIcon />}
138
+ label="API"
139
+ active={route === "api"}
140
+ onClick={() => handleNavigate("api")}
141
+ />
142
+ )}
132
143
  <NavButton
133
144
  icon={<SettingsIcon />}
134
145
  label="Settings"