feat ui: support import provider from template

This commit is contained in:
musistudio
2025-08-02 22:56:18 +08:00
parent cd43a74ab5
commit 996a05d1d6
10 changed files with 161 additions and 104 deletions

View File

@@ -1,53 +1,7 @@
import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode, Dispatch, SetStateAction } from 'react';
import { api } from '@/lib/api';
export interface Transformer {
path: string;
options: {
[key: string]: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
export interface ProviderTransformer {
use: (string | (string | Record<string, unknown> | { max_tokens: number })[])[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; // for model specific transformers
}
export interface Provider {
name: string;
api_base_url: string;
api_key: string;
models: string[];
transformer?: ProviderTransformer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
export interface RouterConfig {
default: string;
background: string;
think: string;
longContext: string;
longContextThreshold: number;
webSearch: string;
}
export interface Config {
LOG: boolean;
CLAUDE_PATH: string;
HOST: string;
PORT: number;
APIKEY: string;
API_TIMEOUT_MS: string;
PROXY_URL: string;
transformers: Transformer[];
Providers: Provider[];
Router: RouterConfig;
}
import type { Config } from '@/types';
interface ConfigContextType {
config: Config | null;
@@ -121,7 +75,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
APIKEY: typeof data.APIKEY === 'string' ? data.APIKEY : '',
API_TIMEOUT_MS: typeof data.API_TIMEOUT_MS === 'string' ? data.API_TIMEOUT_MS : '600000',
PROXY_URL: typeof data.PROXY_URL === 'string' ? data.PROXY_URL : '',
transformers: Array.isArray(data.transformers) ? data.transformers : [],
Transformers: Array.isArray(data.Transformers) ? data.Transformers : [],
Providers: Array.isArray(data.Providers) ? data.Providers : [],
Router: data.Router && typeof data.Router === 'object' ? {
default: typeof data.Router.default === 'string' ? data.Router.default : '',
@@ -155,7 +109,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
APIKEY: '',
API_TIMEOUT_MS: '600000',
PROXY_URL: '',
transformers: [],
Transformers: [],
Providers: [],
Router: {
default: '',

View File

@@ -1,7 +1,7 @@
import { Pencil, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { type Provider } from "./ConfigProvider";
import type { Provider } from "@/types";
interface ProviderListProps {
providers: Provider[];

View File

@@ -19,7 +19,9 @@ import { Badge } from "@/components/ui/badge";
import { Combobox } from "@/components/ui/combobox";
import { ComboInput } from "@/components/ui/combo-input";
import { api } from "@/lib/api";
import type { Provider } from "@/types";
interface ProviderType extends Provider {}
export function Providers() {
const { t } = useTranslation();
@@ -30,10 +32,29 @@ export function Providers() {
const [providerParamInputs, setProviderParamInputs] = useState<Record<string, {name: string, value: string}>>({});
const [modelParamInputs, setModelParamInputs] = useState<Record<string, {name: string, value: string}>>({});
const [availableTransformers, setAvailableTransformers] = useState<{name: string; endpoint: string | null;}[]>([]);
const [editingProviderData, setEditingProviderData] = useState<any>(null);
const [editingProviderData, setEditingProviderData] = useState<ProviderType | null>(null);
const [isNewProvider, setIsNewProvider] = useState<boolean>(false);
const [providerTemplates, setProviderTemplates] = useState<ProviderType[]>([]);
const comboInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const fetchProviderTemplates = async () => {
try {
const response = await fetch('https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/providers.json');
if (response.ok) {
const data = await response.json();
setProviderTemplates(data || []);
} else {
console.error('Failed to fetch provider templates');
}
} catch (error) {
console.error('Failed to fetch provider templates:', error);
}
};
fetchProviderTemplates();
}, []);
// Fetch available transformers when component mounts
useEffect(() => {
const fetchTransformers = async () => {
@@ -67,7 +88,7 @@ export function Providers() {
const handleAddProvider = () => {
const newProvider = { name: "", api_base_url: "", api_key: "", models: [] };
const newProvider: ProviderType = { name: "", api_base_url: "", api_key: "", models: [] };
setEditingProviderIndex(config.Providers.length);
setEditingProviderData(newProvider);
setIsNewProvider(true);
@@ -116,14 +137,14 @@ export function Providers() {
setDeletingProviderIndex(null);
};
const handleProviderChange = (index: number, field: string, value: string) => {
const handleProviderChange = (_index: number, field: string, value: string) => {
if (editingProviderData) {
const updatedProvider = { ...editingProviderData, [field]: value };
setEditingProviderData(updatedProvider);
}
};
const handleProviderTransformerChange = (index: number, transformerPath: string) => {
const handleProviderTransformerChange = (_index: number, transformerPath: string) => {
if (!transformerPath || !editingProviderData) return; // Don't add empty transformers
const updatedProvider = { ...editingProviderData };
@@ -137,7 +158,7 @@ export function Providers() {
setEditingProviderData(updatedProvider);
};
const removeProviderTransformerAtIndex = (index: number, transformerIndex: number) => {
const removeProviderTransformerAtIndex = (_index: number, transformerIndex: number) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -156,7 +177,7 @@ export function Providers() {
setEditingProviderData(updatedProvider);
};
const handleModelTransformerChange = (providerIndex: number, model: string, transformerPath: string) => {
const handleModelTransformerChange = (_providerIndex: number, model: string, transformerPath: string) => {
if (!transformerPath || !editingProviderData) return; // Don't add empty transformers
const updatedProvider = { ...editingProviderData };
@@ -175,7 +196,7 @@ export function Providers() {
setEditingProviderData(updatedProvider);
};
const removeModelTransformerAtIndex = (providerIndex: number, model: string, transformerIndex: number) => {
const removeModelTransformerAtIndex = (_providerIndex: number, model: string, transformerIndex: number) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -195,7 +216,7 @@ export function Providers() {
};
const addProviderTransformerParameter = (providerIndex: number, transformerIndex: number, paramName: string, paramValue: string) => {
const addProviderTransformerParameter = (_providerIndex: number, transformerIndex: number, paramName: string, paramValue: string) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -239,7 +260,7 @@ export function Providers() {
};
const removeProviderTransformerParameterAtIndex = (providerIndex: number, transformerIndex: number, paramName: string) => {
const removeProviderTransformerParameterAtIndex = (_providerIndex: number, transformerIndex: number, paramName: string) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -269,7 +290,7 @@ export function Providers() {
}
};
const addModelTransformerParameter = (providerIndex: number, model: string, transformerIndex: number, paramName: string, paramValue: string) => {
const addModelTransformerParameter = (_providerIndex: number, model: string, transformerIndex: number, paramName: string, paramValue: string) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -317,7 +338,7 @@ export function Providers() {
};
const removeModelTransformerParameterAtIndex = (providerIndex: number, model: string, transformerIndex: number, paramName: string) => {
const removeModelTransformerParameterAtIndex = (_providerIndex: number, model: string, transformerIndex: number, paramName: string) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -347,7 +368,7 @@ export function Providers() {
}
};
const handleAddModel = (index: number, model: string) => {
const handleAddModel = (_index: number, model: string) => {
if (!model.trim() || !editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -363,7 +384,26 @@ export function Providers() {
}
};
const handleRemoveModel = (providerIndex: number, modelIndex: number) => {
const handleTemplateImport = (value: string) => {
if (!value) return;
try {
const selectedTemplate = JSON.parse(value);
if (selectedTemplate) {
const currentName = editingProviderData?.name;
const newProviderData = JSON.parse(JSON.stringify(selectedTemplate));
if (!isNewProvider && currentName) {
newProviderData.name = currentName;
}
setEditingProviderData(newProviderData as ProviderType);
}
} catch (e) {
console.error("Failed to parse template", e);
}
};
const handleRemoveModel = (_providerIndex: number, modelIndex: number) => {
if (!editingProviderData) return;
const updatedProvider = { ...editingProviderData };
@@ -407,6 +447,18 @@ export function Providers() {
</DialogHeader>
{editingProvider && editingProviderIndex !== null && (
<div className="space-y-4 p-4 overflow-y-auto flex-grow">
{providerTemplates.length > 0 && (
<div className="space-y-2">
<Label>{t("providers.import_from_template")}</Label>
<Combobox
options={providerTemplates.map(p => ({ label: p.name, value: JSON.stringify(p) }))}
value=""
onChange={handleTemplateImport}
placeholder={t("providers.select_template")}
emptyPlaceholder={t("providers.no_templates_found")}
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="name">{t("providers.name")}</Label>
<Input id="name" value={editingProvider.name || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'name', e.target.value)} />

View File

@@ -1,6 +1,6 @@
import { Pencil, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { type Transformer } from "./ConfigProvider";
import type { Transformer } from "@/types";
interface TransformerListProps {
transformers: Transformer[];
@@ -46,8 +46,8 @@ export function TransformerList({ transformers, onEdit, onRemove }: TransformerL
// Handle case where transformer.path might be null or undefined
const transformerPath = transformer.path || "Unnamed Transformer";
// Handle case where transformer.options might be null or undefined
const options = transformer.options || {};
// Handle case where transformer.parameters might be null or undefined
const options = transformer.parameters || {};
// Render parameters as tags in a single line
const renderParameters = () => {

View File

@@ -21,7 +21,7 @@ export function Transformers() {
const { config, setConfig } = useConfig();
const [editingTransformerIndex, setEditingTransformerIndex] = useState<number | null>(null);
const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null);
const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null);
const [newTransformer, setNewTransformer] = useState<{ name: string; path: string; project: string; parameters: { [key: string]: string } } | null>(null);
// Handle case where config is null or undefined
if (!config) {
@@ -37,11 +37,11 @@ export function Transformers() {
);
}
// Validate config.transformers to ensure it's an array
const validTransformers = Array.isArray(config.transformers) ? config.transformers : [];
// Validate config.Transformers to ensure it's an array
const validTransformers = Array.isArray(config.Transformers) ? config.Transformers : [];
const handleAddTransformer = () => {
const newTransformer = { path: "", options: {} };
const newTransformer = { name: "", path: "", project: "", parameters: {} };
setNewTransformer(newTransformer);
setEditingTransformerIndex(validTransformers.length); // Use the length as index for the new item
};
@@ -49,26 +49,26 @@ export function Transformers() {
const handleRemoveTransformer = (index: number) => {
const newTransformers = [...validTransformers];
newTransformers.splice(index, 1);
setConfig({ ...config, transformers: newTransformers });
setConfig({ ...config, Transformers: newTransformers });
setDeletingTransformerIndex(null);
};
const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => {
const handleTransformerChange = (index: number, field: string, value: string, parameterKey?: string) => {
if (index < validTransformers.length) {
// Editing an existing transformer
const newTransformers = [...validTransformers];
if (optionKey !== undefined) {
newTransformers[index].options[optionKey] = value;
if (parameterKey !== undefined) {
newTransformers[index].parameters![parameterKey] = value;
} else {
(newTransformers[index] as Record<string, unknown>)[field] = value;
(newTransformers[index] as unknown as Record<string, unknown>)[field] = value;
}
setConfig({ ...config, transformers: newTransformers });
setConfig({ ...config, Transformers: newTransformers });
} else {
// Editing the new transformer
if (newTransformer) {
const updatedTransformer = { ...newTransformer };
if (optionKey !== undefined) {
updatedTransformer.options[optionKey] = value;
if (parameterKey !== undefined) {
updatedTransformer.parameters![parameterKey] = value;
} else {
(updatedTransformer as Record<string, unknown>)[field] = value;
}
@@ -87,7 +87,7 @@ export function Transformers() {
if (newTransformer && editingTransformerIndex === validTransformers.length) {
// Saving a new transformer
const newTransformers = [...validTransformers, newTransformer];
setConfig({ ...config, transformers: newTransformers });
setConfig({ ...config, Transformers: newTransformers });
}
// Close the dialog
setEditingTransformerIndex(null);
@@ -137,16 +137,16 @@ export function Transformers() {
variant="outline"
size="sm"
onClick={() => {
const options = editingTransformer.options || {};
const newKey = `param${Object.keys(options).length + 1}`;
const parameters = editingTransformer.parameters || {};
const newKey = `param${Object.keys(parameters).length + 1}`;
if (editingTransformerIndex !== null) {
const newOptions = { ...options, [newKey]: "" };
const newParameters = { ...parameters, [newKey]: "" };
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
newTransformers[editingTransformerIndex].parameters = newParameters;
setConfig({ ...config, Transformers: newTransformers });
} else if (newTransformer) {
setNewTransformer({ ...newTransformer, options: newOptions });
setNewTransformer({ ...newTransformer, parameters: newParameters });
}
}
}}
@@ -154,22 +154,22 @@ export function Transformers() {
<Plus className="h-4 w-4" />
</Button>
</div>
{Object.entries(editingTransformer.options || {}).map(([key, value]) => (
{Object.entries(editingTransformer.parameters || {}).map(([key, value]) => (
<div key={key} className="flex items-center gap-2">
<Input
value={key}
onChange={(e) => {
const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key];
newOptions[e.target.value] = value;
const parameters = editingTransformer.parameters || {};
const newParameters = { ...parameters };
delete newParameters[key];
newParameters[e.target.value] = value;
if (editingTransformerIndex !== null) {
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
newTransformers[editingTransformerIndex].parameters = newParameters;
setConfig({ ...config, Transformers: newTransformers });
} else if (newTransformer) {
setNewTransformer({ ...newTransformer, options: newOptions });
setNewTransformer({ ...newTransformer, parameters: newParameters });
}
}
}}
@@ -179,7 +179,7 @@ export function Transformers() {
value={value}
onChange={(e) => {
if (editingTransformerIndex !== null) {
handleTransformerChange(editingTransformerIndex, "options", e.target.value, key);
handleTransformerChange(editingTransformerIndex, "parameters", e.target.value, key);
}
}}
className="flex-1"
@@ -189,15 +189,15 @@ export function Transformers() {
size="icon"
onClick={() => {
if (editingTransformerIndex !== null) {
const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key];
const parameters = editingTransformer.parameters || {};
const newParameters = { ...parameters };
delete newParameters[key];
if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers });
newTransformers[editingTransformerIndex].parameters = newParameters;
setConfig({ ...config, Transformers: newTransformers });
} else if (newTransformer) {
setNewTransformer({ ...newTransformer, options: newOptions });
setNewTransformer({ ...newTransformer, parameters: newParameters });
}
}
}}

View File

@@ -1,4 +1,4 @@
import type { Config, Provider, Transformer } from '@/components/ConfigProvider';
import type { Config, Provider, Transformer } from '@/types';
// API Client Class for handling requests with baseUrl and apikey authentication
class ApiClient {

View File

@@ -77,7 +77,11 @@
"add_parameter": "Add Parameter",
"parameter_name": "Parameter Name",
"parameter_value": "Parameter Value",
"selected_transformers": "Selected Transformers"
"selected_transformers": "Selected Transformers",
"import_from_template": "Import from template",
"no_templates_found": "No templates found",
"select_template": "Select a template..."
},
"router": {
"title": "Router",

View File

@@ -77,7 +77,11 @@
"add_parameter": "添加参数",
"parameter_name": "参数名称",
"parameter_value": "参数值",
"selected_transformers": "已选转换器"
"selected_transformers": "已选转换器",
"import_from_template": "从模板导入",
"no_templates_found": "未找到模板",
"select_template": "选择一个模板..."
},
"router": {
"title": "路由",

43
ui/src/types.ts Normal file
View File

@@ -0,0 +1,43 @@
export interface ProviderTransformer {
use: (string | (string | Record<string, unknown> | { max_tokens: number })[])[];
[key: string]: any; // Allow for model-specific transformers
}
export interface Provider {
name: string;
api_base_url: string;
api_key: string;
models: string[];
transformer?: ProviderTransformer;
}
export interface RouterConfig {
default: string;
background: string;
think: string;
longContext: string;
longContextThreshold: number;
webSearch: string;
custom?: any;
}
export interface Transformer {
name: string;
path: string;
project: string;
parameters?: Record<string, any>;
}
export interface Config {
Providers: Provider[];
Router: RouterConfig;
Transformers: Transformer[];
// Top-level settings
LOG: boolean;
CLAUDE_PATH: string;
HOST: string;
PORT: number;
APIKEY: string;
API_TIMEOUT_MS: string;
PROXY_URL: string;
}

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts"],"version":"5.8.3"}