增加 openai 的模型兼容

This commit is contained in:
jwangkun
2026-01-08 17:09:34 +08:00
parent 6558006a4d
commit 579071ac95
18 changed files with 5185 additions and 212 deletions

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Settings, ChevronDown, Menu } from 'lucide-react';
import { MODELS } from '../config';
import { ModelOption } from '../types';
import { getAllModels } from '../config';
import { ModelOption, AppConfig } from '../types';
import Logo from './Logo';
interface HeaderProps {
@@ -11,22 +11,25 @@ interface HeaderProps {
onOpenSettings: () => void;
onToggleSidebar: () => void;
onNewChat: () => void;
config: AppConfig;
}
const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSidebar, onNewChat }: HeaderProps) => {
const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSidebar, onNewChat, config }: HeaderProps) => {
const availableModels = getAllModels(config);
return (
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-md">
<div className="w-full px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-4">
<button
<button
onClick={onToggleSidebar}
className="p-2 -ml-2 text-slate-500 hover:bg-slate-100 rounded-lg transition-colors"
title="Toggle History"
>
<Menu size={20} />
</button>
<div
<div
className="flex items-center gap-3 cursor-pointer group"
onClick={onNewChat}
title="Start New Chat"
@@ -37,22 +40,22 @@ const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSideb
</h1>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<div className="relative group">
<select
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value as ModelOption)}
className="relative bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-auto p-2.5 outline-none appearance-none cursor-pointer pl-3 pr-8 shadow-sm font-medium hover:bg-slate-50 transition-colors"
>
{MODELS.map(m => (
{availableModels.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-3 text-slate-400 pointer-events-none group-hover:text-slate-600 transition-colors" size={14} />
</div>
<button
<button
onClick={onOpenSettings}
className="p-2.5 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 hover:border-slate-300 transition-colors text-slate-500 hover:text-slate-900 shadow-sm"
title="Configuration"

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Key, Globe } from 'lucide-react';
import { Key, Globe, Info } from 'lucide-react';
import { AppConfig } from '../../types';
interface ApiSectionProps {
@@ -12,26 +12,34 @@ const ApiSection = ({ config, setConfig }: ApiSectionProps) => {
return (
<div className="space-y-4 pt-1">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">API Connection</h3>
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Default API Connection</h3>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.enableCustomApi ?? false}
onChange={(e) => setConfig({ ...config, enableCustomApi: e.target.checked })}
className="sr-only peer"
<input
type="checkbox"
checked={config.enableCustomApi ?? false}
onChange={(e) => setConfig({ ...config, enableCustomApi: e.target.checked })}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</label>
</div>
{config.enableCustomApi && (
<div className="space-y-4 p-4 bg-slate-50 rounded-lg border border-slate-100 animate-in fade-in slide-in-from-top-1 duration-200">
<div className="flex items-start gap-2 p-3 bg-blue-50 rounded-lg border border-blue-100">
<Info size={16} className="text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-xs text-blue-800">
<p className="font-medium mb-1">Custom Model Configuration</p>
<p>Each custom model can now be configured with its own API key and base URL in the Custom Models section below. This default configuration is used for preset models.</p>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Key size={14} className="text-slate-400" />
Custom API Key
Default API Key
</label>
<input
<input
type="password"
placeholder="sk-..."
value={config.customApiKey || ''}
@@ -43,11 +51,11 @@ const ApiSection = ({ config, setConfig }: ApiSectionProps) => {
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Globe size={14} className="text-slate-400" />
Custom Base URL
Default Base URL
</label>
<input
<input
type="text"
placeholder="https://generativelanguage.googleapis.com"
placeholder="https://api.example.com/v1"
value={config.customBaseUrl || ''}
onChange={(e) => setConfig({ ...config, customBaseUrl: e.target.value })}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"

View File

@@ -0,0 +1,223 @@
import React, { useState } from 'react';
import { Plus, Trash2, Bot, Key, Globe, ChevronDown, ChevronUp } from 'lucide-react';
import { AppConfig, ApiProvider, CustomModel } from '../../types';
interface ModelSectionProps {
config: AppConfig;
setConfig: (c: AppConfig) => void;
}
const ModelSection = ({ config, setConfig }: ModelSectionProps) => {
const [newModelName, setNewModelName] = useState('');
const [newModelProvider, setNewModelProvider] = useState<ApiProvider>('custom');
const [newModelApiKey, setNewModelApiKey] = useState('');
const [newModelBaseUrl, setNewModelBaseUrl] = useState('');
const [expandedModelId, setExpandedModelId] = useState<string | null>(null);
const customModels = config.customModels || [];
const handleAddModel = () => {
if (!newModelName.trim()) return;
const newModel: CustomModel = {
id: `custom-${Date.now()}`,
name: newModelName.trim(),
provider: newModelProvider,
apiKey: newModelApiKey || undefined,
baseUrl: newModelBaseUrl || undefined
};
setConfig({
...config,
customModels: [...customModels, newModel]
});
setNewModelName('');
setNewModelApiKey('');
setNewModelBaseUrl('');
};
const handleDeleteModel = (modelId: string) => {
setConfig({
...config,
customModels: customModels.filter(m => m.id !== modelId)
});
if (expandedModelId === modelId) {
setExpandedModelId(null);
}
};
const handleUpdateModel = (modelId: string, updates: Partial<CustomModel>) => {
setConfig({
...config,
customModels: customModels.map(m =>
m.id === modelId ? { ...m, ...updates } : m
)
});
};
return (
<div className="space-y-4 pt-1">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Custom Models</h3>
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100 space-y-4">
<div className="space-y-3">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Bot size={14} className="text-slate-400" />
Model Name
</label>
<input
type="text"
placeholder="e.g., llama-3-8b-instruct, qwen-72b-chat"
value={newModelName}
onChange={(e) => setNewModelName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddModel()}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Provider</label>
<select
value={newModelProvider}
onChange={(e) => setNewModelProvider(e.target.value as ApiProvider)}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none"
>
<option value="custom">Custom (OpenAI-compatible)</option>
<option value="openai">OpenAI</option>
<option value="deepseek">DeepSeek</option>
<option value="anthropic">Anthropic</option>
<option value="xai">xAI</option>
<option value="mistral">Mistral</option>
<option value="google">Google</option>
</select>
</div>
{(newModelProvider === 'custom' || newModelProvider === 'openai' || newModelProvider === 'anthropic' || newModelProvider === 'xai' || newModelProvider === 'mistral') && (
<>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Key size={14} className="text-slate-400" />
API Key (optional)
</label>
<input
type="password"
placeholder="sk-..."
value={newModelApiKey}
onChange={(e) => setNewModelApiKey(e.target.value)}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Globe size={14} className="text-slate-400" />
Base URL (optional)
</label>
<input
type="text"
placeholder="https://api.example.com/v1"
value={newModelBaseUrl}
onChange={(e) => setNewModelBaseUrl(e.target.value)}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"
/>
</div>
</>
)}
<button
onClick={handleAddModel}
disabled={!newModelName.trim()}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-all shadow-sm"
>
<Plus size={16} />
Add Model
</button>
</div>
{customModels.length > 0 && (
<div className="border-t border-slate-200 pt-4">
<div className="text-xs font-medium text-slate-500 mb-3">
Added Models ({customModels.length})
</div>
<div className="space-y-2">
{customModels.map((model) => (
<div
key={model.id}
className="bg-white rounded-lg border border-slate-200 hover:border-slate-300 transition-colors"
>
<div
className="flex items-center justify-between p-3 cursor-pointer"
onClick={() => setExpandedModelId(expandedModelId === model.id ? null : model.id)}
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-800 truncate">
{model.name}
</div>
<div className="text-xs text-slate-500 capitalize">
{model.provider} {model.apiKey && '• Configured'}
</div>
</div>
<div className="flex items-center gap-1">
{expandedModelId === model.id ? (
<ChevronUp size={16} className="text-slate-400" />
) : (
<ChevronDown size={16} className="text-slate-400" />
)}
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteModel(model.id);
}}
className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Remove model"
>
<Trash2 size={16} />
</button>
</div>
</div>
{expandedModelId === model.id && (
<div className="px-3 pb-3 pt-0 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Key size={14} className="text-slate-400" />
API Key
</label>
<input
type="password"
placeholder="sk-..."
value={model.apiKey || ''}
onChange={(e) => handleUpdateModel(model.id, { apiKey: e.target.value || undefined })}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700 flex items-center gap-2">
<Globe size={14} className="text-slate-400" />
Base URL
</label>
<input
type="text"
placeholder="https://api.example.com/v1"
value={model.baseUrl || ''}
onChange={(e) => handleUpdateModel(model.id, { baseUrl: e.target.value || undefined })}
className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400"
/>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default ModelSection;