feat: update vector DB provider handling and improve setup page configuration

This commit is contained in:
shiyu
2026-01-31 21:59:18 +08:00
parent e6ab01ef9d
commit d9d2ddf2d1
4 changed files with 154 additions and 18 deletions

View File

@@ -674,6 +674,7 @@
"Coming soon v2": "Coming soon v2",
"Initialization succeeded! Logging you in...": "Initialization succeeded! Logging you in...",
"Initialization failed, please try later": "Initialization failed, please try later",
"Vector DB setup failed, you can configure it later in System Settings": "Vector DB setup failed, you can configure it later in System Settings",
"Database Setup": "Database Setup",
"Choose database driver": "Choose database driver",
"Select database and vector database for system data": "Select database and vector database for system data",

View File

@@ -674,6 +674,7 @@
"Coming soon v2": "敬请期待 v2",
"Initialization succeeded! Logging you in...": "初始化成功!正在为您登录,请不要刷新。",
"Initialization failed, please try later": "初始化失败,请稍后重试",
"Vector DB setup failed, you can configure it later in System Settings": "向量数据库配置失败,可稍后在系统设置中配置",
"Database Setup": "数据库设置",
"Choose database driver": "选择数据库驱动",
"Select database and vector database for system data": "选择用于存储系统数据的数据库和向量数据库。",

View File

@@ -1,20 +1,48 @@
import { useEffect, useState } from 'react';
import { Form, Input, Button, Card, message, Steps, Select, Space, Typography } from 'antd';
import { Form, Input, Button, Card, message, Steps, Select, Space, Typography, Alert, Grid } from 'antd';
import { UserOutlined, LockOutlined, HddOutlined } from '@ant-design/icons';
import { adaptersApi } from '../api/adapters';
import { setConfig } from '../api/config';
import { vectorDBApi, type VectorDBProviderMeta } from '../api/vectorDB';
import { useAuth } from '../contexts/AuthContext';
import { useI18n } from '../i18n';
import LanguageSwitcher from '../components/LanguageSwitcher';
const { Title, Text } = Typography;
const buildProviderConfigValues = (
provider: VectorDBProviderMeta | undefined,
existing?: Record<string, string>,
) => {
if (!provider) return {};
const values: Record<string, string> = {};
const schema = provider.config_schema || [];
schema.forEach((field) => {
const current = existing && existing[field.key] !== undefined && existing[field.key] !== null
? String(existing[field.key])
: undefined;
if (current !== undefined) {
values[field.key] = current;
} else if (field.default !== undefined && field.default !== null) {
values[field.key] = String(field.default);
} else {
values[field.key] = '';
}
});
return values;
};
const SetupPage = () => {
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [form] = Form.useForm();
const { login, register } = useAuth();
const { t } = useI18n();
const [vectorProviders, setVectorProviders] = useState<VectorDBProviderMeta[]>([]);
const [vectorProvidersLoading, setVectorProvidersLoading] = useState(false);
const [selectedVectorProviderType, setSelectedVectorProviderType] = useState<string | null>(null);
useEffect(() => {
const origin = window.location.origin;
@@ -23,6 +51,48 @@ const SetupPage = () => {
file_domain: origin,
});
}, [form]);
useEffect(() => {
let mounted = true;
async function loadVectorProviders() {
setVectorProvidersLoading(true);
try {
const providers = await vectorDBApi.getProviders();
if (!mounted) return;
setVectorProviders(providers);
const enabled = providers.filter((item) => item.enabled);
const currentType = form.getFieldValue('vector_db_type') as string | undefined;
let nextType = currentType;
if (!nextType || !providers.some((item) => item.type === nextType && item.enabled)) {
nextType = enabled.find((item) => item.type === 'milvus_lite')?.type
?? enabled[0]?.type
?? providers[0]?.type
?? 'milvus_lite';
}
setSelectedVectorProviderType(nextType);
const provider = providers.find((item) => item.type === nextType);
const existingConfig = nextType === currentType ? (form.getFieldValue('vector_db_config') as Record<string, string> | undefined) : undefined;
const configValues = buildProviderConfigValues(provider, existingConfig);
form.setFieldsValue({ vector_db_type: nextType, vector_db_config: configValues });
} catch (e: any) {
console.error(e);
message.error(e?.message || t('Load failed'));
if (mounted) {
form.setFieldsValue({ vector_db_type: 'milvus_lite' });
setSelectedVectorProviderType('milvus_lite');
}
} finally {
if (mounted) setVectorProvidersLoading(false);
}
}
loadVectorProviders();
return () => {
mounted = false;
};
}, [form, t]);
const onFinish = async (values: any) => {
setLoading(true);
try {
@@ -43,6 +113,19 @@ const SetupPage = () => {
if (tasks.length) {
await Promise.all(tasks);
}
if (values.vector_db_type) {
const configPayload = Object.fromEntries(
Object.entries(values.vector_db_config || {})
.filter(([, val]) => val !== undefined && val !== null && String(val).trim() !== '')
.map(([key, val]) => [key, String(val)]),
);
try {
await vectorDBApi.updateConfig({ type: values.vector_db_type, config: configPayload });
} catch (e: any) {
console.error(e);
message.warning(e?.message || t('Vector DB setup failed, you can configure it later in System Settings'));
}
}
await adaptersApi.create({
name: values.adapter_name,
type: values.adapter_type,
@@ -67,12 +150,24 @@ const SetupPage = () => {
}
};
const stepFields = [
['db_driver', 'vector_db_driver'],
const selectedVectorProvider = vectorProviders.find((item) => item.type === selectedVectorProviderType) || vectorProviders.find((item) => item.enabled) || vectorProviders[0];
const requiredVectorConfigFields = (selectedVectorProvider?.config_schema || [])
.filter((field) => field.required)
.map((field) => ['vector_db_config', field.key]);
const stepFields: any[] = [
['db_driver', 'vector_db_type', ...requiredVectorConfigFields],
['adapter_name', 'adapter_type', 'path', 'root_dir'],
['username', 'full_name', 'email', 'password', 'confirm'],
]
const handleVectorProviderChange = (value: string) => {
setSelectedVectorProviderType(value);
const provider = vectorProviders.find((item) => item.type === value);
const configValues = buildProviderConfigValues(provider);
form.setFieldsValue({ vector_db_type: value, vector_db_config: configValues });
};
const next = () => {
form.validateFields(stepFields[currentStep]).then(() => {
setCurrentStep(currentStep + 1);
@@ -100,12 +195,44 @@ const SetupPage = () => {
</Form.Item>
<Form.Item
label={t('Vector DB Driver')}
name="vector_db_driver"
initialValue="milvus"
name="vector_db_type"
initialValue="milvus_lite"
rules={[{ required: true }]}
>
<Select size="large" disabled options={[{ label: 'Milvus', value: 'milvus' }]} />
<Select
size="large"
loading={vectorProvidersLoading}
disabled={vectorProvidersLoading || !vectorProviders.length}
options={vectorProviders.map((provider) => ({
value: provider.type,
label: provider.label,
disabled: !provider.enabled,
}))}
onChange={handleVectorProviderChange}
/>
</Form.Item>
{selectedVectorProvider?.description ? (
<Alert
type="info"
showIcon
message={t(selectedVectorProvider.description)}
style={{ marginBottom: 16 }}
/>
) : null}
{selectedVectorProvider?.config_schema?.map((field) => (
<Form.Item
key={field.key}
name={['vector_db_config', field.key]}
label={t(field.label)}
rules={field.required ? [{ required: true, message: t('Please input {label}', { label: t(field.label) }) }] : []}
>
{field.type === 'password' ? (
<Input.Password size="large" placeholder={field.placeholder ? t(field.placeholder) : undefined} />
) : (
<Input size="large" placeholder={field.placeholder ? t(field.placeholder) : undefined} />
)}
</Form.Item>
))}
</>
)
},
@@ -228,23 +355,30 @@ const SetupPage = () => {
return (
<div style={{
display: 'flex',
width: '100vw',
height: '100vh',
alignItems: 'center',
width: '100%',
minHeight: '100vh',
alignItems: isMobile ? 'flex-start' : 'center',
justifyContent: 'center',
padding: isMobile ? '64px 12px 24px' : '32px 24px',
boxSizing: 'border-box',
background: 'linear-gradient(to right, var(--ant-color-bg-layout, #f0f2f5), var(--ant-color-fill-secondary, #d7d7d7))'
}}>
<div style={{ position: 'fixed', top: 12, right: 12, zIndex: 1000 }}>
<LanguageSwitcher />
</div>
<Card style={{ width: 'clamp(400px, 40vw, 600px)', padding: '24px 16px' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Card
style={{ width: '100%', maxWidth: 800 }}
styles={{ body: { padding: isMobile ? '18px 14px' : '24px 20px' } }}
>
<div style={{ textAlign: 'center', marginBottom: isMobile ? 20 : 32 }}>
<img src="/logo.svg" alt="Foxel Logo" style={{ width: 48, marginBottom: 16 }} />
<Title level={2}>{t('System Initialization')}</Title>
</div>
<Steps
current={currentStep}
style={{ marginBottom: 32 }}
direction={isMobile ? 'vertical' : 'horizontal'}
size={isMobile ? 'small' : 'default'}
style={{ marginBottom: isMobile ? 20 : 32 }}
items={steps.map((item) => ({ title: item.title }))}
/>
@@ -256,20 +390,20 @@ const SetupPage = () => {
))}
</Form>
<div style={{ marginTop: 24 }}>
<Space>
<div style={{ marginTop: isMobile ? 16 : 24 }}>
<Space direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: '100%' }}>
{currentStep > 0 && (
<Button style={{ margin: '0 8px' }} onClick={() => prev()}>
<Button block={isMobile} onClick={() => prev()}>
{t('Previous')}
</Button>
)}
{currentStep < steps.length - 1 && (
<Button type="primary" onClick={() => next()}>
<Button type="primary" block={isMobile} onClick={() => next()}>
{t('Next')}
</Button>
)}
{currentStep === steps.length - 1 && (
<Button type="primary" htmlType="submit" loading={loading} onClick={() => form.submit()}>
<Button type="primary" block={isMobile} htmlType="submit" loading={loading} onClick={() => form.submit()}>
{t('Finish Initialization')}
</Button>
)}