330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
import React, { useEffect, useMemo } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
TextField,
|
|
Box,
|
|
Typography,
|
|
CircularProgress,
|
|
MenuItem,
|
|
Select,
|
|
FormControl,
|
|
InputLabel,
|
|
FormHelperText,
|
|
Link,
|
|
} from '@mui/material';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { useTranslation } from 'react-i18next';
|
|
import logger from '@/utils/logger';
|
|
import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm';
|
|
import i18n from '@/locales';
|
|
|
|
// 表单数据接口
|
|
export interface OllamaFormData {
|
|
model_type: string;
|
|
llm_name: string;
|
|
api_base: string;
|
|
api_key?: string;
|
|
max_tokens: number;
|
|
llm_factory: string;
|
|
}
|
|
|
|
|
|
// 对话框 Props 接口
|
|
export interface OllamaDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: OllamaFormData) => void;
|
|
loading: boolean;
|
|
initialData?: OllamaFormData;
|
|
editMode?: boolean;
|
|
}
|
|
|
|
const llmFactoryToUrlMap: { [x: string]: string } = {
|
|
[LLM_FACTORY_LIST.Ollama]:
|
|
'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
|
|
[LLM_FACTORY_LIST.Xinference]:
|
|
'https://inference.readthedocs.io/en/latest/user_guide',
|
|
[LLM_FACTORY_LIST.ModelScope]:
|
|
'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
|
|
[LLM_FACTORY_LIST.LocalAI]: 'https://localai.io/docs/getting-started/models/',
|
|
[LLM_FACTORY_LIST.LMStudio]: 'https://lmstudio.ai/docs/basics',
|
|
[LLM_FACTORY_LIST.OpenAiAPICompatible]:
|
|
'https://platform.openai.com/docs/models/gpt-4',
|
|
[LLM_FACTORY_LIST.TogetherAI]: 'https://docs.together.ai/docs/deployment-options',
|
|
[LLM_FACTORY_LIST.Replicate]: 'https://replicate.com/docs/topics/deployments',
|
|
[LLM_FACTORY_LIST.OpenRouter]: 'https://openrouter.ai/docs',
|
|
[LLM_FACTORY_LIST.HuggingFace]:
|
|
'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
|
|
[LLM_FACTORY_LIST.GPUStack]: 'https://docs.gpustack.ai/latest/quickstart',
|
|
[LLM_FACTORY_LIST.VLLM]: 'https://docs.vllm.ai/en/latest/',
|
|
} as const;
|
|
|
|
function getURLByFactory(factory: LLMFactory) {
|
|
const url = llmFactoryToUrlMap[factory];
|
|
return {
|
|
textTip: `${i18n.t('setting.howToIntegrate')} ${factory}`,
|
|
url,
|
|
}
|
|
}
|
|
|
|
|
|
// 模型类型选项
|
|
const MODEL_TYPE_OPTIONS = [
|
|
{ value: 'chat', label: 'Chat' },
|
|
{ value: 'embedding', label: 'Embedding' },
|
|
{ value: 'rerank', label: 'Rerank' },
|
|
{ value: 'image2text', label: 'Image2Text' },
|
|
{ value: 'speech2text', label: 'Speech2Text' },
|
|
];
|
|
|
|
|
|
/**
|
|
* Ollama / local llm 配置对话框
|
|
*/
|
|
function OllamaDialog({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
loading,
|
|
initialData,
|
|
editMode = false,
|
|
}: OllamaDialogProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
reset,
|
|
} = useForm<OllamaFormData>({
|
|
defaultValues: {
|
|
model_type: initialData?.model_type || 'chat',
|
|
llm_name: initialData?.llm_name || '',
|
|
api_base: initialData?.api_base,
|
|
api_key: initialData?.api_key,
|
|
max_tokens: initialData?.max_tokens,
|
|
llm_factory: initialData?.llm_factory || 'Ollama',
|
|
},
|
|
});
|
|
|
|
const modelTypeOptions = useMemo(() => {
|
|
const factory = initialData?.llm_factory || LLM_FACTORY_LIST.Ollama;
|
|
if (factory == LLM_FACTORY_LIST.HuggingFace) {
|
|
return [
|
|
{ value: 'embedding', label: 'Embedding' },
|
|
{ value: 'chat', label: 'Chat' },
|
|
{ value: 'rerank', label: 'Rerank' },
|
|
]
|
|
} else if (factory == LLM_FACTORY_LIST.Xinference) {
|
|
return [
|
|
{ value: 'chat', label: 'Chat' },
|
|
{ value: 'embedding', label: 'Embedding' },
|
|
{ value: 'rerank', label: 'Rerank' },
|
|
{ value: 'image2text', label: 'Image2Text' },
|
|
{ value: 'speech2text', label: 'Speech2Text' },
|
|
{ value: 'tts', label: 'TTS' },
|
|
]
|
|
} else if (factory == LLM_FACTORY_LIST.ModelScope) {
|
|
return [
|
|
{ value: 'chat', label: 'Chat' },
|
|
]
|
|
} else if (factory == LLM_FACTORY_LIST.GPUStack) {
|
|
return [
|
|
{ value: 'chat', label: 'Chat' },
|
|
{ value: 'embedding', label: 'Embedding' },
|
|
{ value: 'rerank', label: 'Rerank' },
|
|
{ value: 'image2text', label: 'Image2Text' },
|
|
]
|
|
}
|
|
return MODEL_TYPE_OPTIONS;
|
|
}, [initialData])
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
reset({
|
|
model_type: initialData?.model_type || 'chat',
|
|
llm_name: initialData?.llm_name || '',
|
|
api_base: initialData?.api_base,
|
|
api_key: initialData?.api_key,
|
|
max_tokens: initialData?.max_tokens,
|
|
llm_factory: initialData?.llm_factory || 'Ollama',
|
|
});
|
|
}
|
|
}, [open]);
|
|
|
|
const handleFormSubmit = (data: OllamaFormData) => {
|
|
onSubmit(data);
|
|
};
|
|
|
|
// 获取文档链接信息
|
|
const docInfo = getURLByFactory((initialData?.llm_factory || LLM_FACTORY_LIST.Ollama) as LLMFactory);
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
<DialogTitle>
|
|
{editMode ? t('setting.edit') : t('setting.configure')} {initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<Box component="form" sx={{ mt: 2 }}>
|
|
{/* 模型类型选择 */}
|
|
<Controller
|
|
name="model_type"
|
|
control={control}
|
|
rules={{ required: t('setting.modelTypeRequired') }}
|
|
render={({ field }) => (
|
|
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
|
<InputLabel>{t('setting.modelType')} *</InputLabel>
|
|
<Select
|
|
{...field}
|
|
label={`${t('setting.modelType')} *`}
|
|
>
|
|
{modelTypeOptions.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.model_type && (
|
|
<FormHelperText>{errors.model_type.message}</FormHelperText>
|
|
)}
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
{/* 模型名称 */}
|
|
<Controller
|
|
name="llm_name"
|
|
control={control}
|
|
rules={{ required: t('setting.modelNameRequired') }}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label={t('setting.modelName')}
|
|
margin="normal"
|
|
required
|
|
error={!!errors.llm_name}
|
|
helperText={errors.llm_name?.message || t('setting.modelNamePlaceholder')}
|
|
placeholder={t('setting.modelNameExample')}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
{/* 基础 URL */}
|
|
<Controller
|
|
name="api_base"
|
|
control={control}
|
|
rules={{
|
|
required: t('setting.baseUrlRequired'),
|
|
pattern: {
|
|
value: /^https?:\/\/.+/,
|
|
message: t('setting.baseUrlInvalid')
|
|
}
|
|
}}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label={t('setting.baseUrl')}
|
|
margin="normal"
|
|
required
|
|
error={!!errors.api_base}
|
|
helperText={errors.api_base?.message || t('setting.baseUrl')}
|
|
placeholder="http://localhost:8888"
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
{/* API Key (可选) */}
|
|
<Controller
|
|
name="api_key"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="API Key"
|
|
margin="normal"
|
|
error={!!errors.api_key}
|
|
helperText={errors.api_key?.message || t('setting.apiKeyTip')}
|
|
placeholder={t('setting.apiKeyPlaceholder')}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
{/* 最大 Token 数 */}
|
|
<Controller
|
|
name="max_tokens"
|
|
control={control}
|
|
rules={{
|
|
required: t('setting.maxTokensRequired'),
|
|
min: {
|
|
value: 1,
|
|
message: t('setting.maxTokensMin')
|
|
},
|
|
max: {
|
|
value: 100000,
|
|
message: t('setting.maxTokensMax')
|
|
}
|
|
}}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label={t('setting.maxTokens')}
|
|
margin="normal"
|
|
type="number"
|
|
required
|
|
error={!!errors.max_tokens}
|
|
helperText={errors.max_tokens?.message || t('setting.maxTokensValidation')}
|
|
placeholder="4096"
|
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
|
/>
|
|
)}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
|
|
{/* 左侧文档链接 */}
|
|
<Link
|
|
href={docInfo.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{
|
|
ml: 2,
|
|
fontSize: '16',
|
|
textDecoration: 'none',
|
|
'&:hover': {
|
|
textDecoration: 'underline'
|
|
}
|
|
}}
|
|
>
|
|
{docInfo.textTip}
|
|
</Link>
|
|
|
|
{/* 右侧按钮组 */}
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
<Button onClick={onClose} disabled={loading}>
|
|
{t('setting.cancel')}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit(handleFormSubmit)}
|
|
variant="contained"
|
|
disabled={loading}
|
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|
>
|
|
{t('setting.confirm')}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default OllamaDialog; |