feat: [alpha] Providers grid refactor (#1345)
Co-authored-by: Yingjie He <yingjiehe@squareup.com>
This commit is contained in:
@@ -63,6 +63,12 @@ run-ui:
|
|||||||
@echo "Running UI..."
|
@echo "Running UI..."
|
||||||
cd ui/desktop && npm install && npm run start-gui
|
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 with latest (Windows version)
|
||||||
run-ui-windows:
|
run-ui-windows:
|
||||||
@just release-windows
|
@just release-windows
|
||||||
|
|||||||
Generated
+40
@@ -2358,6 +2358,18 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
@@ -15614,6 +15626,34 @@
|
|||||||
"rimraf": "bin.js"
|
"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": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"lint:check": "eslint \"src/**/*.{ts,tsx}\"",
|
"lint:check": "eslint \"src/**/*.{ts,tsx}\"",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
|
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
|
||||||
"format:check": "prettier --check \"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": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.5.0",
|
"@electron-forge/cli": "^7.5.0",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ChatView from './components/ChatView';
|
|||||||
import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView';
|
import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||||
import MoreModelsView from './components/settings/models/MoreModelsView';
|
import MoreModelsView from './components/settings/models/MoreModelsView';
|
||||||
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
||||||
|
import ProviderSettings from './components/settings/providers/providers/NewProviderSettingsPage';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
@@ -26,7 +27,8 @@ export type View =
|
|||||||
| 'settings'
|
| 'settings'
|
||||||
| 'moreModels'
|
| 'moreModels'
|
||||||
| 'configureProviders'
|
| 'configureProviders'
|
||||||
| 'configPage';
|
| 'configPage'
|
||||||
|
| 'alphaConfigureProviders';
|
||||||
|
|
||||||
export type ViewConfig = {
|
export type ViewConfig = {
|
||||||
view: View;
|
view: View;
|
||||||
@@ -224,6 +226,9 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{view === 'alphaConfigureProviders' && (
|
||||||
|
<ProviderSettings onClose={() => setView('chat')} />
|
||||||
|
)}
|
||||||
{view === 'chat' && <ChatView setView={setView} />}
|
{view === 'chat' && <ChatView setView={setView} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover';
|
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { More } from './icons';
|
import { More } from './icons';
|
||||||
import type { View } from '../ChatWindow';
|
import { View } from '../App';
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
current_version: string;
|
current_version: string;
|
||||||
available_versions: string[];
|
available_versions: string[];
|
||||||
@@ -260,6 +259,17 @@ export default function MoreMenu({ setView }: { setView: (view: View) => void })
|
|||||||
>
|
>
|
||||||
Reset Provider
|
Reset Provider
|
||||||
</button>
|
</button>
|
||||||
|
{process.env.ALPHA && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setView('alphaConfigureProviders');
|
||||||
|
}}
|
||||||
|
className="w-full text-left p-2 text-sm hover:bg-bgSubtle transition-colors text-indigo-800"
|
||||||
|
>
|
||||||
|
See new providers grid
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</PopoverPortal>
|
</PopoverPortal>
|
||||||
|
|||||||
@@ -24,22 +24,22 @@ export async function getActiveProviders(): Promise<string[]> {
|
|||||||
const configSettings = await getConfigSettings();
|
const configSettings = await getConfigSettings();
|
||||||
|
|
||||||
const activeProviders = Object.values(configSettings)
|
const activeProviders = Object.values(configSettings)
|
||||||
.filter((provider) => {
|
.filter((provider) => {
|
||||||
// 1. Get provider's config_status
|
// 1. Get provider's config_status
|
||||||
const configStatus = provider.config_status ?? {};
|
const configStatus = provider.config_status ?? {};
|
||||||
|
|
||||||
// 2. Collect only the keys *not* in default_key_value
|
// 2. Collect only the keys *not* in default_key_value
|
||||||
const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k));
|
const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k));
|
||||||
|
|
||||||
// 3. If there are *no* non-default keys, it is NOT active
|
// 3. If there are *no* non-default keys, it is NOT active
|
||||||
if (requiredKeyEntries.length === 0) {
|
if (requiredKeyEntries.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Otherwise, all non-default keys must be `is_set`
|
// 4. Otherwise, all non-default keys must be `is_set`
|
||||||
return requiredKeyEntries.every(([_, value]) => value?.is_set);
|
return requiredKeyEntries.every(([_, value]) => value?.is_set);
|
||||||
})
|
})
|
||||||
.map((provider) => provider.name || 'Unknown Provider');
|
.map((provider) => provider.name || 'Unknown Provider');
|
||||||
|
|
||||||
console.log('[GET ACTIVE PROVIDERS]:', activeProviders);
|
console.log('[GET ACTIVE PROVIDERS]:', activeProviders);
|
||||||
return activeProviders;
|
return activeProviders;
|
||||||
@@ -93,4 +93,4 @@ export async function getProvidersList(): Promise<Provider[]> {
|
|||||||
models: item.details?.models || [], // Nested models array
|
models: item.details?.models || [], // Nested models array
|
||||||
requiredKeys: item.details?.required_keys || [], // Nested required keys array
|
requiredKeys: item.details?.required_keys || [], // Nested required keys array
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -235,8 +235,8 @@ export function ConfigureProvidersGrid() {
|
|||||||
<div className="relative z-[9999]">
|
<div className="relative z-[9999]">
|
||||||
<ProviderSetupModal
|
<ProviderSetupModal
|
||||||
provider={providers.find((p) => p.id === selectedForSetup)?.name || ''}
|
provider={providers.find((p) => p.id === selectedForSetup)?.name || ''}
|
||||||
model="Example Model"
|
_model="Example Model"
|
||||||
endpoint="Example Endpoint"
|
_endpoint="Example Endpoint"
|
||||||
title={
|
title={
|
||||||
modalMode === 'edit'
|
modalMode === 'edit'
|
||||||
? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} Configuration`
|
? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} Configuration`
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="h-screen w-full">
|
||||||
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
<div className="px-8 pt-6 pb-4">
|
||||||
|
<BackButton onClick={onClose} />
|
||||||
|
<h1 className="text-3xl font-medium text-textStandard mt-1">Configure</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=" py-8 pt-[20px]">
|
||||||
|
<div className="flex justify-between items-center mb-6 border-b border-borderSubtle px-8">
|
||||||
|
<h2 className="text-xl font-medium text-textStandard">Providers</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div className="max-w-5xl pt-4 px-8">
|
||||||
|
<div className="relative z-10">
|
||||||
|
<ProviderGrid providers={fakeProviderState} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-3 auto-rows-fr max-w-full [&_*]:z-20">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderCards({ providers }: { providers: ProviderState[] }) {
|
||||||
|
const providerCallbacks = {
|
||||||
|
onShowModal: OnShowModal,
|
||||||
|
onAdd: OnAdd,
|
||||||
|
onDelete: OnDelete,
|
||||||
|
onShowSettings: OnShowSettings,
|
||||||
|
onRefresh: OnRefresh,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{providers.map((provider) => (
|
||||||
|
<ProviderCard
|
||||||
|
key={provider.name} // helps React efficiently update and track components when rendering lists
|
||||||
|
provider={provider}
|
||||||
|
providerCallbacks={providerCallbacks}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProviderGrid({ providers }: { providers: ProviderState[] }) {
|
||||||
|
console.log('got these providers', providers);
|
||||||
|
return (
|
||||||
|
<GridLayout>
|
||||||
|
<ProviderCards providers={providers} />
|
||||||
|
</GridLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<DefaultProviderActions
|
||||||
|
name={provider.name}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<OllamaActions
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
ollamaMetadata={provider.metadata}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onShowSettings={onShowSettings}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// const ACTION_IMPLEMENTATIONS = {
|
||||||
|
// 'default': (provider, callbacks) => [{
|
||||||
|
// id: 'default-provider-actions',
|
||||||
|
// renderButton: () => <DefaultProviderActions {...} />
|
||||||
|
// }],
|
||||||
|
//
|
||||||
|
// 'ollama': (provider, callbacks) => [{
|
||||||
|
// id: 'ollama-actions',
|
||||||
|
// renderButton: () => <OllamaActions {...} />
|
||||||
|
// }]
|
||||||
|
// };
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function OnAdd() {
|
||||||
|
toast.success('adding worked!');
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function OnDelete() {
|
||||||
|
toast.success('deleting worked!');
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function OnRefresh() {
|
||||||
|
toast.success('refreshing worked!');
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function OnShowModal() {
|
||||||
|
toast.success('here is the modal yay!');
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default function OnShowSettings() {
|
||||||
|
toast.success('settings update worked!');
|
||||||
|
}
|
||||||
+7
@@ -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;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
export default interface ProviderCallbacks {
|
||||||
|
onShowModal?: () => void;
|
||||||
|
onAdd?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onShowSettings?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default interface OllamaMetadata {
|
||||||
|
location: 'app' | 'host' | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default interface ParameterSchema {
|
||||||
|
name: string;
|
||||||
|
is_secret: boolean;
|
||||||
|
location?: string; // env, config.yaml, and/or keychain
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// runtime data per instance
|
||||||
|
export default interface ProviderState {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isConfigured: boolean;
|
||||||
|
metadata: any;
|
||||||
|
}
|
||||||
+41
@@ -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 (
|
||||||
|
<ProviderSetupOverlay>
|
||||||
|
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0">
|
||||||
|
<div className="px-4 pb-0 space-y-8">
|
||||||
|
<ProviderSetupHeader headerText={headerText} />
|
||||||
|
|
||||||
|
<ProviderSetupForm
|
||||||
|
configValues={configValues}
|
||||||
|
setConfigValues={setConfigValues}
|
||||||
|
onSubmit={handleSubmitForm}
|
||||||
|
provider={provider}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProviderSetupActions onCancel={onCancel} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</ProviderSetupOverlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
+33
@@ -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 (
|
||||||
|
<div className="mt-[8px] -ml-8 -mr-8 pt-8">
|
||||||
|
{/* We rely on the <form> "onSubmit" for the actual Submit logic */}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-regular"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-md font-regular"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+50
@@ -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 (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="mt-[24px] space-y-4">
|
||||||
|
{parameters.map((parameter) => (
|
||||||
|
<div key={parameter.name}>
|
||||||
|
<Input
|
||||||
|
type={parameter.is_secret ? 'password' : 'text'}
|
||||||
|
value={configValues[parameter.name] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex text-gray-600 dark:text-gray-300">
|
||||||
|
<Lock className="w-6 h-6" />
|
||||||
|
<span className="text-sm font-light ml-4 mt-[2px]">
|
||||||
|
Your configuration values will be stored securely in the keychain and used only for
|
||||||
|
making requests to {provider.name}.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* The action buttons are not in this form; they're in ProviderSetupActions. */}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
@@ -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 (
|
||||||
|
<div className="flex">
|
||||||
|
<h2 className="text-2xl font-regular text-textStandard">{headerText}</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
@@ -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 (
|
||||||
|
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+9
@@ -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;
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ProviderDetails from '../../interfaces/ProviderDetails';
|
||||||
|
|
||||||
|
export default interface ProviderSetupFormProps {
|
||||||
|
configValues: { [key: string]: string };
|
||||||
|
setConfigValues: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
provider: ProviderDetails;
|
||||||
|
}
|
||||||
@@ -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<string[]>([]); // 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 (
|
||||||
|
<ActiveKeysContext.Provider value={{ activeKeys, setActiveKeys }}>
|
||||||
|
{!isLoading ? children : <div>Loading...</div>} {/* Conditional rendering */}
|
||||||
|
</ActiveKeysContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export interface ProviderResponse {
|
||||||
|
supported: boolean;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
models?: string[];
|
||||||
|
config_status: Record<string, ConfigDetails>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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<string[]> {
|
||||||
|
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<Record<string, ProviderResponse>> {
|
||||||
|
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<string, ProviderResponse>;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProvidersList(): Promise<Provider[]> {
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ConfigurationAction from '../interfaces/ConfigurationAction';
|
||||||
|
|
||||||
|
interface CardActionsProps {
|
||||||
|
actions: ConfigurationAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CardActions({ actions }: CardActionsProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-x-2">
|
||||||
|
{actions.map((action) => {
|
||||||
|
// Store the rendered button in a variable first
|
||||||
|
const ButtonElement = action.renderButton();
|
||||||
|
return <React.Fragment key={action.id}>{ButtonElement}</React.Fragment>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="space-x-2 text-center flex items-center justify-between">
|
||||||
|
<CardActions actions={actions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CardContainerProps {
|
||||||
|
header: React.ReactNode;
|
||||||
|
body: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GlowingRing() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute pointer-events-none w-[260px] h-[260px] top-[-50px] left-[-30px] origin-center
|
||||||
|
bg-[linear-gradient(45deg,#13BBAF,#FF4F00)]
|
||||||
|
animate-[rotate_6s_linear_infinite] z-[-1]
|
||||||
|
opacity-0 group-hover/card:opacity-100`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderContainer({ children }: HeaderContainerProps) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CardContainer({ header, body }: CardContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
|
||||||
|
<GlowingRing />
|
||||||
|
<div className="relative bg-bgApp rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col justify-between hover:border-borderStandard">
|
||||||
|
<HeaderContainer>{header}</HeaderContainer>
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 <h3 className="text-base font-medium text-textStandard truncate mr-2">{name}</h3>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CardTitle name={name} />
|
||||||
|
|
||||||
|
{/* Configured state: Green check */}
|
||||||
|
{isConfigured && <GreenCheckButton tooltip={ConfiguredProviderTooltipMessage(name)} />}
|
||||||
|
|
||||||
|
{/* Not Configured + Ollama => Exclamation */}
|
||||||
|
{ollamaNotConfigured && <ExclamationButton tooltip={OllamaNotConfiguredTooltipMessage()} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a container div to the CardHeader
|
||||||
|
export default function CardHeader({ name, description, isConfigured }: CardHeaderProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProviderNameAndStatus name={name} isConfigured={isConfigured} />
|
||||||
|
<ProviderDescription description={description} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<CardContainer
|
||||||
|
header={
|
||||||
|
<CardHeader
|
||||||
|
name={providerDetails.name}
|
||||||
|
description={providerDetails.description}
|
||||||
|
isConfigured={provider.isConfigured}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
body={<CardBody actions={actions} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error rendering provider card for ${provider.name}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -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<typeof Button> {
|
||||||
|
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
|
||||||
|
icon?: React.ComponentType<React.SVGProps<globalThis.SVGSVGElement>>;
|
||||||
|
/** 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 <Button/> component -- below is the default
|
||||||
|
const baseActionButtonClasses = `
|
||||||
|
rounded-full h-7 w-7 p-0
|
||||||
|
bg-bgApp hover:bg-bgApp shadow-none
|
||||||
|
text-textSubtle
|
||||||
|
border border-borderSubtle
|
||||||
|
hover:border-borderStandard
|
||||||
|
hover:text-textStandard
|
||||||
|
transition-colors
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function ActionButton({
|
||||||
|
icon: Icon,
|
||||||
|
size = 'sm',
|
||||||
|
variant = 'default',
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ActionButtonProps) {
|
||||||
|
const ButtonElement = (
|
||||||
|
<Button
|
||||||
|
size={size}
|
||||||
|
variant={variant}
|
||||||
|
className={clsx(baseActionButtonClasses, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="!size-4" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a tooltip is provided, wrap the Button in a tooltip.
|
||||||
|
// Otherwise, just return the button as is.
|
||||||
|
if (tooltip) {
|
||||||
|
return <TooltipWrapper tooltipContent={tooltip}>{ButtonElement}</TooltipWrapper>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ButtonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GreenCheckButton({
|
||||||
|
tooltip,
|
||||||
|
className = '', // Provide a default value to prevent undefined errors
|
||||||
|
...props
|
||||||
|
}: ActionButtonProps) {
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
icon={Check}
|
||||||
|
tooltip={tooltip}
|
||||||
|
className={`
|
||||||
|
bg-green-100
|
||||||
|
dark:bg-green-900/30
|
||||||
|
text-green-600
|
||||||
|
dark:text-green-500
|
||||||
|
hover:bg-green-100
|
||||||
|
hover:text-green-600
|
||||||
|
border-none
|
||||||
|
shadow-none
|
||||||
|
w-5 h-5
|
||||||
|
cursor-default
|
||||||
|
${className} // Removed the nullish coalescing operator as default is provided
|
||||||
|
`}
|
||||||
|
onClick={() => {}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExclamationButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={CircleHelp} tooltip={tooltip} onClick={() => {}} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GearSettingsButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={Settings} tooltip={tooltip} className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={Plus} tooltip={tooltip} className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={X} tooltip={tooltip} className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RefreshButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={RefreshCw} tooltip={tooltip} className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RocketButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||||
|
return <ActionButton icon={Rocket} tooltip={tooltip} className={className} {...props} />;
|
||||||
|
}
|
||||||
+67
@@ -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 && (
|
||||||
|
<AddButton
|
||||||
|
tooltip={getDefaultTooltipMessages(name, 'add')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAdd?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/*Edit settings of configured provider*/}
|
||||||
|
{isConfigured && (
|
||||||
|
<GearSettingsButton
|
||||||
|
tooltip={getDefaultTooltipMessages(name, 'edit')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onShowSettings?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/*Delete configuration*/}
|
||||||
|
{isConfigured && (
|
||||||
|
<DeleteButton
|
||||||
|
tooltip={getDefaultTooltipMessages(name, 'delete')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+87
@@ -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?
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* (1) Refresh button if not configured */}
|
||||||
|
{showRefreshButton && (
|
||||||
|
<RefreshButton
|
||||||
|
tooltip="Refresh to check if Ollama is running."
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRefresh?.(e);
|
||||||
|
}}
|
||||||
|
></RefreshButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* (2) If configured location = 'app', show a plus button to switch / set host */}
|
||||||
|
{showAddHostUrlButton && (
|
||||||
|
<AddButton
|
||||||
|
tooltip="Switch to custom OLLAMA_HOST."
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAdd?.();
|
||||||
|
}}
|
||||||
|
></AddButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* (3) If configured location = 'host', show an X to delete or revert config */}
|
||||||
|
{showHostDeleteButton && (
|
||||||
|
<DeleteButton
|
||||||
|
tooltip="Delete OLLAMA_HOST."
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete?.();
|
||||||
|
}}
|
||||||
|
></DeleteButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* (4) If configured location = 'host', show a gear to view and edit config */}
|
||||||
|
{showHostUrlSettingsButton && (
|
||||||
|
<GearSettingsButton
|
||||||
|
tooltip={'View and edit OLLAMA_HOST'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onShowSettings?.();
|
||||||
|
}}
|
||||||
|
></GearSettingsButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+38
@@ -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 (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
<Portal>
|
||||||
|
<TooltipContent side={side} align={align} className={className}>
|
||||||
|
{typeof tooltipContent === 'string' ? <p>{tooltipContent}</p> : tooltipContent}
|
||||||
|
</TooltipContent>
|
||||||
|
</Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+35
@@ -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 (
|
||||||
|
<p>
|
||||||
|
To use, either the{' '}
|
||||||
|
<a
|
||||||
|
href="https://ollama.com/download"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Ollama app
|
||||||
|
</a>{' '}
|
||||||
|
must be installed on your machine and open, or you must enter a value for OLLAMA_HOST.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfiguredProviderTooltipMessage(name: string) {
|
||||||
|
return `${name} provider is configured`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderDescriptionProps {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProviderDescription({ description }: ProviderDescriptionProps) {
|
||||||
|
return (
|
||||||
|
<p className="text-xs text-textSubtle mt-1.5 mb-3 leading-normal overflow-y-auto max-h-[54px]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// https://vitejs.dev/config
|
// 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'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user