mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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..."
|
||||
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
|
||||
|
||||
Generated
+40
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' && (
|
||||
<ProviderSettings onClose={() => setView('chat')} />
|
||||
)}
|
||||
{view === 'chat' && <ChatView setView={setView} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
</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>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
|
||||
@@ -24,22 +24,22 @@ export async function getActiveProviders(): Promise<string[]> {
|
||||
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<Provider[]> {
|
||||
models: item.details?.models || [], // Nested models array
|
||||
requiredKeys: item.details?.required_keys || [], // Nested required keys array
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -235,8 +235,8 @@ export function ConfigureProvidersGrid() {
|
||||
<div className="relative z-[9999]">
|
||||
<ProviderSetupModal
|
||||
provider={providers.find((p) => 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`
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 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