Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import React, { useState, useEffect } from 'react'; | |
| import { useLanguage } from '@/contexts/LanguageContext'; | |
| // Define the interfaces for our model configuration | |
| interface Model { | |
| id: string; | |
| name: string; | |
| } | |
| interface Provider { | |
| id: string; | |
| name: string; | |
| models: Model[]; | |
| supportsCustomModel?: boolean; | |
| } | |
| interface ModelConfig { | |
| providers: Provider[]; | |
| defaultProvider: string; | |
| } | |
| interface ModelSelectorProps { | |
| provider: string; | |
| setProvider: (value: string) => void; | |
| model: string; | |
| setModel: (value: string) => void; | |
| isCustomModel: boolean; | |
| setIsCustomModel: (value: boolean) => void; | |
| customModel: string; | |
| setCustomModel: (value: string) => void; | |
| // File filter configuration | |
| showFileFilters?: boolean; | |
| excludedDirs?: string; | |
| setExcludedDirs?: (value: string) => void; | |
| excludedFiles?: string; | |
| setExcludedFiles?: (value: string) => void; | |
| includedDirs?: string; | |
| setIncludedDirs?: (value: string) => void; | |
| includedFiles?: string; | |
| setIncludedFiles?: (value: string) => void; | |
| } | |
| export default function UserSelector({ | |
| provider, | |
| setProvider, | |
| model, | |
| setModel, | |
| isCustomModel, | |
| setIsCustomModel, | |
| customModel, | |
| setCustomModel, | |
| // File filter configuration | |
| showFileFilters = false, | |
| excludedDirs = '', | |
| setExcludedDirs, | |
| excludedFiles = '', | |
| setExcludedFiles, | |
| includedDirs = '', | |
| setIncludedDirs, | |
| includedFiles = '', | |
| setIncludedFiles | |
| }: ModelSelectorProps) { | |
| // State to manage the visibility of the filters modal and filter section | |
| const [isFilterSectionOpen, setIsFilterSectionOpen] = useState(false); | |
| // State to manage filter mode: 'exclude' or 'include' | |
| const [filterMode, setFilterMode] = useState<'exclude' | 'include'>('exclude'); | |
| const { messages: t } = useLanguage(); | |
| // State for model configurations from backend | |
| const [modelConfig, setModelConfig] = useState<ModelConfig | null>(null); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| // State for viewing default values | |
| const [showDefaultDirs, setShowDefaultDirs] = useState(false); | |
| const [showDefaultFiles, setShowDefaultFiles] = useState(false); | |
| // Fetch model configurations from the backend | |
| useEffect(() => { | |
| const fetchModelConfig = async () => { | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| const response = await fetch('/api/models/config'); | |
| if (!response.ok) { | |
| throw new Error(`Error fetching model configurations: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| setModelConfig(data); | |
| // Initialize provider and model with defaults from API if not already set | |
| if (!provider && data.defaultProvider) { | |
| setProvider(data.defaultProvider); | |
| // Find the default provider and set its default model | |
| const selectedProvider = data.providers.find((p: Provider) => p.id === data.defaultProvider); | |
| if (selectedProvider && selectedProvider.models.length > 0) { | |
| setModel(selectedProvider.models[0].id); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Failed to fetch model configurations:', err); | |
| setError('Failed to load model configurations. Using default options.'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| fetchModelConfig(); | |
| }, [provider, setModel, setProvider]); | |
| // Handler for changing provider | |
| const handleProviderChange = (newProvider: string) => { | |
| setProvider(newProvider); | |
| setTimeout(() => { | |
| // Reset custom model state when changing providers | |
| setIsCustomModel(false); | |
| // Set default model for the selected provider | |
| if (modelConfig) { | |
| const selectedProvider = modelConfig.providers.find((p: Provider) => p.id === newProvider); | |
| if (selectedProvider && selectedProvider.models.length > 0) { | |
| setModel(selectedProvider.models[0].id); | |
| } | |
| } | |
| }, 10); | |
| }; | |
| // Default excluded directories from config.py | |
| const defaultExcludedDirs = | |
| `./.venv/ | |
| ./venv/ | |
| ./env/ | |
| ./virtualenv/ | |
| ./node_modules/ | |
| ./bower_components/ | |
| ./jspm_packages/ | |
| ./.git/ | |
| ./.svn/ | |
| ./.hg/ | |
| ./.bzr/ | |
| ./__pycache__/ | |
| ./.pytest_cache/ | |
| ./.mypy_cache/ | |
| ./.ruff_cache/ | |
| ./.coverage/ | |
| ./dist/ | |
| ./build/ | |
| ./out/ | |
| ./target/ | |
| ./bin/ | |
| ./obj/ | |
| ./docs/ | |
| ./_docs/ | |
| ./site-docs/ | |
| ./_site/ | |
| ./.idea/ | |
| ./.vscode/ | |
| ./.vs/ | |
| ./.eclipse/ | |
| ./.settings/ | |
| ./logs/ | |
| ./log/ | |
| ./tmp/ | |
| ./temp/ | |
| ./.eng`; | |
| // Default excluded files from config.py | |
| const defaultExcludedFiles = | |
| `package-lock.json | |
| yarn.lock | |
| pnpm-lock.yaml | |
| npm-shrinkwrap.json | |
| poetry.lock | |
| Pipfile.lock | |
| requirements.txt.lock | |
| Cargo.lock | |
| composer.lock | |
| .lock | |
| .DS_Store | |
| Thumbs.db | |
| desktop.ini | |
| *.lnk | |
| .env | |
| .env.* | |
| *.env | |
| *.cfg | |
| *.ini | |
| .flaskenv | |
| .gitignore | |
| .gitattributes | |
| .gitmodules | |
| .github | |
| .gitlab-ci.yml | |
| .prettierrc | |
| .eslintrc | |
| .eslintignore | |
| .stylelintrc | |
| .editorconfig | |
| .jshintrc | |
| .pylintrc | |
| .flake8 | |
| mypy.ini | |
| pyproject.toml | |
| tsconfig.json | |
| webpack.config.js | |
| babel.config.js | |
| rollup.config.js | |
| jest.config.js | |
| karma.conf.js | |
| vite.config.js | |
| next.config.js | |
| *.min.js | |
| *.min.css | |
| *.bundle.js | |
| *.bundle.css | |
| *.map | |
| *.gz | |
| *.zip | |
| *.tar | |
| *.tgz | |
| *.rar | |
| *.pyc | |
| *.pyo | |
| *.pyd | |
| *.so | |
| *.dll | |
| *.class | |
| *.exe | |
| *.o | |
| *.a | |
| *.jpg | |
| *.jpeg | |
| *.png | |
| *.gif | |
| *.ico | |
| *.svg | |
| *.webp | |
| *.mp3 | |
| *.mp4 | |
| *.wav | |
| *.avi | |
| *.mov | |
| *.webm | |
| *.csv | |
| *.tsv | |
| *.xls | |
| *.xlsx | |
| *.db | |
| *.sqlite | |
| *.sqlite3 | |
| *.docx | |
| *.pptx`; | |
| // Display loading state | |
| if (isLoading) { | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| <div className="text-sm text-[var(--muted)]">Loading model configurations...</div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="flex flex-col gap-3"> | |
| <div className="space-y-4"> | |
| {error && ( | |
| <div className="text-sm text-red-500 mb-2">{error}</div> | |
| )} | |
| {/* Provider Selection */} | |
| <div> | |
| <label htmlFor="provider-dropdown" className="block text-xs font-medium text-[var(--foreground)] mb-1.5"> | |
| {t.form?.modelProvider || 'Model Provider'} | |
| </label> | |
| <select | |
| id="provider-dropdown" | |
| value={provider} | |
| onChange={(e) => handleProviderChange(e.target.value)} | |
| className="input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]" | |
| > | |
| <option value="" disabled>{t.form?.selectProvider || 'Select Provider'}</option> | |
| {modelConfig?.providers.map((providerOption) => ( | |
| <option key={providerOption.id} value={providerOption.id}> | |
| {t.form?.[`provider${providerOption.id.charAt(0).toUpperCase() + providerOption.id.slice(1)}`] || providerOption.name} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| {/* Model Selection - consistent height regardless of type */} | |
| <div> | |
| <label htmlFor={isCustomModel ? "custom-model-input" : "model-dropdown"} className="block text-xs font-medium text-[var(--foreground)] mb-1.5"> | |
| {t.form?.modelSelection || 'Model Selection'} | |
| </label> | |
| {isCustomModel ? ( | |
| <input | |
| id="custom-model-input" | |
| type="text" | |
| value={customModel} | |
| onChange={(e) => { | |
| setCustomModel(e.target.value); | |
| setModel(e.target.value); | |
| }} | |
| placeholder={t.form?.customModelPlaceholder || 'Enter custom model name'} | |
| className="input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]" | |
| /> | |
| ) : ( | |
| <select | |
| id="model-dropdown" | |
| value={model} | |
| onChange={(e) => setModel(e.target.value)} | |
| className="input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]" | |
| disabled={!provider || isLoading || !modelConfig?.providers.find(p => p.id === provider)?.models?.length} | |
| > | |
| {modelConfig?.providers.find((p: Provider) => p.id === provider)?.models.map((modelOption) => ( | |
| <option key={modelOption.id} value={modelOption.id}> | |
| {modelOption.name} | |
| </option> | |
| )) || <option value="">{t.form?.selectModel || 'Select Model'}</option>} | |
| </select> | |
| )} | |
| </div> | |
| {/* Custom model toggle - only when provider supports it */} | |
| {modelConfig?.providers.find((p: Provider) => p.id === provider)?.supportsCustomModel && ( | |
| <div className="mb-2"> | |
| <div className="flex items-center pb-1"> | |
| <div | |
| className="relative flex items-center cursor-pointer" | |
| onClick={() => { | |
| const newValue = !isCustomModel; | |
| setIsCustomModel(newValue); | |
| if (newValue) { | |
| setCustomModel(model); | |
| } | |
| }} | |
| > | |
| <input | |
| id="use-custom-model" | |
| type="checkbox" | |
| checked={isCustomModel} | |
| onChange={() => {}} | |
| className="sr-only" | |
| /> | |
| <div className={`w-10 h-5 rounded-full transition-colors ${isCustomModel ? 'bg-[var(--accent-primary)]' : 'bg-gray-300 dark:bg-gray-600'}`}></div> | |
| <div className={`absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-white transition-transform transform ${isCustomModel ? 'translate-x-5' : ''}`}></div> | |
| </div> | |
| <label | |
| htmlFor="use-custom-model" | |
| className="ml-2 text-sm font-medium text-[var(--muted)] cursor-pointer" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| const newValue = !isCustomModel; | |
| setIsCustomModel(newValue); | |
| if (newValue) { | |
| setCustomModel(model); | |
| } | |
| }} | |
| > | |
| {t.form?.useCustomModel || 'Use custom model'} | |
| </label> | |
| </div> | |
| </div> | |
| )} | |
| {showFileFilters && ( | |
| <div className="mt-4"> | |
| <button | |
| type="button" | |
| onClick={() => setIsFilterSectionOpen(!isFilterSectionOpen)} | |
| className="flex items-center text-sm text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors" | |
| > | |
| <span className="mr-1.5 text-xs">{isFilterSectionOpen ? '▼' : '►'}</span> | |
| {t.form?.advancedOptions || 'Advanced Options'} | |
| </button> | |
| {isFilterSectionOpen && ( | |
| <div className="mt-3 p-3 border border-[var(--border-color)]/70 rounded-md bg-[var(--background)]/30"> | |
| {/* Filter Mode Selection */} | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium text-[var(--foreground)] mb-2"> | |
| {t.form?.filterMode || 'Filter Mode'} | |
| </label> | |
| <div className="flex gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => setFilterMode('exclude')} | |
| className={`flex-1 px-3 py-2 rounded-md border text-sm transition-colors ${ | |
| filterMode === 'exclude' | |
| ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)]' | |
| : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]' | |
| }`} | |
| > | |
| {t.form?.excludeMode || 'Exclude Paths'} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setFilterMode('include')} | |
| className={`flex-1 px-3 py-2 rounded-md border text-sm transition-colors ${ | |
| filterMode === 'include' | |
| ? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)]' | |
| : 'border-[var(--border-color)] text-[var(--foreground)] hover:bg-[var(--background)]' | |
| }`} | |
| > | |
| {t.form?.includeMode || 'Include Only Paths'} | |
| </button> | |
| </div> | |
| <p className="text-xs text-[var(--muted)] mt-1"> | |
| {filterMode === 'exclude' | |
| ? (t.form?.excludeModeDescription || 'Specify paths to exclude from processing (default behavior)') | |
| : (t.form?.includeModeDescription || 'Specify only the paths to include, ignoring all others') | |
| } | |
| </p> | |
| </div> | |
| {/* Directories Section */} | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium text-[var(--muted)] mb-1.5"> | |
| {filterMode === 'exclude' | |
| ? (t.form?.excludedDirs || 'Excluded Directories') | |
| : (t.form?.includedDirs || 'Included Directories') | |
| } | |
| </label> | |
| <textarea | |
| value={filterMode === 'exclude' ? excludedDirs : includedDirs} | |
| onChange={(e) => { | |
| if (filterMode === 'exclude') { | |
| setExcludedDirs?.(e.target.value); | |
| } else { | |
| setIncludedDirs?.(e.target.value); | |
| } | |
| }} | |
| rows={4} | |
| className="block w-full rounded-md border border-[var(--border-color)]/50 bg-[var(--input-bg)] text-[var(--foreground)] px-3 py-2 text-sm focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-opacity-50 shadow-sm" | |
| placeholder={filterMode === 'exclude' | |
| ? (t.form?.enterExcludedDirs || 'Enter excluded directories, one per line...') | |
| : (t.form?.enterIncludedDirs || 'Enter included directories, one per line...') | |
| } | |
| /> | |
| {filterMode === 'exclude' && ( | |
| <> | |
| <div className="flex mt-1.5"> | |
| <button | |
| type="button" | |
| onClick={() => setShowDefaultDirs(!showDefaultDirs)} | |
| className="text-xs text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors" | |
| > | |
| {showDefaultDirs ? (t.form?.hideDefault || 'Hide Default') : (t.form?.viewDefault || 'View Default')} | |
| </button> | |
| </div> | |
| {showDefaultDirs && ( | |
| <div className="mt-2 p-2 rounded bg-[var(--background)]/50 text-xs"> | |
| <p className="mb-1 text-[var(--muted)]">{t.form?.defaultNote || 'These defaults are already applied. Add your custom exclusions above.'}</p> | |
| <pre className="whitespace-pre-wrap font-mono text-[var(--muted)] overflow-y-auto max-h-32">{defaultExcludedDirs}</pre> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| {/* Files Section */} | |
| <div> | |
| <label className="block text-sm font-medium text-[var(--muted)] mb-1.5"> | |
| {filterMode === 'exclude' | |
| ? (t.form?.excludedFiles || 'Excluded Files') | |
| : (t.form?.includedFiles || 'Included Files') | |
| } | |
| </label> | |
| <textarea | |
| value={filterMode === 'exclude' ? excludedFiles : includedFiles} | |
| onChange={(e) => { | |
| if (filterMode === 'exclude') { | |
| setExcludedFiles?.(e.target.value); | |
| } else { | |
| setIncludedFiles?.(e.target.value); | |
| } | |
| }} | |
| rows={4} | |
| className="block w-full rounded-md border border-[var(--border-color)]/50 bg-[var(--input-bg)] text-[var(--foreground)] px-3 py-2 text-sm focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-opacity-50 shadow-sm" | |
| placeholder={filterMode === 'exclude' | |
| ? (t.form?.enterExcludedFiles || 'Enter excluded files, one per line...') | |
| : (t.form?.enterIncludedFiles || 'Enter included files, one per line...') | |
| } | |
| /> | |
| {filterMode === 'exclude' && ( | |
| <> | |
| <div className="flex mt-1.5"> | |
| <button | |
| type="button" | |
| onClick={() => setShowDefaultFiles(!showDefaultFiles)} | |
| className="text-xs text-[var(--accent-primary)] hover:text-[var(--accent-primary)]/80 transition-colors" | |
| > | |
| {showDefaultFiles ? (t.form?.hideDefault || 'Hide Default') : (t.form?.viewDefault || 'View Default')} | |
| </button> | |
| </div> | |
| {showDefaultFiles && ( | |
| <div className="mt-2 p-2 rounded bg-[var(--background)]/50 text-xs"> | |
| <p className="mb-1 text-[var(--muted)]">{t.form?.defaultNote || 'These defaults are already applied. Add your custom exclusions above.'}</p> | |
| <pre className="whitespace-pre-wrap font-mono text-[var(--muted)] overflow-y-auto max-h-32">{defaultExcludedFiles}</pre> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |