diff --git a/Justfile b/Justfile index 1a59759c56..6ab6f4d6a2 100644 --- a/Justfile +++ b/Justfile @@ -63,6 +63,12 @@ run-ui: @echo "Running UI..." cd ui/desktop && npm install && npm run start-gui +# Run UI with alpha changes +run-ui-alpha: + @just release-binary + @echo "Running UI..." + cd ui/desktop && npm install && ALPHA=true npm run start-alpha-gui + # Run UI with latest (Windows version) run-ui-windows: @just release-windows diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 7c05bc1780..4d8b10c6e7 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -2358,6 +2358,18 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -15614,6 +15626,34 @@ "rimraf": "bin.js" } }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index b5a07c7258..f1ac648ab3 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -19,7 +19,8 @@ "lint:check": "eslint \"src/**/*.{ts,tsx}\"", "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", - "prepare": "cd ../.. && husky install" + "prepare": "cd ../.. && husky install", + "start-alpha-gui": "ALPHA=true npm run start-gui" }, "devDependencies": { "@electron-forge/cli": "^7.5.0", diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index a9bb1cd9fa..bbbfe80606 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -16,6 +16,7 @@ import ChatView from './components/ChatView'; import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView'; import MoreModelsView from './components/settings/models/MoreModelsView'; import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView'; +import ProviderSettings from './components/settings/providers/providers/NewProviderSettingsPage'; import 'react-toastify/dist/ReactToastify.css'; @@ -26,7 +27,8 @@ export type View = | 'settings' | 'moreModels' | 'configureProviders' - | 'configPage'; + | 'configPage' + | 'alphaConfigureProviders'; export type ViewConfig = { view: View; @@ -224,6 +226,9 @@ export default function App() { }} /> )} + {view === 'alphaConfigureProviders' && ( + setView('chat')} /> + )} {view === 'chat' && } diff --git a/ui/desktop/src/components/MoreMenu.tsx b/ui/desktop/src/components/MoreMenu.tsx index 7cab710b85..b1bd51aee2 100644 --- a/ui/desktop/src/components/MoreMenu.tsx +++ b/ui/desktop/src/components/MoreMenu.tsx @@ -1,8 +1,7 @@ import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover'; import React, { useEffect, useState } from 'react'; import { More } from './icons'; -import type { View } from '../ChatWindow'; - +import { View } from '../App'; interface VersionInfo { current_version: string; available_versions: string[]; @@ -260,6 +259,17 @@ export default function MoreMenu({ setView }: { setView: (view: View) => void }) > Reset Provider + {process.env.ALPHA && ( + + )} diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx index b29014db80..976984665a 100644 --- a/ui/desktop/src/components/settings/api_keys/utils.tsx +++ b/ui/desktop/src/components/settings/api_keys/utils.tsx @@ -24,22 +24,22 @@ export async function getActiveProviders(): Promise { const configSettings = await getConfigSettings(); const activeProviders = Object.values(configSettings) - .filter((provider) => { - // 1. Get provider's config_status - const configStatus = provider.config_status ?? {}; + .filter((provider) => { + // 1. Get provider's config_status + const configStatus = provider.config_status ?? {}; - // 2. Collect only the keys *not* in default_key_value - const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k)); + // 2. Collect only the keys *not* in default_key_value + const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k)); - // 3. If there are *no* non-default keys, it is NOT active - if (requiredKeyEntries.length === 0) { - return false; - } + // 3. If there are *no* non-default keys, it is NOT active + if (requiredKeyEntries.length === 0) { + return false; + } - // 4. Otherwise, all non-default keys must be `is_set` - return requiredKeyEntries.every(([_, value]) => value?.is_set); - }) - .map((provider) => provider.name || 'Unknown Provider'); + // 4. Otherwise, all non-default keys must be `is_set` + return requiredKeyEntries.every(([_, value]) => value?.is_set); + }) + .map((provider) => provider.name || 'Unknown Provider'); console.log('[GET ACTIVE PROVIDERS]:', activeProviders); return activeProviders; @@ -93,4 +93,4 @@ export async function getProvidersList(): Promise { models: item.details?.models || [], // Nested models array requiredKeys: item.details?.required_keys || [], // Nested required keys array })); -} +} \ No newline at end of file diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx index 504ea15402..b5194943d6 100644 --- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx @@ -235,8 +235,8 @@ export function ConfigureProvidersGrid() {
p.id === selectedForSetup)?.name || ''} - model="Example Model" - endpoint="Example Endpoint" + _model="Example Model" + _endpoint="Example Endpoint" title={ modalMode === 'edit' ? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} Configuration` diff --git a/ui/desktop/src/components/settings/providers/providers/NewProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/providers/NewProviderSettingsPage.tsx new file mode 100644 index 0000000000..8cd41870f8 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/NewProviderSettingsPage.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { ScrollArea } from '../../../ui/scroll-area'; +import BackButton from '../../../ui/BackButton'; +import ProviderGrid from './ProviderGrid'; +import ProviderState from './interfaces/ProviderState'; + +const fakeProviderState: ProviderState[] = [ + { + id: 'openai', + name: 'OpenAI', + isConfigured: true, + metadata: null, + }, + { + id: 'anthropic', + name: 'Anthropic', + isConfigured: false, + metadata: null, + }, + { + id: 'groq', + name: 'Groq', + isConfigured: false, + metadata: null, + }, + { + id: 'google', + name: 'Google', + isConfigured: false, + metadata: null, + }, + { + id: 'openrouter', + name: 'OpenRouter', + isConfigured: false, + metadata: null, + }, + { + id: 'databricks', + name: 'Databricks', + isConfigured: false, + metadata: null, + }, + { + id: 'ollama', + name: 'Ollama', + isConfigured: false, + metadata: { location: null }, + }, +]; + +export default function ProviderSettings({ onClose }: { onClose: () => void }) { + return ( +
+
+ + +
+ +

Configure

+
+ +
+
+

Providers

+
+ + {/* Content Area */} +
+
+ +
+
+
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/providers/ProviderGrid.tsx new file mode 100644 index 0000000000..3f9cdd858e --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/ProviderGrid.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { ProviderCard } from './subcomponents/ProviderCard'; +import ProviderState from './interfaces/ProviderState'; +import OnShowModal from './callbacks/ShowModal'; +import OnAdd from './callbacks/AddProviderParameters'; +import OnDelete from './callbacks/DeleteProviderParameters'; +import OnShowSettings from './callbacks/UpdateProviderParameters'; +import OnRefresh from './callbacks/RefreshActiveProviders'; +import DefaultProviderActions from './subcomponents/actions/DefaultProviderActions'; + +function GridLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function ProviderCards({ providers }: { providers: ProviderState[] }) { + const providerCallbacks = { + onShowModal: OnShowModal, + onAdd: OnAdd, + onDelete: OnDelete, + onShowSettings: OnShowSettings, + onRefresh: OnRefresh, + }; + return ( + <> + {providers.map((provider) => ( + + ))} + + ); +} + +export default function ProviderGrid({ providers }: { providers: ProviderState[] }) { + console.log('got these providers', providers); + return ( + + + + ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/ProviderRegistry.tsx b/ui/desktop/src/components/settings/providers/providers/ProviderRegistry.tsx new file mode 100644 index 0000000000..3a0f7a57be --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/ProviderRegistry.tsx @@ -0,0 +1,242 @@ +import React from 'react'; +import ProviderDetails from './interfaces/ProviderDetails'; +import DefaultProviderActions from './subcomponents/actions/DefaultProviderActions'; +import OllamaActions from './subcomponents/actions/OllamaActions'; + +export interface ProviderRegistry { + name: string; + details: ProviderDetails; +} + +export const PROVIDER_REGISTRY: ProviderRegistry[] = [ + { + name: 'OpenAI', + details: { + id: 'openai', + name: 'OpenAI', + description: 'Access GPT-4, GPT-3.5 Turbo, and other OpenAI models', + parameters: [ + { + name: 'OPENAI_API_KEY', + is_secret: true, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'Anthropic', + details: { + id: 'anthropic', + name: 'Anthropic', + description: 'Access Claude and other Anthropic models', + parameters: [ + { + name: 'ANTHROPIC_API_KEY', + is_secret: true, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'Google', + details: { + id: 'google', + name: 'Google', + description: 'Access Gemini and other Google AI models', + parameters: [ + { + name: 'GOOGLE_API_KEY', + is_secret: true, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'Groq', + details: { + id: 'groq', + name: 'Groq', + description: 'Access Mixtral and other Groq-hosted models', + parameters: [ + { + name: 'GROQ_API_KEY', + is_secret: true, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'Databricks', + details: { + id: 'databricks', + name: 'Databricks', + description: 'Access models hosted on your Databricks instance', + parameters: [ + { + name: 'DATABRICKS_HOST', + is_secret: false, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'OpenRouter', + details: { + id: 'openrouter', + name: 'OpenRouter', + description: 'Access a variety of AI models through OpenRouter', + parameters: [ + { + name: 'OPENROUTER_API_KEY', + is_secret: true, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onShowSettings } = callbacks || {}; + return [ + { + id: 'default-provider-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, + { + name: 'Ollama', + details: { + id: 'ollama', + name: 'Ollama', + description: 'Run and use open-source models locally', + parameters: [ + { + name: 'OLLAMA_HOST', + is_secret: false, + }, + ], + getActions: (provider, callbacks) => { + const { onAdd, onDelete, onRefresh, onShowSettings } = callbacks || {}; + return [ + { + id: 'ollama-actions', + renderButton: () => ( + + ), + }, + ]; + }, + }, + }, +]; + +// const ACTION_IMPLEMENTATIONS = { +// 'default': (provider, callbacks) => [{ +// id: 'default-provider-actions', +// renderButton: () => +// }], +// +// 'ollama': (provider, callbacks) => [{ +// id: 'ollama-actions', +// renderButton: () => +// }] +// }; diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/AddProviderParameters.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/AddProviderParameters.tsx new file mode 100644 index 0000000000..8a15d0706a --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/AddProviderParameters.tsx @@ -0,0 +1,5 @@ +import { toast } from 'react-toastify'; + +export default function OnAdd() { + toast.success('adding worked!'); +} diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/CallbackRegistry.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/CallbackRegistry.tsx new file mode 100644 index 0000000000..baf4a6ce7d --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/CallbackRegistry.tsx @@ -0,0 +1,45 @@ +// First define the default handlers separately +import OnAdd from './AddProviderParameters'; +import OnDelete from './DeleteProviderParameters'; + +const DEFAULT_HANDLERS = { + onAdd: (providerId: string, config: any) => { + OnAdd(); + }, + onDelete: (providerId: string) => { + OnDelete(); + }, + onShowSettings: (providerId: string) => { + /* default settings behavior */ + }, +}; + +// Then use them in the registry +export const CALLBACK_REGISTRY = { + default: DEFAULT_HANDLERS, + + anthropic: { + onAdd: (providerId: string, config: any) => { + /* Anthropic-specific add */ + }, + // Fall back to default handlers + onDelete: DEFAULT_HANDLERS.onDelete, + onShowSettings: DEFAULT_HANDLERS.onShowSettings, + }, + + ollama: { + onAdd: (providerId: string, config: any) => { + /* Ollama-specific add */ + }, + onDelete: (providerId: string) => { + /* Ollama-specific delete */ + }, + onRefresh: (providerId: string) => { + /* Ollama-specific refresh */ + }, + }, +} as const; + +// Type for the handlers +export type ActionHandler = typeof DEFAULT_HANDLERS; +export type ProviderId = keyof typeof CALLBACK_REGISTRY; diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/DeleteProviderParameters.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/DeleteProviderParameters.tsx new file mode 100644 index 0000000000..227c2e13b3 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/DeleteProviderParameters.tsx @@ -0,0 +1,5 @@ +import { toast } from 'react-toastify'; + +export default function OnDelete() { + toast.success('deleting worked!'); +} diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/RefreshActiveProviders.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/RefreshActiveProviders.tsx new file mode 100644 index 0000000000..669676b1d5 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/RefreshActiveProviders.tsx @@ -0,0 +1,5 @@ +import { toast } from 'react-toastify'; + +export default function OnRefresh() { + toast.success('refreshing worked!'); +} diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/ShowModal.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/ShowModal.tsx new file mode 100644 index 0000000000..0121fd9a3a --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/ShowModal.tsx @@ -0,0 +1,5 @@ +import { toast } from 'react-toastify'; + +export default function OnShowModal() { + toast.success('here is the modal yay!'); +} diff --git a/ui/desktop/src/components/settings/providers/providers/callbacks/UpdateProviderParameters.tsx b/ui/desktop/src/components/settings/providers/providers/callbacks/UpdateProviderParameters.tsx new file mode 100644 index 0000000000..bb5b3bab14 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/callbacks/UpdateProviderParameters.tsx @@ -0,0 +1,5 @@ +import { toast } from 'react-toastify'; + +export default function OnShowSettings() { + toast.success('settings update worked!'); +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationAction.tsx b/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationAction.tsx new file mode 100644 index 0000000000..44c9b91544 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationAction.tsx @@ -0,0 +1,7 @@ +// contains basic, common actions like edit, add, delete etc +// logic for whether or not these buttons get shown is stored in the actions/ folder +// specific providers may want specific methods to handle these operations -- TODO +export default interface ConfigurationAction { + id: string; + renderButton: () => React.JSX.Element; +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationCallbacks.tsx b/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationCallbacks.tsx new file mode 100644 index 0000000000..145cf0d3ec --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/ConfigurationCallbacks.tsx @@ -0,0 +1,7 @@ +export default interface ProviderCallbacks { + onShowModal?: () => void; + onAdd?: () => void; + onDelete?: () => void; + onShowSettings?: () => void; + onRefresh?: () => void; +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/OllamaMetadata.tsx b/ui/desktop/src/components/settings/providers/providers/interfaces/OllamaMetadata.tsx new file mode 100644 index 0000000000..aa83bdf1fc --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/OllamaMetadata.tsx @@ -0,0 +1,3 @@ +export default interface OllamaMetadata { + location: 'app' | 'host' | null; +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/ParameterSchema.ts b/ui/desktop/src/components/settings/providers/providers/interfaces/ParameterSchema.ts new file mode 100644 index 0000000000..52359c6fcc --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/ParameterSchema.ts @@ -0,0 +1,5 @@ +export default interface ParameterSchema { + name: string; + is_secret: boolean; + location?: string; // env, config.yaml, and/or keychain +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderDetails.tsx b/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderDetails.tsx new file mode 100644 index 0000000000..7b0f088c5b --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderDetails.tsx @@ -0,0 +1,14 @@ +// metadata and action builder +import ProviderState from './ProviderState'; +import ConfigurationAction from './ConfigurationAction'; +import ParameterSchema from '../parameters/interfaces/ParameterSchema'; +import ProviderCallbacks from './ConfigurationCallbacks'; + +export default interface ProviderDetails { + id: string; + name: string; + description: string; + parameters: ParameterSchema[]; + getTags?: (name: string) => string[]; + getActions?: (provider: ProviderState, callbacks: ProviderCallbacks) => ConfigurationAction[]; +} diff --git a/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderState.tsx b/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderState.tsx new file mode 100644 index 0000000000..f47206794a --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/interfaces/ProviderState.tsx @@ -0,0 +1,7 @@ +// runtime data per instance +export default interface ProviderState { + id: string; + name: string; + isConfigured: boolean; + metadata: any; +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/providers/modals/ProviderConfiguationModal.tsx new file mode 100644 index 0000000000..87134e3233 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/ProviderConfiguationModal.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Card } from '../../../../ui/card'; +import ProviderSetupOverlay from './configuration_modal_subcomponents/ProviderSetupOverlay'; +import ProviderSetupHeader from './configuration_modal_subcomponents/ProviderSetupHeader'; +import ProviderSetupForm from './configuration_modal_subcomponents/ProviderSetupForm'; +import ProviderSetupActions from './configuration_modal_subcomponents/ProviderSetupActions'; +import ProviderConfiguationModalProps from './interfaces/ProviderConfigurationModalProps'; + +export default function ProviderConfigurationModal({ + provider, + title, + onSubmit, + onCancel, +}: ProviderConfiguationModalProps) { + const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>({}); + const headerText = title || `Setup ${provider}`; + + const handleSubmitForm = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(configValues); + }; + + return ( + + +
+ + + + + +
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupActions.tsx b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupActions.tsx new file mode 100644 index 0000000000..94de8f75ae --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupActions.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Button } from '../../../../../ui/button'; + +interface ProviderSetupActionsProps { + onCancel: () => void; +} + +/** + * Renders the "Submit" and "Cancel" buttons at the bottom. + * Notice we rely on the parent's `onSubmit` in the form, so we only handle Cancel here. + */ +export default function ProviderSetupActions({ onCancel }: ProviderSetupActionsProps) { + return ( +
+ {/* We rely on the
"onSubmit" for the actual Submit logic */} + + +
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupForm.tsx new file mode 100644 index 0000000000..d9ffa38437 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupForm.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Input } from '../../../../../ui/input'; +import { Lock } from 'lucide-react'; +import { isSecretKey } from '../../../../api_keys/utils'; +import ProviderSetupFormProps from '../interfaces/ProviderSetupFormProps'; +import ParameterSchema from '../../interfaces/ParameterSchema'; + +/** + * Renders the form with required input fields and the "lock" info row. + * The submit/cancel buttons are in a separate ProviderSetupActions component. + */ +export default function ProviderSetupForm({ + configValues, + setConfigValues, + onSubmit, + provider, +}: ProviderSetupFormProps) { + const parameters: ParameterSchema[] = provider.parameters; + return ( + +
+ {parameters.map((parameter) => ( +
+ + setConfigValues((prev) => ({ + ...prev, + [parameter.name]: e.target.value, + })) + } + placeholder={parameter.name} + className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900" + required + /> +
+ ))} +
+ + + Your configuration values will be stored securely in the keychain and used only for + making requests to {provider.name}. + +
+
+ {/* The action buttons are not in this form; they're in ProviderSetupActions. */} + + ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupHeader.tsx b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupHeader.tsx new file mode 100644 index 0000000000..21bd1a702f --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupHeader.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface ProviderSetupHeaderProps { + headerText: string; +} + +/** + * Renders the header (title) for the modal. + */ +export default function ProviderSetupHeader({ headerText }: ProviderSetupHeaderProps) { + return ( +
+

{headerText}

+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupOverlay.tsx b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupOverlay.tsx new file mode 100644 index 0000000000..ff127d97ad --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/configuration_modal_subcomponents/ProviderSetupOverlay.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface ProviderSetupOverlayProps { + children: React.ReactNode; +} + +/** + * Renders the semi-transparent backdrop + blur for the modal. + */ +export default function ProviderSetupOverlay({ children }: ProviderSetupOverlayProps) { + return ( +
+ {children} +
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderConfigurationModalProps.tsx b/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderConfigurationModalProps.tsx new file mode 100644 index 0000000000..8a4869889f --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderConfigurationModalProps.tsx @@ -0,0 +1,9 @@ +// used both for initial config and editing config +import ProviderDetails from '../../interfaces/ProviderDetails'; + +export default interface ProviderConfiguationModalProps { + provider: ProviderDetails; + title?: string; + onSubmit: (configValues: { [key: string]: string }) => void; + onCancel: () => void; +} diff --git a/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderSetupFormProps.tsx b/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderSetupFormProps.tsx new file mode 100644 index 0000000000..a2b16fa1fb --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/modals/interfaces/ProviderSetupFormProps.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ProviderDetails from '../../interfaces/ProviderDetails'; + +export default interface ProviderSetupFormProps { + configValues: { [key: string]: string }; + setConfigValues: React.Dispatch>; + onSubmit: (e: React.FormEvent) => void; + provider: ProviderDetails; +} diff --git a/ui/desktop/src/components/settings/providers/providers/parameters/UpdateSecrets.tsx b/ui/desktop/src/components/settings/providers/providers/parameters/UpdateSecrets.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/desktop/src/components/settings/providers/providers/state/ActiveKeysContext.tsx b/ui/desktop/src/components/settings/providers/providers/state/ActiveKeysContext.tsx new file mode 100644 index 0000000000..0528234bc4 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/state/ActiveKeysContext.tsx @@ -0,0 +1,48 @@ +import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react'; +import { getActiveProviders } from './utils'; + +// Create a context for active keys +const ActiveKeysContext = createContext< + | { + activeKeys: string[]; + setActiveKeys: (keys: string[]) => void; + } + | undefined +>(undefined); + +export const ActiveKeysProvider = ({ children }: { children: ReactNode }) => { + const [activeKeys, setActiveKeys] = useState([]); // Start with an empty list + const [isLoading, setIsLoading] = useState(true); // Track loading state + + // Fetch active keys from the backend + useEffect(() => { + const fetchActiveProviders = async () => { + try { + const providers = await getActiveProviders(); // Fetch the active providers + setActiveKeys(providers); // Update state with fetched providers + } catch (error) { + console.error('Error fetching active providers:', error); + } finally { + setIsLoading(false); // Ensure loading is marked as complete + } + }; + + fetchActiveProviders(); // Call the async function + }, []); + + // Provide active keys and ability to update them + return ( + + {!isLoading ? children :
Loading...
} {/* Conditional rendering */} +
+ ); +}; + +// Custom hook to access active keys +export const useActiveKeys = () => { + const context = useContext(ActiveKeysContext); + if (!context) { + throw new Error('useActiveKeys must be used within an ActiveKeysProvider'); + } + return context; +}; diff --git a/ui/desktop/src/components/settings/providers/providers/state/types.ts b/ui/desktop/src/components/settings/providers/providers/state/types.ts new file mode 100644 index 0000000000..78dc57b38c --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/state/types.ts @@ -0,0 +1,21 @@ +export interface ProviderResponse { + supported: boolean; + name?: string; + description?: string; + models?: string[]; + config_status: Record; +} + +export interface ConfigDetails { + key: string; + is_set: boolean; + location?: string; +} + +export interface Provider { + id: string; // Lowercase key (e.g., "openai") + name: string; // Provider name (e.g., "OpenAI") + description: string; // Description of the provider + models: string[]; // List of supported models + requiredKeys: string[]; // List of required keys +} diff --git a/ui/desktop/src/components/settings/providers/providers/state/utils.tsx b/ui/desktop/src/components/settings/providers/providers/state/utils.tsx new file mode 100644 index 0000000000..764e3e10e0 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/state/utils.tsx @@ -0,0 +1,98 @@ +import { Provider, ProviderResponse } from './types'; +import { getApiUrl, getSecretKey } from '../../../config'; +import { special_provider_cases } from '../providers/utils'; + +export function isSecretKey(keyName: string): boolean { + // Endpoints and hosts should not be stored as secrets + const nonSecretKeys = [ + 'DATABRICKS_HOST', + 'OLLAMA_HOST', + 'AZURE_OPENAI_ENDPOINT', + 'AZURE_OPENAI_DEPLOYMENT_NAME', + ]; + return !nonSecretKeys.includes(keyName); +} + +export async function getActiveProviders(): Promise { + try { + // Fetch the secrets settings + const configSettings = await getConfigSettings(); + console.log('[getActiveProviders]:', configSettings); + + // Check for special provider cases (e.g. ollama running locally) + const specialCasesResults = await Promise.all( + Object.entries(special_provider_cases).map(async ([providerName, checkFunction]) => { + const isActive = await checkFunction(); // Dynamically re-check status + console.log(`Special case result for ${providerName}:`, isActive); + return isActive ? providerName : null; + }) + ); + // Extract active providers based on `is_set` in `secret_status` or providers with no keys + const activeProviders = Object.values(configSettings) // Convert object to array + .filter((provider) => { + const apiKeyStatus = Object.values(provider.config_status || {}); // Get all key statuses + + // Include providers if all required keys are set + return apiKeyStatus.length > 0 && apiKeyStatus.every((key) => key.is_set); + }) + .map((provider) => provider.name || 'Unknown Provider'); // Extract provider name + + // Combine active providers from secrets settings and special cases (avoiding repeats) + const allActiveProviders = activeProviders.concat( + specialCasesResults.filter( + (provider) => provider !== null && !activeProviders.includes(provider) + ) + ); + + return allActiveProviders; + } catch (error) { + console.error('Failed to get active providers:', error); + return []; + } +} + +export async function getConfigSettings(): Promise> { + const providerList = await getProvidersList(); + // Extract the list of IDs + const providerIds = providerList.map((provider) => provider.id); + + // Fetch configs state (set/unset) using the provider IDs + const response = await fetch(getApiUrl('/configs/providers'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + providers: providerIds, + }), + }); + + if (!response.ok) { + throw new Error('Failed to fetch secrets'); + } + + const data = (await response.json()) as Record; + return data; +} + +export async function getProvidersList(): Promise { + const response = await fetch(getApiUrl('/agent/providers'), { + method: 'GET', + }); + + if (!response.ok) { + throw new Error(`Failed to fetch providers: ${response.statusText}`); + } + + const data = await response.json(); + + // Format the response into an array of providers + return data.map((item: any) => ({ + id: item.id, // Root-level ID + name: item.details?.name || 'Unknown Provider', // Nested name in details + description: item.details?.description || 'No description available.', // Nested description + models: item.details?.models || [], // Nested models array + requiredKeys: item.details?.required_keys || [], // Nested required keys array + })); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/CardActions.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardActions.tsx new file mode 100644 index 0000000000..6171d0e71e --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardActions.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ConfigurationAction from '../interfaces/ConfigurationAction'; + +interface CardActionsProps { + actions: ConfigurationAction[]; +} + +export default function CardActions({ actions }: CardActionsProps) { + return ( +
+ {actions.map((action) => { + // Store the rendered button in a variable first + const ButtonElement = action.renderButton(); + return {ButtonElement}; + })} +
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/CardBody.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardBody.tsx new file mode 100644 index 0000000000..343b4d1984 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardBody.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import CardActions from './CardActions'; +import ConfigurationAction from '../interfaces/ConfigurationAction'; + +interface CardBodyProps { + actions: ConfigurationAction[]; +} + +export default function CardBody({ actions }: CardBodyProps) { + console.log('in card body'); + return ( +
+ +
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/CardContainer.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardContainer.tsx new file mode 100644 index 0000000000..dfd4ec4690 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardContainer.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +interface CardContainerProps { + header: React.ReactNode; + body: React.ReactNode; +} + +function GlowingRing() { + return ( +
+ ); +} + +interface HeaderContainerProps { + children: React.ReactNode; +} + +function HeaderContainer({ children }: HeaderContainerProps) { + return
{children}
; +} + +export default function CardContainer({ header, body }: CardContainerProps) { + return ( +
+ +
+ {header} + {body} +
+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/CardHeader.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardHeader.tsx new file mode 100644 index 0000000000..82d347a74b --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/CardHeader.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { ExclamationButton, GreenCheckButton } from './actions/ActionButtons'; +import { + ConfiguredProviderTooltipMessage, + OllamaNotConfiguredTooltipMessage, + ProviderDescription, +} from './utils/StringUtils'; + +interface CardHeaderProps { + name: string; + description: string; + isConfigured: boolean; +} + +// Make CardTitle a proper React component +function CardTitle({ name }: { name: string }) { + return

{name}

; +} + +// Properly type ProviderNameAndStatus props +interface ProviderNameAndStatusProps { + name: string; + isConfigured: boolean; +} + +function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProps) { + console.log(`Provider Name: ${name}, Is Configured: ${isConfigured}`); + const ollamaNotConfigured = !isConfigured && name === 'Ollama'; + + return ( +
+ + + {/* Configured state: Green check */} + {isConfigured && } + + {/* Not Configured + Ollama => Exclamation */} + {ollamaNotConfigured && } +
+ ); +} + +// Add a container div to the CardHeader +export default function CardHeader({ name, description, isConfigured }: CardHeaderProps) { + return ( + <> + + + + ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/ProviderCard.tsx new file mode 100644 index 0000000000..fb1a8c3b39 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/ProviderCard.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import CardContainer from './CardContainer'; +import CardHeader from './CardHeader'; +import ProviderState from '../interfaces/ProviderState'; +import CardBody from './CardBody'; +import ProviderCallbacks from '../interfaces/ConfigurationCallbacks'; +import { PROVIDER_REGISTRY } from '../ProviderRegistry'; + +interface ProviderCardProps { + provider: ProviderState; + providerCallbacks: ProviderCallbacks; +} + +export function ProviderCard({ provider, providerCallbacks }: ProviderCardProps) { + const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name); + + // Add safety check + if (!providerEntry) { + console.error(`Provider ${provider.name} not found in registry`); + return null; + } + + const providerDetails = providerEntry.details; + // Add another safety check + if (!providerDetails) { + console.error(`Provider ${provider.name} has no details`); + return null; + } + console.log('provider details', providerDetails); + + try { + const actions = providerDetails.getActions(provider, providerCallbacks); + + return ( + + } + body={} + /> + ); + } catch (error) { + console.error(`Error rendering provider card for ${provider.name}:`, error); + return null; + } +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/ActionButtons.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/ActionButtons.tsx new file mode 100644 index 0000000000..e5882dc625 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/ActionButtons.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Button } from '../../../../../ui/button'; +import clsx from 'clsx'; +import { TooltipWrapper } from './TooltipWrapper'; +import { Check, CircleHelp, Plus, RefreshCw, Rocket, Settings, X } from 'lucide-react'; + +interface ActionButtonProps extends React.ComponentProps { + /** Icon component to render, e.g. `RefreshCw` from lucide-react */ + icon?: React.ComponentType>; + /** Tooltip text to show; optional if you want no tooltip. */ + tooltip?: React.ReactNode; + /** Additional classes for styling. */ + className?: string; +} + +// className is the styling for the + ); + + // If a tooltip is provided, wrap the Button in a tooltip. + // Otherwise, just return the button as is. + if (tooltip) { + return {ButtonElement}; + } + + return ButtonElement; +} + +export function GreenCheckButton({ + tooltip, + className = '', // Provide a default value to prevent undefined errors + ...props +}: ActionButtonProps) { + return ( + {}} + {...props} + /> + ); +} + +export function ExclamationButton({ tooltip, className, ...props }: ActionButtonProps) { + return {}} {...props} />; +} + +export function GearSettingsButton({ tooltip, className, ...props }: ActionButtonProps) { + return ; +} + +export function AddButton({ tooltip, className, ...props }: ActionButtonProps) { + return ; +} + +export function DeleteButton({ tooltip, className, ...props }: ActionButtonProps) { + return ; +} + +export function RefreshButton({ tooltip, className, ...props }: ActionButtonProps) { + return ; +} + +export function RocketButton({ tooltip, className, ...props }: ActionButtonProps) { + return ; +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/DefaultProviderActions.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/DefaultProviderActions.tsx new file mode 100644 index 0000000000..dff612ca76 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/DefaultProviderActions.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { AddButton, DeleteButton, GearSettingsButton } from './ActionButtons'; + +interface ProviderActionsProps { + name: string; + isConfigured: boolean; + onAdd?: () => void; + onConfigure?: () => void; + onDelete?: () => void; + onShowSettings?: () => void; +} + +function getDefaultTooltipMessages(name: string, actionType: string) { + switch (actionType) { + case 'add': + return `Configure ${name} settings`; + case 'edit': + return `Edit ${name} settings`; + case 'delete': + return `Delete ${name} settings`; + default: + return null; + } +} + +export default function DefaultProviderActions({ + name, + isConfigured, + onAdd, + onDelete, + onShowSettings, +}: ProviderActionsProps) { + return ( + <> + {/*Set up an unconfigured provider */} + {!isConfigured && ( + { + e.stopPropagation(); + onAdd?.(); + }} + /> + )} + {/*Edit settings of configured provider*/} + {isConfigured && ( + { + e.stopPropagation(); + onShowSettings?.(); + }} + /> + )} + {/*Delete configuration*/} + {isConfigured && ( + { + e.stopPropagation(); + onDelete?.(); + }} + /> + )} + + ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/OllamaActions.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/OllamaActions.tsx new file mode 100644 index 0000000000..876620b215 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/OllamaActions.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { AddButton, DeleteButton, GearSettingsButton, RefreshButton } from './ActionButtons'; +import OllamaMetadata from '../../interfaces/OllamaMetadata'; + +interface OllamaActionsProps { + isConfigured: boolean; + ollamaMetadata: OllamaMetadata; + onRefresh?: (e: React.MouseEvent) => void; + onAdd?: () => void; + onDelete?: () => void; + onShowSettings?: () => void; +} + +export default function OllamaActions({ + isConfigured, + ollamaMetadata, + onRefresh, + onAdd, + onDelete, + onShowSettings, +}: OllamaActionsProps) { + const showHostDeleteButton = isConfigured && ollamaMetadata.location === 'host' && !onDelete; + + const showRefreshButton = !isConfigured && onRefresh; + + // add host url to overwrite the app url OR if not configured at all yet + const showAddHostUrlButton = + (isConfigured && ollamaMetadata.location === 'app' && onAdd) || (!isConfigured && onAdd); + + const showHostUrlSettingsButton = + isConfigured && ollamaMetadata.location === 'host' && onShowSettings; + + // We’ll figure out which buttons to render: + + // 1) Refresh button if not configured + // 2) If configured via app => show "plus" to switch to host config + // 3) If configured via host => show "X" to remove the host and "gear" to edit + + return ( + // TODO: is this the right class name? +
+ {/* (1) Refresh button if not configured */} + {showRefreshButton && ( + { + e.stopPropagation(); + onRefresh?.(e); + }} + > + )} + + {/* (2) If configured location = 'app', show a plus button to switch / set host */} + {showAddHostUrlButton && ( + { + e.stopPropagation(); + onAdd?.(); + }} + > + )} + + {/* (3) If configured location = 'host', show an X to delete or revert config */} + {showHostDeleteButton && ( + { + e.stopPropagation(); + onDelete?.(); + }} + > + )} + + {/* (4) If configured location = 'host', show a gear to view and edit config */} + {showHostUrlSettingsButton && ( + { + e.stopPropagation(); + onShowSettings?.(); + }} + > + )} +
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/TooltipWrapper.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/TooltipWrapper.tsx new file mode 100644 index 0000000000..ca291ad99a --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/actions/TooltipWrapper.tsx @@ -0,0 +1,38 @@ +// TooltipWrapper.tsx +import React from 'react'; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, +} from '../../../../../ui/Tooltip'; +import { Portal } from '@radix-ui/react-portal'; + +interface TooltipWrapperProps { + children: React.ReactNode; + tooltipContent: React.ReactNode; + side?: 'top' | 'bottom' | 'left' | 'right'; + align?: 'start' | 'center' | 'end'; + className?: string; +} + +export function TooltipWrapper({ + children, + tooltipContent, + side = 'top', + align = 'center', + className = '', +}: TooltipWrapperProps) { + return ( + + + {children} + + + {typeof tooltipContent === 'string' ?

{tooltipContent}

: tooltipContent} +
+
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/providers/subcomponents/utils/StringUtils.tsx b/ui/desktop/src/components/settings/providers/providers/subcomponents/utils/StringUtils.tsx new file mode 100644 index 0000000000..2f3a0812e6 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/providers/subcomponents/utils/StringUtils.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +// Functions for string / string-based element creation (e.g. tooltips for each provider, descriptions, etc) +export function OllamaNotConfiguredTooltipMessage() { + return ( +

+ To use, either the{' '} + + Ollama app + {' '} + must be installed on your machine and open, or you must enter a value for OLLAMA_HOST. +

+ ); +} + +export function ConfiguredProviderTooltipMessage(name: string) { + return `${name} provider is configured`; +} + +interface ProviderDescriptionProps { + description: string; +} + +export function ProviderDescription({ description }: ProviderDescriptionProps) { + return ( +

+ {description} +

+ ); +} diff --git a/ui/desktop/src/utils/deleteAllKeys.tsx b/ui/desktop/src/utils/deleteAllKeys.tsx new file mode 100644 index 0000000000..c95aacb3b0 --- /dev/null +++ b/ui/desktop/src/utils/deleteAllKeys.tsx @@ -0,0 +1,32 @@ +import { getApiUrl, getSecretKey } from '../config'; +import { required_keys } from '../components/settings/models/hardcoded_stuff'; + +export async function DeleteProviderKeysFromKeychain() { + for (const [provider, keys] of Object.entries(required_keys)) { + for (const keyName of keys) { + try { + const deleteResponse = await fetch(getApiUrl('/configs/delete'), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + key: keyName, + is_secret: true, // get rid of keychain keys only + }), + }); + + if (!deleteResponse.ok) { + const errorText = await deleteResponse.text(); + console.error('Delete response error:', errorText); + throw new Error('Failed to delete key: ' + keyName); + } else { + console.log('Successfully deleted key:', keyName); + } + } catch (error) { + console.error('Error deleting key:', keyName, error); + } + } + } +} diff --git a/ui/desktop/vite.renderer.config.ts b/ui/desktop/vite.renderer.config.ts index 690be5b1a9..cdafde9842 100644 --- a/ui/desktop/vite.renderer.config.ts +++ b/ui/desktop/vite.renderer.config.ts @@ -1,4 +1,9 @@ import { defineConfig } from 'vite'; // https://vitejs.dev/config -export default defineConfig({}); +export default defineConfig({ + define: { + // This replaces process.env.ALPHA with a literal at build time + 'process.env.ALPHA': JSON.stringify(process.env.ALPHA === 'true'), + }, +});