promptenhancer / src /components /ModelSelector.tsx
Tobs248's picture
# Rolle
eee3ce2 verified
Raw
History Blame Contribute Delete
11.1 kB
tsx
import React, { useState, useRef, useEffect } from 'react'
import { useAppStore } from '@/store/useAppStore'
import { AI_MODELS, CATEGORY_NAMES, CATEGORY_DESCRIPTIONS, getModelsByCategory } from '@/utils/constants'
import { AIModel, AICategory } from '@/types'
import { ChevronDown, Search, Zap } from 'lucide-react'
/**
* Component for selecting AI models with categorized dropdown
*/
export function ModelSelector() {
const { selectedModel, setSelectedModel } = useAppStore()
const [isOpen, setIsOpen] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState<AICategory | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const searchRef = useRef<HTMLInputElement>(null)
/**
* Filter models based on search term and category
*/
const filteredModels = AI_MODELS.filter(model => {
const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
model.description.toLowerCase().includes(searchTerm.toLowerCase())
const matchesCategory = !selectedCategory || model.category === selectedCategory
return matchesSearch && matchesCategory
})
/**
* Close dropdown when clicking outside
*/
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
/**
* Focus search input when dropdown opens
*/
useEffect(() => {
if (isOpen && searchRef.current) {
searchRef.current.focus()
}
}, [isOpen])
/**
* Handle keyboard navigation
*/
const handleKeyDown = (event: React.KeyboardEvent) => {
if (!isOpen && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault()
setIsOpen(true)
} else if (isOpen && event.key === 'Escape') {
setIsOpen(false)
}
}
/**
* Handle model selection
*/
const handleModelSelect = (model: AIModel) => {
setSelectedModel(model)
setIsOpen(false)
setSearchTerm('')
setSelectedCategory(null)
}
/**
* Handle category selection
*/
const handleCategorySelect = (category: AICategory) => {
setSelectedCategory(selectedCategory === category ? null : category)
}
/**
* Get selected model info
*/
const selectedModelInfo = selectedModel ? AI_MODELS.find(m => m.id === selectedModel) : null
/**
* Get category icon
*/
const getCategoryIcon = (category: AICategory) => {
switch (category) {
case 'llm':
return <Zap className="w-4 h-4" />
case 'code':
return <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
case 'image':
return <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
case 'special':
return <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
}
}
return (
<div className="space-y-4">
{/* Selected Model Display */}
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
className="w-full input-field flex items-center justify-between text-left"
aria-label="KI-Modell auswählen"
aria-expanded={isOpen}
aria-haspopup="listbox"
>
{selectedModelInfo ? (
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-secondary-500 rounded-lg flex items-center justify-center flex-shrink-0">
{getCategoryIcon(selectedModelInfo.category)}
</div>
<div>
<div className="font-medium text-gray-900 dark:text-gray-100">
{selectedModelInfo.name}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 truncate max-w-[200px]">
{selectedModelInfo.description}
</div>
</div>
</div>
) : (
<span className="text-gray-500 dark:text-gray-400">
Wähle eine KI-Plattform aus...
</span>
)}
<ChevronDown className={`w-5 h-5 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown Menu */}
{isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-96 overflow-hidden">
{/* Search Input */}
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
ref={searchRef}
type="text"
placeholder="KI suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-2 text-sm input-field"
/>
</div>
</div>
{/* Categories */}
<div className="p-2 space-y-2 max-h-80 overflow-y-auto">
{Object.entries(CATEGORY_NAMES).map(([categoryKey, categoryName]) => {
const category = categoryKey as AICategory
const categoryModels = getModelsByCategory(category)
const filteredCategoryModels = categoryModels.filter(model =>
filteredModels.includes(model)
)
if (filteredCategoryModels.length === 0) return null
return (
<div key={category} className="space-y-1">
{/* Category Header */}
<button
type="button"
onClick={() => handleCategorySelect(category)}
className="w-full flex items-center justify-between p-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
>
<div className="flex items-center space-x-2">
{getCategoryIcon(category)}
<span>{categoryName}</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
({filteredCategoryModels.length})
</span>
</div>
<div className={`transform transition-transform ${selectedCategory === category ? 'rotate-180' : ''}`}>
<ChevronDown className="w-3 h-3" />
</div>
</button>
{/* Models in Category */}
{(selectedCategory === category || selectedCategory === null) && (
<div className="ml-6 space-y-1">
{filteredCategoryModels.map((model) => (
<button
key={model.id}
type="button"
onClick={() => handleModelSelect(model.id)}
className={`w-full text-left p-2 rounded-md transition-colors ${
selectedModel === model.id
? 'bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100'
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-sm">{model.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
{model.description}
</div>
</div>
{model.supportsStreaming && (
<div className="w-2 h-2 bg-green-400 rounded-full flex-shrink-0 ml-2" />
)}
</div>
</button>
))}
</div>
)}
</div>
)
})}
</div>
</div>
)}
</div>
{/* Selected Model Info */}
{selectedModelInfo && (
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-start space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-primary-500 to-secondary-500 rounded-lg flex items-center justify-center flex-shrink-0">
{getCategoryIcon(selectedModelInfo.category)}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-gray-100">
{selectedModelInfo.name}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{selectedModelInfo.description}
</p>
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
<span className="flex items-center">
<span className="capitalize">{CATEGORY_NAMES[selectedModelInfo.category]}</span>
</span>
{selectedModelInfo.maxTokens && (
<span>Max: {(selectedModelInfo.maxTokens / 1000).toFixed(0)}k Tokens</span>
)}
{selectedModelInfo.supportsStreaming && (
<span className="flex items-center">
<span className="w-2 h-2 bg-green-400 rounded-full mr-1" />
Streaming
</span>
)}
</div>
</div>
</div>
</div>
)}
</div>
)
}
</html>