mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat: update vector DB provider handling and improve setup page configuration
This commit is contained in:
+1
-1
@@ -250,7 +250,7 @@ async def get_vector_db_stats(request: Request, user: User = Depends(get_current
|
|||||||
|
|
||||||
@audit(action=AuditAction.READ, description="获取向量数据库提供者列表")
|
@audit(action=AuditAction.READ, description="获取向量数据库提供者列表")
|
||||||
@router_vector_db.get("/providers", summary="列出可用向量数据库提供者")
|
@router_vector_db.get("/providers", summary="列出可用向量数据库提供者")
|
||||||
async def list_vector_providers(request: Request, user: User = Depends(get_current_active_user)):
|
async def list_vector_providers(request: Request):
|
||||||
return success(list_providers())
|
return success(list_providers())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -674,6 +674,7 @@
|
|||||||
"Coming soon v2": "Coming soon v2",
|
"Coming soon v2": "Coming soon v2",
|
||||||
"Initialization succeeded! Logging you in...": "Initialization succeeded! Logging you in...",
|
"Initialization succeeded! Logging you in...": "Initialization succeeded! Logging you in...",
|
||||||
"Initialization failed, please try later": "Initialization failed, please try later",
|
"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",
|
"Database Setup": "Database Setup",
|
||||||
"Choose database driver": "Choose database driver",
|
"Choose database driver": "Choose database driver",
|
||||||
"Select database and vector database for system data": "Select database and vector database for system data",
|
"Select database and vector database for system data": "Select database and vector database for system data",
|
||||||
|
|||||||
@@ -674,6 +674,7 @@
|
|||||||
"Coming soon v2": "敬请期待 v2",
|
"Coming soon v2": "敬请期待 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": "向量数据库配置失败,可稍后在系统设置中配置",
|
||||||
"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": "选择用于存储系统数据的数据库和向量数据库。",
|
||||||
|
|||||||
+151
-17
@@ -1,20 +1,48 @@
|
|||||||
import { useEffect, useState } from 'react';
|
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 { UserOutlined, LockOutlined, HddOutlined } from '@ant-design/icons';
|
||||||
import { adaptersApi } from '../api/adapters';
|
import { adaptersApi } from '../api/adapters';
|
||||||
import { setConfig } from '../api/config';
|
import { setConfig } from '../api/config';
|
||||||
|
import { vectorDBApi, type VectorDBProviderMeta } from '../api/vectorDB';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useI18n } from '../i18n';
|
import { useI18n } from '../i18n';
|
||||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
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 SetupPage = () => {
|
||||||
|
const screens = Grid.useBreakpoint();
|
||||||
|
const isMobile = !screens.md;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { login, register } = useAuth();
|
const { login, register } = useAuth();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [vectorProviders, setVectorProviders] = useState<VectorDBProviderMeta[]>([]);
|
||||||
|
const [vectorProvidersLoading, setVectorProvidersLoading] = useState(false);
|
||||||
|
const [selectedVectorProviderType, setSelectedVectorProviderType] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const origin = window.location.origin;
|
const origin = window.location.origin;
|
||||||
@@ -23,6 +51,48 @@ const SetupPage = () => {
|
|||||||
file_domain: origin,
|
file_domain: origin,
|
||||||
});
|
});
|
||||||
}, [form]);
|
}, [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) => {
|
const onFinish = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +113,19 @@ const SetupPage = () => {
|
|||||||
if (tasks.length) {
|
if (tasks.length) {
|
||||||
await Promise.all(tasks);
|
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({
|
await adaptersApi.create({
|
||||||
name: values.adapter_name,
|
name: values.adapter_name,
|
||||||
type: values.adapter_type,
|
type: values.adapter_type,
|
||||||
@@ -67,12 +150,24 @@ const SetupPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepFields = [
|
const selectedVectorProvider = vectorProviders.find((item) => item.type === selectedVectorProviderType) || vectorProviders.find((item) => item.enabled) || vectorProviders[0];
|
||||||
['db_driver', 'vector_db_driver'],
|
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'],
|
['adapter_name', 'adapter_type', 'path', 'root_dir'],
|
||||||
['username', 'full_name', 'email', 'password', 'confirm'],
|
['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 = () => {
|
const next = () => {
|
||||||
form.validateFields(stepFields[currentStep]).then(() => {
|
form.validateFields(stepFields[currentStep]).then(() => {
|
||||||
setCurrentStep(currentStep + 1);
|
setCurrentStep(currentStep + 1);
|
||||||
@@ -100,12 +195,44 @@ const SetupPage = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('Vector DB Driver')}
|
label={t('Vector DB Driver')}
|
||||||
name="vector_db_driver"
|
name="vector_db_type"
|
||||||
initialValue="milvus"
|
initialValue="milvus_lite"
|
||||||
rules={[{ required: true }]}
|
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>
|
</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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100vw',
|
width: '100%',
|
||||||
height: '100vh',
|
minHeight: '100vh',
|
||||||
alignItems: 'center',
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
justifyContent: '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))'
|
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 }}>
|
<div style={{ position: 'fixed', top: 12, right: 12, zIndex: 1000 }}>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
<Card style={{ width: 'clamp(400px, 40vw, 600px)', padding: '24px 16px' }}>
|
<Card
|
||||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
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 }} />
|
<img src="/logo.svg" alt="Foxel Logo" style={{ width: 48, marginBottom: 16 }} />
|
||||||
<Title level={2}>{t('System Initialization')}</Title>
|
<Title level={2}>{t('System Initialization')}</Title>
|
||||||
</div>
|
</div>
|
||||||
<Steps
|
<Steps
|
||||||
current={currentStep}
|
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 }))}
|
items={steps.map((item) => ({ title: item.title }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -256,20 +390,20 @@ const SetupPage = () => {
|
|||||||
))}
|
))}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: isMobile ? 16 : 24 }}>
|
||||||
<Space>
|
<Space direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: '100%' }}>
|
||||||
{currentStep > 0 && (
|
{currentStep > 0 && (
|
||||||
<Button style={{ margin: '0 8px' }} onClick={() => prev()}>
|
<Button block={isMobile} onClick={() => prev()}>
|
||||||
{t('Previous')}
|
{t('Previous')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentStep < steps.length - 1 && (
|
{currentStep < steps.length - 1 && (
|
||||||
<Button type="primary" onClick={() => next()}>
|
<Button type="primary" block={isMobile} onClick={() => next()}>
|
||||||
{t('Next')}
|
{t('Next')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentStep === steps.length - 1 && (
|
{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')}
|
{t('Finish Initialization')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user