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 { 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: 'minimax-m2.1', name: 'MiniMax M2.1', description: 'Via Novita', modelPath: 'huggingface/novita/MiniMaxAI/MiniMax-M2.1', avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.1'), recommended: true, }, { id: 'claude-opus', name: 'Claude Opus 4.5', description: 'Anthropic', modelPath: 'anthropic/claude-opus-4-5-20251101', avatarUrl: 'https://huggingface.co/api/avatars/Anthropic', 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; disabled?: boolean; isProcessing?: boolean; } export default function ChatInput({ onSend, disabled = false }: ChatInputProps) { const [input, setInput] = useState(''); const inputRef = useRef(null); const [selectedModelId, setSelectedModelId] = useState(MODEL_OPTIONS[0].id); const [modelAnchorEl, setModelAnchorEl] = useState(null); // Sync with backend on mount 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); } }) .catch(() => { /* ignore */ }); }, []); const selectedModel = MODEL_OPTIONS.find(m => m.id === selectedModelId) || MODEL_OPTIONS[0]; // Auto-focus the textarea when the session becomes ready (disabled -> false) useEffect(() => { if (!disabled && inputRef.current) { inputRef.current.focus(); } }, [disabled]); 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); } } catch { /* ignore */ } }; return ( ) => setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Ask anything..." disabled={disabled} 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', } }} /> {disabled ? : } {/* 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)' } }} /> ))} ); }