mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-30 04:29:50 +08:00
feat(vector_db): Implement Vector Database Service with multiple providers
This commit is contained in:
@@ -1,5 +1,65 @@
|
||||
import client from './client';
|
||||
|
||||
export interface VectorDBIndexInfo {
|
||||
index_name: string;
|
||||
index_type?: string;
|
||||
metric_type?: string;
|
||||
indexed_rows: number;
|
||||
pending_index_rows: number;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export interface VectorDBCollectionStats {
|
||||
name: string;
|
||||
row_count: number;
|
||||
dimension: number | null;
|
||||
estimated_memory_bytes: number;
|
||||
is_vector_collection: boolean;
|
||||
indexes: VectorDBIndexInfo[];
|
||||
}
|
||||
|
||||
export interface VectorDBStats {
|
||||
collections: VectorDBCollectionStats[];
|
||||
collection_count: number;
|
||||
total_vectors: number;
|
||||
estimated_total_memory_bytes: number;
|
||||
db_file_size_bytes: number | null;
|
||||
}
|
||||
|
||||
export interface VectorDBProviderField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'text' | 'password';
|
||||
required?: boolean;
|
||||
default?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface VectorDBProviderMeta {
|
||||
type: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
config_schema: VectorDBProviderField[];
|
||||
}
|
||||
|
||||
export interface VectorDBCurrentConfig {
|
||||
type: string;
|
||||
config: Record<string, string>;
|
||||
label?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateVectorDBConfigResponse {
|
||||
config: VectorDBCurrentConfig;
|
||||
stats: VectorDBStats;
|
||||
}
|
||||
|
||||
export const vectorDBApi = {
|
||||
getProviders: () => client<VectorDBProviderMeta[]>('/vector-db/providers', { method: 'GET' }),
|
||||
getConfig: () => client<VectorDBCurrentConfig>('/vector-db/config', { method: 'GET' }),
|
||||
getStats: () => client<VectorDBStats>('/vector-db/stats', { method: 'GET' }),
|
||||
updateConfig: (payload: { type: string; config: Record<string, string> }) =>
|
||||
client<UpdateVectorDBConfigResponse>('/vector-db/config', { method: 'POST', json: payload }),
|
||||
clearAll: () => client('/vector-db/clear-all', { method: 'POST' }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -205,6 +205,32 @@ export const en = {
|
||||
'Embedding Dimension': 'Embedding Dimension',
|
||||
'Vector Database': 'Vector Database',
|
||||
'Vector Database Settings': 'Vector Database Settings',
|
||||
'Current Statistics': 'Current Statistics',
|
||||
'Collections': 'Collections',
|
||||
'Vectors': 'Vectors',
|
||||
'Database Size': 'Database Size',
|
||||
'Estimated Memory': 'Estimated Memory',
|
||||
'No collections': 'No collections',
|
||||
'Dimension': 'Dimension',
|
||||
'Non-vector collection': 'Non-vector collection',
|
||||
'Estimated memory': 'Estimated memory',
|
||||
'Indexes': 'Indexes',
|
||||
'Unnamed index': 'Unnamed index',
|
||||
'Indexed rows': 'Indexed rows',
|
||||
'Pending rows': 'Pending rows',
|
||||
'Estimated memory is calculated as vectors x dimension x 4 bytes (float32).': 'Estimated memory is calculated as vectors x dimension x 4 bytes (float32).',
|
||||
'Database Provider': 'Database Provider',
|
||||
'Please select a provider': 'Please select a provider',
|
||||
'Coming soon': 'Coming soon',
|
||||
'This provider is not available yet': 'This provider is not available yet',
|
||||
'Database file path': 'Database file path',
|
||||
'Server URI': 'Server URI',
|
||||
'Token': 'Token',
|
||||
'Server URL': 'Server URL',
|
||||
'API Key': 'API Key',
|
||||
'Embedded Milvus Lite (local file storage).': 'Embedded Milvus Lite (local file storage).',
|
||||
'Remote Milvus instance accessed via URI.': 'Remote Milvus instance accessed via URI.',
|
||||
'Qdrant vector database (HTTP API).': 'Qdrant vector database (HTTP API).',
|
||||
'Database Type': 'Database Type',
|
||||
'Confirm embedding dimension change': 'Confirm embedding dimension change',
|
||||
'Changing the embedding dimension will clear the vector database automatically. You will need to rebuild indexes afterwards. Continue?': 'Changing the embedding dimension will clear the vector database automatically. You will need to rebuild indexes afterwards. Continue?',
|
||||
|
||||
@@ -207,6 +207,32 @@ export const zh = {
|
||||
'Embedding Dimension': '向量维度',
|
||||
'Vector Database': '向量数据库',
|
||||
'Vector Database Settings': '向量数据库设置',
|
||||
'Current Statistics': '当前统计',
|
||||
'Collections': '集合',
|
||||
'Vectors': '向量',
|
||||
'Database Size': '数据库大小',
|
||||
'Estimated Memory': '估算内存',
|
||||
'No collections': '暂无集合',
|
||||
'Dimension': '维度',
|
||||
'Non-vector collection': '非向量集合',
|
||||
'Estimated memory': '估算内存',
|
||||
'Indexes': '索引',
|
||||
'Unnamed index': '未命名索引',
|
||||
'Indexed rows': '已索引行数',
|
||||
'Pending rows': '待索引行数',
|
||||
'Estimated memory is calculated as vectors x dimension x 4 bytes (float32).': '估算内存 = 向量数量 x 维度 x 4 字节(float32)。',
|
||||
'Database Provider': '数据库提供者',
|
||||
'Please select a provider': '请选择提供者',
|
||||
'Coming soon': '敬请期待',
|
||||
'This provider is not available yet': '该提供者暂不可用',
|
||||
'Database file path': '数据库文件路径',
|
||||
'Server URI': '服务器 URI',
|
||||
'Token': '令牌',
|
||||
'Server URL': '服务器地址',
|
||||
'API Key': 'API Key',
|
||||
'Embedded Milvus Lite (local file storage).': '嵌入式 Milvus Lite,本地文件存储。',
|
||||
'Remote Milvus instance accessed via URI.': '通过 URI 访问的远程 Milvus 实例。',
|
||||
'Qdrant vector database (HTTP API).': 'Qdrant 向量数据库(HTTP API)。',
|
||||
'Database Type': '数据库类型',
|
||||
'Confirm embedding dimension change': '确认修改向量维度',
|
||||
'Changing the embedding dimension will clear the vector database automatically. You will need to rebuild indexes afterwards. Continue?': '修改向量维度会自动清空向量数据库,之后需要重建索引,是否继续?',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Form, Input, Button, message, Tabs, Space, Card, Select, Modal, Radio, InputNumber } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Input, Button, message, Tabs, Space, Card, Select, Modal, Radio, InputNumber, Spin, Empty, Alert } from 'antd';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import PageCard from '../../components/PageCard';
|
||||
import { getAllConfig, setConfig } from '../../api/config';
|
||||
import { vectorDBApi } from '../../api/vectorDB';
|
||||
import { vectorDBApi, type VectorDBStats, type VectorDBProviderMeta, type VectorDBCurrentConfig } from '../../api/vectorDB';
|
||||
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import '../../styles/settings-tabs.css';
|
||||
@@ -32,6 +32,20 @@ const EMBED_CONFIG_KEYS = [
|
||||
|
||||
const ALL_AI_KEYS = [...VISION_CONFIG_KEYS, ...EMBED_CONFIG_KEYS, { key: EMBED_DIM_KEY, default: DEFAULT_EMBED_DIMENSION }];
|
||||
|
||||
const formatBytes = (bytes?: number | null) => {
|
||||
if (bytes === null || bytes === undefined) return '-';
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
const precision = value >= 10 || unitIndex === 0 ? 0 : 1;
|
||||
return `${value.toFixed(precision)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
// Theme related config keys
|
||||
const THEME_KEYS = {
|
||||
MODE: 'THEME_MODE',
|
||||
@@ -42,9 +56,19 @@ const THEME_KEYS = {
|
||||
};
|
||||
|
||||
export default function SystemSettingsPage() {
|
||||
const [vectorConfigForm] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfigState] = useState<Record<string, string> | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('appearance');
|
||||
const [vectorStats, setVectorStats] = useState<VectorDBStats | null>(null);
|
||||
const [vectorStatsLoading, setVectorStatsLoading] = useState(false);
|
||||
const [vectorStatsError, setVectorStatsError] = useState<string | null>(null);
|
||||
const [vectorProviders, setVectorProviders] = useState<VectorDBProviderMeta[]>([]);
|
||||
const [vectorConfig, setVectorConfig] = useState<VectorDBCurrentConfig | null>(null);
|
||||
const [vectorConfigLoading, setVectorConfigLoading] = useState(false);
|
||||
const [vectorConfigSaving, setVectorConfigSaving] = useState(false);
|
||||
const [vectorMetaError, setVectorMetaError] = useState<string | null>(null);
|
||||
const [selectedProviderType, setSelectedProviderType] = useState<string | null>(null);
|
||||
const { refreshTheme, previewTheme } = useTheme();
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -52,6 +76,72 @@ export default function SystemSettingsPage() {
|
||||
getAllConfig().then((data) => setConfigState(data as Record<string, string>));
|
||||
}, []);
|
||||
|
||||
const fetchVectorStats = useCallback(async () => {
|
||||
setVectorStatsLoading(true);
|
||||
setVectorStatsError(null);
|
||||
try {
|
||||
const data = await vectorDBApi.getStats();
|
||||
setVectorStats(data);
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || t('Load failed');
|
||||
setVectorStatsError(msg);
|
||||
message.error(msg);
|
||||
} finally {
|
||||
setVectorStatsLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const buildProviderConfigValues = useCallback((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 fetchVectorMeta = useCallback(async () => {
|
||||
setVectorConfigLoading(true);
|
||||
setVectorMetaError(null);
|
||||
try {
|
||||
const [providers, current] = await Promise.all([
|
||||
vectorDBApi.getProviders(),
|
||||
vectorDBApi.getConfig(),
|
||||
]);
|
||||
setVectorProviders(providers);
|
||||
setVectorConfig(current);
|
||||
|
||||
const enabled = providers.filter((item) => item.enabled);
|
||||
let nextType: string | null = current?.type ?? null;
|
||||
if (nextType && !providers.some((item) => item.type === nextType)) {
|
||||
nextType = null;
|
||||
}
|
||||
if (!nextType) {
|
||||
nextType = enabled[0]?.type ?? providers[0]?.type ?? null;
|
||||
}
|
||||
setSelectedProviderType(nextType);
|
||||
const provider = providers.find((item) => item.type === nextType);
|
||||
const configValues = buildProviderConfigValues(provider, nextType === current?.type ? current?.config : undefined);
|
||||
vectorConfigForm.setFieldsValue({ type: nextType || undefined, config: configValues });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || t('Load failed');
|
||||
setVectorMetaError(msg);
|
||||
message.error(msg);
|
||||
} finally {
|
||||
setVectorConfigLoading(false);
|
||||
}
|
||||
}, [buildProviderConfigValues, message, t, vectorConfigForm]);
|
||||
|
||||
const handleSave = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -70,6 +160,40 @@ export default function SystemSettingsPage() {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleProviderChange = useCallback((value: string) => {
|
||||
setSelectedProviderType(value);
|
||||
const provider = vectorProviders.find((item) => item.type === value);
|
||||
const existing = value === vectorConfig?.type ? vectorConfig?.config : undefined;
|
||||
const configValues = buildProviderConfigValues(provider, existing);
|
||||
vectorConfigForm.setFieldsValue({ type: value, config: configValues });
|
||||
}, [vectorProviders, vectorConfig, buildProviderConfigValues, vectorConfigForm]);
|
||||
|
||||
const handleVectorConfigSave = useCallback(async (values: { type: string; config?: Record<string, string> }) => {
|
||||
if (!values?.type) {
|
||||
return;
|
||||
}
|
||||
setVectorConfigSaving(true);
|
||||
try {
|
||||
const configPayload = Object.fromEntries(
|
||||
Object.entries(values.config || {}).filter(([, val]) => val !== undefined && val !== null && String(val).trim() !== '')
|
||||
.map(([key, val]) => [key, String(val)])
|
||||
);
|
||||
const response = await vectorDBApi.updateConfig({ type: values.type, config: configPayload });
|
||||
setVectorConfig(response.config);
|
||||
setVectorStats(response.stats);
|
||||
setVectorStatsError(null);
|
||||
setSelectedProviderType(response.config.type);
|
||||
const provider = vectorProviders.find((item) => item.type === response.config.type);
|
||||
const mergedValues = buildProviderConfigValues(provider, response.config.config);
|
||||
vectorConfigForm.setFieldsValue({ type: response.config.type, config: mergedValues });
|
||||
message.success(t('Saved successfully'));
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || t('Save failed'));
|
||||
} finally {
|
||||
setVectorConfigSaving(false);
|
||||
}
|
||||
}, [buildProviderConfigValues, message, t, vectorConfigForm, vectorProviders]);
|
||||
|
||||
// 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览)
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'appearance') {
|
||||
@@ -77,6 +201,27 @@ export default function SystemSettingsPage() {
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'vector-db') {
|
||||
if (!vectorProviders.length && !vectorConfigLoading) {
|
||||
fetchVectorMeta();
|
||||
}
|
||||
if (!vectorStats && !vectorStatsLoading) {
|
||||
fetchVectorStats();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
activeTab,
|
||||
fetchVectorMeta,
|
||||
fetchVectorStats,
|
||||
vectorProviders.length,
|
||||
vectorConfigLoading,
|
||||
vectorStats,
|
||||
vectorStatsLoading,
|
||||
]);
|
||||
|
||||
const selectedProvider = vectorProviders.find((item) => item.type === selectedProviderType || (!selectedProviderType && item.enabled));
|
||||
|
||||
if (!config) {
|
||||
return <PageCard title={t('System Settings')}><div>{t('Loading...')}</div></PageCard>;
|
||||
}
|
||||
@@ -275,41 +420,187 @@ export default function SystemSettingsPage() {
|
||||
),
|
||||
children: (
|
||||
<Card title={t('Vector Database Settings')} style={{ marginTop: 24 }}>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label={t('Database Type')}>
|
||||
<Select
|
||||
size="large"
|
||||
value={'Milvus Lite'}
|
||||
disabled
|
||||
options={[{ value: 'Milvus Lite', label: 'Milvus Lite' }]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
danger
|
||||
block
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: t('Confirm clear vector database?'),
|
||||
content: t('This will delete all collections irreversibly.'),
|
||||
okText: t('Confirm Clear'),
|
||||
okType: 'danger',
|
||||
cancelText: t('Cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await vectorDBApi.clearAll();
|
||||
message.success(t('Vector database cleared'));
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('Clear failed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
<Space direction="vertical" size={24} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<strong>{t('Current Statistics')}</strong>
|
||||
<Button onClick={() => { fetchVectorMeta(); fetchVectorStats(); }} loading={vectorStatsLoading || vectorConfigLoading} disabled={(vectorStatsLoading || vectorConfigLoading) && !vectorStats}>
|
||||
{t('Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
{vectorMetaError ? (
|
||||
<Alert type="error" showIcon message={vectorMetaError} />
|
||||
) : null}
|
||||
{vectorStatsLoading && !vectorStats ? (
|
||||
<Spin />
|
||||
) : vectorStats ? (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}>
|
||||
<div>
|
||||
<div style={{ color: '#888' }}>{t('Collections')}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{vectorStats.collection_count}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#888' }}>{t('Vectors')}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{vectorStats.total_vectors}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#888' }}>{t('Database Size')}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{formatBytes(vectorStats.db_file_size_bytes)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#888' }}>{t('Estimated Memory')}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{formatBytes(vectorStats.estimated_total_memory_bytes)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{vectorStats.collections.length ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
{vectorStats.collections.map((collection) => (
|
||||
<div key={collection.name} style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16 }}>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<strong>{collection.name}</strong>
|
||||
<span style={{ color: '#888' }}>
|
||||
{collection.is_vector_collection && collection.dimension
|
||||
? `${t('Dimension')}: ${collection.dimension}`
|
||||
: t('Non-vector collection')}
|
||||
</span>
|
||||
</div>
|
||||
<div>{t('Vectors')}: {collection.row_count}</div>
|
||||
{collection.is_vector_collection ? (
|
||||
<div>{t('Estimated memory')}: {formatBytes(collection.estimated_memory_bytes)}</div>
|
||||
) : null}
|
||||
{collection.indexes.length ? (
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<span>{t('Indexes')}:</span>
|
||||
<ul style={{ paddingLeft: 20, margin: 0 }}>
|
||||
{collection.indexes.map((index) => (
|
||||
<li key={`${collection.name}-${index.index_name || 'default'}`}>
|
||||
<span>{index.index_name || t('Unnamed index')}</span>
|
||||
<span>{' · '}{index.index_type || '-'}</span>
|
||||
<span>{' · '}{index.metric_type || '-'}</span>
|
||||
<span>{' · '}{t('Indexed rows')}: {index.indexed_rows}</span>
|
||||
<span>{' · '}{t('Pending rows')}: {index.pending_index_rows}</span>
|
||||
<span>{' · '}{t('Status')}: {index.state || '-'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Space>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Empty description={t('No collections')} />
|
||||
)}
|
||||
<div style={{ color: '#888' }}>
|
||||
{t('Estimated memory is calculated as vectors x dimension x 4 bytes (float32).')}
|
||||
</div>
|
||||
</Space>
|
||||
) : vectorStatsError ? (
|
||||
<div style={{ color: '#ff4d4f' }}>{vectorStatsError}</div>
|
||||
) : (
|
||||
<Empty description={t('No collections')} />
|
||||
)}
|
||||
</Space>
|
||||
{vectorConfigLoading && !vectorProviders.length ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={vectorConfigForm}
|
||||
onFinish={handleVectorConfigSave}
|
||||
initialValues={{ type: selectedProviderType || undefined, config: {} }}
|
||||
>
|
||||
{t('Clear Vector DB')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('Database Provider')}
|
||||
rules={[{ required: true, message: t('Please select a provider') }]}
|
||||
>
|
||||
<Select
|
||||
size="large"
|
||||
options={vectorProviders.map((provider) => ({
|
||||
value: provider.type,
|
||||
label: provider.enabled ? provider.label : `${provider.label} (${t('Coming soon')})`,
|
||||
disabled: !provider.enabled,
|
||||
}))}
|
||||
onChange={handleProviderChange}
|
||||
loading={vectorConfigLoading && !vectorProviders.length}
|
||||
/>
|
||||
</Form.Item>
|
||||
{selectedProvider?.description ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={t(selectedProvider.description)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
) : null}
|
||||
{selectedProvider?.config_schema?.map((field) => (
|
||||
<Form.Item
|
||||
key={field.key}
|
||||
name={['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>
|
||||
))}
|
||||
{selectedProvider && !selectedProvider.enabled ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t('This provider is not available yet')}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
) : null}
|
||||
<Form.Item>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={vectorConfigSaving}
|
||||
block
|
||||
disabled={!selectedProvider?.enabled}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
htmlType="button"
|
||||
block
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: t('Confirm clear vector database?'),
|
||||
content: t('This will delete all collections irreversibly.'),
|
||||
okText: t('Confirm Clear'),
|
||||
okType: 'danger',
|
||||
cancelText: t('Cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await vectorDBApi.clearAll();
|
||||
message.success(t('Vector database cleared'));
|
||||
await fetchVectorStats();
|
||||
await fetchVectorMeta();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('Clear failed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Clear Vector DB')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user