import React, { useState, useEffect } from 'react'; import { ModelConfig, ModelType, AppSettings, KnowledgeGroup } from '../../types'; import { useLanguage } from '../../contexts/LanguageContext'; import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Shield, Key, LogOut, Globe, Settings as SettingsIcon, ToggleLeft, ToggleRight, Database } from 'lucide-react'; import { userService } from '../../services/userService'; // import { settingsService } from '../../services/settingsService'; import { userSettingService } from '../../services/userSettingService'; import { knowledgeGroupService } from '../../services/knowledgeGroupService'; interface SettingsViewProps { // Model Props models: ModelConfig[]; authToken: string | null; onUpdateModels: (action: 'create' | 'update' | 'delete', model: ModelConfig) => Promise; isAdmin?: boolean; // Added isAdmin prop currentUser?: any; // Added current user prop } type TabType = 'general' | 'user' | 'model'; export const SettingsView: React.FC = ({ models, authToken, onUpdateModels, isAdmin = false, currentUser, }) => { const { t } = useLanguage(); const [activeTab, setActiveTab] = useState('general'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // --- Model Manager State --- const [editingId, setEditingId] = useState(null); const [modelFormData, setModelFormData] = useState>({ type: ModelType.LLM, supportsVision: false, baseUrl: '', apiKey: '', modelId: '', name: '', dimensions: 1536, maxInputTokens: 8191, maxBatchSize: 2048 }); const [skipValidation, setSkipValidation] = useState(false); // --- User Management State --- interface UserType { id: string; username: string; isAdmin: boolean; createdAt: string; } const [users, setUsers] = useState([]); const [isUserLoading, setIsUserLoading] = useState(false); const [showAddUser, setShowAddUser] = useState(false); const [newUser, setNewUser] = useState({ username: '', password: '', isAdmin: false }); const [userSuccess, setUserSuccess] = useState(''); // --- Change Password State --- const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' }); const [passwordSuccess, setPasswordSuccess] = useState(''); // --- App Settings State --- const [appSettings, setAppSettings] = useState(null); const [knowledgeGroups, setKnowledgeGroups] = useState([]); const [isSettingsLoading, setIsSettingsLoading] = useState(false); // ユーザー一覧の取得(ユーザータブがアクティブな場合) useEffect(() => { if (activeTab === 'user') { fetchUsers(); } else if (activeTab === 'general') { fetchSettingsAndGroups(); } }, [activeTab]); // 只依赖activeTab,不需要currentUser const fetchSettingsAndGroups = async () => { if (!authToken) return; setIsSettingsLoading(true); try { const [settings, groups] = await Promise.all([ userSettingService.get(authToken), knowledgeGroupService.getGroups() ]); setAppSettings(settings); setKnowledgeGroups(groups); } catch (error) { console.error('Failed to fetch settings or groups:', error); } finally { setIsSettingsLoading(false); } }; // --- 一般タブのハンドラー --- const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setPasswordSuccess(''); if (passwordForm.new !== passwordForm.confirm) { setError(t('passwordMismatch')); return; } if (passwordForm.new.length < 6) { setError(t('newPasswordMinLength')); return; } setIsLoading(true); try { await userService.changePassword(passwordForm.current, passwordForm.new); setPasswordSuccess(t('passwordChangeSuccess')); setPasswordForm({ current: '', new: '', confirm: '' }); } catch (err: any) { setError(err.message || t('passwordChangeFailed')); } finally { setIsLoading(false); } }; // --- ユーザータブのハンドラー --- const fetchUsers = async () => { setIsUserLoading(true); try { const userList = await userService.getUsers(); setUsers(userList); } catch (error: any) { setError(error.message || t('getUserListFailed')); } finally { setIsUserLoading(false); } }; const handleCreateUser = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setUserSuccess(''); if (newUser.password.length < 6) { setError(t('passwordMinLength')); return; } try { await userService.createUser(newUser.username, newUser.password, newUser.isAdmin); setUserSuccess(t('userCreatedSuccess')); setNewUser({ username: '', password: '', isAdmin: false }); setShowAddUser(false); fetchUsers(); } catch (error: any) { setError(error.message || t('createUserFailed')); } }; const [passwordChangeUserData, setPasswordChangeUserData] = useState<{ userId: string, newPassword: string } | null>(null); const handleToggleUserAdmin = async (userId: string, newAdminStatus: boolean) => { try { await userService.updateUser(userId, newAdminStatus); // 重新获取用户列表 fetchUsers(); setUserSuccess(newAdminStatus ? t('userPromotedToAdmin') : t('userDemotedFromAdmin')); } catch (error: any) { setError(error.message || t('updateUserFailed')); } }; const handleUserPasswordChange = async () => { if (!passwordChangeUserData || !passwordChangeUserData.newPassword) return; try { // Update user password await userService.updateUserInfo(passwordChangeUserData.userId, { password: passwordChangeUserData.newPassword }); setUserSuccess(t('passwordChangeSuccess')); setPasswordChangeUserData(null); fetchUsers(); // Refresh the user list } catch (error: any) { setError(error.message || t('passwordChangeFailed')); } }; const handleDeleteUser = async (userId: string) => { if (window.confirm(t('confirmDeleteUser'))) { try { await userService.deleteUser(userId); // 重新获取用户列表 fetchUsers(); setUserSuccess(t('userDeletedSuccessfully')); } catch (error: any) { setError(error.message || t('deleteUserFailed')); } } }; // --- モデルタブのハンドラー --- const validateApiKey = async (config: Partial) => { // 許可されたドメインを検証して、任意の外部API接続を防ぎます if (config.baseUrl) { try { // 注意:この検証はモデルAPI設定にのみ適用され、外部API呼び出しをセキュリティ制限します // 本番環境では、より柔軟な設定を使用することをお勧めします const url = new URL(config.baseUrl); // 環境変数から許可されたホストを取得(なければデフォルトを使用) const envAllowedHosts = process.env.VITE_ALLOWED_HOSTS || 'localhost,127.0.0.1,0.0.0.0'; const allowedHosts = envAllowedHosts.split(',').map(host => host.trim()); const isLocalhost = allowedHosts.includes(url.hostname) || url.hostname.startsWith('192.168.') || url.hostname.startsWith('10.') || url.hostname.startsWith('172.'); // 本番環境では、ホワイトリスト設定をハードコードされた許可リストの代わりに実装できます if (!isLocalhost) { // 設定ファイルまたは環境変数によって外部API接続を許可するかどうかを決定する必要があります // このデモでは、外部APIへの制限を維持します throw new Error(t('mmErrorExternalApiNotAllowed')); } } catch (urlError) { throw new Error(t('mmErrorInvalidBaseUrl')); } } try { let apiUrl = config.baseUrl || ''; let body = {}; if (config.type === ModelType.LLM) { apiUrl = apiUrl.endsWith('/chat/completions') ? apiUrl : `${apiUrl.replace(/\/+$/, '')}/chat/completions`; body = { model: config.modelId, messages: [{ role: 'user', content: 'Hi' }], max_tokens: 1 }; } else if (config.type === ModelType.RERANK) { apiUrl = apiUrl.replace(/\/+$/, ''); // SiliconFlow API テストボディ body = { model: config.modelId, query: 'test', documents: ['test'], top_n: 1 }; } else { apiUrl = apiUrl.endsWith('/embeddings') ? apiUrl : `${apiUrl.replace(/\/+$/, '')}/embeddings`; body = { input: ['test'], model: config.modelId }; } const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }, body: JSON.stringify(body), }); if (!response.ok) { const errText = await response.text(); throw new Error(`API Request Failed: ${response.status} ${response.statusText} - ${errText.substring(0, 100)}`); } } catch (error: any) { console.error('Validation Warning:', error); throw new Error(t('validationFailedMsg').replace('$1', error.message)); } }; const handleSaveModel = async () => { if (!authToken) { setError(t('mmErrorNotAuthenticated')); return; } setError(null); if (!modelFormData.name?.trim()) { setError(t('mmErrorNameRequired')); return; } if (!modelFormData.modelId?.trim()) { setError(t('mmErrorModelIdRequired')); return; } if (!modelFormData.baseUrl?.trim()) { setError(t('mmErrorBaseUrlRequired')); return; } if (editingId === 'new' && !modelFormData.apiKey?.trim()) { setError(t('mmErrorApiKeyRequired')); return; } setIsLoading(true); try { if (modelFormData.apiKey?.trim() && !skipValidation) { await validateApiKey(modelFormData); } const saveData = { ...modelFormData } as ModelConfig; if (saveData.type !== ModelType.EMBEDDING) { delete saveData.dimensions; delete saveData.maxInputTokens; delete saveData.maxBatchSize; } if (editingId !== 'new' && !modelFormData.apiKey?.trim()) { delete saveData.apiKey; } await onUpdateModels(editingId === 'new' ? 'create' : 'update', saveData); setEditingId(null); } catch (err: any) { setError(err.message || t('errorGeneric')); } finally { setIsLoading(false); } }; const handleToggleModel = async (model: ModelConfig) => { try { await onUpdateModels('update', { ...model, isEnabled: !model.isEnabled }); } catch (error) { console.error('Failed to toggle model:', error); } }; const handleDeleteModel = async (id: string) => { if (confirm(t('confirmClear'))) { await onUpdateModels('delete', { id } as ModelConfig); } }; const getTypeLabel = (type: ModelType) => { switch (type) { case ModelType.LLM: return t('typeLLM'); case ModelType.EMBEDDING: return t('typeEmbedding'); case ModelType.RERANK: return t('typeRerank'); } }; // --- レンダリング関数 --- const renderGeneralTab = () => (
{/* パスワード変更セクション */}

