/** * Processing mode selection (Fast/Precise) support */ import React, { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { ModelConfig, RawFile, IndexingConfig } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import { useToast } from '../contexts/ToastContext'; import { useConfirm } from '../contexts/ConfirmContext'; import { Layers, FileText, Database, X, ArrowRight, Files, Info, Zap, Target, AlertTriangle, Clock, DollarSign } from 'lucide-react'; import { formatBytes } from '../utils/fileUtils'; import { chunkConfigService } from '../services/chunkConfigService'; import { uploadService } from '../services/uploadService'; interface IndexingModalWithModeProps { isOpen: boolean; onClose: () => void; files?: RawFile[]; embeddingModels: ModelConfig[]; defaultEmbeddingId: string; onConfirm: (config: IndexingConfig) => void; isReconfiguring?: boolean; } const IndexingModalWithMode: React.FC = ({ isOpen, onClose, files = [], embeddingModels, defaultEmbeddingId, onConfirm, isReconfiguring = false }) => { const { t } = useLanguage(); const { showWarning, showInfo } = useToast(); const { confirm } = useConfirm(); // Configuration state const [chunkSize, setChunkSize] = useState(200); const [chunkOverlap, setChunkOverlap] = useState(40); const [selectedEmbedding, setSelectedEmbedding] = useState(''); const [mode, setMode] = useState<'fast' | 'precise'>('fast'); const [userSelectedMode, setUserSelectedMode] = useState(false); // Track if user manually selected mode // Mode recommendation info const [modeRecommendation, setModeRecommendation] = useState(null); const [isLoadingRecommendation, setIsLoadingRecommendation] = useState(false); // Limit info state const [limits, setLimits] = useState<{ maxChunkSize: number; maxOverlapSize: number; minOverlapSize: number; defaultChunkSize: number; defaultOverlapSize: number; modelInfo: { name: string; maxInputTokens: number; maxBatchSize: number; expectedDimensions: number; }; } | null>(null); const [isLoadingLimits, setIsLoadingLimits] = useState(false); // Get auth token const getAuthToken = () => { return localStorage.getItem('authToken') || ''; }; // Load mode recommendation when files change useEffect(() => { if (!isOpen || !files || files.length === 0) return; const loadRecommendation = async () => { setIsLoadingRecommendation(true); try { // Use first file for recommendation (assume similar types) const file = files[0]; const rec = await uploadService.recommendMode(file.file); setModeRecommendation(rec); // Auto-select recommended mode if user hasn't manually selected one if (!isReconfiguring && !userSelectedMode) { setMode(rec.recommendedMode); showInfo(t('recommendationMsg', rec.recommendedMode === 'precise' ? t('preciseMode') : t('fastMode'), t(rec.reason, ...(rec.reasonArgs || [])))); } } catch (error) { console.error('モード推奨の取得に失敗しました:', error); } finally { setIsLoadingRecommendation(false); } }; loadRecommendation(); }, [isOpen, files, isReconfiguring]); // Load config limits when selected model changes useEffect(() => { if (!isOpen || !selectedEmbedding) { setLimits(null); return; } const loadLimits = async () => { setIsLoadingLimits(true); try { const token = getAuthToken(); if (!token) return; const limitData = await chunkConfigService.getLimits(selectedEmbedding, token); setLimits(limitData); // Auto-adjust if current values exceed new limits if (chunkSize > limitData.maxChunkSize) { setChunkSize(limitData.maxChunkSize); showWarning(t('autoAdjustChunk', limitData.maxChunkSize)); } if (chunkOverlap > limitData.maxOverlapSize) { setChunkOverlap(limitData.maxOverlapSize); showWarning(t('autoAdjustOverlap', limitData.maxOverlapSize)); } if (chunkOverlap < limitData.minOverlapSize) { setChunkOverlap(limitData.minOverlapSize); // Only show warning if it was manually set below the new minimum if (chunkOverlap < limitData.minOverlapSize) { showWarning(t('autoAdjustOverlapMin', limitData.minOverlapSize)); } } } catch (error) { console.error('設定制限の読み込みに失敗しました:', error); showWarning(t('loadLimitsFailed')); } finally { setIsLoadingLimits(false); } }; loadLimits(); }, [isOpen, selectedEmbedding]); // Track isOpen state change, reset only on open const [prevOpen, setPrevOpen] = useState(false); // Initialize modal useEffect(() => { if (isOpen && !prevOpen) { // Execute initialization only when going from closed to open console.log('DEBUG: IndexingModalWithMode opening, files:', files); // Set default embedding model const enabledModels = embeddingModels.filter(m => m.isEnabled !== false); const validDefault = enabledModels.find(m => m.id === defaultEmbeddingId); if (validDefault) { setSelectedEmbedding(defaultEmbeddingId); } else if (enabledModels.length > 0) { setSelectedEmbedding(enabledModels[0].id); } else { setSelectedEmbedding(''); } // Reset to defaults setChunkSize(200); setChunkOverlap(40); if (!isReconfiguring) { setMode('fast'); setUserSelectedMode(false); // Reset user selection status } setModeRecommendation(null); } setPrevOpen(isOpen); }, [isOpen, prevOpen, defaultEmbeddingId, embeddingModels, isReconfiguring]); // Handle chunk size change const handleChunkSizeChange = (value: number) => { if (limits && value > limits.maxChunkSize) { showWarning(t('maxValueMsg', limits.maxChunkSize)); setChunkSize(limits.maxChunkSize); return; } setChunkSize(value); // Auto-adjust overlap if it exceeds 50% of new chunk size if (chunkOverlap > value * 0.5) { setChunkOverlap(Math.floor(value * 0.5)); } }; // Handle overlap size change const handleChunkOverlapChange = (value: number) => { if (limits && value > limits.maxOverlapSize) { showWarning(t('maxValueMsg', limits.maxOverlapSize)); setChunkOverlap(limits.maxOverlapSize); return; } if (limits && value < limits.minOverlapSize) { // Don't show warning here, just set to min if they slide too low setChunkOverlap(limits.minOverlapSize); return; } // Check if it exceeds 50% of chunk size const maxOverlapByRatio = Math.floor(chunkSize * 0.5); if (value > maxOverlapByRatio) { showWarning(t('overlapRatioLimit', maxOverlapByRatio)); setChunkOverlap(maxOverlapByRatio); return; } setChunkOverlap(value); }; // Render limits info const renderLimitsInfo = () => { if (!limits || isLoadingLimits) { return null; } return (
{t('modelLimitsInfo')}
{t('model')}: {limits.modelInfo.name}
{t('maxChunkSize')}: {limits.maxChunkSize} tokens
{t('maxOverlapSize')}: {limits.maxOverlapSize} tokens
{t('maxBatchSize')}: {limits.modelInfo.maxBatchSize}
{limits.modelInfo.maxInputTokens > limits.maxChunkSize && (
⚠️ {t('envLimitWeaker')}: {limits.maxChunkSize} < {limits.modelInfo.maxInputTokens}
)}
); }; // Render mode recommendation info const renderModeRecommendation = () => { if (!modeRecommendation || isLoadingRecommendation) { return null; } return (
{t('processingMode')}
{t('recommendationReason')}: {t(modeRecommendation.reason, ...(modeRecommendation.reasonArgs || []))}
{modeRecommendation.warnings && modeRecommendation.warnings.length > 0 && (
{modeRecommendation.warnings.map((warning: string, idx: number) => (
{t(warning as any)}
))}
)}
); }; // Render current mode description const renderModeDescription = () => { if (mode === 'fast') { return (
{t('fastModeFeatures')}
  • {t('fastFeature1')}
  • {t('fastFeature2')}
  • {t('fastFeature3')}
  • {t('fastFeature4')}
  • {t('fastFeature5')}
); } return (
{t('preciseModeFeatures')}
  • {t('preciseFeature1')}
  • {t('preciseFeature2')}
  • {t('preciseFeature3')}
  • {t('preciseFeature4')}
  • {t('preciseFeature5')}
  • {t('preciseFeature6')}
); }; if (!isOpen) return null; return createPortal( <>
{/* Header */}

{isReconfiguring ? t('reconfigureTitle') : t('indexingConfigTitle')}

{isReconfiguring ? t('reconfigureDesc') : t('indexingConfigDesc')}

{/* Pending files - only show when there are files */} {files && files.length > 0 && (

{t('pendingFiles')}

{files.map((file, index) => (
{file.name} {formatBytes(file.size)}
))}
)} {/* Processing mode selection */} {!isReconfiguring && (

{t('processingMode')} {isLoadingRecommendation && {t('analyzingFile')}}

{/* Mode recommendation info */} {renderModeRecommendation()} {/* Mode selection */}
{/* Fast Mode */} {/* Precise Mode */}
{/* Mode description */}
{renderModeDescription()}
)} {/* Embedding model selection */}

{t('embeddingModel')}

{/* Chunk config */}

{t('chunkConfig')}

{/* Chunk size */}
{t('chunkSize')} {chunkSize}
handleChunkSizeChange(Number(e.target.value))} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600" disabled={!selectedEmbedding || isLoadingLimits} />
{t('min')}: 50 {t('max')}: {limits?.maxChunkSize || '-'}
{/* Overlap size */}
{t('chunkOverlap')} {chunkOverlap}
handleChunkOverlapChange(Number(e.target.value))} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600" disabled={!selectedEmbedding || isLoadingLimits} />
{t('min')}: {limits?.minOverlapSize || 25} {t('max')}: {limits?.maxOverlapSize || '-'}
{isReconfiguring && renderLimitsInfo()} {/* Optimization tips */} {limits && (

💡 {t('optimizationTips')}

    {chunkSize > 800 &&
  • {t('tipChunkTooLarge')}
  • } {chunkOverlap < chunkSize * 0.1 &&
  • {t('tipOverlapSmall').replace('$1', `${Math.floor(chunkSize * 0.1)}`)}
  • } {chunkSize === limits.maxChunkSize &&
  • {t('tipMaxValues')}
  • } {mode === 'precise' &&
  • {t('tipPreciseCost')}
  • }
)}
{/* Footer buttons */}
, document.body ); }; export default IndexingModalWithMode;