import React, { useState, useEffect } from 'react'; import { ModelConfig, RawFile, IndexingConfig } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import { useToast } from '../contexts/ToastContext'; import { Layers, FileText, Database, X, ArrowRight, Files, Info } from 'lucide-react'; import { formatBytes } from '../utils/fileUtils'; import { chunkConfigService } from '../services/chunkConfigService'; interface IndexingModalProps { isOpen: boolean; onClose: () => void; files: RawFile[]; embeddingModels: ModelConfig[]; defaultEmbeddingId: string; onConfirm: (config: IndexingConfig) => void; isReconfiguring?: boolean; } const IndexingModal: React.FC = ({ isOpen, onClose, files, embeddingModels, defaultEmbeddingId, onConfirm, isReconfiguring = false }) => { const { t } = useLanguage(); const { showWarning } = useToast(); // Configuration state const [chunkSize, setChunkSize] = useState(200); const [chunkOverlap, setChunkOverlap] = useState(40); const [selectedEmbedding, setSelectedEmbedding] = useState(''); // Limit info state const [limits, setLimits] = useState<{ maxChunkSize: number; maxOverlapSize: 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('kb_api_key') || localStorage.getItem('authToken') || ''; }; // 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)); } } catch (error) { console.error('設定制限の読み込みに失敗しました:', error); showWarning(t('loadLimitsFailed')); } finally { setIsLoadingLimits(false); } }; loadLimits(); }, [isOpen, selectedEmbedding]); // Initialize modal useEffect(() => { if (isOpen) { // Set default embedding model const validDefault = embeddingModels.find(m => m.id === defaultEmbeddingId); if (validDefault) { setSelectedEmbedding(defaultEmbeddingId); } else if (embeddingModels.length > 0) { setSelectedEmbedding(embeddingModels[0].id); } else { setSelectedEmbedding(''); } // Reset to defaults setChunkSize(200); setChunkOverlap(40); } }, [isOpen, defaultEmbeddingId, embeddingModels]); // 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; } // 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}
)}
); }; if (!isOpen) return null; return (
{/* Header */}

{isReconfiguring ? t('reconfigureFile') : t('idxModalTitle')}

{isReconfiguring ? t('modifySettings') : t('idxDesc')}

{/* Pending Files */}

{t('idxFiles')}

{files.map((file, index) => (
{file.name} {formatBytes(file.size)}
))}
{/* Embedding Model Selection */}

{t('idxEmbeddingModel')}

{/* Chunk Configuration */}

{t('idxMethod')}

{/* 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')}: 0 {t('max')}: {limits?.maxOverlapSize || '—'}
{/* Model Limits Info */} {renderLimitsInfo()} {/* Optimization Tips */} {limits && (

💡 {t('optimizationTips')}

    {chunkSize > 800 &&
  • {t('tipChunkTooLarge')}
  • } {chunkOverlap < chunkSize * 0.1 &&
  • {t('tipOverlapSmall').replace('$1', String(Math.floor(chunkSize * 0.1)))}
  • } {chunkSize === limits.maxChunkSize &&
  • {t('tipMaxValues')}
  • }
)}
{/* Footer Buttons */}
); }; export default IndexingModal;