{t('changePassword')}

setPasswordForm({ ...passwordForm, current: e.target.value })} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" required />
setPasswordForm({ ...passwordForm, new: e.target.value })} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" required />
setPasswordForm({ ...passwordForm, confirm: e.target.value })} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" required />
{passwordSuccess &&

{passwordSuccess}

}
); const renderUserTab = () => (

{t('userList')}

{showAddUser && (
setNewUser({ ...newUser, username: e.target.value })} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md" required /> setNewUser({ ...newUser, password: e.target.value })} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md" required />
setNewUser({ ...newUser, isAdmin: e.target.checked })} className="w-4 h-4 text-blue-600 rounded border border-slate-300" />
)} {/* Password Change Modal */} {passwordChangeUserData && (

{t('changeUserPassword')}

{ e.preventDefault(); handleUserPasswordChange(); }} className="space-y-4">
setPasswordChangeUserData({ ...passwordChangeUserData, newPassword: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder={t('enterNewPassword')} required />

{t('passwordMinLength')}

)} {userSuccess &&

{userSuccess}

}
{users.map(user => (
{user.isAdmin ? : }

{user.username}

{new Date(user.createdAt).toLocaleDateString()}

{user.isAdmin ? t('admin') : t('user')} {user.id !== currentUser?.id && ( // 不允许删除自己 )}
))}
); const renderModelTab = () => (
{editingId ? (

{editingId === 'new' ? t('mmAddBtn') : t('mmEdit')}

setModelFormData({ ...modelFormData, name: e.target.value })} disabled={isLoading} />
setModelFormData({ ...modelFormData, modelId: e.target.value })} disabled={isLoading} />
{(modelFormData.type === ModelType.LLM || String(modelFormData.type).toLowerCase() === 'llm') && (
setModelFormData({ ...modelFormData, supportsVision: e.target.checked })} />
)}
setModelFormData({ ...modelFormData, baseUrl: e.target.value })} disabled={isLoading} autoComplete="off" />
setModelFormData({ ...modelFormData, apiKey: e.target.value })} placeholder={editingId === 'new' ? '' : t('leaveEmptyNoChange')} disabled={isLoading} autoComplete="new-password" />
setSkipValidation(e.target.checked)} />
{modelFormData.type === ModelType.EMBEDDING && (
setModelFormData({ ...modelFormData, maxInputTokens: parseInt(e.target.value) })} />
setModelFormData({ ...modelFormData, dimensions: parseInt(e.target.value) })} />
)}
) : (
{models.map(model => (

{model.name}

{getTypeLabel(model.type)} {model.modelId}
))}
)}
); return (
{/* サイドバー */}

{t('settings')}

{/* コンテンツエリア */}

{activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : activeTab === 'model' ? t('modelManagement') : t('generalSettings')}

{error && (
Error: {error}
)} {activeTab === 'general' && renderGeneralTab()} {activeTab === 'user' && renderUserTab()} {activeTab === 'model' && isAdmin && renderModelTab()} {/* Only render model tab if user is admin */}
); };