import React, { useCallback, useEffect, useState, useRef } from 'react' import ChatInterface from '../../components/ChatInterface' import IndexingModalWithMode from '../../components/IndexingModalWithMode' import { GroupManager } from '../../components/GroupManager' import { GroupSelector } from '../../components/GroupSelector' import { SearchHistoryList } from '../../components/SearchHistoryList' import { HistoryDrawer } from '../../components/HistoryDrawer' import { GroupSelectionDrawer } from '../../components/GroupSelectionDrawer' import { PDFPreview } from '../../components/PDFPreview' import { SourcePreviewDrawer } from '../../components/SourcePreviewDrawer' import { ChatSource } from '../../services/chatService' import { AppSettings, DEFAULT_MODELS, DEFAULT_SETTINGS, IndexingConfig, KnowledgeFile, ModelConfig, ModelType, RawFile, KnowledgeGroup, } from '../../types' import { readFile, formatBytes } from '../../utils/fileUtils' import { isFormatSupportedForPreview } from '../../constants/fileSupport' import { Key, LogOut, Menu, Users, X, Folder, History, Plus, Sparkles, Settings } from 'lucide-react' import { useLanguage } from '../../contexts/LanguageContext' import { useToast } from '../../contexts/ToastContext' import { modelConfigService } from '../../services/modelConfigService' import { userSettingService } from '../../services/userSettingService' import { uploadService } from '../../services/uploadService' import { knowledgeBaseService } from '../../services/knowledgeBaseService' import { knowledgeGroupService } from '../../services/knowledgeGroupService' import { searchHistoryService } from '../../services/searchHistoryService' import { userService } from '../../services/userService' interface ChatViewProps { authToken: string; onLogout: () => void; modelConfigs?: ModelConfig[]; // Optional to allow backward compat while refactoring onNavigate: (view: any) => void; initialChatContext?: { selectedGroups?: string[], selectedFiles?: string[] } | null; onClearContext?: () => void; isAdmin?: boolean; } export const ChatView: React.FC = ({ authToken, onLogout, modelConfigs = DEFAULT_MODELS, onNavigate, initialChatContext, onClearContext, isAdmin = false }) => { const { showError, showWarning } = useToast() const [files, setFiles] = useState([]) const [groups, setGroups] = useState([]) const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [isLoadingSettings, setIsLoadingSettings] = useState(true) const [isGroupManagerOpen, setIsGroupManagerOpen] = useState(false) const [isHistoryOpen, setIsHistoryOpen] = useState(false) const [isGroupSelectionOpen, setIsGroupSelectionOpen] = useState(false) // New state const [currentHistoryId, setCurrentHistoryId] = useState() const [historyMessages, setHistoryMessages] = useState(null) const [selectedGroups, setSelectedGroups] = useState([]) const [selectedFiles, setSelectedFiles] = useState([]) const [pdfPreview, setPdfPreview] = useState<{ fileId: string; fileName: string } | null>(null) const [previewSource, setPreviewSource] = useState(null) const [isIndexingModalOpen, setIsIndexingModalOpen] = useState(false) const [pendingFiles, setPendingFiles] = useState([]) // Modals state removed as they are moved to Settings const [isLanguageLoading, setIsLanguageLoading] = useState(false) const { t, language, setLanguage } = useLanguage() const fileInputRef = useRef(null) const handleNewChat = () => { const currentLanguage = language localStorage.removeItem('chatHistory') localStorage.removeItem('chatMessages') localStorage.removeItem('chatSources') localStorage.setItem('userLanguage', currentLanguage) setCurrentHistoryId(undefined) setHistoryMessages(null) window.location.reload() } // Function to fetch user settings from backend const fetchAndSetSettings = useCallback(async () => { if (!authToken) return try { const [personalSettings, tenantSettings] = await Promise.all([ userSettingService.getPersonal(authToken).catch(() => null), userSettingService.get(authToken).catch(() => ({} as Partial)) ]); const appSettings: AppSettings = { ...DEFAULT_SETTINGS, ...tenantSettings, language: personalSettings?.language || tenantSettings?.language || DEFAULT_SETTINGS.language, }; setSettings(appSettings) } catch (error) { console.error('Failed to fetch settings:', error) setSettings(DEFAULT_SETTINGS) } finally { setIsLoadingSettings(false) } }, [authToken]) // Function to fetch files from backend const fetchAndSetFiles = useCallback(async () => { if (!authToken) return try { const remoteFiles = await knowledgeBaseService.getAll(authToken) setFiles(remoteFiles) } catch (error) { console.error('Failed to fetch files:', error) } }, [authToken]) // Function to fetch groups from backend const fetchAndSetGroups = useCallback(async () => { if (!authToken) return try { const remoteGroups = await knowledgeGroupService.getGroups() setGroups(remoteGroups) // Filter out selected groups that no longer exist setSelectedGroups(prev => { const validGroupIds = new Set(remoteGroups.map(g => g.id)) return prev.filter(id => validGroupIds.has(id)) }) } catch (error) { console.error('Failed to fetch groups:', error) } }, [authToken]) useEffect(() => { if (authToken) { fetchAndSetSettings() fetchAndSetFiles() fetchAndSetGroups() } }, [authToken, fetchAndSetSettings, fetchAndSetFiles, fetchAndSetGroups]) // Handle Initial Context useEffect(() => { if (initialChatContext) { if (initialChatContext.selectedGroups) { setSelectedGroups(initialChatContext.selectedGroups) } if (initialChatContext.selectedFiles) { setSelectedFiles(initialChatContext.selectedFiles) } } }, [initialChatContext]) // Load chat history from localStorage on mount useEffect(() => { const savedHistory = localStorage.getItem('chatMessages'); if (savedHistory) { try { const parsedHistory = JSON.parse(savedHistory); if (Array.isArray(parsedHistory) && parsedHistory.length > 0) { setHistoryMessages(parsedHistory); } } catch (error) { console.error('Failed to parse saved chat history:', error); } } }, []); const handleFileUpload = async (fileList: FileList) => { if (!authToken) { showWarning(t('loginToUpload')) return } const MAX_FILE_SIZE = 104857600 const MAX_SIZE_MB = 100 const rawFiles: RawFile[] = [] const errors: string[] = [] for (let i = 0; i < fileList.length; i++) { const file = fileList[i] if (file.size > MAX_FILE_SIZE) { errors.push(t('fileSizeLimitExceeded').replace('$1', file.name).replace('$2', formatBytes(file.size)).replace('$3', MAX_SIZE_MB.toString())) continue } const allowedTypes = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.graphics', 'text/plain', 'text/markdown', 'text/html', 'text/csv', 'text/xml', 'application/xml', 'application/json', 'text/x-python', 'text/x-java', 'text/x-c', 'text/x-c++', 'text/javascript', 'text/typescript', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff', 'image/bmp', 'image/svg+xml', 'application/zip', 'application/x-tar', 'application/gzip', 'application/x-7z-compressed', 'application/rtf', 'application/epub+zip', 'application/x-mobipocket-ebook', ] const ext = file.name.toLowerCase().split('.').pop() const allowedExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'html', 'csv', 'rtf', 'odt', 'ods', 'odp', 'json', 'xml'] const isAllowed = allowedTypes.includes(file.type) || file.type.startsWith('text/') || file.type.startsWith('application/vnd.') || file.type.startsWith('application/x-') || file.type === '' || allowedExtensions.includes(ext || '') if (!isAllowed) { errors.push(t('unsupportedFileType').replace('$1', file.name).replace('$2', file.type || 'unknown')) continue } try { const rawFile = await readFile(file) rawFiles.push(rawFile) } catch (error) { console.error(`Error reading file ${file.name}:`, error) errors.push(t('readFailed').replace('$1', file.name)) } } if (errors.length > 0) { showError(`${t('uploadErrors')}:\n${errors.join('\n')}`) } if (rawFiles.length === 0) return if (errors.length > 0 && rawFiles.length > 0) { showWarning(t('uploadWarning').replace('$1', rawFiles.length.toString()).replace('$2', errors.length.toString())) } setPendingFiles(rawFiles); setIsIndexingModalOpen(true); } const handleConfirmIndexing = async (config: IndexingConfig) => { if (!authToken) return let hasSuccess = false for (const rawFile of pendingFiles) { try { await uploadService.uploadFileWithConfig(rawFile.file, config, authToken) hasSuccess = true } catch (error) { console.error(`Error uploading file ${rawFile.name}:`, error) showError(t('uploadFailed').replace('$1', rawFile.name).replace('$2', error.message)) } } if (hasSuccess) { await fetchAndSetFiles() } setPendingFiles([]) setIsIndexingModalOpen(false) } const handleCancelIndexing = () => { setPendingFiles([]) setIsIndexingModalOpen(false) } const handleGroupsChange = (newGroups: KnowledgeGroup[]) => { setGroups(newGroups) } const handleSelectHistory = async (historyId: string) => { try { const historyDetail = await searchHistoryService.getHistoryDetail(historyId) setCurrentHistoryId(historyId) setIsHistoryOpen(false) setHistoryMessages(historyDetail.messages) } catch (error) { console.error('Failed to load history detail:', error) showError(t('loadHistoryFailed')) } } const handleShowHistory = () => { setIsHistoryOpen(true) } const handleInputFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { handleFileUpload(e.target.files) } if (fileInputRef.current) { fileInputRef.current.value = '' } } if (isLoadingSettings) { return (

{t('loadingUserData')}

) } return (
{/* Main Content */}
{/* Header */}

{t('chatTitle')}

{t('chatDesc')}

{/* 历史记录按钮 */} {/* 新建对话按钮 */}
setIsGroupSelectionOpen(true)} // Pass handler selectedFiles={selectedFiles} onClearFileSelection={() => setSelectedFiles([])} onMobileUploadClick={() => { fileInputRef.current?.click() }} currentHistoryId={currentHistoryId} historyMessages={historyMessages} onHistoryMessagesLoaded={() => setHistoryMessages(null)} onHistoryIdCreated={setCurrentHistoryId} onPreviewSource={setPreviewSource} onOpenFile={(source) => { if (source.fileId) { if (isFormatSupportedForPreview(source.fileName)) { setPdfPreview({ fileId: source.fileId, fileName: source.fileName }); } else { showWarning(t('previewNotSupported')); } } }} authToken={authToken} />
{/* Modals */} m.type === ModelType.EMBEDDING)} defaultEmbeddingId={settings.selectedEmbeddingId} onConfirm={handleConfirmIndexing} /> {/* Group Selection Drawer */} setIsGroupSelectionOpen(false)} groups={groups} selectedGroups={selectedGroups} onSelectionChange={setSelectedGroups} /> {/* 知识库增强功能模态框 (Legacy) */} {isGroupManagerOpen && (

{t('notebooks')}

)} setIsHistoryOpen(false)} groups={groups} onSelectHistory={handleSelectHistory} /> {pdfPreview && ( setPdfPreview(null)} /> )} setPreviewSource(null)} source={previewSource} onOpenFile={(source) => { if (source.fileId) { if (isFormatSupportedForPreview(source.fileName)) { setPdfPreview({ fileId: source.fileId, fileName: source.fileName }); } else { showWarning(t('previewNotSupported')); } } }} />
) }