import { useState, useCallback, useEffect, useRef, KeyboardEvent } from 'react'; import { Box, TextField, IconButton, CircularProgress, Typography, Menu, MenuItem, ListItemIcon, ListItemText, Chip } from '@mui/material'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import CloseIcon from '@mui/icons-material/Close'; import { apiFetch } from '@/utils/api'; // Model configuration interface ModelOption { id: string; name: string; description: string; modelPath: string; avatarUrl: string; recommended?: boolean; } const getHfAvatarUrl = (modelId: string) => { const org = modelId.split('/')[0]; return `https://huggingface.co/api/avatars/${org}`; }; const MODEL_OPTIONS: ModelOption[] = [ { id: 'claude-opus', name: 'Claude Opus 4.6', description: 'Anthropic', modelPath: 'anthropic/claude-opus-4-6', avatarUrl: 'https://huggingface.co/api/avatars/Anthropic', recommended: true, }, { id: 'minimax-m2.5', name: 'MiniMax M2.5', description: 'Via Fireworks', modelPath: 'huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5', avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.5'), recommended: true, }, { id: 'kimi-k2.5', name: 'Kimi K2.5', description: 'Via Novita', modelPath: 'huggingface/novita/moonshotai/kimi-k2.5', avatarUrl: getHfAvatarUrl('moonshotai/Kimi-K2.5'), }, { id: 'glm-5', name: 'GLM 5', description: 'Via Novita', modelPath: 'huggingface/novita/zai-org/glm-5', avatarUrl: getHfAvatarUrl('zai-org/GLM-5'), }, ]; const findModelByPath = (path: string): ModelOption | undefined => { return MODEL_OPTIONS.find(m => m.modelPath === path || path?.includes(m.id)); }; interface ChatInputProps { onSend: (text: string) => void; onStop?: () => void; isProcessing?: boolean; disabled?: boolean; placeholder?: string; } export default function ChatInput({ onSend, onStop, isProcessing = false, disabled = false, placeholder = 'Ask anything...' }: ChatInputProps) { const [input, setInput] = useState(''); const [stopHovered, setStopHovered] = useState(false); const inputRef = useRef(null); const [selectedModelId, setSelectedModelId] = useState(() => { try { const stored = localStorage.getItem('hf-agent-model'); if (stored && MODEL_OPTIONS.some(m => m.id === stored)) return stored; } catch { /* localStorage unavailable */ } return MODEL_OPTIONS[0].id; }); const [modelAnchorEl, setModelAnchorEl] = useState(null); // Sync with backend on mount (backend is source of truth, localStorage is just a cache) useEffect(() => { fetch('/api/config/model') .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (data?.current) { const model = findModelByPath(data.current); if (model) { setSelectedModelId(model.id); try { localStorage.setItem('hf-agent-model', model.id); } catch { /* ignore */ } } } }) .catch(() => { /* ignore */ }); }, []); const selectedModel = MODEL_OPTIONS.find(m => m.id === selectedModelId) || MODEL_OPTIONS[0]; // Auto-focus the textarea when the session becomes ready useEffect(() => { if (!disabled && !isProcessing && inputRef.current) { inputRef.current.focus(); } }, [disabled, isProcessing]); const handleSend = useCallback(() => { if (input.trim() && !disabled) { onSend(input); setInput(''); } }, [input, disabled, onSend]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }, [handleSend] ); const handleModelClick = (event: React.MouseEvent) => { setModelAnchorEl(event.currentTarget); }; const handleModelClose = () => { setModelAnchorEl(null); }; const handleSelectModel = async (model: ModelOption) => { handleModelClose(); try { const res = await apiFetch('/api/config/model', { method: 'POST', body: JSON.stringify({ model: model.modelPath }), }); if (res.ok) { setSelectedModelId(model.id); try { localStorage.setItem('hf-agent-model', model.id); } catch { /* ignore */ } } } catch { /* ignore */ } }; return ( setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={disabled || isProcessing} variant="standard" inputRef={inputRef} InputProps={{ disableUnderline: true, sx: { color: 'var(--text)', fontSize: '15px', fontFamily: 'inherit', padding: 0, lineHeight: 1.5, minHeight: { xs: '44px', md: '56px' }, alignItems: 'flex-start', } }} sx={{ flex: 1, '& .MuiInputBase-root': { p: 0, backgroundColor: 'transparent', }, '& textarea': { resize: 'none', padding: '0 !important', } }} /> {isProcessing ? ( setStopHovered(true)} onMouseLeave={() => setStopHovered(false)} sx={{ mt: 1, p: 1, borderRadius: '10px', color: stopHovered ? 'var(--accent-yellow)' : 'var(--muted-text)', transition: 'all 0.2s', '&:hover': { bgcolor: 'var(--hover-bg)', }, }} > {stopHovered ? : } ) : ( )} {/* Powered By Badge */} powered by {selectedModel.name} {selectedModel.name} {/* Model Selection Menu */} {MODEL_OPTIONS.map((model) => ( handleSelectModel(model)} selected={selectedModelId === model.id} sx={{ py: 1.5, '&.Mui-selected': { bgcolor: 'rgba(255,255,255,0.05)', } }} > {model.name} {model.name} {model.recommended && ( )} } secondary={model.description} secondaryTypographyProps={{ sx: { fontSize: '12px', color: 'var(--muted-text)' } }} /> ))} ); }