import React, { useState, useRef, useEffect } from 'react'; import { Send, Loader2, Paperclip, X, Search, Database } from 'lucide-react'; import { useLanguage } from '../contexts/LanguageContext'; import { AppSettings, KnowledgeFile, ModelConfig, Message, Role, KnowledgeGroup } from '../types'; import ChatMessage from './ChatMessage'; // Assuming ChatMessage is a default export based on original code import SearchResultsPanel from './SearchResultsPanel'; import { chatService, ChatMessage as ChatMsg, ChatSource } from '../services/chatService'; import { generateUUID } from '../utils/uuid'; interface ChatInterfaceProps { files: KnowledgeFile[]; settings: AppSettings; models: ModelConfig[]; groups: KnowledgeGroup[]; selectedGroups: string[]; onGroupSelectionChange?: (groupIds: string[]) => void; onOpenGroupSelection?: () => void; // New prop selectedFiles?: string[]; onClearFileSelection?: () => void; onMobileUploadClick: () => void; currentHistoryId?: string; historyMessages?: any[] | null; onHistoryMessagesLoaded?: () => void; onPreviewSource?: (source: ChatSource) => void; onOpenFile?: (source: ChatSource) => void; onHistoryIdCreated?: (historyId: string) => void; } const ChatInterface: React.FC = ({ files, settings, models, groups, selectedGroups, onGroupSelectionChange, onOpenGroupSelection, selectedFiles, onClearFileSelection, onMobileUploadClick, currentHistoryId, historyMessages, onHistoryMessagesLoaded, onPreviewSource, onOpenFile, onHistoryIdCreated }) => { const { t, language } = useLanguage(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [sources, setSources] = useState([]); const [showSources, setShowSources] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const lastSubmitTime = useRef(0); // Debug logging // console.log('ChatInterface Render:', { // selectedFilesCount: selectedFiles?.length, // totalFilesCount: files.length, // selectedFiles: selectedFiles, // matchedFiles: files.filter(f => selectedFiles?.includes(f.id)).map(f => f.name) // }); // 履歴メッセージの読み込みを処理 // 履歴メッセージの読み込みを処理 useEffect(() => { if (historyMessages && historyMessages.length > 0) { const convertedMessages: Message[] = historyMessages.map(msg => ({ id: msg.id, role: msg.role === 'user' ? Role.USER : Role.MODEL, text: msg.content, timestamp: new Date(msg.createdAt).getTime(), sources: msg.sources // Attach sources to message })); setMessages(convertedMessages); // 履歴メッセージが読み込まれたことを親コンポーネントに通知 onHistoryMessagesLoaded?.(); } }, [historyMessages, onHistoryMessagesLoaded]); useEffect(() => { const welcomeText = t('welcomeMessage'); setMessages((prevMessages) => { if (prevMessages.length === 0) { return [ { id: 'welcome', role: Role.MODEL, text: welcomeText, timestamp: Date.now(), }, ]; } const hasWelcome = prevMessages.some(m => m.id === 'welcome'); if (hasWelcome) { return prevMessages.map(m => m.id === 'welcome' ? { ...m, text: welcomeText } : m ); } return prevMessages; }); }, [t]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); const handleSend = async () => { if (!input.trim() || isLoading) return; // デバウンス機構:500ms以内の重複送信を防止 const now = Date.now(); if (now - lastSubmitTime.current < 500) { console.log('Preventing duplicate submission'); return; } lastSubmitTime.current = now; const userText = input.trim(); // 入力欄を即座にクリアして高さをリセットし、重複送信を防止 setInput(''); if (inputRef.current) { inputRef.current.style.height = 'auto'; inputRef.current.blur(); // フォーカスを外す } // Resolve Model Config const selectedModel = models.find(m => m.id === settings.selectedLLMId && m.type === 'llm'); if (!selectedModel) { const errorMsg: Message = { id: generateUUID(), role: Role.MODEL, text: `${t('errorNoModel')} - LLM ID: ${settings.selectedLLMId}, Models: ${models.length} `, timestamp: Date.now(), isError: true, }; setMessages(prev => [...prev, errorMsg]); return; } const newMessage: Message = { id: generateUUID(), role: Role.USER, text: userText, timestamp: Date.now(), }; setIsLoading(true); setMessages((prev) => [...prev, newMessage]); const authToken = localStorage.getItem('authToken'); if (!authToken) { const errorMsg: Message = { id: generateUUID(), role: Role.MODEL, text: t('needLogin'), timestamp: Date.now(), isError: true, }; setMessages(prev => [...prev, errorMsg]); setIsLoading(false); return; } try { const history: ChatMsg[] = messages .filter(m => m.id !== 'welcome') .map(m => ({ role: m.role === Role.USER ? 'user' : 'assistant', content: m.text, })); const botMessageId = generateUUID(); let botContent = ''; // 初期ボットメッセージを追加 const botMessage: Message = { id: botMessageId, role: Role.MODEL, text: '', timestamp: Date.now(), }; setMessages(prev => [...prev, botMessage]); const stream = chatService.streamChat( userText, history, authToken, language, settings.selectedEmbeddingId, settings.selectedLLMId, // Pass selected LLM ID selectedGroups.length > 0 ? selectedGroups : undefined, // グループフィルタを渡す selectedFiles?.length > 0 ? selectedFiles : undefined, // ファイルフィルタを渡す currentHistoryId, // 履歴IDを渡す settings.enableRerank, // Rerankスイッチを渡す settings.selectedRerankId, // RerankモデルIDを渡す settings.temperature, // 温度パラメータを渡す settings.maxTokens, // 最大トークン数を渡す settings.topK, // Top-Kパラメータを渡す settings.similarityThreshold, // 類似度しきい値を渡す settings.rerankSimilarityThreshold // Rerankしきい値を渡す ); for await (const chunk of stream) { if (chunk.type === 'content') { botContent += chunk.data; setMessages(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: botContent } : msg ) ); } else if (chunk.type === 'sources') { // Attach sources to the current bot message setMessages(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, sources: chunk.data } : msg ) ); } else if (chunk.type === 'historyId') { onHistoryIdCreated?.(chunk.data); } else if (chunk.type === 'error') { setMessages(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: t('errorMessage').replace('$1', chunk.data), isError: true } : msg ) ); break; } } } catch (error: any) { console.error('Chat error:', error); let errorText = t('errorGeneric'); if (error.message === "API_KEY_MISSING") errorText = t('apiError'); else if (error.message.includes("OpenAI API Error")) errorText = `OpenAI Error: ${error.message} `; else if (error.message === "GEMINI_API_ERROR") errorText = t('geminiError'); else if (error.message) errorText = `Error: ${error.message} `; const errorMessage: Message = { id: generateUUID(), role: Role.MODEL, text: errorText, timestamp: Date.now(), isError: true, }; setMessages((prev) => [...prev, errorMessage]); } finally { setIsLoading(false); lastSubmitTime.current = 0; } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!isLoading && input.trim()) { handleSend(); } } }; const handleInputResize = (e: React.ChangeEvent) => { setInput(e.target.value); e.target.style.height = 'auto'; e.target.style.height = `${Math.min(e.target.scrollHeight, 200)} px`; }; return (
{messages.map((msg) => ( ))} {isLoading && (
{t('analyzing')}
)}
{((selectedFiles && selectedFiles.length > 0) || true) && (
{/* Group Selection Button */} {files.filter(f => selectedFiles?.includes(f.id)).map(file => (
{file.name}
))}
)}