增加 openai 的模型兼容
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
223
prisma/components/settings/ModelSection.tsx
Normal file
223
prisma/components/settings/ModelSection.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user