feat: update config endpoints for use with providers (#1563)
This commit is contained in:
+138
-4
@@ -137,6 +137,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "providers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All configuration values retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProviderDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/read": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -240,12 +263,39 @@
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ConfigKey": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"required",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ConfigKeyQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key"
|
||||
"key",
|
||||
"is_secret"
|
||||
],
|
||||
"properties": {
|
||||
"is_secret": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -276,16 +326,100 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderDetails": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"metadata",
|
||||
"is_configured"
|
||||
],
|
||||
"properties": {
|
||||
"is_configured": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the provider is fully configured"
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/components/schemas/ProviderMetadata"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier and name of the provider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderMetadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata about a provider's configuration requirements and capabilities",
|
||||
"required": [
|
||||
"name",
|
||||
"display_name",
|
||||
"description",
|
||||
"default_model",
|
||||
"known_models",
|
||||
"model_doc_link",
|
||||
"config_keys"
|
||||
],
|
||||
"properties": {
|
||||
"config_keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ConfigKey"
|
||||
},
|
||||
"description": "Required configuration keys"
|
||||
},
|
||||
"default_model": {
|
||||
"type": "string",
|
||||
"description": "The default/recommended model for this provider"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the provider's capabilities"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the provider in UIs"
|
||||
},
|
||||
"known_models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of currently known models\nTODO: eventually query the apis directly"
|
||||
},
|
||||
"model_doc_link": {
|
||||
"type": "string",
|
||||
"description": "Link to the docs where models can be found"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this provider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProvidersResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"providers"
|
||||
],
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProviderDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpsertConfigQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
"value",
|
||||
"is_secret"
|
||||
],
|
||||
"properties": {
|
||||
"is_secret": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
|
||||
import type { ReadAllConfigData, ReadAllConfigResponse, RemoveExtensionData, RemoveExtensionResponse, AddExtensionData, AddExtensionResponse, UpdateExtensionData, UpdateExtensionResponse, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen';
|
||||
import type { ReadAllConfigData, ReadAllConfigResponse, RemoveExtensionData, RemoveExtensionResponse, AddExtensionData, AddExtensionResponse, UpdateExtensionData, UpdateExtensionResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen';
|
||||
import { client as _heyApiClient } from './client.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -58,6 +58,13 @@ export const updateExtension = <ThrowOnError extends boolean = false>(options: O
|
||||
});
|
||||
};
|
||||
|
||||
export const providers = <ThrowOnError extends boolean = false>(options?: Options<ProvidersData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).get<ProvidersResponse2, unknown, ThrowOnError>({
|
||||
url: '/config/providers',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const readConfig = <ThrowOnError extends boolean = false>(options: Options<ReadConfigData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).get<unknown, unknown, ThrowOnError>({
|
||||
url: '/config/read',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type ConfigKey = {
|
||||
default?: string | null;
|
||||
name: string;
|
||||
required: boolean;
|
||||
secret: boolean;
|
||||
};
|
||||
|
||||
export type ConfigKeyQuery = {
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
};
|
||||
|
||||
@@ -13,8 +21,59 @@ export type ExtensionQuery = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ProviderDetails = {
|
||||
/**
|
||||
* Indicates whether the provider is fully configured
|
||||
*/
|
||||
is_configured: boolean;
|
||||
metadata: ProviderMetadata;
|
||||
/**
|
||||
* Unique identifier and name of the provider
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata about a provider's configuration requirements and capabilities
|
||||
*/
|
||||
export type ProviderMetadata = {
|
||||
/**
|
||||
* Required configuration keys
|
||||
*/
|
||||
config_keys: Array<ConfigKey>;
|
||||
/**
|
||||
* The default/recommended model for this provider
|
||||
*/
|
||||
default_model: string;
|
||||
/**
|
||||
* Description of the provider's capabilities
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Display name for the provider in UIs
|
||||
*/
|
||||
display_name: string;
|
||||
/**
|
||||
* A list of currently known models
|
||||
* TODO: eventually query the apis directly
|
||||
*/
|
||||
known_models: Array<string>;
|
||||
/**
|
||||
* Link to the docs where models can be found
|
||||
*/
|
||||
model_doc_link: string;
|
||||
/**
|
||||
* The unique identifier for this provider
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ProvidersResponse = {
|
||||
providers: Array<ProviderDetails>;
|
||||
};
|
||||
|
||||
export type UpsertConfigQuery = {
|
||||
is_secret?: boolean | null;
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
@@ -116,6 +175,22 @@ export type UpdateExtensionResponses = {
|
||||
|
||||
export type UpdateExtensionResponse = UpdateExtensionResponses[keyof UpdateExtensionResponses];
|
||||
|
||||
export type ProvidersData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/providers';
|
||||
};
|
||||
|
||||
export type ProvidersResponses = {
|
||||
/**
|
||||
* All configuration values retrieved successfully
|
||||
*/
|
||||
200: Array<ProviderDetails>;
|
||||
};
|
||||
|
||||
export type ProvidersResponse2 = ProvidersResponses[keyof ProvidersResponses];
|
||||
|
||||
export type ReadConfigData = {
|
||||
body: ConfigKeyQuery;
|
||||
path?: never;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
readAllConfig,
|
||||
readConfig,
|
||||
@@ -7,8 +7,16 @@ import {
|
||||
addExtension as apiAddExtension,
|
||||
removeExtension as apiRemoveExtension,
|
||||
updateExtension as apiUpdateExtension,
|
||||
providers,
|
||||
} from '../api';
|
||||
import { client } from '../api/client.gen';
|
||||
import type {
|
||||
ConfigResponse,
|
||||
UpsertConfigQuery,
|
||||
ConfigKeyQuery,
|
||||
ExtensionQuery,
|
||||
ProviderDetails,
|
||||
} from '../api/types.gen';
|
||||
|
||||
// Initialize client configuration
|
||||
client.setConfig({
|
||||
@@ -20,13 +28,15 @@ client.setConfig({
|
||||
});
|
||||
|
||||
interface ConfigContextType {
|
||||
config: Record<string, any>;
|
||||
upsert: (key: string, value: any, isSecret?: boolean) => Promise<void>;
|
||||
read: (key: string) => Promise<any>;
|
||||
remove: (key: string) => Promise<void>;
|
||||
addExtension: (name: string, config: any) => Promise<void>;
|
||||
updateExtension: (name: string, config: any) => Promise<void>;
|
||||
config: ConfigResponse['config'];
|
||||
providersList: ProviderDetails[];
|
||||
upsert: (key: string, value: unknown, is_secret: boolean) => Promise<void>;
|
||||
read: (key: string, is_secret: boolean) => Promise<unknown>;
|
||||
remove: (key: string, is_secret: boolean) => Promise<void>;
|
||||
addExtension: (name: string, config: unknown) => Promise<void>;
|
||||
updateExtension: (name: string, config: unknown) => Promise<void>;
|
||||
removeExtension: (name: string) => Promise<void>;
|
||||
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
|
||||
}
|
||||
|
||||
interface ConfigProviderProps {
|
||||
@@ -36,13 +46,23 @@ interface ConfigProviderProps {
|
||||
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
|
||||
export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const [config, setConfig] = useState<Record<string, any>>({});
|
||||
const [config, setConfig] = useState<ConfigResponse['config']>({});
|
||||
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load all configuration data on mount
|
||||
// Load all configuration data and providers on mount
|
||||
(async () => {
|
||||
const response = await readAllConfig();
|
||||
setConfig(response.data.config || {});
|
||||
// Load config
|
||||
const configResponse = await readAllConfig();
|
||||
setConfig(configResponse.data.config || {});
|
||||
|
||||
// Load providers
|
||||
try {
|
||||
const providersResponse = await providers();
|
||||
setProvidersList(providersResponse.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -51,58 +71,86 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
setConfig(response.data.config || {});
|
||||
};
|
||||
|
||||
const upsert = async (key: string, value: any, isSecret?: boolean) => {
|
||||
const upsert = async (key: string, value: unknown, isSecret?: boolean) => {
|
||||
const query: UpsertConfigQuery = {
|
||||
key,
|
||||
value,
|
||||
is_secret: isSecret || null,
|
||||
};
|
||||
|
||||
await upsertConfig({
|
||||
body: {
|
||||
key,
|
||||
value,
|
||||
is_secret: isSecret,
|
||||
},
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const read = async (key: string) => {
|
||||
return await readConfig({
|
||||
body: { key },
|
||||
const read = async (key: string, is_secret: boolean = false) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
const response = await readConfig({
|
||||
body: query,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const remove = async (key: string) => {
|
||||
const remove = async (key: string, is_secret: boolean) => {
|
||||
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
|
||||
await removeConfig({
|
||||
body: { key },
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const addExtension = async (name: string, config: any) => {
|
||||
const addExtension = async (name: string, config: unknown) => {
|
||||
const query: ExtensionQuery = { name, config };
|
||||
await apiAddExtension({
|
||||
body: { name, config },
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const removeExtension = async (name: string) => {
|
||||
const query: ConfigKeyQuery = { key: name, is_secret: false };
|
||||
await apiRemoveExtension({
|
||||
body: { key: name },
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const updateExtension = async (name: string, config: any) => {
|
||||
const updateExtension = async (name: string, config: unknown) => {
|
||||
const query: ExtensionQuery = { name, config };
|
||||
await apiUpdateExtension({
|
||||
body: { name, config },
|
||||
body: query,
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{ config, upsert, read, remove, addExtension, updateExtension, removeExtension }}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
const getProviders = async (forceRefresh = false): Promise<ProviderDetails[]> => {
|
||||
if (forceRefresh || providersList.length === 0) {
|
||||
// If a refresh is forced or we don't have providers yet
|
||||
const response = await providers();
|
||||
setProvidersList(response.data);
|
||||
return response.data;
|
||||
}
|
||||
// Otherwise return the cached providers
|
||||
return providersList;
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
config,
|
||||
providersList,
|
||||
upsert,
|
||||
read,
|
||||
remove,
|
||||
addExtension,
|
||||
updateExtension,
|
||||
removeExtension,
|
||||
getProviders,
|
||||
}),
|
||||
[config, providersList]
|
||||
); // Functions don't need to be dependencies as they don't change
|
||||
|
||||
return <ConfigContext.Provider value={contextValue}>{children}</ConfigContext.Provider>;
|
||||
};
|
||||
|
||||
export const useConfig = () => {
|
||||
|
||||
@@ -1,70 +1,104 @@
|
||||
import React from 'react';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { ProviderCard } from './subcomponents/ProviderCard';
|
||||
import ProviderState from './interfaces/ProviderState';
|
||||
import OnRefresh from './callbacks/RefreshActiveProviders';
|
||||
import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider';
|
||||
import ProviderConfigurationModal from './modal/ProviderConfiguationModal';
|
||||
import { ProviderDetails } from '../../../api';
|
||||
|
||||
function GridLayout({ children }: { children: React.ReactNode }) {
|
||||
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[repeat(auto-fill,_minmax(140px,_1fr))] gap-3 [&_*]:z-20">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function ProviderCards({
|
||||
// Memoize the ProviderCards component
|
||||
const ProviderCards = memo(function ProviderCards({
|
||||
providers,
|
||||
isOnboarding,
|
||||
}: {
|
||||
providers: ProviderState[];
|
||||
providers: ProviderDetails[];
|
||||
isOnboarding: boolean;
|
||||
}) {
|
||||
const { openModal } = useProviderModal();
|
||||
|
||||
const configureProviderViaModal = (provider: ProviderState) => {
|
||||
openModal(provider, {
|
||||
onSubmit: (values: any) => {
|
||||
console.log(`Configuring ${provider.name}:`, values);
|
||||
// Your logic to save the configuration
|
||||
},
|
||||
formProps: {},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
OnRefresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{providers.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.name}
|
||||
provider={provider}
|
||||
onConfigure={() => configureProviderViaModal(provider)}
|
||||
onLaunch={handleLaunch}
|
||||
isOnboarding={isOnboarding}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
// Memoize these functions so they don't get recreated on every render
|
||||
const configureProviderViaModal = useCallback(
|
||||
(provider: ProviderDetails) => {
|
||||
openModal(provider, {
|
||||
onSubmit: (values: any) => {
|
||||
// Your logic to save the configuration
|
||||
},
|
||||
formProps: {},
|
||||
});
|
||||
},
|
||||
[openModal]
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProviderGrid({
|
||||
const handleLaunch = useCallback(() => {
|
||||
OnRefresh();
|
||||
}, []);
|
||||
|
||||
// Use useMemo to memoize the cards array
|
||||
const providerCards = useMemo(() => {
|
||||
return providers.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.name}
|
||||
provider={provider}
|
||||
onConfigure={() => configureProviderViaModal(provider)}
|
||||
onLaunch={handleLaunch}
|
||||
isOnboarding={isOnboarding}
|
||||
/>
|
||||
));
|
||||
}, [providers, isOnboarding, configureProviderViaModal, handleLaunch]);
|
||||
|
||||
return <>{providerCards}</>;
|
||||
});
|
||||
|
||||
// Fix the ProviderModalProvider
|
||||
export const OptimizedProviderModalProvider = memo(function OptimizedProviderModalProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
isOpen: false,
|
||||
currentProvider: null,
|
||||
modalProps: {},
|
||||
openModal: (provider, additionalProps = {}) => {
|
||||
// Implementation
|
||||
},
|
||||
closeModal: () => {
|
||||
// Implementation
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return <ProviderModalProvider>{children}</ProviderModalProvider>;
|
||||
});
|
||||
|
||||
export default memo(function ProviderGrid({
|
||||
providers,
|
||||
isOnboarding,
|
||||
}: {
|
||||
providers: ProviderState[];
|
||||
providers: ProviderDetails[];
|
||||
isOnboarding: boolean;
|
||||
}) {
|
||||
console.log('(1) Provider Grid -- is this the onboarding page?', isOnboarding);
|
||||
return (
|
||||
<GridLayout>
|
||||
// Remove the console.log
|
||||
console.log('provider grid');
|
||||
// Memoize the modal provider and its children to avoid recreating on every render
|
||||
const modalProviderContent = useMemo(
|
||||
() => (
|
||||
<ProviderModalProvider>
|
||||
<ProviderCards providers={providers} isOnboarding={isOnboarding} />
|
||||
<ProviderConfigurationModal />
|
||||
</ProviderModalProvider>
|
||||
</GridLayout>
|
||||
),
|
||||
[providers, isOnboarding]
|
||||
);
|
||||
}
|
||||
|
||||
return <GridLayout>{modalProviderContent}</GridLayout>;
|
||||
});
|
||||
|
||||
@@ -1,61 +1,45 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } 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 },
|
||||
},
|
||||
{
|
||||
id: 'gcp_vertex_ai',
|
||||
name: 'GCP Vertex AI',
|
||||
isConfigured: true,
|
||||
metadata: { location: null },
|
||||
},
|
||||
];
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ProviderDetails } from '../../../api/types.gen';
|
||||
|
||||
export default function ProviderSettings({ onClose }: { onClose: () => void }) {
|
||||
const { getProviders } = useConfig();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [providers, setProviders] = useState<ProviderDetails[]>([]);
|
||||
|
||||
// Load providers only once when component mounts
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
// Force refresh to ensure we have the latest data
|
||||
const result = await getProviders(true);
|
||||
// Only update state if component is still mounted
|
||||
if (isMounted && result) {
|
||||
setProviders(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadProviders();
|
||||
|
||||
// Cleanup function to prevent state updates on unmounted component
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []); // Empty dependency array ensures this only runs once
|
||||
|
||||
console.log(providers);
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||
@@ -66,7 +50,7 @@ export default function ProviderSettings({ onClose }: { onClose: () => void }) {
|
||||
<h1 className="text-3xl font-medium text-textStandard mt-1">Configure</h1>
|
||||
</div>
|
||||
|
||||
<div className=" py-8 pt-[20px]">
|
||||
<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>
|
||||
@@ -74,7 +58,11 @@ export default function ProviderSettings({ onClose }: { onClose: () => void }) {
|
||||
{/* Content Area */}
|
||||
<div className="max-w-5xl pt-4 px-8">
|
||||
<div className="relative z-10">
|
||||
<ProviderGrid providers={fakeProviderState} isOnboarding={false} />
|
||||
{loading ? (
|
||||
<div>Loading providers...</div>
|
||||
) : (
|
||||
<ProviderGrid providers={providers} isOnboarding={false} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-19
@@ -5,10 +5,18 @@ import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetup
|
||||
import ProviderSetupActions from './subcomponents/ProviderSetupActions';
|
||||
import ProviderLogo from './subcomponents/ProviderLogo';
|
||||
import { useProviderModal } from './ProviderModalProvider';
|
||||
import { toast } from 'react-toastify';
|
||||
import { PROVIDER_REGISTRY } from '../ProviderRegistry';
|
||||
import { SecureStorageNotice } from './subcomponents/SecureStorageNotice';
|
||||
import DefaultSubmitHandler from './subcomponents/handlers/DefaultSubmitHandler';
|
||||
import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler';
|
||||
import OllamaForm from './subcomponents/forms/OllamaForm';
|
||||
|
||||
const customSubmitHandler = {
|
||||
provider_name: OllamaSubmitHandler, // example
|
||||
};
|
||||
|
||||
const customForms = {
|
||||
provider_name: OllamaForm, // example
|
||||
};
|
||||
|
||||
export default function ProviderConfigurationModal() {
|
||||
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
|
||||
@@ -32,23 +40,11 @@ export default function ProviderConfigurationModal() {
|
||||
|
||||
if (!isOpen || !currentProvider) return null;
|
||||
|
||||
const headerText = `Configure ${currentProvider.name}`;
|
||||
const headerText = `Configure ${currentProvider.metadata.display_name}`;
|
||||
const descriptionText = `Add your API key(s) for this provider to integrate into Goose`;
|
||||
|
||||
// Find the provider in the registry to get the details with customForm
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === currentProvider.name);
|
||||
|
||||
// Get the custom submit handler from the provider details
|
||||
const customSubmitHandler = providerEntry?.details?.customSubmit;
|
||||
|
||||
// Use custom submit handler otherwise use default
|
||||
const SubmitHandler = customSubmitHandler || DefaultSubmitHandler;
|
||||
|
||||
// Get the custom form component from the provider details
|
||||
const CustomForm = providerEntry?.details?.customForm;
|
||||
|
||||
// Use custom form component if available, otherwise use default
|
||||
const FormComponent = CustomForm || DefaultProviderSetupForm;
|
||||
const SubmitHandler = customSubmitHandler[currentProvider.name] || DefaultSubmitHandler;
|
||||
const FormComponent = customForms[currentProvider.name] || DefaultProviderSetupForm;
|
||||
|
||||
const handleSubmitForm = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -74,7 +70,7 @@ export default function ProviderConfigurationModal() {
|
||||
<Modal>
|
||||
<div className="space-y-1">
|
||||
{/* Logo area - centered above title */}
|
||||
<ProviderLogo providerName={currentProvider.id} />
|
||||
<ProviderLogo providerName={currentProvider.name} />
|
||||
{/* Title and some information - centered */}
|
||||
<ProviderSetupHeader title={headerText} body={descriptionText} />
|
||||
</div>
|
||||
@@ -87,7 +83,7 @@ export default function ProviderConfigurationModal() {
|
||||
{...(modalProps.formProps || {})} // Spread any custom form props
|
||||
/>
|
||||
|
||||
{providerEntry?.details?.parameters && providerEntry.details.parameters.length > 0 && (
|
||||
{currentProvider.metadata.config_keys && currentProvider.metadata.config_keys.length > 0 && (
|
||||
<SecureStorageNotice />
|
||||
)}
|
||||
<ProviderSetupActions onCancel={handleCancel} onSubmit={handleSubmitForm} />
|
||||
|
||||
@@ -1,55 +1,60 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import ProviderState from '../interfaces/ProviderState';
|
||||
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';
|
||||
import { ProviderDetails } from '../../../../api';
|
||||
|
||||
interface ProviderModalContextType {
|
||||
isOpen: boolean;
|
||||
currentProvider: ProviderState | null;
|
||||
currentProvider: ProviderDetails | null;
|
||||
modalProps: any;
|
||||
openModal: (provider: ProviderState, additionalProps: any) => void;
|
||||
openModal: (provider: ProviderDetails, additionalProps: any) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ProviderModalContext = createContext({
|
||||
const defaultContext: ProviderModalContextType = {
|
||||
isOpen: false,
|
||||
currentProvider: null,
|
||||
modalProps: {},
|
||||
openModal: (provider, additionalProps) => {},
|
||||
openModal: () => {},
|
||||
closeModal: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
const ProviderModalContext = createContext<ProviderModalContextType>(defaultContext);
|
||||
|
||||
export const useProviderModal = () => useContext<ProviderModalContextType>(ProviderModalContext);
|
||||
|
||||
export const ProviderModalProvider = ({ children }) => {
|
||||
export const ProviderModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentProvider, setCurrentProvider] = useState(null);
|
||||
const [currentProvider, setCurrentProvider] = useState<ProviderDetails | null>(null);
|
||||
const [modalProps, setModalProps] = useState({});
|
||||
|
||||
const openModal = (provider, additionalProps = {}) => {
|
||||
// Use useCallback to prevent function recreation on each render
|
||||
const openModal = useCallback((provider: ProviderDetails, additionalProps = {}) => {
|
||||
setCurrentProvider(provider);
|
||||
setModalProps(additionalProps);
|
||||
setIsOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const closeModal = () => {
|
||||
const closeModal = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
// Use a small timeout to prevent UI flicker
|
||||
setTimeout(() => {
|
||||
setCurrentProvider(null);
|
||||
setModalProps({});
|
||||
}, 200);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
isOpen,
|
||||
currentProvider,
|
||||
modalProps,
|
||||
openModal,
|
||||
closeModal,
|
||||
}),
|
||||
[isOpen, currentProvider, modalProps, openModal, closeModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProviderModalContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
currentProvider,
|
||||
modalProps,
|
||||
openModal,
|
||||
closeModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ProviderModalContext.Provider>
|
||||
<ProviderModalContext.Provider value={contextValue}>{children}</ProviderModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-1
@@ -20,7 +20,11 @@ const providerLogos = {
|
||||
default: DefaultLogo,
|
||||
};
|
||||
|
||||
export default function ProviderLogo({ providerName }) {
|
||||
interface ProviderLogoProps {
|
||||
providerName: string;
|
||||
}
|
||||
|
||||
export default function ProviderLogo({ providerName }: ProviderLogoProps) {
|
||||
// Convert provider name to lowercase and fetch the logo
|
||||
const logoKey = providerName.toLowerCase();
|
||||
const logo = providerLogos[logoKey] || DefaultLogo;
|
||||
|
||||
+61
-22
@@ -1,16 +1,29 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
|
||||
|
||||
export default function DefaultProviderSetupForm({ configValues, setConfigValues, provider }) {
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
const parameters = providerEntry?.details?.parameters || [];
|
||||
interface DefaultProviderSetupFormProps {
|
||||
configValues: Record<string, any>;
|
||||
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
provider: any;
|
||||
}
|
||||
|
||||
export default function DefaultProviderSetupForm({
|
||||
configValues,
|
||||
setConfigValues,
|
||||
provider,
|
||||
}: DefaultProviderSetupFormProps) {
|
||||
const parameters = provider.metadata.config_keys || [];
|
||||
|
||||
// Initialize default values when the component mounts or provider changes
|
||||
useEffect(() => {
|
||||
const defaultValues = {};
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.default !== undefined && !configValues[parameter.name]) {
|
||||
if (
|
||||
parameter.required &&
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
defaultValues[parameter.name] = parameter.default;
|
||||
}
|
||||
});
|
||||
@@ -24,25 +37,51 @@ export default function DefaultProviderSetupForm({ configValues, setConfigValues
|
||||
}
|
||||
}, [provider.name, parameters, setConfigValues, configValues]);
|
||||
|
||||
// Filter parameters to only show required ones
|
||||
const requiredParameters = useMemo(() => {
|
||||
return parameters.filter((param) => param.required === true);
|
||||
}, [parameters]);
|
||||
|
||||
// Helper function to generate appropriate placeholder text
|
||||
const getPlaceholder = (parameter) => {
|
||||
// If default is defined and not null, show it
|
||||
if (parameter.default !== undefined && parameter.default !== null) {
|
||||
return `Default: ${parameter.default}`;
|
||||
}
|
||||
|
||||
// Otherwise, use the parameter name as a hint
|
||||
return parameter.name.toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 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
|
||||
/>
|
||||
{requiredParameters.length === 0 ? (
|
||||
<div className="text-center text-gray-500">
|
||||
No required configuration for this provider.
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
requiredParameters.map((parameter) => (
|
||||
<div key={parameter.name}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{parameter.name}
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type={parameter.secret ? 'password' : 'text'}
|
||||
value={configValues[parameter.name] || ''}
|
||||
onChange={(e) =>
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
[parameter.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={getPlaceholder(parameter)}
|
||||
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={true}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ExclamationButton, GreenCheckButton } from './buttons/CardButtons';
|
||||
import {
|
||||
ConfiguredProviderTooltipMessage,
|
||||
OllamaNotConfiguredTooltipMessage,
|
||||
ProviderDescription,
|
||||
} from './utils/StringUtils';
|
||||
import React, { memo } from 'react';
|
||||
import { GreenCheckButton } from './buttons/CardButtons';
|
||||
import { ConfiguredProviderTooltipMessage, ProviderDescription } from './utils/StringUtils';
|
||||
|
||||
interface CardHeaderProps {
|
||||
name: string;
|
||||
@@ -13,9 +9,10 @@ interface CardHeaderProps {
|
||||
}
|
||||
|
||||
// Make CardTitle a proper React component
|
||||
function CardTitle({ name }: { name: string }) {
|
||||
const CardTitle = memo(({ name }: { name: string }) => {
|
||||
return <h3 className="text-base font-medium text-textStandard truncate mr-2">{name}</h3>;
|
||||
}
|
||||
});
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
// Properly type ProviderNameAndStatus props
|
||||
interface ProviderNameAndStatusProps {
|
||||
@@ -23,9 +20,8 @@ interface ProviderNameAndStatusProps {
|
||||
isConfigured: boolean;
|
||||
}
|
||||
|
||||
function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProps) {
|
||||
console.log(`Provider Name: ${name}, Is Configured: ${isConfigured}`);
|
||||
|
||||
const ProviderNameAndStatus = memo(({ name, isConfigured }: ProviderNameAndStatusProps) => {
|
||||
// Remove the console.log completely
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CardTitle name={name} />
|
||||
@@ -34,14 +30,18 @@ function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProp
|
||||
{isConfigured && <GreenCheckButton tooltip={ConfiguredProviderTooltipMessage(name)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
ProviderNameAndStatus.displayName = 'ProviderNameAndStatus';
|
||||
|
||||
// Add a container div to the CardHeader
|
||||
export default function CardHeader({ name, description, isConfigured }: CardHeaderProps) {
|
||||
const CardHeader = memo(function CardHeader({ name, description, isConfigured }: CardHeaderProps) {
|
||||
return (
|
||||
<>
|
||||
<ProviderNameAndStatus name={name} isConfigured={isConfigured} />
|
||||
<ProviderDescription description={description} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
export default CardHeader;
|
||||
|
||||
@@ -1,74 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import CardContainer from './CardContainer';
|
||||
import CardHeader from './CardHeader';
|
||||
import ProviderState from '../interfaces/ProviderState';
|
||||
import CardBody from './CardBody';
|
||||
import { PROVIDER_REGISTRY } from '../ProviderRegistry';
|
||||
import DefaultCardButtons from './buttons/DefaultCardButtons';
|
||||
import { ProviderDetails, ProviderMetadata } from '../../../../api';
|
||||
|
||||
type ProviderCardProps = {
|
||||
provider: ProviderState;
|
||||
provider: ProviderDetails;
|
||||
onConfigure: () => void;
|
||||
onLaunch: () => void;
|
||||
isOnboarding: boolean;
|
||||
};
|
||||
|
||||
// export function ProviderCard({ provider, buttonCallbacks, isOnboarding }: 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, buttonCallbacks, isOnboarding);
|
||||
//
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
export const ProviderCard = memo(function ProviderCard({
|
||||
provider,
|
||||
onConfigure,
|
||||
onLaunch,
|
||||
isOnboarding,
|
||||
}: ProviderCardProps) {
|
||||
// Safely access metadata with null checks
|
||||
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
|
||||
|
||||
export function ProviderCard({ provider, onConfigure, onLaunch, isOnboarding }: ProviderCardProps) {
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
// Instead of useEffect for logging, use useMemo to memoize the metadata
|
||||
const metadata = useMemo(() => providerMetadata, [provider]);
|
||||
|
||||
// Add safety check
|
||||
if (!providerEntry?.details) {
|
||||
console.error(`Provider ${provider.name} not found in registry or has no details`);
|
||||
return null;
|
||||
// Remove the logging completely
|
||||
|
||||
if (!metadata) {
|
||||
return <div>ProviderCard error: No metadata provided</div>;
|
||||
}
|
||||
|
||||
const providerDetails = providerEntry.details;
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
header={
|
||||
<CardHeader
|
||||
name={providerDetails.name}
|
||||
description={providerDetails.description}
|
||||
isConfigured={provider.isConfigured}
|
||||
name={metadata.display_name || provider?.name || 'Unknown Provider'}
|
||||
description={metadata.description || ''}
|
||||
isConfigured={provider?.is_configured || false}
|
||||
/>
|
||||
}
|
||||
body={
|
||||
@@ -83,4 +51,4 @@ export function ProviderCard({ provider, onConfigure, onLaunch, isOnboarding }:
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { ConfigureSettingsButton, RocketButton } from './CardButtons';
|
||||
import ProviderState from '@/src/components/settings_v2/providers/interfaces/ProviderState';
|
||||
import { ProviderDetails } from '../../../../../api';
|
||||
|
||||
// can define other optional callbacks as needed
|
||||
interface CardButtonsProps {
|
||||
provider: ProviderState;
|
||||
provider: ProviderDetails;
|
||||
isOnboardingPage: boolean;
|
||||
onConfigure: (provider: ProviderState) => void;
|
||||
onLaunch: (provider: ProviderState) => void;
|
||||
onConfigure: (provider: ProviderDetails) => void;
|
||||
onLaunch: (provider: ProviderDetails) => void;
|
||||
}
|
||||
|
||||
function getDefaultTooltipMessages(name: string, actionType: string) {
|
||||
@@ -32,7 +32,7 @@ export default function DefaultCardButtons({
|
||||
return (
|
||||
<>
|
||||
{/*Set up an unconfigured provider */}
|
||||
{!provider.isConfigured && (
|
||||
{!provider.is_configured && (
|
||||
<ConfigureSettingsButton
|
||||
tooltip={getDefaultTooltipMessages(provider.name, 'add')}
|
||||
onClick={(e) => {
|
||||
@@ -42,7 +42,7 @@ export default function DefaultCardButtons({
|
||||
/>
|
||||
)}
|
||||
{/*show edit tooltip instead when hovering over button for configured providers*/}
|
||||
{provider.isConfigured && !isOnboardingPage && (
|
||||
{provider.is_configured && !isOnboardingPage && (
|
||||
<ConfigureSettingsButton
|
||||
tooltip={getDefaultTooltipMessages(provider.name, 'edit')}
|
||||
onClick={(e) => {
|
||||
@@ -52,7 +52,7 @@ export default function DefaultCardButtons({
|
||||
/>
|
||||
)}
|
||||
{/*show Launch button for configured providers on onboarding page*/}
|
||||
{provider.isConfigured && isOnboardingPage && (
|
||||
{provider.is_configured && isOnboardingPage && (
|
||||
<RocketButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
+1
-26
@@ -14,30 +14,5 @@ export function snakeToTitleCase(snake: string): string {
|
||||
|
||||
export function patchConsoleLogging() {
|
||||
// Intercept console methods
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
};
|
||||
|
||||
console.log = (...args: any[]) => {
|
||||
window.electron.logInfo(`[LOG] ${args.join(' ')}`);
|
||||
originalConsole.log(...args);
|
||||
};
|
||||
|
||||
console.error = (...args: any[]) => {
|
||||
window.electron.logInfo(`[ERROR] ${args.join(' ')}`);
|
||||
originalConsole.error(...args);
|
||||
};
|
||||
|
||||
console.warn = (...args: any[]) => {
|
||||
window.electron.logInfo(`[WARN] ${args.join(' ')}`);
|
||||
originalConsole.warn(...args);
|
||||
};
|
||||
|
||||
console.info = (...args: any[]) => {
|
||||
window.electron.logInfo(`[INFO] ${args.join(' ')}`);
|
||||
originalConsole.info(...args);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user