cowork-os 0.3.21 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -10
- package/connectors/README.md +20 -0
- package/connectors/asana-mcp/README.md +24 -0
- package/connectors/asana-mcp/dist/index.js +427 -0
- package/connectors/asana-mcp/package.json +15 -0
- package/connectors/asana-mcp/src/index.ts +553 -0
- package/connectors/asana-mcp/tsconfig.json +13 -0
- package/connectors/hubspot-mcp/README.md +35 -0
- package/connectors/hubspot-mcp/dist/index.js +454 -0
- package/connectors/hubspot-mcp/package.json +15 -0
- package/connectors/hubspot-mcp/src/index.ts +562 -0
- package/connectors/hubspot-mcp/tsconfig.json +13 -0
- package/connectors/jira-mcp/README.md +49 -0
- package/connectors/jira-mcp/dist/index.js +588 -0
- package/connectors/jira-mcp/package.json +15 -0
- package/connectors/jira-mcp/src/index.ts +711 -0
- package/connectors/jira-mcp/tsconfig.json +13 -0
- package/connectors/linear-mcp/README.md +22 -0
- package/connectors/linear-mcp/dist/index.js +402 -0
- package/connectors/linear-mcp/package.json +15 -0
- package/connectors/linear-mcp/src/index.ts +522 -0
- package/connectors/linear-mcp/tsconfig.json +13 -0
- package/connectors/okta-mcp/README.md +24 -0
- package/connectors/okta-mcp/dist/index.js +411 -0
- package/connectors/okta-mcp/package.json +15 -0
- package/connectors/okta-mcp/src/index.ts +520 -0
- package/connectors/okta-mcp/tsconfig.json +13 -0
- package/connectors/salesforce-mcp/README.md +47 -0
- package/connectors/salesforce-mcp/dist/index.js +584 -0
- package/connectors/salesforce-mcp/package.json +15 -0
- package/connectors/salesforce-mcp/src/index.ts +722 -0
- package/connectors/salesforce-mcp/tsconfig.json +13 -0
- package/connectors/servicenow-mcp/README.md +26 -0
- package/connectors/servicenow-mcp/dist/index.js +400 -0
- package/connectors/servicenow-mcp/package.json +15 -0
- package/connectors/servicenow-mcp/src/index.ts +500 -0
- package/connectors/servicenow-mcp/tsconfig.json +13 -0
- package/connectors/templates/mcp-connector/README.md +31 -0
- package/connectors/templates/mcp-connector/package.json +15 -0
- package/connectors/templates/mcp-connector/src/index.ts +330 -0
- package/connectors/templates/mcp-connector/tsconfig.json +13 -0
- package/connectors/zendesk-mcp/README.md +40 -0
- package/connectors/zendesk-mcp/dist/index.js +431 -0
- package/connectors/zendesk-mcp/package.json +15 -0
- package/connectors/zendesk-mcp/src/index.ts +543 -0
- package/connectors/zendesk-mcp/tsconfig.json +13 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
- package/dist/electron/electron/agent/daemon.js +189 -13
- package/dist/electron/electron/agent/executor.js +895 -78
- package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
- package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
- package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
- package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
- package/dist/electron/electron/agent/llm/index.js +13 -1
- package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
- package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
- package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
- package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
- package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
- package/dist/electron/electron/agent/llm/types.js +66 -1
- package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
- package/dist/electron/electron/agent/search/provider-factory.js +38 -2
- package/dist/electron/electron/agent/tools/box-tools.js +231 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
- package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
- package/dist/electron/electron/agent/tools/file-tools.js +66 -3
- package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
- package/dist/electron/electron/agent/tools/image-tools.js +11 -1
- package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
- package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
- package/dist/electron/electron/agent/tools/registry.js +548 -10
- package/dist/electron/electron/agent/tools/search-tools.js +28 -10
- package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
- package/dist/electron/electron/agent/tools/x-tools.js +1 -1
- package/dist/electron/electron/agents/agent-dispatch.js +63 -0
- package/dist/electron/electron/database/repositories.js +19 -5
- package/dist/electron/electron/database/schema.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
- package/dist/electron/electron/gateway/index.js +75 -1
- package/dist/electron/electron/gateway/router.js +209 -154
- package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
- package/dist/electron/electron/ipc/handlers.js +763 -267
- package/dist/electron/electron/main.js +63 -0
- package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
- package/dist/electron/electron/memory/MemoryService.js +2 -1
- package/dist/electron/electron/preload.js +78 -1
- package/dist/electron/electron/settings/appearance-manager.js +18 -1
- package/dist/electron/electron/settings/box-manager.js +54 -0
- package/dist/electron/electron/settings/dropbox-manager.js +54 -0
- package/dist/electron/electron/settings/google-drive-manager.js +54 -0
- package/dist/electron/electron/settings/notion-manager.js +56 -0
- package/dist/electron/electron/settings/onedrive-manager.js +54 -0
- package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
- package/dist/electron/electron/utils/box-api.js +153 -0
- package/dist/electron/electron/utils/dropbox-api.js +144 -0
- package/dist/electron/electron/utils/env-migration.js +19 -0
- package/dist/electron/electron/utils/google-drive-api.js +152 -0
- package/dist/electron/electron/utils/notion-api.js +103 -0
- package/dist/electron/electron/utils/onedrive-api.js +113 -0
- package/dist/electron/electron/utils/sharepoint-api.js +109 -0
- package/dist/electron/electron/utils/validation.js +98 -3
- package/dist/electron/electron/utils/x-cli.js +1 -1
- package/dist/electron/shared/channelMessages.js +284 -3
- package/dist/electron/shared/llm-provider-catalog.js +198 -0
- package/dist/electron/shared/types.js +90 -1
- package/package.json +14 -3
- package/resources/skills/nano-banana-pro.json +4 -4
- package/resources/skills/openai-image-gen.json +3 -3
- package/resources/skills/scripts/gen.py +163 -0
- package/resources/skills/scripts/generate_image.py +91 -0
- package/src/electron/agent/custom-skill-loader.ts +34 -1
- package/src/electron/agent/daemon.ts +210 -14
- package/src/electron/agent/executor.ts +1124 -85
- package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
- package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
- package/src/electron/agent/llm/bedrock-provider.ts +62 -9
- package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
- package/src/electron/agent/llm/groq-provider.ts +39 -0
- package/src/electron/agent/llm/index.ts +6 -0
- package/src/electron/agent/llm/kimi-provider.ts +39 -0
- package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
- package/src/electron/agent/llm/openai-compatible.ts +133 -0
- package/src/electron/agent/llm/openai-oauth.ts +2 -1
- package/src/electron/agent/llm/openrouter-provider.ts +2 -1
- package/src/electron/agent/llm/provider-factory.ts +459 -6
- package/src/electron/agent/llm/types.ts +95 -1
- package/src/electron/agent/llm/xai-provider.ts +39 -0
- package/src/electron/agent/search/provider-factory.ts +43 -2
- package/src/electron/agent/tools/box-tools.ts +239 -0
- package/src/electron/agent/tools/builtin-settings.ts +36 -0
- package/src/electron/agent/tools/dropbox-tools.ts +237 -0
- package/src/electron/agent/tools/file-tools.ts +66 -3
- package/src/electron/agent/tools/gmail-tools.ts +240 -0
- package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
- package/src/electron/agent/tools/google-drive-tools.ts +228 -0
- package/src/electron/agent/tools/grep-tools.ts +97 -12
- package/src/electron/agent/tools/image-tools.ts +11 -1
- package/src/electron/agent/tools/notion-tools.ts +330 -0
- package/src/electron/agent/tools/onedrive-tools.ts +217 -0
- package/src/electron/agent/tools/registry.ts +794 -10
- package/src/electron/agent/tools/search-tools.ts +29 -11
- package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
- package/src/electron/agent/tools/shell-tools.ts +11 -3
- package/src/electron/agent/tools/x-tools.ts +1 -1
- package/src/electron/agents/agent-dispatch.ts +79 -0
- package/src/electron/database/SecureSettingsRepository.ts +7 -1
- package/src/electron/database/repositories.ts +58 -6
- package/src/electron/database/schema.ts +8 -0
- package/src/electron/gateway/channels/discord.ts +4 -0
- package/src/electron/gateway/channels/google-chat.ts +3 -0
- package/src/electron/gateway/channels/line.ts +3 -0
- package/src/electron/gateway/channels/matrix-client.ts +15 -0
- package/src/electron/gateway/channels/matrix.ts +31 -0
- package/src/electron/gateway/channels/mattermost.ts +3 -0
- package/src/electron/gateway/channels/signal.ts +3 -0
- package/src/electron/gateway/channels/slack.ts +9 -4
- package/src/electron/gateway/channels/teams.ts +4 -0
- package/src/electron/gateway/channels/telegram.ts +2 -0
- package/src/electron/gateway/channels/twitch.ts +2 -0
- package/src/electron/gateway/channels/types.ts +8 -0
- package/src/electron/gateway/channels/whatsapp.ts +66 -0
- package/src/electron/gateway/index.ts +95 -2
- package/src/electron/gateway/router.ts +231 -161
- package/src/electron/gateway/security.ts +21 -9
- package/src/electron/ipc/canvas-handlers.ts +10 -0
- package/src/electron/ipc/handlers.ts +848 -292
- package/src/electron/main.ts +35 -0
- package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
- package/src/electron/memory/MemoryService.ts +7 -1
- package/src/electron/preload.ts +200 -5
- package/src/electron/settings/appearance-manager.ts +20 -2
- package/src/electron/settings/box-manager.ts +58 -0
- package/src/electron/settings/dropbox-manager.ts +58 -0
- package/src/electron/settings/google-workspace-manager.ts +59 -0
- package/src/electron/settings/notion-manager.ts +60 -0
- package/src/electron/settings/onedrive-manager.ts +58 -0
- package/src/electron/settings/sharepoint-manager.ts +58 -0
- package/src/electron/utils/box-api.ts +184 -0
- package/src/electron/utils/dropbox-api.ts +171 -0
- package/src/electron/utils/env-migration.ts +22 -0
- package/src/electron/utils/gmail-api.ts +121 -0
- package/src/electron/utils/google-calendar-api.ts +115 -0
- package/src/electron/utils/google-workspace-api.ts +228 -0
- package/src/electron/utils/google-workspace-auth.ts +109 -0
- package/src/electron/utils/google-workspace-oauth.ts +232 -0
- package/src/electron/utils/notion-api.ts +126 -0
- package/src/electron/utils/onedrive-api.ts +137 -0
- package/src/electron/utils/sharepoint-api.ts +132 -0
- package/src/electron/utils/validation.ts +128 -1
- package/src/electron/utils/x-cli.ts +1 -1
- package/src/renderer/App.tsx +119 -8
- package/src/renderer/components/ActivityFeedItem.tsx +34 -17
- package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
- package/src/renderer/components/AppearanceSettings.tsx +37 -2
- package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
- package/src/renderer/components/BoxSettings.tsx +203 -0
- package/src/renderer/components/BrowserView.tsx +101 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
- package/src/renderer/components/CanvasPreview.tsx +68 -1
- package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
- package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
- package/src/renderer/components/ConnectorsSettings.tsx +397 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
- package/src/renderer/components/DiscordSettings.tsx +18 -7
- package/src/renderer/components/DropboxSettings.tsx +202 -0
- package/src/renderer/components/EmailSettings.tsx +18 -7
- package/src/renderer/components/FileViewer.tsx +21 -13
- package/src/renderer/components/GoogleChatSettings.tsx +17 -7
- package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
- package/src/renderer/components/ImessageSettings.tsx +22 -11
- package/src/renderer/components/LineIcons.tsx +376 -0
- package/src/renderer/components/LineSettings.tsx +18 -7
- package/src/renderer/components/MCPSettings.tsx +56 -0
- package/src/renderer/components/MainContent.tsx +740 -76
- package/src/renderer/components/MatrixSettings.tsx +18 -7
- package/src/renderer/components/MattermostSettings.tsx +18 -7
- package/src/renderer/components/NodesSettings.tsx +58 -99
- package/src/renderer/components/NotificationPanel.tsx +25 -11
- package/src/renderer/components/NotionSettings.tsx +231 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
- package/src/renderer/components/OnboardingModal.tsx +70 -1
- package/src/renderer/components/OneDriveSettings.tsx +212 -0
- package/src/renderer/components/RightPanel.tsx +141 -28
- package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
- package/src/renderer/components/SearchSettings.tsx +118 -114
- package/src/renderer/components/Settings.tsx +1425 -651
- package/src/renderer/components/SharePointSettings.tsx +224 -0
- package/src/renderer/components/Sidebar.tsx +94 -19
- package/src/renderer/components/SignalSettings.tsx +18 -7
- package/src/renderer/components/SkillHubBrowser.tsx +144 -185
- package/src/renderer/components/SlackSettings.tsx +18 -7
- package/src/renderer/components/TaskQuickActions.tsx +11 -6
- package/src/renderer/components/TaskTimeline.tsx +58 -26
- package/src/renderer/components/TeamsSettings.tsx +18 -7
- package/src/renderer/components/TelegramSettings.tsx +18 -7
- package/src/renderer/components/ThemeIcon.tsx +16 -0
- package/src/renderer/components/TwitchSettings.tsx +18 -7
- package/src/renderer/components/VoiceSettings.tsx +30 -74
- package/src/renderer/components/WhatsAppSettings.tsx +48 -37
- package/src/renderer/components/WorkingStateHistory.tsx +7 -5
- package/src/renderer/components/WorkspaceSelector.tsx +42 -13
- package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
- package/src/renderer/styles/index.css +2333 -209
- package/src/shared/channelMessages.ts +367 -4
- package/src/shared/llm-provider-catalog.ts +217 -0
- package/src/shared/types.ts +251 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { LLMSettingsData, ThemeMode, AccentColor } from '../../shared/types';
|
|
2
|
+
import { LLMSettingsData, ThemeMode, VisualTheme, AccentColor, type LLMProviderType, type CustomProviderConfig } from '../../shared/types';
|
|
3
|
+
import { CUSTOM_PROVIDER_MAP } from '../../shared/llm-provider-catalog';
|
|
3
4
|
import { TelegramSettings } from './TelegramSettings';
|
|
4
5
|
import { DiscordSettings } from './DiscordSettings';
|
|
5
6
|
import { SlackSettings } from './SlackSettings';
|
|
@@ -15,6 +16,12 @@ import { EmailSettings } from './EmailSettings';
|
|
|
15
16
|
import { TeamsSettings } from './TeamsSettings';
|
|
16
17
|
import { GoogleChatSettings } from './GoogleChatSettings';
|
|
17
18
|
import { XSettings } from './XSettings';
|
|
19
|
+
import { NotionSettings } from './NotionSettings';
|
|
20
|
+
import { BoxSettings } from './BoxSettings';
|
|
21
|
+
import { OneDriveSettings } from './OneDriveSettings';
|
|
22
|
+
import { GoogleWorkspaceSettings } from './GoogleWorkspaceSettings';
|
|
23
|
+
import { DropboxSettings } from './DropboxSettings';
|
|
24
|
+
import { SharePointSettings } from './SharePointSettings';
|
|
18
25
|
import { SearchSettings } from './SearchSettings';
|
|
19
26
|
import { UpdateSettings } from './UpdateSettings';
|
|
20
27
|
import { GuardrailSettings } from './GuardrailSettings';
|
|
@@ -23,6 +30,7 @@ import { QueueSettings } from './QueueSettings';
|
|
|
23
30
|
import { SkillsSettings } from './SkillsSettings';
|
|
24
31
|
import { SkillHubBrowser } from './SkillHubBrowser';
|
|
25
32
|
import { MCPSettings } from './MCPSettings';
|
|
33
|
+
import { ConnectorsSettings } from './ConnectorsSettings';
|
|
26
34
|
import { BuiltinToolsSettings } from './BuiltinToolsSettings';
|
|
27
35
|
import { TraySettings } from './TraySettings';
|
|
28
36
|
import { ScheduledTasksSettings } from './ScheduledTasksSettings';
|
|
@@ -34,17 +42,22 @@ import { ExtensionsSettings } from './ExtensionsSettings';
|
|
|
34
42
|
import { VoiceSettings } from './VoiceSettings';
|
|
35
43
|
import { MissionControlPanel } from './MissionControlPanel';
|
|
36
44
|
|
|
37
|
-
type SettingsTab = 'appearance' | 'personality' | 'missioncontrol' | 'tray' | 'voice' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'morechannels' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'skillhub' | 'mcp' | 'tools' | 'scheduled' | 'hooks' | 'controlplane' | 'nodes' | 'extensions';
|
|
45
|
+
type SettingsTab = 'appearance' | 'personality' | 'missioncontrol' | 'tray' | 'voice' | 'llm' | 'search' | 'telegram' | 'slack' | 'whatsapp' | 'teams' | 'x' | 'morechannels' | 'integrations' | 'updates' | 'guardrails' | 'queue' | 'skills' | 'skillhub' | 'connectors' | 'mcp' | 'tools' | 'scheduled' | 'hooks' | 'controlplane' | 'nodes' | 'extensions';
|
|
38
46
|
|
|
39
47
|
// Secondary channels shown inside "More Channels" tab
|
|
40
|
-
type SecondaryChannel = 'discord' | 'imessage' | 'signal' | 'mattermost' | 'matrix' | 'twitch' | 'line' | 'bluebubbles' | 'email' | 'googlechat'
|
|
48
|
+
type SecondaryChannel = 'discord' | 'imessage' | 'signal' | 'mattermost' | 'matrix' | 'twitch' | 'line' | 'bluebubbles' | 'email' | 'googlechat';
|
|
49
|
+
|
|
50
|
+
// App integrations shown inside "Integrations" tab
|
|
51
|
+
type IntegrationChannel = 'notion' | 'box' | 'onedrive' | 'googleworkspace' | 'dropbox' | 'sharepoint';
|
|
41
52
|
|
|
42
53
|
interface SettingsProps {
|
|
43
54
|
onBack: () => void;
|
|
44
55
|
onSettingsChanged?: () => void;
|
|
45
56
|
themeMode: ThemeMode;
|
|
57
|
+
visualTheme: VisualTheme;
|
|
46
58
|
accentColor: AccentColor;
|
|
47
59
|
onThemeChange: (theme: ThemeMode) => void;
|
|
60
|
+
onVisualThemeChange: (theme: VisualTheme) => void;
|
|
48
61
|
onAccentChange: (accent: AccentColor) => void;
|
|
49
62
|
initialTab?: SettingsTab;
|
|
50
63
|
onShowOnboarding?: () => void;
|
|
@@ -246,12 +259,15 @@ const sidebarItems: Array<{ tab: SettingsTab; label: string; icon: React.ReactNo
|
|
|
246
259
|
{ tab: 'telegram', label: 'Telegram', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" /></svg> },
|
|
247
260
|
{ tab: 'slack', label: 'Slack', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z" /><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" /><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z" /><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z" /><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z" /><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z" /><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z" /><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z" /></svg> },
|
|
248
261
|
{ tab: 'teams', label: 'Teams', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg> },
|
|
262
|
+
{ tab: 'x', label: 'X (Twitter)', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 4l14 16" /><path d="M19 4L5 20" /></svg> },
|
|
249
263
|
{ tab: 'morechannels', label: 'More Channels', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg> },
|
|
264
|
+
{ tab: 'integrations', label: 'Integrations', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2v4" /><path d="M12 18v4" /><path d="M4.93 4.93l2.83 2.83" /><path d="M16.24 16.24l2.83 2.83" /><path d="M2 12h4" /><path d="M18 12h4" /><path d="M4.93 19.07l2.83-2.83" /><path d="M16.24 7.76l2.83-2.83" /><circle cx="12" cy="12" r="3" /></svg> },
|
|
250
265
|
{ tab: 'guardrails', label: 'Guardrails', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg> },
|
|
251
266
|
{ tab: 'queue', label: 'Task Queue', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="4" rx="1" /><rect x="3" y="10" width="18" height="4" rx="1" /><rect x="3" y="16" width="18" height="4" rx="1" /></svg> },
|
|
252
267
|
{ tab: 'skills', label: 'Custom Skills', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" /></svg> },
|
|
253
268
|
{ tab: 'skillhub', label: 'SkillHub', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /><path d="M8 12h8" /></svg> },
|
|
254
269
|
{ tab: 'scheduled', label: 'Scheduled Tasks', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg> },
|
|
270
|
+
{ tab: 'connectors', label: 'Connectors', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="7" height="7" rx="1" /><rect x="14" y="4" width="7" height="7" rx="1" /><rect x="3" y="13" width="7" height="7" rx="1" /><rect x="14" y="13" width="7" height="7" rx="1" /></svg> },
|
|
255
271
|
{ tab: 'mcp', label: 'MCP Servers', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" /><path d="M7 8h2M15 8h2" /><path d="M9 12h6" /></svg> },
|
|
256
272
|
{ tab: 'tools', label: 'Built-in Tools', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" /></svg> },
|
|
257
273
|
{ tab: 'hooks', label: 'Webhooks', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></svg> },
|
|
@@ -273,12 +289,100 @@ const secondaryChannelItems: Array<{ key: SecondaryChannel; label: string; icon:
|
|
|
273
289
|
{ key: 'matrix', label: 'Matrix', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="2" width="20" height="20" rx="2" /><path d="M7 7h10M7 12h10M7 17h10" /></svg> },
|
|
274
290
|
{ key: 'twitch', label: 'Twitch', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 2H3v16h5v4l4-4h5l4-4V2zM11 11V7M16 11V7" /></svg> },
|
|
275
291
|
{ key: 'bluebubbles', label: 'BlueBubbles', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg> },
|
|
276
|
-
{ key: 'x', label: 'X (Twitter)', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 4l14 16" /><path d="M19 4L5 20" /></svg> },
|
|
277
292
|
];
|
|
278
293
|
|
|
279
|
-
|
|
294
|
+
// App integrations configuration for "Integrations" tab
|
|
295
|
+
const integrationItems: Array<{ key: IntegrationChannel; label: string; icon: React.ReactNode }> = [
|
|
296
|
+
{ key: 'notion', label: 'Notion', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="4" y="3" width="16" height="18" rx="2" /><path d="M8 7h8M8 11h8M8 15h6" /></svg> },
|
|
297
|
+
{ key: 'sharepoint', label: 'SharePoint', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M7 8h10M7 12h6" /></svg> },
|
|
298
|
+
{ key: 'onedrive', label: 'OneDrive', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M7 18h10a4 4 0 0 0 0-8 5 5 0 0 0-9.7-1.6A4 4 0 0 0 7 18z" /></svg> },
|
|
299
|
+
{ key: 'googleworkspace', label: 'Google Workspace', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 7l4-4h8l4 4-8 14H8L4 7z" /></svg> },
|
|
300
|
+
{ key: 'box', label: 'Box', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="3" /><path d="M7 8h10M7 12h10M7 16h6" /></svg> },
|
|
301
|
+
{ key: 'dropbox', label: 'Dropbox', icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M7 5l5 3-5 3-5-3 5-3zm10 0l5 3-5 3-5-3 5-3zM7 13l5 3-5 3-5-3 5-3zm10 0l5 3-5 3-5-3 5-3z" /></svg> },
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const LLM_PROVIDER_ICONS: Record<string, JSX.Element> = {
|
|
305
|
+
anthropic: (
|
|
306
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
307
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
308
|
+
</svg>
|
|
309
|
+
),
|
|
310
|
+
openai: (
|
|
311
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
312
|
+
<circle cx="12" cy="12" r="10" />
|
|
313
|
+
<path d="M12 6v6l4 2" />
|
|
314
|
+
</svg>
|
|
315
|
+
),
|
|
316
|
+
azure: (
|
|
317
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
318
|
+
<path d="M5 16a4 4 0 0 1 2-7.46A5 5 0 0 1 17 9h1a4 4 0 1 1 0 8H6a3 3 0 0 1-1-1z" />
|
|
319
|
+
</svg>
|
|
320
|
+
),
|
|
321
|
+
gemini: (
|
|
322
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
323
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
324
|
+
</svg>
|
|
325
|
+
),
|
|
326
|
+
openrouter: (
|
|
327
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
328
|
+
<circle cx="12" cy="12" r="10" />
|
|
329
|
+
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
330
|
+
</svg>
|
|
331
|
+
),
|
|
332
|
+
ollama: (
|
|
333
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
334
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
335
|
+
<path d="M9 9h6v6H9z" />
|
|
336
|
+
</svg>
|
|
337
|
+
),
|
|
338
|
+
groq: (
|
|
339
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
340
|
+
<path d="M4 12h16" />
|
|
341
|
+
<path d="M12 4v16" />
|
|
342
|
+
<circle cx="12" cy="12" r="9" />
|
|
343
|
+
</svg>
|
|
344
|
+
),
|
|
345
|
+
xai: (
|
|
346
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
347
|
+
<path d="M4 4l16 16" />
|
|
348
|
+
<path d="M20 4L4 20" />
|
|
349
|
+
</svg>
|
|
350
|
+
),
|
|
351
|
+
kimi: (
|
|
352
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
353
|
+
<path d="M12 2l3 7 7 3-7 3-3 7-3-7-7-3 7-3 3-7z" />
|
|
354
|
+
</svg>
|
|
355
|
+
),
|
|
356
|
+
bedrock: (
|
|
357
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
358
|
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
359
|
+
</svg>
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const getLLMProviderIcon = (providerType: string, customEntry?: { compatibility?: string }) => {
|
|
364
|
+
if (LLM_PROVIDER_ICONS[providerType]) {
|
|
365
|
+
return LLM_PROVIDER_ICONS[providerType];
|
|
366
|
+
}
|
|
367
|
+
if (customEntry?.compatibility === 'anthropic') {
|
|
368
|
+
return LLM_PROVIDER_ICONS.anthropic;
|
|
369
|
+
}
|
|
370
|
+
if (customEntry?.compatibility === 'openai') {
|
|
371
|
+
return LLM_PROVIDER_ICONS.openai;
|
|
372
|
+
}
|
|
373
|
+
return (
|
|
374
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
375
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
376
|
+
<path d="M8 12h8" />
|
|
377
|
+
<path d="M12 8v8" />
|
|
378
|
+
</svg>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export function Settings({ onBack, onSettingsChanged, themeMode, visualTheme, accentColor, onThemeChange, onVisualThemeChange, onAccentChange, initialTab = 'appearance', onShowOnboarding, onboardingCompletedAt }: SettingsProps) {
|
|
280
383
|
const [activeTab, setActiveTab] = useState<SettingsTab>(initialTab);
|
|
281
384
|
const [activeSecondaryChannel, setActiveSecondaryChannel] = useState<SecondaryChannel>('discord');
|
|
385
|
+
const [activeIntegration, setActiveIntegration] = useState<IntegrationChannel>('notion');
|
|
282
386
|
const [sidebarSearch, setSidebarSearch] = useState('');
|
|
283
387
|
const [settings, setSettings] = useState<LLMSettingsData>({
|
|
284
388
|
providerType: 'anthropic',
|
|
@@ -314,6 +418,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
314
418
|
|
|
315
419
|
// OpenRouter state
|
|
316
420
|
const [openrouterApiKey, setOpenrouterApiKey] = useState('');
|
|
421
|
+
const [openrouterBaseUrl, setOpenrouterBaseUrl] = useState('');
|
|
317
422
|
const [openrouterModel, setOpenrouterModel] = useState('anthropic/claude-3.5-sonnet');
|
|
318
423
|
const [openrouterModels, setOpenrouterModels] = useState<Array<{ id: string; name: string; context_length: number }>>([]);
|
|
319
424
|
const [loadingOpenRouterModels, setLoadingOpenRouterModels] = useState(false);
|
|
@@ -327,6 +432,37 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
327
432
|
const [openaiOAuthConnected, setOpenaiOAuthConnected] = useState(false);
|
|
328
433
|
const [openaiOAuthLoading, setOpenaiOAuthLoading] = useState(false);
|
|
329
434
|
|
|
435
|
+
// Azure OpenAI state
|
|
436
|
+
const [azureApiKey, setAzureApiKey] = useState('');
|
|
437
|
+
const [azureEndpoint, setAzureEndpoint] = useState('');
|
|
438
|
+
const [azureDeployment, setAzureDeployment] = useState('');
|
|
439
|
+
const [azureDeploymentsText, setAzureDeploymentsText] = useState('');
|
|
440
|
+
const [azureApiVersion, setAzureApiVersion] = useState('2024-02-15-preview');
|
|
441
|
+
|
|
442
|
+
// Groq state
|
|
443
|
+
const [groqApiKey, setGroqApiKey] = useState('');
|
|
444
|
+
const [groqBaseUrl, setGroqBaseUrl] = useState('');
|
|
445
|
+
const [groqModel, setGroqModel] = useState('llama-3.1-8b-instant');
|
|
446
|
+
const [groqModels, setGroqModels] = useState<Array<{ id: string; name: string }>>([]);
|
|
447
|
+
const [loadingGroqModels, setLoadingGroqModels] = useState(false);
|
|
448
|
+
|
|
449
|
+
// xAI state
|
|
450
|
+
const [xaiApiKey, setXaiApiKey] = useState('');
|
|
451
|
+
const [xaiBaseUrl, setXaiBaseUrl] = useState('');
|
|
452
|
+
const [xaiModel, setXaiModel] = useState('grok-4-fast-non-reasoning');
|
|
453
|
+
const [xaiModels, setXaiModels] = useState<Array<{ id: string; name: string }>>([]);
|
|
454
|
+
const [loadingXaiModels, setLoadingXaiModels] = useState(false);
|
|
455
|
+
|
|
456
|
+
// Kimi state
|
|
457
|
+
const [kimiApiKey, setKimiApiKey] = useState('');
|
|
458
|
+
const [kimiBaseUrl, setKimiBaseUrl] = useState('');
|
|
459
|
+
const [kimiModel, setKimiModel] = useState('kimi-k2.5');
|
|
460
|
+
const [kimiModels, setKimiModels] = useState<Array<{ id: string; name: string }>>([]);
|
|
461
|
+
const [loadingKimiModels, setLoadingKimiModels] = useState(false);
|
|
462
|
+
|
|
463
|
+
// Custom provider state
|
|
464
|
+
const [customProviders, setCustomProviders] = useState<Record<string, CustomProviderConfig>>({});
|
|
465
|
+
|
|
330
466
|
// Bedrock state
|
|
331
467
|
const [bedrockModel, setBedrockModel] = useState('');
|
|
332
468
|
const [bedrockModels, setBedrockModels] = useState<Array<{ id: string; name: string; description: string }>>([]);
|
|
@@ -336,6 +472,78 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
336
472
|
loadConfigStatus();
|
|
337
473
|
}, []);
|
|
338
474
|
|
|
475
|
+
const resolveCustomProviderId = (providerType: LLMProviderType) =>
|
|
476
|
+
providerType === 'kimi-coding' ? 'kimi-code' : providerType;
|
|
477
|
+
|
|
478
|
+
const updateCustomProvider = (providerType: LLMProviderType, updates: Partial<CustomProviderConfig>) => {
|
|
479
|
+
const resolvedType = resolveCustomProviderId(providerType);
|
|
480
|
+
setCustomProviders((prev) => ({
|
|
481
|
+
...prev,
|
|
482
|
+
[resolvedType]: {
|
|
483
|
+
...(prev[resolvedType] || {}),
|
|
484
|
+
...updates,
|
|
485
|
+
},
|
|
486
|
+
}));
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const sanitizeCustomProviders = (providers: Record<string, CustomProviderConfig>) => {
|
|
490
|
+
const sanitized: Record<string, CustomProviderConfig> = {};
|
|
491
|
+
Object.entries(providers).forEach(([key, value]) => {
|
|
492
|
+
const apiKey = value.apiKey?.trim();
|
|
493
|
+
const model = value.model?.trim();
|
|
494
|
+
const baseUrl = value.baseUrl?.trim();
|
|
495
|
+
if (apiKey || model || baseUrl) {
|
|
496
|
+
sanitized[key] = {
|
|
497
|
+
...(apiKey ? { apiKey } : {}),
|
|
498
|
+
...(model ? { model } : {}),
|
|
499
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const parseAzureDeployments = (value: string): string[] => {
|
|
507
|
+
const seen = new Set<string>();
|
|
508
|
+
return value
|
|
509
|
+
.split(/[\n,]+/)
|
|
510
|
+
.map((entry) => entry.trim())
|
|
511
|
+
.filter(Boolean)
|
|
512
|
+
.filter((entry) => {
|
|
513
|
+
if (seen.has(entry)) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
seen.add(entry);
|
|
517
|
+
return true;
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const buildAzureSettings = () => {
|
|
522
|
+
const deployments = parseAzureDeployments(azureDeploymentsText);
|
|
523
|
+
let deployment = azureDeployment.trim();
|
|
524
|
+
if (deployment) {
|
|
525
|
+
if (!deployments.includes(deployment)) {
|
|
526
|
+
deployments.unshift(deployment);
|
|
527
|
+
}
|
|
528
|
+
} else if (deployments.length > 0) {
|
|
529
|
+
deployment = deployments[0];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
deployment: deployment || undefined,
|
|
534
|
+
deployments: deployments.length > 0 ? deployments : undefined,
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
if (!azureDeployment) {
|
|
540
|
+
const deployments = parseAzureDeployments(azureDeploymentsText);
|
|
541
|
+
if (deployments[0]) {
|
|
542
|
+
setAzureDeployment(deployments[0]);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}, [azureDeploymentsText, azureDeployment]);
|
|
546
|
+
|
|
339
547
|
const loadConfigStatus = async () => {
|
|
340
548
|
try {
|
|
341
549
|
setLoading(true);
|
|
@@ -349,6 +557,18 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
349
557
|
// Load full settings separately for bedrock config
|
|
350
558
|
const loadedSettings = await window.electronAPI.getLLMSettings();
|
|
351
559
|
setSettings(loadedSettings);
|
|
560
|
+
if (loadedSettings.customProviders) {
|
|
561
|
+
const normalized = { ...loadedSettings.customProviders };
|
|
562
|
+
if (normalized['kimi-coding'] && !normalized['kimi-code']) {
|
|
563
|
+
normalized['kimi-code'] = normalized['kimi-coding'];
|
|
564
|
+
}
|
|
565
|
+
if (normalized['kimi-coding']) {
|
|
566
|
+
delete normalized['kimi-coding'];
|
|
567
|
+
}
|
|
568
|
+
setCustomProviders(normalized);
|
|
569
|
+
} else {
|
|
570
|
+
setCustomProviders({});
|
|
571
|
+
}
|
|
352
572
|
|
|
353
573
|
// Set form state from loaded settings
|
|
354
574
|
if (loadedSettings.bedrock?.region) {
|
|
@@ -387,6 +607,9 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
387
607
|
if (loadedSettings.openrouter?.apiKey) {
|
|
388
608
|
setOpenrouterApiKey(loadedSettings.openrouter.apiKey);
|
|
389
609
|
}
|
|
610
|
+
if (loadedSettings.openrouter?.baseUrl) {
|
|
611
|
+
setOpenrouterBaseUrl(loadedSettings.openrouter.baseUrl);
|
|
612
|
+
}
|
|
390
613
|
if (loadedSettings.openrouter?.model) {
|
|
391
614
|
setOpenrouterModel(loadedSettings.openrouter.model);
|
|
392
615
|
}
|
|
@@ -419,6 +642,62 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
419
642
|
setOpenaiAuthMethod('oauth');
|
|
420
643
|
}
|
|
421
644
|
|
|
645
|
+
// Set Azure OpenAI form state
|
|
646
|
+
if (loadedSettings.azure?.apiKey) {
|
|
647
|
+
setAzureApiKey(loadedSettings.azure.apiKey);
|
|
648
|
+
}
|
|
649
|
+
if (loadedSettings.azure?.endpoint) {
|
|
650
|
+
setAzureEndpoint(loadedSettings.azure.endpoint);
|
|
651
|
+
}
|
|
652
|
+
{
|
|
653
|
+
const loadedDeployments = (loadedSettings.azure?.deployments && loadedSettings.azure.deployments.length > 0)
|
|
654
|
+
? loadedSettings.azure.deployments
|
|
655
|
+
: (loadedSettings.azure?.deployment ? [loadedSettings.azure.deployment] : []);
|
|
656
|
+
if (loadedDeployments.length > 0) {
|
|
657
|
+
setAzureDeploymentsText(loadedDeployments.join('\n'));
|
|
658
|
+
}
|
|
659
|
+
const selectedDeployment = loadedSettings.azure?.deployment || loadedDeployments[0];
|
|
660
|
+
if (selectedDeployment) {
|
|
661
|
+
setAzureDeployment(selectedDeployment);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (loadedSettings.azure?.apiVersion) {
|
|
665
|
+
setAzureApiVersion(loadedSettings.azure.apiVersion);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Set Groq form state
|
|
669
|
+
if (loadedSettings.groq?.apiKey) {
|
|
670
|
+
setGroqApiKey(loadedSettings.groq.apiKey);
|
|
671
|
+
}
|
|
672
|
+
if (loadedSettings.groq?.baseUrl) {
|
|
673
|
+
setGroqBaseUrl(loadedSettings.groq.baseUrl);
|
|
674
|
+
}
|
|
675
|
+
if (loadedSettings.groq?.model) {
|
|
676
|
+
setGroqModel(loadedSettings.groq.model);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Set xAI form state
|
|
680
|
+
if (loadedSettings.xai?.apiKey) {
|
|
681
|
+
setXaiApiKey(loadedSettings.xai.apiKey);
|
|
682
|
+
}
|
|
683
|
+
if (loadedSettings.xai?.baseUrl) {
|
|
684
|
+
setXaiBaseUrl(loadedSettings.xai.baseUrl);
|
|
685
|
+
}
|
|
686
|
+
if (loadedSettings.xai?.model) {
|
|
687
|
+
setXaiModel(loadedSettings.xai.model);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Set Kimi form state
|
|
691
|
+
if (loadedSettings.kimi?.apiKey) {
|
|
692
|
+
setKimiApiKey(loadedSettings.kimi.apiKey);
|
|
693
|
+
}
|
|
694
|
+
if (loadedSettings.kimi?.baseUrl) {
|
|
695
|
+
setKimiBaseUrl(loadedSettings.kimi.baseUrl);
|
|
696
|
+
}
|
|
697
|
+
if (loadedSettings.kimi?.model) {
|
|
698
|
+
setKimiModel(loadedSettings.kimi.model);
|
|
699
|
+
}
|
|
700
|
+
|
|
422
701
|
// Set Bedrock form state (access key and secret key are set earlier)
|
|
423
702
|
if (loadedSettings.bedrock?.accessKeyId) {
|
|
424
703
|
setAwsAccessKeyId(loadedSettings.bedrock.accessKeyId);
|
|
@@ -514,7 +793,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
514
793
|
const loadOpenRouterModels = async (apiKey?: string) => {
|
|
515
794
|
try {
|
|
516
795
|
setLoadingOpenRouterModels(true);
|
|
517
|
-
const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey);
|
|
796
|
+
const models = await window.electronAPI.getOpenRouterModels(apiKey || openrouterApiKey, openrouterBaseUrl || undefined);
|
|
518
797
|
setOpenrouterModels(models || []);
|
|
519
798
|
// If we got models and current model isn't in the list, select the first one
|
|
520
799
|
if (models && models.length > 0 && !models.some(m => m.id === openrouterModel)) {
|
|
@@ -549,6 +828,93 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
549
828
|
}
|
|
550
829
|
};
|
|
551
830
|
|
|
831
|
+
const loadGroqModels = async (apiKey?: string) => {
|
|
832
|
+
try {
|
|
833
|
+
setLoadingGroqModels(true);
|
|
834
|
+
const models = await window.electronAPI.getGroqModels(apiKey || groqApiKey, groqBaseUrl || undefined);
|
|
835
|
+
setGroqModels(models || []);
|
|
836
|
+
if (models && models.length > 0 && !models.some(m => m.id === groqModel)) {
|
|
837
|
+
setGroqModel(models[0].id);
|
|
838
|
+
}
|
|
839
|
+
onSettingsChanged?.();
|
|
840
|
+
} catch (error) {
|
|
841
|
+
console.error('Failed to load Groq models:', error);
|
|
842
|
+
setGroqModels([]);
|
|
843
|
+
} finally {
|
|
844
|
+
setLoadingGroqModels(false);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const loadXAIModels = async (apiKey?: string) => {
|
|
849
|
+
try {
|
|
850
|
+
setLoadingXaiModels(true);
|
|
851
|
+
const models = await window.electronAPI.getXAIModels(apiKey || xaiApiKey, xaiBaseUrl || undefined);
|
|
852
|
+
setXaiModels(models || []);
|
|
853
|
+
if (models && models.length > 0 && !models.some(m => m.id === xaiModel)) {
|
|
854
|
+
setXaiModel(models[0].id);
|
|
855
|
+
}
|
|
856
|
+
onSettingsChanged?.();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.error('Failed to load xAI models:', error);
|
|
859
|
+
setXaiModels([]);
|
|
860
|
+
} finally {
|
|
861
|
+
setLoadingXaiModels(false);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const loadKimiModels = async (apiKey?: string) => {
|
|
866
|
+
try {
|
|
867
|
+
setLoadingKimiModels(true);
|
|
868
|
+
const models = await window.electronAPI.getKimiModels(apiKey || kimiApiKey, kimiBaseUrl || undefined);
|
|
869
|
+
setKimiModels(models || []);
|
|
870
|
+
if (models && models.length > 0 && !models.some(m => m.id === kimiModel)) {
|
|
871
|
+
setKimiModel(models[0].id);
|
|
872
|
+
}
|
|
873
|
+
onSettingsChanged?.();
|
|
874
|
+
} catch (error) {
|
|
875
|
+
console.error('Failed to load Kimi models:', error);
|
|
876
|
+
setKimiModels([]);
|
|
877
|
+
} finally {
|
|
878
|
+
setLoadingKimiModels(false);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const handleProviderSelect = (providerType: LLMProviderType) => {
|
|
883
|
+
setSettings((prev) => ({ ...prev, providerType }));
|
|
884
|
+
|
|
885
|
+
const resolvedCustomType = resolveCustomProviderId(providerType);
|
|
886
|
+
const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedCustomType);
|
|
887
|
+
if (customEntry) {
|
|
888
|
+
setCustomProviders((prev) => {
|
|
889
|
+
const existing = prev[resolvedCustomType] || {};
|
|
890
|
+
const updated: CustomProviderConfig = { ...existing };
|
|
891
|
+
if (!updated.model && customEntry.defaultModel) {
|
|
892
|
+
updated.model = customEntry.defaultModel;
|
|
893
|
+
}
|
|
894
|
+
if (!updated.baseUrl && customEntry.baseUrl) {
|
|
895
|
+
updated.baseUrl = customEntry.baseUrl;
|
|
896
|
+
}
|
|
897
|
+
return { ...prev, [resolvedCustomType]: updated };
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (providerType === 'ollama') {
|
|
902
|
+
loadOllamaModels();
|
|
903
|
+
} else if (providerType === 'gemini') {
|
|
904
|
+
loadGeminiModels();
|
|
905
|
+
} else if (providerType === 'openrouter') {
|
|
906
|
+
loadOpenRouterModels();
|
|
907
|
+
} else if (providerType === 'openai') {
|
|
908
|
+
loadOpenAIModels();
|
|
909
|
+
} else if (providerType === 'groq') {
|
|
910
|
+
loadGroqModels();
|
|
911
|
+
} else if (providerType === 'xai') {
|
|
912
|
+
loadXAIModels();
|
|
913
|
+
} else if (providerType === 'kimi') {
|
|
914
|
+
loadKimiModels();
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
552
918
|
const handleOpenAIOAuthLogin = async () => {
|
|
553
919
|
try {
|
|
554
920
|
setOpenaiOAuthLoading(true);
|
|
@@ -613,6 +979,22 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
613
979
|
setSaving(true);
|
|
614
980
|
setTestResult(null);
|
|
615
981
|
|
|
982
|
+
const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
|
|
983
|
+
const resolvedProviderTypeForSave = resolveCustomProviderId(settings.providerType as LLMProviderType);
|
|
984
|
+
const selectedCustomEntry = CUSTOM_PROVIDER_MAP.get(resolvedProviderTypeForSave);
|
|
985
|
+
if (selectedCustomEntry) {
|
|
986
|
+
const existing = sanitizedCustomProviders[resolvedProviderTypeForSave] || {};
|
|
987
|
+
const withDefaults: CustomProviderConfig = { ...existing };
|
|
988
|
+
if (!withDefaults.model && selectedCustomEntry.defaultModel) {
|
|
989
|
+
withDefaults.model = selectedCustomEntry.defaultModel;
|
|
990
|
+
}
|
|
991
|
+
if (!withDefaults.baseUrl && selectedCustomEntry.baseUrl) {
|
|
992
|
+
withDefaults.baseUrl = selectedCustomEntry.baseUrl;
|
|
993
|
+
}
|
|
994
|
+
sanitizedCustomProviders[resolvedProviderTypeForSave] = withDefaults;
|
|
995
|
+
}
|
|
996
|
+
const azureSettings = buildAzureSettings();
|
|
997
|
+
|
|
616
998
|
// Always save settings for ALL providers to preserve API keys and model selections
|
|
617
999
|
// when switching between providers
|
|
618
1000
|
const settingsToSave: LLMSettingsData = {
|
|
@@ -648,6 +1030,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
648
1030
|
openrouter: {
|
|
649
1031
|
apiKey: openrouterApiKey || undefined,
|
|
650
1032
|
model: openrouterModel || undefined,
|
|
1033
|
+
baseUrl: openrouterBaseUrl || undefined,
|
|
651
1034
|
},
|
|
652
1035
|
// Always include openai settings
|
|
653
1036
|
openai: {
|
|
@@ -655,6 +1038,33 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
655
1038
|
model: openaiModel || undefined,
|
|
656
1039
|
authMethod: openaiAuthMethod,
|
|
657
1040
|
},
|
|
1041
|
+
// Always include Azure OpenAI settings
|
|
1042
|
+
azure: {
|
|
1043
|
+
apiKey: azureApiKey || undefined,
|
|
1044
|
+
endpoint: azureEndpoint || undefined,
|
|
1045
|
+
deployment: azureSettings.deployment,
|
|
1046
|
+
deployments: azureSettings.deployments,
|
|
1047
|
+
apiVersion: azureApiVersion || undefined,
|
|
1048
|
+
},
|
|
1049
|
+
// Always include Groq settings
|
|
1050
|
+
groq: {
|
|
1051
|
+
apiKey: groqApiKey || undefined,
|
|
1052
|
+
model: groqModel || undefined,
|
|
1053
|
+
baseUrl: groqBaseUrl || undefined,
|
|
1054
|
+
},
|
|
1055
|
+
// Always include xAI settings
|
|
1056
|
+
xai: {
|
|
1057
|
+
apiKey: xaiApiKey || undefined,
|
|
1058
|
+
model: xaiModel || undefined,
|
|
1059
|
+
baseUrl: xaiBaseUrl || undefined,
|
|
1060
|
+
},
|
|
1061
|
+
// Always include Kimi settings
|
|
1062
|
+
kimi: {
|
|
1063
|
+
apiKey: kimiApiKey || undefined,
|
|
1064
|
+
model: kimiModel || undefined,
|
|
1065
|
+
baseUrl: kimiBaseUrl || undefined,
|
|
1066
|
+
},
|
|
1067
|
+
customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
|
|
658
1068
|
};
|
|
659
1069
|
|
|
660
1070
|
await window.electronAPI.saveLLMSettings(settingsToSave);
|
|
@@ -672,6 +1082,9 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
672
1082
|
setTesting(true);
|
|
673
1083
|
setTestResult(null);
|
|
674
1084
|
|
|
1085
|
+
const sanitizedCustomProviders = sanitizeCustomProviders(customProviders) || {};
|
|
1086
|
+
const azureSettings = buildAzureSettings();
|
|
1087
|
+
|
|
675
1088
|
const testConfig = {
|
|
676
1089
|
providerType: settings.providerType,
|
|
677
1090
|
modelKey: settings.modelKey,
|
|
@@ -699,6 +1112,7 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
699
1112
|
openrouter: settings.providerType === 'openrouter' ? {
|
|
700
1113
|
apiKey: openrouterApiKey || undefined,
|
|
701
1114
|
model: openrouterModel || undefined,
|
|
1115
|
+
baseUrl: openrouterBaseUrl || undefined,
|
|
702
1116
|
} : undefined,
|
|
703
1117
|
openai: settings.providerType === 'openai' ? {
|
|
704
1118
|
apiKey: openaiAuthMethod === 'api_key' ? (openaiApiKey || undefined) : undefined,
|
|
@@ -706,6 +1120,29 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
706
1120
|
authMethod: openaiAuthMethod,
|
|
707
1121
|
// OAuth tokens are handled by the backend from stored settings
|
|
708
1122
|
} : undefined,
|
|
1123
|
+
azure: settings.providerType === 'azure' ? {
|
|
1124
|
+
apiKey: azureApiKey || undefined,
|
|
1125
|
+
endpoint: azureEndpoint || undefined,
|
|
1126
|
+
deployment: azureSettings.deployment,
|
|
1127
|
+
deployments: azureSettings.deployments,
|
|
1128
|
+
apiVersion: azureApiVersion || undefined,
|
|
1129
|
+
} : undefined,
|
|
1130
|
+
groq: settings.providerType === 'groq' ? {
|
|
1131
|
+
apiKey: groqApiKey || undefined,
|
|
1132
|
+
model: groqModel || undefined,
|
|
1133
|
+
baseUrl: groqBaseUrl || undefined,
|
|
1134
|
+
} : undefined,
|
|
1135
|
+
xai: settings.providerType === 'xai' ? {
|
|
1136
|
+
apiKey: xaiApiKey || undefined,
|
|
1137
|
+
model: xaiModel || undefined,
|
|
1138
|
+
baseUrl: xaiBaseUrl || undefined,
|
|
1139
|
+
} : undefined,
|
|
1140
|
+
kimi: settings.providerType === 'kimi' ? {
|
|
1141
|
+
apiKey: kimiApiKey || undefined,
|
|
1142
|
+
model: kimiModel || undefined,
|
|
1143
|
+
baseUrl: kimiBaseUrl || undefined,
|
|
1144
|
+
} : undefined,
|
|
1145
|
+
customProviders: Object.keys(sanitizedCustomProviders).length > 0 ? sanitizedCustomProviders : undefined,
|
|
709
1146
|
};
|
|
710
1147
|
|
|
711
1148
|
const result = await window.electronAPI.testLLMProvider(testConfig);
|
|
@@ -717,21 +1154,21 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
717
1154
|
}
|
|
718
1155
|
};
|
|
719
1156
|
|
|
1157
|
+
const resolvedProviderType = resolveCustomProviderId(settings.providerType as LLMProviderType);
|
|
1158
|
+
const selectedCustomProvider = CUSTOM_PROVIDER_MAP.get(resolvedProviderType);
|
|
1159
|
+
const selectedCustomConfig = selectedCustomProvider ? (customProviders[resolvedProviderType] || {}) : {};
|
|
1160
|
+
|
|
720
1161
|
return (
|
|
721
1162
|
<div className="settings-page">
|
|
722
|
-
<div className="settings-page-header">
|
|
723
|
-
<h1>Settings</h1>
|
|
724
|
-
</div>
|
|
725
|
-
|
|
726
1163
|
<div className="settings-page-layout">
|
|
727
1164
|
<div className="settings-sidebar">
|
|
1165
|
+
<h1 className="settings-sidebar-title">Settings</h1>
|
|
728
1166
|
<button className="settings-back-btn" onClick={onBack}>
|
|
729
1167
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
730
1168
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
731
1169
|
</svg>
|
|
732
1170
|
Back
|
|
733
1171
|
</button>
|
|
734
|
-
<div className="settings-nav-divider" />
|
|
735
1172
|
<div className="settings-sidebar-search">
|
|
736
1173
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
737
1174
|
<circle cx="11" cy="11" r="8" />
|
|
@@ -783,720 +1220,1057 @@ export function Settings({ onBack, onSettingsChanged, themeMode, accentColor, on
|
|
|
783
1220
|
if (item.macOnly && !navigator.platform.toLowerCase().includes('mac')) return false;
|
|
784
1221
|
return item.label.toLowerCase().includes(sidebarSearch.toLowerCase());
|
|
785
1222
|
}).length === 0 && (
|
|
786
|
-
|
|
787
|
-
|
|
1223
|
+
<div className="settings-nav-no-results">No matching settings</div>
|
|
1224
|
+
)}
|
|
788
1225
|
</div>
|
|
789
1226
|
</div>
|
|
790
1227
|
|
|
791
|
-
<div className="settings-content">
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
<
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
<div className="more-channels-
|
|
824
|
-
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1228
|
+
<div className="settings-content-card">
|
|
1229
|
+
<div className="settings-content">
|
|
1230
|
+
{activeTab === 'appearance' ? (
|
|
1231
|
+
<AppearanceSettings
|
|
1232
|
+
themeMode={themeMode}
|
|
1233
|
+
visualTheme={visualTheme}
|
|
1234
|
+
accentColor={accentColor}
|
|
1235
|
+
onThemeChange={onThemeChange}
|
|
1236
|
+
onVisualThemeChange={onVisualThemeChange}
|
|
1237
|
+
onAccentChange={onAccentChange}
|
|
1238
|
+
onShowOnboarding={onShowOnboarding}
|
|
1239
|
+
onboardingCompletedAt={onboardingCompletedAt}
|
|
1240
|
+
/>
|
|
1241
|
+
) : activeTab === 'personality' ? (
|
|
1242
|
+
<PersonalitySettings onSettingsChanged={onSettingsChanged} />
|
|
1243
|
+
) : activeTab === 'missioncontrol' ? (
|
|
1244
|
+
<MissionControlPanel />
|
|
1245
|
+
) : activeTab === 'tray' ? (
|
|
1246
|
+
<TraySettings />
|
|
1247
|
+
) : activeTab === 'voice' ? (
|
|
1248
|
+
<VoiceSettings />
|
|
1249
|
+
) : activeTab === 'telegram' ? (
|
|
1250
|
+
<TelegramSettings />
|
|
1251
|
+
) : activeTab === 'slack' ? (
|
|
1252
|
+
<SlackSettings />
|
|
1253
|
+
) : activeTab === 'whatsapp' ? (
|
|
1254
|
+
<WhatsAppSettings />
|
|
1255
|
+
) : activeTab === 'teams' ? (
|
|
1256
|
+
<TeamsSettings />
|
|
1257
|
+
) : activeTab === 'x' ? (
|
|
1258
|
+
<XSettings />
|
|
1259
|
+
) : activeTab === 'morechannels' ? (
|
|
1260
|
+
<div className="more-channels-panel">
|
|
1261
|
+
<div className="more-channels-header">
|
|
1262
|
+
<h2>More Channels</h2>
|
|
1263
|
+
<p className="settings-description">Configure additional messaging platforms</p>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div className="more-channels-tabs">
|
|
1266
|
+
{secondaryChannelItems.map(item => (
|
|
1267
|
+
<button
|
|
1268
|
+
key={item.key}
|
|
1269
|
+
className={`more-channels-tab ${activeSecondaryChannel === item.key ? 'active' : ''}`}
|
|
1270
|
+
onClick={() => setActiveSecondaryChannel(item.key)}
|
|
1271
|
+
>
|
|
1272
|
+
{item.icon}
|
|
1273
|
+
<span>{item.label}</span>
|
|
1274
|
+
</button>
|
|
1275
|
+
))}
|
|
1276
|
+
</div>
|
|
1277
|
+
<div className="more-channels-content">
|
|
1278
|
+
{activeSecondaryChannel === 'discord' && <DiscordSettings />}
|
|
1279
|
+
{activeSecondaryChannel === 'imessage' && <ImessageSettings />}
|
|
1280
|
+
{activeSecondaryChannel === 'signal' && <SignalSettings />}
|
|
1281
|
+
{activeSecondaryChannel === 'mattermost' && <MattermostSettings />}
|
|
1282
|
+
{activeSecondaryChannel === 'matrix' && <MatrixSettings />}
|
|
1283
|
+
{activeSecondaryChannel === 'twitch' && <TwitchSettings />}
|
|
1284
|
+
{activeSecondaryChannel === 'line' && <LineSettings />}
|
|
1285
|
+
{activeSecondaryChannel === 'bluebubbles' && <BlueBubblesSettings />}
|
|
1286
|
+
{activeSecondaryChannel === 'email' && <EmailSettings />}
|
|
1287
|
+
{activeSecondaryChannel === 'googlechat' && <GoogleChatSettings />}
|
|
1288
|
+
</div>
|
|
834
1289
|
</div>
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1290
|
+
) : activeTab === 'integrations' ? (
|
|
1291
|
+
<div className="integrations-panel">
|
|
1292
|
+
<div className="integrations-header">
|
|
1293
|
+
<h2>Integrations</h2>
|
|
1294
|
+
<p className="settings-description">Connect productivity and storage tools for the agent</p>
|
|
1295
|
+
</div>
|
|
1296
|
+
<div className="integrations-tabs">
|
|
1297
|
+
{integrationItems.map(item => (
|
|
1298
|
+
<button
|
|
1299
|
+
key={item.key}
|
|
1300
|
+
className={`integrations-tab ${activeIntegration === item.key ? 'active' : ''}`}
|
|
1301
|
+
onClick={() => setActiveIntegration(item.key)}
|
|
1302
|
+
>
|
|
1303
|
+
{item.icon}
|
|
1304
|
+
<span>{item.label}</span>
|
|
1305
|
+
</button>
|
|
1306
|
+
))}
|
|
1307
|
+
</div>
|
|
1308
|
+
<div className="integrations-content">
|
|
1309
|
+
{activeIntegration === 'notion' && <NotionSettings />}
|
|
1310
|
+
{activeIntegration === 'box' && <BoxSettings />}
|
|
1311
|
+
{activeIntegration === 'onedrive' && <OneDriveSettings />}
|
|
1312
|
+
{activeIntegration === 'googleworkspace' && <GoogleWorkspaceSettings />}
|
|
1313
|
+
{activeIntegration === 'dropbox' && <DropboxSettings />}
|
|
1314
|
+
{activeIntegration === 'sharepoint' && <SharePointSettings />}
|
|
1315
|
+
</div>
|
|
847
1316
|
</div>
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
<div className="
|
|
880
|
-
<
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1317
|
+
) : activeTab === 'search' ? (
|
|
1318
|
+
<SearchSettings />
|
|
1319
|
+
) : activeTab === 'updates' ? (
|
|
1320
|
+
<UpdateSettings />
|
|
1321
|
+
) : activeTab === 'guardrails' ? (
|
|
1322
|
+
<GuardrailSettings />
|
|
1323
|
+
) : activeTab === 'queue' ? (
|
|
1324
|
+
<QueueSettings />
|
|
1325
|
+
) : activeTab === 'skills' ? (
|
|
1326
|
+
<SkillsSettings />
|
|
1327
|
+
) : activeTab === 'skillhub' ? (
|
|
1328
|
+
<SkillHubBrowser />
|
|
1329
|
+
) : activeTab === 'scheduled' ? (
|
|
1330
|
+
<ScheduledTasksSettings />
|
|
1331
|
+
) : activeTab === 'connectors' ? (
|
|
1332
|
+
<ConnectorsSettings />
|
|
1333
|
+
) : activeTab === 'mcp' ? (
|
|
1334
|
+
<MCPSettings />
|
|
1335
|
+
) : activeTab === 'tools' ? (
|
|
1336
|
+
<BuiltinToolsSettings />
|
|
1337
|
+
) : activeTab === 'hooks' ? (
|
|
1338
|
+
<HooksSettings />
|
|
1339
|
+
) : activeTab === 'controlplane' ? (
|
|
1340
|
+
<ControlPlaneSettings />
|
|
1341
|
+
) : activeTab === 'nodes' ? (
|
|
1342
|
+
<NodesSettings />
|
|
1343
|
+
) : activeTab === 'extensions' ? (
|
|
1344
|
+
<ExtensionsSettings />
|
|
1345
|
+
) : loading ? (
|
|
1346
|
+
<div className="settings-loading">Loading settings...</div>
|
|
1347
|
+
) : (
|
|
1348
|
+
<div className="llm-provider-panel">
|
|
1349
|
+
<div className="llm-provider-header">
|
|
1350
|
+
<h2>LLM Provider</h2>
|
|
1351
|
+
<p className="settings-description">
|
|
1352
|
+
Choose which service to use for AI model calls
|
|
1353
|
+
</p>
|
|
1354
|
+
</div>
|
|
1355
|
+
<div className="llm-provider-tabs">
|
|
886
1356
|
{providers.map(provider => {
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
const isOpenRouter = provider.type === 'openrouter';
|
|
892
|
-
const isOpenAI = provider.type === 'openai';
|
|
1357
|
+
const providerType = provider.type as LLMProviderType;
|
|
1358
|
+
const resolvedCustomType = resolveCustomProviderId(providerType);
|
|
1359
|
+
const customEntry = CUSTOM_PROVIDER_MAP.get(resolvedCustomType);
|
|
1360
|
+
const icon = getLLMProviderIcon(providerType, customEntry);
|
|
893
1361
|
|
|
894
1362
|
return (
|
|
895
|
-
<
|
|
1363
|
+
<button
|
|
896
1364
|
key={provider.type}
|
|
897
|
-
|
|
1365
|
+
type="button"
|
|
1366
|
+
className={`llm-provider-tab ${settings.providerType === provider.type ? 'active' : ''} ${provider.configured ? 'configured' : ''}`}
|
|
1367
|
+
onClick={() => handleProviderSelect(providerType)}
|
|
898
1368
|
>
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
setSettings({ ...settings, providerType: provider.type as 'anthropic' | 'bedrock' | 'ollama' | 'gemini' | 'openrouter' | 'openai' });
|
|
906
|
-
// Load models when selecting provider
|
|
907
|
-
if (provider.type === 'ollama') {
|
|
908
|
-
loadOllamaModels();
|
|
909
|
-
} else if (provider.type === 'gemini') {
|
|
910
|
-
loadGeminiModels();
|
|
911
|
-
} else if (provider.type === 'openrouter') {
|
|
912
|
-
loadOpenRouterModels();
|
|
913
|
-
} else if (provider.type === 'openai') {
|
|
914
|
-
loadOpenAIModels();
|
|
915
|
-
}
|
|
916
|
-
}}
|
|
917
|
-
/>
|
|
918
|
-
<div className="provider-option-content">
|
|
919
|
-
<div className="provider-option-title">
|
|
920
|
-
{provider.name}
|
|
921
|
-
{provider.configured && (
|
|
922
|
-
<span className="provider-configured" title="Credentials detected">
|
|
923
|
-
[Configured]
|
|
924
|
-
</span>
|
|
925
|
-
)}
|
|
926
|
-
</div>
|
|
927
|
-
<div className="provider-option-description">
|
|
928
|
-
{isAnthropic && provider.configured && (
|
|
929
|
-
<>API key configured</>
|
|
930
|
-
)}
|
|
931
|
-
{isAnthropic && !provider.configured && (
|
|
932
|
-
<>Enter your Anthropic API key below</>
|
|
933
|
-
)}
|
|
934
|
-
{isGemini && provider.configured && (
|
|
935
|
-
<>API key configured</>
|
|
936
|
-
)}
|
|
937
|
-
{isGemini && !provider.configured && (
|
|
938
|
-
<>Enter your Gemini API key below</>
|
|
939
|
-
)}
|
|
940
|
-
{isOpenRouter && provider.configured && (
|
|
941
|
-
<>API key configured</>
|
|
942
|
-
)}
|
|
943
|
-
{isOpenRouter && !provider.configured && (
|
|
944
|
-
<>Enter your OpenRouter API key below</>
|
|
945
|
-
)}
|
|
946
|
-
{isOpenAI && provider.configured && openaiOAuthConnected && (
|
|
947
|
-
<>Connected via ChatGPT account</>
|
|
948
|
-
)}
|
|
949
|
-
{isOpenAI && provider.configured && !openaiOAuthConnected && (
|
|
950
|
-
<>API key configured</>
|
|
951
|
-
)}
|
|
952
|
-
{isOpenAI && !provider.configured && (
|
|
953
|
-
<>Sign in with ChatGPT or enter API key</>
|
|
954
|
-
)}
|
|
955
|
-
{isBedrock && provider.configured && (
|
|
956
|
-
<>AWS credentials configured</>
|
|
957
|
-
)}
|
|
958
|
-
{isBedrock && !provider.configured && (
|
|
959
|
-
<>Configure your AWS credentials below</>
|
|
960
|
-
)}
|
|
961
|
-
{isOllama && provider.configured && (
|
|
962
|
-
<>Ollama server detected - configure model below</>
|
|
963
|
-
)}
|
|
964
|
-
{isOllama && !provider.configured && (
|
|
965
|
-
<>Run local LLM models with Ollama</>
|
|
966
|
-
)}
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
</label>
|
|
1369
|
+
{icon}
|
|
1370
|
+
<span className="llm-provider-tab-label">{provider.name}</span>
|
|
1371
|
+
{provider.configured && (
|
|
1372
|
+
<span className="llm-provider-tab-status" title="Configured" />
|
|
1373
|
+
)}
|
|
1374
|
+
</button>
|
|
970
1375
|
);
|
|
971
1376
|
})}
|
|
972
1377
|
</div>
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
|
|
982
|
-
>
|
|
983
|
-
{models.map(model => (
|
|
984
|
-
<option key={model.key} value={model.key}>
|
|
985
|
-
{model.displayName}
|
|
986
|
-
</option>
|
|
987
|
-
))}
|
|
988
|
-
</select>
|
|
989
|
-
</div>
|
|
990
|
-
)}
|
|
991
|
-
|
|
992
|
-
{settings.providerType === 'anthropic' && (
|
|
993
|
-
<div className="settings-section">
|
|
994
|
-
<h3>Anthropic API Key</h3>
|
|
995
|
-
<p className="settings-description">
|
|
996
|
-
Enter your API key from{' '}
|
|
997
|
-
<a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
|
|
998
|
-
console.anthropic.com
|
|
999
|
-
</a>
|
|
1000
|
-
</p>
|
|
1001
|
-
<input
|
|
1002
|
-
type="password"
|
|
1003
|
-
className="settings-input"
|
|
1004
|
-
placeholder="sk-ant-..."
|
|
1005
|
-
value={anthropicApiKey}
|
|
1006
|
-
onChange={(e) => setAnthropicApiKey(e.target.value)}
|
|
1007
|
-
/>
|
|
1008
|
-
</div>
|
|
1009
|
-
)}
|
|
1010
|
-
|
|
1011
|
-
{settings.providerType === 'gemini' && (
|
|
1012
|
-
<>
|
|
1013
|
-
<div className="settings-section">
|
|
1014
|
-
<h3>Gemini API Key</h3>
|
|
1015
|
-
<p className="settings-description">
|
|
1016
|
-
Enter your API key from{' '}
|
|
1017
|
-
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
|
|
1018
|
-
Google AI Studio
|
|
1019
|
-
</a>
|
|
1020
|
-
</p>
|
|
1021
|
-
<div className="settings-input-group">
|
|
1022
|
-
<input
|
|
1023
|
-
type="password"
|
|
1024
|
-
className="settings-input"
|
|
1025
|
-
placeholder="AIza..."
|
|
1026
|
-
value={geminiApiKey}
|
|
1027
|
-
onChange={(e) => setGeminiApiKey(e.target.value)}
|
|
1028
|
-
/>
|
|
1029
|
-
<button
|
|
1030
|
-
className="button-small button-secondary"
|
|
1031
|
-
onClick={() => loadGeminiModels(geminiApiKey)}
|
|
1032
|
-
disabled={loadingGeminiModels}
|
|
1378
|
+
<div className="llm-provider-content">
|
|
1379
|
+
{settings.providerType === 'anthropic' && (
|
|
1380
|
+
<div className="settings-section">
|
|
1381
|
+
<h3>Model</h3>
|
|
1382
|
+
<select
|
|
1383
|
+
className="settings-select"
|
|
1384
|
+
value={settings.modelKey}
|
|
1385
|
+
onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
|
|
1033
1386
|
>
|
|
1034
|
-
{
|
|
1035
|
-
|
|
1387
|
+
{models.map(model => (
|
|
1388
|
+
<option key={model.key} value={model.key}>
|
|
1389
|
+
{model.displayName}
|
|
1390
|
+
</option>
|
|
1391
|
+
))}
|
|
1392
|
+
</select>
|
|
1036
1393
|
</div>
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
<div className="settings-section">
|
|
1040
|
-
<h3>Model</h3>
|
|
1041
|
-
<p className="settings-description">
|
|
1042
|
-
Select a Gemini model. Enter your API key and click "Refresh Models" to load available models.
|
|
1043
|
-
</p>
|
|
1044
|
-
{geminiModels.length > 0 ? (
|
|
1045
|
-
<SearchableSelect
|
|
1046
|
-
options={geminiModels.map(model => ({
|
|
1047
|
-
value: model.name,
|
|
1048
|
-
label: model.displayName,
|
|
1049
|
-
description: model.description,
|
|
1050
|
-
}))}
|
|
1051
|
-
value={geminiModel}
|
|
1052
|
-
onChange={setGeminiModel}
|
|
1053
|
-
placeholder="Select a model..."
|
|
1054
|
-
/>
|
|
1055
|
-
) : (
|
|
1056
|
-
<input
|
|
1057
|
-
type="text"
|
|
1058
|
-
className="settings-input"
|
|
1059
|
-
placeholder="gemini-2.0-flash"
|
|
1060
|
-
value={geminiModel}
|
|
1061
|
-
onChange={(e) => setGeminiModel(e.target.value)}
|
|
1062
|
-
/>
|
|
1063
|
-
)}
|
|
1064
|
-
</div>
|
|
1065
|
-
</>
|
|
1066
|
-
)}
|
|
1394
|
+
)}
|
|
1067
1395
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
</
|
|
1077
|
-
</p>
|
|
1078
|
-
<div className="settings-input-group">
|
|
1396
|
+
{settings.providerType === 'anthropic' && (
|
|
1397
|
+
<div className="settings-section">
|
|
1398
|
+
<h3>Anthropic API Key</h3>
|
|
1399
|
+
<p className="settings-description">
|
|
1400
|
+
Enter your API key from{' '}
|
|
1401
|
+
<a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
|
|
1402
|
+
console.anthropic.com
|
|
1403
|
+
</a>
|
|
1404
|
+
</p>
|
|
1079
1405
|
<input
|
|
1080
1406
|
type="password"
|
|
1081
1407
|
className="settings-input"
|
|
1082
|
-
placeholder="sk-
|
|
1083
|
-
value={
|
|
1084
|
-
onChange={(e) =>
|
|
1408
|
+
placeholder="sk-ant-..."
|
|
1409
|
+
value={anthropicApiKey}
|
|
1410
|
+
onChange={(e) => setAnthropicApiKey(e.target.value)}
|
|
1085
1411
|
/>
|
|
1086
|
-
<button
|
|
1087
|
-
className="button-small button-secondary"
|
|
1088
|
-
onClick={() => loadOpenRouterModels(openrouterApiKey)}
|
|
1089
|
-
disabled={loadingOpenRouterModels}
|
|
1090
|
-
>
|
|
1091
|
-
{loadingOpenRouterModels ? 'Loading...' : 'Refresh Models'}
|
|
1092
|
-
</button>
|
|
1093
1412
|
</div>
|
|
1094
|
-
|
|
1413
|
+
)}
|
|
1095
1414
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
</p>
|
|
1124
|
-
</div>
|
|
1125
|
-
</>
|
|
1126
|
-
)}
|
|
1415
|
+
{settings.providerType === 'gemini' && (
|
|
1416
|
+
<>
|
|
1417
|
+
<div className="settings-section">
|
|
1418
|
+
<h3>Gemini API Key</h3>
|
|
1419
|
+
<p className="settings-description">
|
|
1420
|
+
Enter your API key from{' '}
|
|
1421
|
+
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">
|
|
1422
|
+
Google AI Studio
|
|
1423
|
+
</a>
|
|
1424
|
+
</p>
|
|
1425
|
+
<div className="settings-input-group">
|
|
1426
|
+
<input
|
|
1427
|
+
type="password"
|
|
1428
|
+
className="settings-input"
|
|
1429
|
+
placeholder="AIza..."
|
|
1430
|
+
value={geminiApiKey}
|
|
1431
|
+
onChange={(e) => setGeminiApiKey(e.target.value)}
|
|
1432
|
+
/>
|
|
1433
|
+
<button
|
|
1434
|
+
className="button-small button-secondary"
|
|
1435
|
+
onClick={() => loadGeminiModels(geminiApiKey)}
|
|
1436
|
+
disabled={loadingGeminiModels}
|
|
1437
|
+
>
|
|
1438
|
+
{loadingGeminiModels ? 'Loading...' : 'Refresh Models'}
|
|
1439
|
+
</button>
|
|
1440
|
+
</div>
|
|
1441
|
+
</div>
|
|
1127
1442
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
</div>
|
|
1443
|
+
<div className="settings-section">
|
|
1444
|
+
<h3>Model</h3>
|
|
1445
|
+
<p className="settings-description">
|
|
1446
|
+
Select a Gemini model. Enter your API key and click "Refresh Models" to load available models.
|
|
1447
|
+
</p>
|
|
1448
|
+
{geminiModels.length > 0 ? (
|
|
1449
|
+
<SearchableSelect
|
|
1450
|
+
options={geminiModels.map(model => ({
|
|
1451
|
+
value: model.name,
|
|
1452
|
+
label: model.displayName,
|
|
1453
|
+
description: model.description,
|
|
1454
|
+
}))}
|
|
1455
|
+
value={geminiModel}
|
|
1456
|
+
onChange={setGeminiModel}
|
|
1457
|
+
placeholder="Select a model..."
|
|
1458
|
+
/>
|
|
1459
|
+
) : (
|
|
1460
|
+
<input
|
|
1461
|
+
type="text"
|
|
1462
|
+
className="settings-input"
|
|
1463
|
+
placeholder="gemini-2.0-flash"
|
|
1464
|
+
value={geminiModel}
|
|
1465
|
+
onChange={(e) => setGeminiModel(e.target.value)}
|
|
1466
|
+
/>
|
|
1467
|
+
)}
|
|
1468
|
+
</div>
|
|
1469
|
+
</>
|
|
1470
|
+
)}
|
|
1157
1471
|
|
|
1158
|
-
{
|
|
1159
|
-
|
|
1160
|
-
<
|
|
1161
|
-
|
|
1162
|
-
<
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1472
|
+
{settings.providerType === 'openrouter' && (
|
|
1473
|
+
<>
|
|
1474
|
+
<div className="settings-section">
|
|
1475
|
+
<h3>OpenRouter API Key</h3>
|
|
1476
|
+
<p className="settings-description">
|
|
1477
|
+
Enter your API key from{' '}
|
|
1478
|
+
<a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">
|
|
1479
|
+
OpenRouter
|
|
1480
|
+
</a>
|
|
1481
|
+
</p>
|
|
1482
|
+
<div className="settings-input-group">
|
|
1483
|
+
<input
|
|
1484
|
+
type="password"
|
|
1485
|
+
className="settings-input"
|
|
1486
|
+
placeholder="sk-or-..."
|
|
1487
|
+
value={openrouterApiKey}
|
|
1488
|
+
onChange={(e) => setOpenrouterApiKey(e.target.value)}
|
|
1489
|
+
/>
|
|
1173
1490
|
<button
|
|
1174
1491
|
className="button-small button-secondary"
|
|
1175
|
-
onClick={
|
|
1176
|
-
disabled={
|
|
1492
|
+
onClick={() => loadOpenRouterModels(openrouterApiKey)}
|
|
1493
|
+
disabled={loadingOpenRouterModels}
|
|
1177
1494
|
>
|
|
1178
|
-
{
|
|
1495
|
+
{loadingOpenRouterModels ? 'Loading...' : 'Refresh Models'}
|
|
1179
1496
|
</button>
|
|
1180
1497
|
</div>
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
<div className="settings-section">
|
|
1501
|
+
<h3>Base URL</h3>
|
|
1502
|
+
<p className="settings-description">
|
|
1503
|
+
Optional override for the OpenRouter API endpoint.
|
|
1504
|
+
</p>
|
|
1505
|
+
<input
|
|
1506
|
+
type="text"
|
|
1507
|
+
className="settings-input"
|
|
1508
|
+
placeholder="https://openrouter.ai/api/v1"
|
|
1509
|
+
value={openrouterBaseUrl}
|
|
1510
|
+
onChange={(e) => setOpenrouterBaseUrl(e.target.value)}
|
|
1511
|
+
/>
|
|
1512
|
+
</div>
|
|
1513
|
+
|
|
1514
|
+
<div className="settings-section">
|
|
1515
|
+
<h3>Model</h3>
|
|
1516
|
+
<p className="settings-description">
|
|
1517
|
+
Select a model from OpenRouter's catalog. Enter your API key and click "Refresh Models" to load available models.
|
|
1518
|
+
</p>
|
|
1519
|
+
{openrouterModels.length > 0 ? (
|
|
1520
|
+
<SearchableSelect
|
|
1521
|
+
options={openrouterModels.map(model => ({
|
|
1522
|
+
value: model.id,
|
|
1523
|
+
label: model.name,
|
|
1524
|
+
description: `${Math.round(model.context_length / 1000)}k context`,
|
|
1525
|
+
}))}
|
|
1526
|
+
value={openrouterModel}
|
|
1527
|
+
onChange={setOpenrouterModel}
|
|
1528
|
+
placeholder="Select a model..."
|
|
1529
|
+
/>
|
|
1530
|
+
) : (
|
|
1531
|
+
<input
|
|
1532
|
+
type="text"
|
|
1533
|
+
className="settings-input"
|
|
1534
|
+
placeholder="anthropic/claude-3.5-sonnet"
|
|
1535
|
+
value={openrouterModel}
|
|
1536
|
+
onChange={(e) => setOpenrouterModel(e.target.value)}
|
|
1537
|
+
/>
|
|
1538
|
+
)}
|
|
1539
|
+
<p className="settings-hint">
|
|
1540
|
+
OpenRouter provides access to many models from different providers (Claude, GPT-4, Llama, etc.) through a unified API.
|
|
1541
|
+
</p>
|
|
1542
|
+
</div>
|
|
1543
|
+
</>
|
|
1544
|
+
)}
|
|
1545
|
+
|
|
1546
|
+
{settings.providerType === 'openai' && (
|
|
1547
|
+
<>
|
|
1548
|
+
<div className="settings-section">
|
|
1549
|
+
<h3>Authentication Method</h3>
|
|
1550
|
+
<p className="settings-description">
|
|
1551
|
+
Choose how to authenticate with OpenAI
|
|
1552
|
+
</p>
|
|
1553
|
+
<div className="auth-method-tabs">
|
|
1186
1554
|
<button
|
|
1187
|
-
className=
|
|
1188
|
-
onClick={
|
|
1189
|
-
disabled={openaiOAuthLoading}
|
|
1555
|
+
className={`auth-method-tab ${openaiAuthMethod === 'oauth' ? 'active' : ''}`}
|
|
1556
|
+
onClick={() => setOpenaiAuthMethod('oauth')}
|
|
1190
1557
|
>
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
Sign in with ChatGPT
|
|
1206
|
-
</>
|
|
1207
|
-
)}
|
|
1558
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1559
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
1560
|
+
<circle cx="12" cy="7" r="4" />
|
|
1561
|
+
</svg>
|
|
1562
|
+
Sign in with ChatGPT
|
|
1563
|
+
</button>
|
|
1564
|
+
<button
|
|
1565
|
+
className={`auth-method-tab ${openaiAuthMethod === 'api_key' ? 'active' : ''}`}
|
|
1566
|
+
onClick={() => setOpenaiAuthMethod('api_key')}
|
|
1567
|
+
>
|
|
1568
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1569
|
+
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
|
1570
|
+
</svg>
|
|
1571
|
+
API Key
|
|
1208
1572
|
</button>
|
|
1209
1573
|
</div>
|
|
1574
|
+
</div>
|
|
1575
|
+
|
|
1576
|
+
{openaiAuthMethod === 'oauth' && (
|
|
1577
|
+
<div className="settings-section">
|
|
1578
|
+
<h3>ChatGPT Account</h3>
|
|
1579
|
+
{openaiOAuthConnected ? (
|
|
1580
|
+
<div className="oauth-connected">
|
|
1581
|
+
<div className="oauth-status">
|
|
1582
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1583
|
+
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
|
|
1584
|
+
<path d="M22 4L12 14.01l-3-3" />
|
|
1585
|
+
</svg>
|
|
1586
|
+
<span>Connected to ChatGPT</span>
|
|
1587
|
+
</div>
|
|
1588
|
+
<p className="settings-description">
|
|
1589
|
+
Your ChatGPT account is connected. You can use GPT-4o and other models with your subscription.
|
|
1590
|
+
</p>
|
|
1591
|
+
<button
|
|
1592
|
+
className="button-small button-secondary"
|
|
1593
|
+
onClick={handleOpenAIOAuthLogout}
|
|
1594
|
+
disabled={openaiOAuthLoading}
|
|
1595
|
+
>
|
|
1596
|
+
{openaiOAuthLoading ? 'Disconnecting...' : 'Disconnect Account'}
|
|
1597
|
+
</button>
|
|
1598
|
+
</div>
|
|
1599
|
+
) : (
|
|
1600
|
+
<div className="oauth-login">
|
|
1601
|
+
<p className="settings-description">
|
|
1602
|
+
Sign in with your ChatGPT account to use GPT-4o, o1, and other models with your subscription.
|
|
1603
|
+
</p>
|
|
1604
|
+
<button
|
|
1605
|
+
className="button-primary oauth-login-btn"
|
|
1606
|
+
onClick={handleOpenAIOAuthLogin}
|
|
1607
|
+
disabled={openaiOAuthLoading}
|
|
1608
|
+
>
|
|
1609
|
+
{openaiOAuthLoading ? (
|
|
1610
|
+
<>
|
|
1611
|
+
<svg className="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1612
|
+
<path d="M21 12a9 9 0 11-6.219-8.56" />
|
|
1613
|
+
</svg>
|
|
1614
|
+
Connecting...
|
|
1615
|
+
</>
|
|
1616
|
+
) : (
|
|
1617
|
+
<>
|
|
1618
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
1619
|
+
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
|
1620
|
+
<polyline points="10 17 15 12 10 7" />
|
|
1621
|
+
<line x1="15" y1="12" x2="3" y2="12" />
|
|
1622
|
+
</svg>
|
|
1623
|
+
Sign in with ChatGPT
|
|
1624
|
+
</>
|
|
1625
|
+
)}
|
|
1626
|
+
</button>
|
|
1627
|
+
</div>
|
|
1628
|
+
)}
|
|
1629
|
+
</div>
|
|
1210
1630
|
)}
|
|
1211
|
-
|
|
1631
|
+
|
|
1632
|
+
{openaiAuthMethod === 'api_key' && (
|
|
1633
|
+
<div className="settings-section">
|
|
1634
|
+
<h3>OpenAI API Key</h3>
|
|
1635
|
+
<p className="settings-description">
|
|
1636
|
+
Enter your API key from{' '}
|
|
1637
|
+
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
|
|
1638
|
+
OpenAI Platform
|
|
1639
|
+
</a>
|
|
1640
|
+
</p>
|
|
1641
|
+
<div className="settings-input-group">
|
|
1642
|
+
<input
|
|
1643
|
+
type="password"
|
|
1644
|
+
className="settings-input"
|
|
1645
|
+
placeholder="sk-..."
|
|
1646
|
+
value={openaiApiKey}
|
|
1647
|
+
onChange={(e) => setOpenaiApiKey(e.target.value)}
|
|
1648
|
+
/>
|
|
1649
|
+
<button
|
|
1650
|
+
className="button-small button-secondary"
|
|
1651
|
+
onClick={() => loadOpenAIModels(openaiApiKey)}
|
|
1652
|
+
disabled={loadingOpenAIModels}
|
|
1653
|
+
>
|
|
1654
|
+
{loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
|
|
1655
|
+
</button>
|
|
1656
|
+
</div>
|
|
1657
|
+
</div>
|
|
1658
|
+
)}
|
|
1659
|
+
|
|
1660
|
+
<div className="settings-section">
|
|
1661
|
+
<h3>Model</h3>
|
|
1662
|
+
<p className="settings-description">
|
|
1663
|
+
{openaiAuthMethod === 'oauth' && openaiOAuthConnected
|
|
1664
|
+
? 'Select a GPT model to use with your ChatGPT subscription.'
|
|
1665
|
+
: 'Select a GPT model. Enter your API key and click "Refresh Models" to load available models.'}
|
|
1666
|
+
</p>
|
|
1667
|
+
{openaiModels.length > 0 ? (
|
|
1668
|
+
<SearchableSelect
|
|
1669
|
+
options={openaiModels.map(model => ({
|
|
1670
|
+
value: model.id,
|
|
1671
|
+
label: model.name,
|
|
1672
|
+
description: model.description,
|
|
1673
|
+
}))}
|
|
1674
|
+
value={openaiModel}
|
|
1675
|
+
onChange={setOpenaiModel}
|
|
1676
|
+
placeholder="Select a model..."
|
|
1677
|
+
/>
|
|
1678
|
+
) : (
|
|
1679
|
+
<input
|
|
1680
|
+
type="text"
|
|
1681
|
+
className="settings-input"
|
|
1682
|
+
placeholder="gpt-4o-mini"
|
|
1683
|
+
value={openaiModel}
|
|
1684
|
+
onChange={(e) => setOpenaiModel(e.target.value)}
|
|
1685
|
+
/>
|
|
1686
|
+
)}
|
|
1687
|
+
{openaiAuthMethod === 'oauth' && openaiOAuthConnected && (
|
|
1688
|
+
<button
|
|
1689
|
+
className="button-small button-secondary"
|
|
1690
|
+
onClick={() => loadOpenAIModels()}
|
|
1691
|
+
disabled={loadingOpenAIModels}
|
|
1692
|
+
style={{ marginTop: '8px' }}
|
|
1693
|
+
>
|
|
1694
|
+
{loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
|
|
1695
|
+
</button>
|
|
1696
|
+
)}
|
|
1697
|
+
</div>
|
|
1698
|
+
</>
|
|
1212
1699
|
)}
|
|
1213
1700
|
|
|
1214
|
-
{
|
|
1215
|
-
|
|
1216
|
-
<
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1701
|
+
{settings.providerType === 'azure' && (
|
|
1702
|
+
<>
|
|
1703
|
+
<div className="settings-section">
|
|
1704
|
+
<h3>Azure OpenAI Endpoint</h3>
|
|
1705
|
+
<p className="settings-description">
|
|
1706
|
+
Enter your Azure OpenAI resource endpoint (for example, <code>https://your-resource.openai.azure.com</code>).
|
|
1707
|
+
</p>
|
|
1708
|
+
<input
|
|
1709
|
+
type="text"
|
|
1710
|
+
className="settings-input"
|
|
1711
|
+
placeholder="https://your-resource.openai.azure.com"
|
|
1712
|
+
value={azureEndpoint}
|
|
1713
|
+
onChange={(e) => setAzureEndpoint(e.target.value)}
|
|
1714
|
+
/>
|
|
1715
|
+
</div>
|
|
1716
|
+
|
|
1717
|
+
<div className="settings-section">
|
|
1718
|
+
<h3>Azure OpenAI API Key</h3>
|
|
1719
|
+
<p className="settings-description">
|
|
1720
|
+
Enter the API key for your Azure OpenAI resource.
|
|
1721
|
+
</p>
|
|
1224
1722
|
<input
|
|
1225
1723
|
type="password"
|
|
1226
1724
|
className="settings-input"
|
|
1227
|
-
placeholder="
|
|
1228
|
-
value={
|
|
1229
|
-
onChange={(e) =>
|
|
1725
|
+
placeholder="Azure API key"
|
|
1726
|
+
value={azureApiKey}
|
|
1727
|
+
onChange={(e) => setAzureApiKey(e.target.value)}
|
|
1230
1728
|
/>
|
|
1231
|
-
<button
|
|
1232
|
-
className="button-small button-secondary"
|
|
1233
|
-
onClick={() => loadOpenAIModels(openaiApiKey)}
|
|
1234
|
-
disabled={loadingOpenAIModels}
|
|
1235
|
-
>
|
|
1236
|
-
{loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
|
|
1237
|
-
</button>
|
|
1238
1729
|
</div>
|
|
1239
|
-
|
|
1730
|
+
|
|
1731
|
+
<div className="settings-section">
|
|
1732
|
+
<h3>Deployment Names</h3>
|
|
1733
|
+
<p className="settings-description">
|
|
1734
|
+
Enter one or more deployment names (one per line). These appear in the model selector.
|
|
1735
|
+
</p>
|
|
1736
|
+
<textarea
|
|
1737
|
+
className="settings-input"
|
|
1738
|
+
placeholder="gpt-4o-mini\nmy-other-deployment"
|
|
1739
|
+
rows={3}
|
|
1740
|
+
value={azureDeploymentsText}
|
|
1741
|
+
onChange={(e) => setAzureDeploymentsText(e.target.value)}
|
|
1742
|
+
/>
|
|
1743
|
+
</div>
|
|
1744
|
+
|
|
1745
|
+
<div className="settings-section">
|
|
1746
|
+
<h3>Default Deployment</h3>
|
|
1747
|
+
<p className="settings-description">
|
|
1748
|
+
Optional. Used for connection tests and initial selection. You can switch models in the main view.
|
|
1749
|
+
</p>
|
|
1750
|
+
<input
|
|
1751
|
+
type="text"
|
|
1752
|
+
className="settings-input"
|
|
1753
|
+
placeholder="gpt-4o-mini"
|
|
1754
|
+
value={azureDeployment}
|
|
1755
|
+
onChange={(e) => setAzureDeployment(e.target.value)}
|
|
1756
|
+
/>
|
|
1757
|
+
</div>
|
|
1758
|
+
|
|
1759
|
+
<div className="settings-section">
|
|
1760
|
+
<h3>API Version</h3>
|
|
1761
|
+
<p className="settings-description">
|
|
1762
|
+
Optional override for the Azure OpenAI API version.
|
|
1763
|
+
</p>
|
|
1764
|
+
<input
|
|
1765
|
+
type="text"
|
|
1766
|
+
className="settings-input"
|
|
1767
|
+
placeholder="2024-02-15-preview"
|
|
1768
|
+
value={azureApiVersion}
|
|
1769
|
+
onChange={(e) => setAzureApiVersion(e.target.value)}
|
|
1770
|
+
/>
|
|
1771
|
+
</div>
|
|
1772
|
+
</>
|
|
1240
1773
|
)}
|
|
1241
1774
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
{openaiAuthMethod === 'oauth' && openaiOAuthConnected && (
|
|
1270
|
-
<button
|
|
1271
|
-
className="button-small button-secondary"
|
|
1272
|
-
onClick={() => loadOpenAIModels()}
|
|
1273
|
-
disabled={loadingOpenAIModels}
|
|
1274
|
-
style={{ marginTop: '8px' }}
|
|
1275
|
-
>
|
|
1276
|
-
{loadingOpenAIModels ? 'Loading...' : 'Refresh Models'}
|
|
1277
|
-
</button>
|
|
1278
|
-
)}
|
|
1279
|
-
</div>
|
|
1280
|
-
</>
|
|
1281
|
-
)}
|
|
1775
|
+
{settings.providerType === 'groq' && (
|
|
1776
|
+
<>
|
|
1777
|
+
<div className="settings-section">
|
|
1778
|
+
<h3>Groq API Key</h3>
|
|
1779
|
+
<p className="settings-description">
|
|
1780
|
+
Enter your API key from{' '}
|
|
1781
|
+
<a href="https://console.groq.com/keys" target="_blank" rel="noopener noreferrer">
|
|
1782
|
+
Groq Console
|
|
1783
|
+
</a>
|
|
1784
|
+
</p>
|
|
1785
|
+
<div className="settings-input-group">
|
|
1786
|
+
<input
|
|
1787
|
+
type="password"
|
|
1788
|
+
className="settings-input"
|
|
1789
|
+
placeholder="gsk_..."
|
|
1790
|
+
value={groqApiKey}
|
|
1791
|
+
onChange={(e) => setGroqApiKey(e.target.value)}
|
|
1792
|
+
/>
|
|
1793
|
+
<button
|
|
1794
|
+
className="button-small button-secondary"
|
|
1795
|
+
onClick={() => loadGroqModels(groqApiKey)}
|
|
1796
|
+
disabled={loadingGroqModels}
|
|
1797
|
+
>
|
|
1798
|
+
{loadingGroqModels ? 'Loading...' : 'Refresh Models'}
|
|
1799
|
+
</button>
|
|
1800
|
+
</div>
|
|
1801
|
+
</div>
|
|
1282
1802
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
|
|
1297
|
-
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
|
1298
|
-
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
|
|
1299
|
-
</select>
|
|
1300
|
-
</div>
|
|
1803
|
+
<div className="settings-section">
|
|
1804
|
+
<h3>Base URL</h3>
|
|
1805
|
+
<p className="settings-description">
|
|
1806
|
+
Optional override for the Groq API endpoint.
|
|
1807
|
+
</p>
|
|
1808
|
+
<input
|
|
1809
|
+
type="text"
|
|
1810
|
+
className="settings-input"
|
|
1811
|
+
placeholder="https://api.groq.com/openai/v1"
|
|
1812
|
+
value={groqBaseUrl}
|
|
1813
|
+
onChange={(e) => setGroqBaseUrl(e.target.value)}
|
|
1814
|
+
/>
|
|
1815
|
+
</div>
|
|
1301
1816
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1817
|
+
<div className="settings-section">
|
|
1818
|
+
<h3>Model</h3>
|
|
1819
|
+
<p className="settings-description">
|
|
1820
|
+
Select a Groq model. Enter your API key and click "Refresh Models" to load available models.
|
|
1821
|
+
</p>
|
|
1822
|
+
{groqModels.length > 0 ? (
|
|
1823
|
+
<SearchableSelect
|
|
1824
|
+
options={groqModels.map(model => ({
|
|
1825
|
+
value: model.id,
|
|
1826
|
+
label: model.name,
|
|
1827
|
+
}))}
|
|
1828
|
+
value={groqModel}
|
|
1829
|
+
onChange={setGroqModel}
|
|
1830
|
+
placeholder="Select a model..."
|
|
1831
|
+
/>
|
|
1832
|
+
) : (
|
|
1833
|
+
<input
|
|
1834
|
+
type="text"
|
|
1835
|
+
className="settings-input"
|
|
1836
|
+
placeholder="llama-3.1-8b-instant"
|
|
1837
|
+
value={groqModel}
|
|
1838
|
+
onChange={(e) => setGroqModel(e.target.value)}
|
|
1839
|
+
/>
|
|
1840
|
+
)}
|
|
1841
|
+
</div>
|
|
1842
|
+
</>
|
|
1843
|
+
)}
|
|
1304
1844
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1845
|
+
{settings.providerType === 'xai' && (
|
|
1846
|
+
<>
|
|
1847
|
+
<div className="settings-section">
|
|
1848
|
+
<h3>xAI API Key</h3>
|
|
1849
|
+
<p className="settings-description">
|
|
1850
|
+
Enter your API key from{' '}
|
|
1851
|
+
<a href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
|
|
1852
|
+
xAI Console
|
|
1853
|
+
</a>
|
|
1854
|
+
</p>
|
|
1855
|
+
<div className="settings-input-group">
|
|
1856
|
+
<input
|
|
1857
|
+
type="password"
|
|
1858
|
+
className="settings-input"
|
|
1859
|
+
placeholder="xai-..."
|
|
1860
|
+
value={xaiApiKey}
|
|
1861
|
+
onChange={(e) => setXaiApiKey(e.target.value)}
|
|
1862
|
+
/>
|
|
1863
|
+
<button
|
|
1864
|
+
className="button-small button-secondary"
|
|
1865
|
+
onClick={() => loadXAIModels(xaiApiKey)}
|
|
1866
|
+
disabled={loadingXaiModels}
|
|
1867
|
+
>
|
|
1868
|
+
{loadingXaiModels ? 'Loading...' : 'Refresh Models'}
|
|
1869
|
+
</button>
|
|
1870
|
+
</div>
|
|
1871
|
+
</div>
|
|
1313
1872
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1873
|
+
<div className="settings-section">
|
|
1874
|
+
<h3>Base URL</h3>
|
|
1316
1875
|
<p className="settings-description">
|
|
1317
|
-
|
|
1876
|
+
Optional override for the xAI API endpoint.
|
|
1318
1877
|
</p>
|
|
1319
1878
|
<input
|
|
1320
1879
|
type="text"
|
|
1321
1880
|
className="settings-input"
|
|
1322
|
-
placeholder="
|
|
1323
|
-
value={
|
|
1324
|
-
onChange={(e) =>
|
|
1881
|
+
placeholder="https://api.x.ai/v1"
|
|
1882
|
+
value={xaiBaseUrl}
|
|
1883
|
+
onChange={(e) => setXaiBaseUrl(e.target.value)}
|
|
1325
1884
|
/>
|
|
1326
1885
|
</div>
|
|
1327
|
-
|
|
1328
|
-
<div className="settings-
|
|
1886
|
+
|
|
1887
|
+
<div className="settings-section">
|
|
1888
|
+
<h3>Model</h3>
|
|
1889
|
+
<p className="settings-description">
|
|
1890
|
+
Select a Grok model. Enter your API key and click "Refresh Models" to load available models.
|
|
1891
|
+
</p>
|
|
1892
|
+
{xaiModels.length > 0 ? (
|
|
1893
|
+
<SearchableSelect
|
|
1894
|
+
options={xaiModels.map(model => ({
|
|
1895
|
+
value: model.id,
|
|
1896
|
+
label: model.name,
|
|
1897
|
+
}))}
|
|
1898
|
+
value={xaiModel}
|
|
1899
|
+
onChange={setXaiModel}
|
|
1900
|
+
placeholder="Select a model..."
|
|
1901
|
+
/>
|
|
1902
|
+
) : (
|
|
1903
|
+
<input
|
|
1904
|
+
type="text"
|
|
1905
|
+
className="settings-input"
|
|
1906
|
+
placeholder="grok-4-fast-non-reasoning"
|
|
1907
|
+
value={xaiModel}
|
|
1908
|
+
onChange={(e) => setXaiModel(e.target.value)}
|
|
1909
|
+
/>
|
|
1910
|
+
)}
|
|
1911
|
+
</div>
|
|
1912
|
+
</>
|
|
1913
|
+
)}
|
|
1914
|
+
|
|
1915
|
+
{settings.providerType === 'kimi' && (
|
|
1916
|
+
<>
|
|
1917
|
+
<div className="settings-section">
|
|
1918
|
+
<h3>Kimi API Key</h3>
|
|
1919
|
+
<p className="settings-description">
|
|
1920
|
+
Enter your API key from{' '}
|
|
1921
|
+
<a href="https://platform.moonshot.ai/" target="_blank" rel="noopener noreferrer">
|
|
1922
|
+
Moonshot Platform
|
|
1923
|
+
</a>
|
|
1924
|
+
</p>
|
|
1925
|
+
<div className="settings-input-group">
|
|
1926
|
+
<input
|
|
1927
|
+
type="password"
|
|
1928
|
+
className="settings-input"
|
|
1929
|
+
placeholder="sk-..."
|
|
1930
|
+
value={kimiApiKey}
|
|
1931
|
+
onChange={(e) => setKimiApiKey(e.target.value)}
|
|
1932
|
+
/>
|
|
1933
|
+
<button
|
|
1934
|
+
className="button-small button-secondary"
|
|
1935
|
+
onClick={() => loadKimiModels(kimiApiKey)}
|
|
1936
|
+
disabled={loadingKimiModels}
|
|
1937
|
+
>
|
|
1938
|
+
{loadingKimiModels ? 'Loading...' : 'Refresh Models'}
|
|
1939
|
+
</button>
|
|
1940
|
+
</div>
|
|
1941
|
+
</div>
|
|
1942
|
+
|
|
1943
|
+
<div className="settings-section">
|
|
1944
|
+
<h3>Base URL</h3>
|
|
1945
|
+
<p className="settings-description">
|
|
1946
|
+
Optional override for the Kimi API endpoint.
|
|
1947
|
+
</p>
|
|
1329
1948
|
<input
|
|
1330
1949
|
type="text"
|
|
1331
1950
|
className="settings-input"
|
|
1332
|
-
placeholder="
|
|
1333
|
-
value={
|
|
1334
|
-
onChange={(e) =>
|
|
1951
|
+
placeholder="https://api.moonshot.ai/v1"
|
|
1952
|
+
value={kimiBaseUrl}
|
|
1953
|
+
onChange={(e) => setKimiBaseUrl(e.target.value)}
|
|
1335
1954
|
/>
|
|
1955
|
+
</div>
|
|
1956
|
+
|
|
1957
|
+
<div className="settings-section">
|
|
1958
|
+
<h3>Model</h3>
|
|
1959
|
+
<p className="settings-description">
|
|
1960
|
+
Select a Kimi model. Enter your API key and click "Refresh Models" to load available models.
|
|
1961
|
+
</p>
|
|
1962
|
+
{kimiModels.length > 0 ? (
|
|
1963
|
+
<SearchableSelect
|
|
1964
|
+
options={kimiModels.map(model => ({
|
|
1965
|
+
value: model.id,
|
|
1966
|
+
label: model.name,
|
|
1967
|
+
}))}
|
|
1968
|
+
value={kimiModel}
|
|
1969
|
+
onChange={setKimiModel}
|
|
1970
|
+
placeholder="Select a model..."
|
|
1971
|
+
/>
|
|
1972
|
+
) : (
|
|
1973
|
+
<input
|
|
1974
|
+
type="text"
|
|
1975
|
+
className="settings-input"
|
|
1976
|
+
placeholder="kimi-k2.5"
|
|
1977
|
+
value={kimiModel}
|
|
1978
|
+
onChange={(e) => setKimiModel(e.target.value)}
|
|
1979
|
+
/>
|
|
1980
|
+
)}
|
|
1981
|
+
</div>
|
|
1982
|
+
</>
|
|
1983
|
+
)}
|
|
1984
|
+
|
|
1985
|
+
{selectedCustomProvider && (
|
|
1986
|
+
<>
|
|
1987
|
+
<div className="settings-section">
|
|
1988
|
+
<h3>{selectedCustomProvider.apiKeyLabel}</h3>
|
|
1989
|
+
{selectedCustomProvider.apiKeyUrl ? (
|
|
1990
|
+
<p className="settings-description">
|
|
1991
|
+
Enter your API key from{' '}
|
|
1992
|
+
<a href={selectedCustomProvider.apiKeyUrl} target="_blank" rel="noopener noreferrer">
|
|
1993
|
+
{selectedCustomProvider.name}
|
|
1994
|
+
</a>
|
|
1995
|
+
</p>
|
|
1996
|
+
) : selectedCustomProvider.description ? (
|
|
1997
|
+
<p className="settings-description">
|
|
1998
|
+
{selectedCustomProvider.description}
|
|
1999
|
+
</p>
|
|
2000
|
+
) : null}
|
|
1336
2001
|
<input
|
|
1337
2002
|
type="password"
|
|
1338
2003
|
className="settings-input"
|
|
1339
|
-
placeholder=
|
|
1340
|
-
value={
|
|
1341
|
-
onChange={(e) =>
|
|
2004
|
+
placeholder={selectedCustomProvider.apiKeyPlaceholder || 'sk-...'}
|
|
2005
|
+
value={selectedCustomConfig.apiKey || ''}
|
|
2006
|
+
onChange={(e) => updateCustomProvider(resolvedProviderType, { apiKey: e.target.value })}
|
|
1342
2007
|
/>
|
|
2008
|
+
{selectedCustomProvider.apiKeyOptional && (
|
|
2009
|
+
<p className="settings-hint">API key is optional for this provider.</p>
|
|
2010
|
+
)}
|
|
1343
2011
|
</div>
|
|
1344
|
-
)}
|
|
1345
|
-
</div>
|
|
1346
2012
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
label: model.name,
|
|
1365
|
-
description: model.description,
|
|
1366
|
-
}))}
|
|
1367
|
-
value={bedrockModel}
|
|
1368
|
-
onChange={setBedrockModel}
|
|
1369
|
-
placeholder="Select a model..."
|
|
1370
|
-
/>
|
|
1371
|
-
) : (
|
|
1372
|
-
<select
|
|
1373
|
-
className="settings-select"
|
|
1374
|
-
value={settings.modelKey}
|
|
1375
|
-
onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
|
|
1376
|
-
>
|
|
1377
|
-
{models.map(model => (
|
|
1378
|
-
<option key={model.key} value={model.key}>
|
|
1379
|
-
{model.displayName}
|
|
1380
|
-
</option>
|
|
1381
|
-
))}
|
|
1382
|
-
</select>
|
|
1383
|
-
)}
|
|
1384
|
-
</div>
|
|
1385
|
-
</>
|
|
1386
|
-
)}
|
|
2013
|
+
{(selectedCustomProvider.requiresBaseUrl || selectedCustomProvider.baseUrl) && (
|
|
2014
|
+
<div className="settings-section">
|
|
2015
|
+
<h3>Base URL</h3>
|
|
2016
|
+
<p className="settings-description">
|
|
2017
|
+
{selectedCustomProvider.requiresBaseUrl
|
|
2018
|
+
? 'Base URL is required for this provider.'
|
|
2019
|
+
: 'Override the default base URL if needed.'}
|
|
2020
|
+
</p>
|
|
2021
|
+
<input
|
|
2022
|
+
type="text"
|
|
2023
|
+
className="settings-input"
|
|
2024
|
+
placeholder={selectedCustomProvider.baseUrl || 'https://...'}
|
|
2025
|
+
value={selectedCustomConfig.baseUrl || ''}
|
|
2026
|
+
onChange={(e) => updateCustomProvider(resolvedProviderType, { baseUrl: e.target.value })}
|
|
2027
|
+
/>
|
|
2028
|
+
</div>
|
|
2029
|
+
)}
|
|
1387
2030
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
<button
|
|
1404
|
-
className="button-small button-secondary"
|
|
1405
|
-
onClick={() => loadOllamaModels(ollamaBaseUrl)}
|
|
1406
|
-
disabled={loadingOllamaModels}
|
|
1407
|
-
>
|
|
1408
|
-
{loadingOllamaModels ? 'Loading...' : 'Refresh Models'}
|
|
1409
|
-
</button>
|
|
1410
|
-
</div>
|
|
1411
|
-
</div>
|
|
2031
|
+
<div className="settings-section">
|
|
2032
|
+
<h3>Model</h3>
|
|
2033
|
+
<p className="settings-description">
|
|
2034
|
+
Enter the model ID to use for {selectedCustomProvider.name}.
|
|
2035
|
+
</p>
|
|
2036
|
+
<input
|
|
2037
|
+
type="text"
|
|
2038
|
+
className="settings-input"
|
|
2039
|
+
placeholder={selectedCustomProvider.defaultModel || 'model-id'}
|
|
2040
|
+
value={selectedCustomConfig.model || ''}
|
|
2041
|
+
onChange={(e) => updateCustomProvider(resolvedProviderType, { model: e.target.value })}
|
|
2042
|
+
/>
|
|
2043
|
+
</div>
|
|
2044
|
+
</>
|
|
2045
|
+
)}
|
|
1412
2046
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
type="text"
|
|
1432
|
-
className="settings-input"
|
|
1433
|
-
placeholder="llama3.2"
|
|
1434
|
-
value={ollamaModel}
|
|
1435
|
-
onChange={(e) => setOllamaModel(e.target.value)}
|
|
1436
|
-
/>
|
|
1437
|
-
)}
|
|
1438
|
-
<p className="settings-hint">
|
|
1439
|
-
Don't have models? Run <code>ollama pull llama3.2</code> to download a model.
|
|
1440
|
-
</p>
|
|
1441
|
-
</div>
|
|
2047
|
+
{settings.providerType === 'bedrock' && (
|
|
2048
|
+
<>
|
|
2049
|
+
<div className="settings-section">
|
|
2050
|
+
<h3>AWS Region</h3>
|
|
2051
|
+
<select
|
|
2052
|
+
className="settings-select"
|
|
2053
|
+
value={awsRegion}
|
|
2054
|
+
onChange={(e) => setAwsRegion(e.target.value)}
|
|
2055
|
+
>
|
|
2056
|
+
<option value="us-east-1">US East (N. Virginia)</option>
|
|
2057
|
+
<option value="us-west-2">US West (Oregon)</option>
|
|
2058
|
+
<option value="eu-west-1">Europe (Ireland)</option>
|
|
2059
|
+
<option value="eu-central-1">Europe (Frankfurt)</option>
|
|
2060
|
+
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
|
|
2061
|
+
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
|
2062
|
+
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
|
|
2063
|
+
</select>
|
|
2064
|
+
</div>
|
|
1442
2065
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2066
|
+
<div className="settings-section">
|
|
2067
|
+
<h3>AWS Credentials</h3>
|
|
2068
|
+
|
|
2069
|
+
<label className="settings-checkbox">
|
|
2070
|
+
<input
|
|
2071
|
+
type="checkbox"
|
|
2072
|
+
checked={useDefaultCredentials}
|
|
2073
|
+
onChange={(e) => setUseDefaultCredentials(e.target.checked)}
|
|
2074
|
+
/>
|
|
2075
|
+
<span>Use default credential chain (recommended)</span>
|
|
2076
|
+
</label>
|
|
2077
|
+
|
|
2078
|
+
{useDefaultCredentials ? (
|
|
2079
|
+
<div className="settings-subsection">
|
|
2080
|
+
<p className="settings-description">
|
|
2081
|
+
Uses AWS credentials from environment variables, shared credentials file (~/.aws/credentials), or IAM role.
|
|
2082
|
+
</p>
|
|
2083
|
+
<input
|
|
2084
|
+
type="text"
|
|
2085
|
+
className="settings-input"
|
|
2086
|
+
placeholder="AWS Profile (optional, e.g., 'default')"
|
|
2087
|
+
value={awsProfile}
|
|
2088
|
+
onChange={(e) => setAwsProfile(e.target.value)}
|
|
2089
|
+
/>
|
|
2090
|
+
</div>
|
|
2091
|
+
) : (
|
|
2092
|
+
<div className="settings-subsection">
|
|
2093
|
+
<input
|
|
2094
|
+
type="text"
|
|
2095
|
+
className="settings-input"
|
|
2096
|
+
placeholder="AWS Access Key ID"
|
|
2097
|
+
value={awsAccessKeyId}
|
|
2098
|
+
onChange={(e) => setAwsAccessKeyId(e.target.value)}
|
|
2099
|
+
/>
|
|
2100
|
+
<input
|
|
2101
|
+
type="password"
|
|
2102
|
+
className="settings-input"
|
|
2103
|
+
placeholder="AWS Secret Access Key"
|
|
2104
|
+
value={awsSecretAccessKey}
|
|
2105
|
+
onChange={(e) => setAwsSecretAccessKey(e.target.value)}
|
|
2106
|
+
/>
|
|
2107
|
+
</div>
|
|
2108
|
+
)}
|
|
2109
|
+
</div>
|
|
1458
2110
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
2111
|
+
<div className="settings-section">
|
|
2112
|
+
<h3>Model</h3>
|
|
2113
|
+
<p className="settings-description">
|
|
2114
|
+
Select a Claude model from AWS Bedrock.{' '}
|
|
2115
|
+
<button
|
|
2116
|
+
className="button-small button-secondary"
|
|
2117
|
+
onClick={loadBedrockModels}
|
|
2118
|
+
disabled={loadingBedrockModels}
|
|
2119
|
+
style={{ marginLeft: '8px' }}
|
|
2120
|
+
>
|
|
2121
|
+
{loadingBedrockModels ? 'Loading...' : 'Refresh Models'}
|
|
2122
|
+
</button>
|
|
2123
|
+
</p>
|
|
2124
|
+
{bedrockModels.length > 0 ? (
|
|
2125
|
+
<SearchableSelect
|
|
2126
|
+
options={bedrockModels.map(model => ({
|
|
2127
|
+
value: model.id,
|
|
2128
|
+
label: model.name,
|
|
2129
|
+
description: model.description,
|
|
2130
|
+
}))}
|
|
2131
|
+
value={bedrockModel}
|
|
2132
|
+
onChange={setBedrockModel}
|
|
2133
|
+
placeholder="Select a model..."
|
|
2134
|
+
/>
|
|
2135
|
+
) : (
|
|
2136
|
+
<select
|
|
2137
|
+
className="settings-select"
|
|
2138
|
+
value={settings.modelKey}
|
|
2139
|
+
onChange={(e) => setSettings({ ...settings, modelKey: e.target.value })}
|
|
2140
|
+
>
|
|
2141
|
+
{models.map(model => (
|
|
2142
|
+
<option key={model.key} value={model.key}>
|
|
2143
|
+
{model.displayName}
|
|
2144
|
+
</option>
|
|
2145
|
+
))}
|
|
2146
|
+
</select>
|
|
2147
|
+
)}
|
|
2148
|
+
</div>
|
|
1468
2149
|
</>
|
|
1469
|
-
)
|
|
2150
|
+
)}
|
|
2151
|
+
|
|
2152
|
+
{settings.providerType === 'ollama' && (
|
|
1470
2153
|
<>
|
|
1471
|
-
<
|
|
1472
|
-
<
|
|
1473
|
-
<
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
2154
|
+
<div className="settings-section">
|
|
2155
|
+
<h3>Ollama Server URL</h3>
|
|
2156
|
+
<p className="settings-description">
|
|
2157
|
+
URL of your Ollama server. Default is http://localhost:11434 for local installations.
|
|
2158
|
+
</p>
|
|
2159
|
+
<div className="settings-input-group">
|
|
2160
|
+
<input
|
|
2161
|
+
type="text"
|
|
2162
|
+
className="settings-input"
|
|
2163
|
+
placeholder="http://localhost:11434"
|
|
2164
|
+
value={ollamaBaseUrl}
|
|
2165
|
+
onChange={(e) => setOllamaBaseUrl(e.target.value)}
|
|
2166
|
+
/>
|
|
2167
|
+
<button
|
|
2168
|
+
className="button-small button-secondary"
|
|
2169
|
+
onClick={() => loadOllamaModels(ollamaBaseUrl)}
|
|
2170
|
+
disabled={loadingOllamaModels}
|
|
2171
|
+
>
|
|
2172
|
+
{loadingOllamaModels ? 'Loading...' : 'Refresh Models'}
|
|
2173
|
+
</button>
|
|
2174
|
+
</div>
|
|
2175
|
+
</div>
|
|
2176
|
+
|
|
2177
|
+
<div className="settings-section">
|
|
2178
|
+
<h3>Model</h3>
|
|
2179
|
+
<p className="settings-description">
|
|
2180
|
+
Select from models available on your Ollama server, or enter a custom model name.
|
|
2181
|
+
</p>
|
|
2182
|
+
{ollamaModels.length > 0 ? (
|
|
2183
|
+
<SearchableSelect
|
|
2184
|
+
options={ollamaModels.map(model => ({
|
|
2185
|
+
value: model.name,
|
|
2186
|
+
label: model.name,
|
|
2187
|
+
description: formatBytes(model.size),
|
|
2188
|
+
}))}
|
|
2189
|
+
value={ollamaModel}
|
|
2190
|
+
onChange={setOllamaModel}
|
|
2191
|
+
placeholder="Select a model..."
|
|
2192
|
+
/>
|
|
2193
|
+
) : (
|
|
2194
|
+
<input
|
|
2195
|
+
type="text"
|
|
2196
|
+
className="settings-input"
|
|
2197
|
+
placeholder="llama3.2"
|
|
2198
|
+
value={ollamaModel}
|
|
2199
|
+
onChange={(e) => setOllamaModel(e.target.value)}
|
|
2200
|
+
/>
|
|
2201
|
+
)}
|
|
2202
|
+
<p className="settings-hint">
|
|
2203
|
+
Don't have models? Run <code>ollama pull llama3.2</code> to download a model.
|
|
2204
|
+
</p>
|
|
2205
|
+
</div>
|
|
2206
|
+
|
|
2207
|
+
<div className="settings-section">
|
|
2208
|
+
<h3>API Key (Optional)</h3>
|
|
2209
|
+
<p className="settings-description">
|
|
2210
|
+
Only needed if connecting to a remote Ollama server that requires authentication.
|
|
2211
|
+
</p>
|
|
2212
|
+
<input
|
|
2213
|
+
type="password"
|
|
2214
|
+
className="settings-input"
|
|
2215
|
+
placeholder="Optional API key for remote servers"
|
|
2216
|
+
value={ollamaApiKey}
|
|
2217
|
+
onChange={(e) => setOllamaApiKey(e.target.value)}
|
|
2218
|
+
/>
|
|
2219
|
+
</div>
|
|
1477
2220
|
</>
|
|
1478
2221
|
)}
|
|
1479
|
-
</div>
|
|
1480
|
-
)}
|
|
1481
2222
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
2223
|
+
{testResult && (
|
|
2224
|
+
<div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
|
|
2225
|
+
{testResult.success ? (
|
|
2226
|
+
<>
|
|
2227
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2228
|
+
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
|
|
2229
|
+
<path d="M22 4L12 14.01l-3-3" />
|
|
2230
|
+
</svg>
|
|
2231
|
+
Connection successful!
|
|
2232
|
+
</>
|
|
2233
|
+
) : (
|
|
2234
|
+
<>
|
|
2235
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2236
|
+
<circle cx="12" cy="12" r="10" />
|
|
2237
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
2238
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
2239
|
+
</svg>
|
|
2240
|
+
<span title={testResult.error}>
|
|
2241
|
+
{(() => {
|
|
2242
|
+
const error = testResult.error || 'Connection failed';
|
|
2243
|
+
// Extract meaningful part before JSON details
|
|
2244
|
+
const jsonStart = error.indexOf(' [{');
|
|
2245
|
+
const truncated = jsonStart > 0 ? error.slice(0, jsonStart) : error;
|
|
2246
|
+
return truncated.length > 200 ? truncated.slice(0, 200) + '...' : truncated;
|
|
2247
|
+
})()}
|
|
2248
|
+
</span>
|
|
2249
|
+
</>
|
|
2250
|
+
)}
|
|
2251
|
+
</div>
|
|
2252
|
+
)}
|
|
2253
|
+
|
|
2254
|
+
<div className="settings-actions">
|
|
2255
|
+
<button
|
|
2256
|
+
className="button-secondary"
|
|
2257
|
+
onClick={handleTestConnection}
|
|
2258
|
+
disabled={loading || testing}
|
|
2259
|
+
>
|
|
2260
|
+
{testing ? 'Testing...' : 'Test Connection'}
|
|
2261
|
+
</button>
|
|
2262
|
+
<button
|
|
2263
|
+
className="button-primary"
|
|
2264
|
+
onClick={handleSave}
|
|
2265
|
+
disabled={loading || saving}
|
|
2266
|
+
>
|
|
2267
|
+
{saving ? 'Saving...' : 'Save Settings'}
|
|
2268
|
+
</button>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
1497
2271
|
</div>
|
|
1498
|
-
|
|
1499
|
-
|
|
2272
|
+
)}
|
|
2273
|
+
</div>
|
|
1500
2274
|
</div>
|
|
1501
2275
|
</div>
|
|
1502
2276
|
</div>
|