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 StopIcon from '@mui/icons-material/Stop'; 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.7', name: 'MiniMax M2.7', description: 'Novita', modelPath: 'MiniMaxAI/MiniMax-M2.7', avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.7'), recommended: true, }, { id: 'kimi-k2.6', name: 'Kimi K2.6', description: 'Novita', modelPath: 'moonshotai/Kimi-K2.6', avatarUrl: getHfAvatarUrl('moonshotai/Kimi-K2.6'), }, { id: 'glm-5.1', name: 'GLM 5.1', description: 'Together', modelPath: 'zai-org/GLM-5.1', avatarUrl: getHfAvatarUrl('zai-org/GLM-5.1'), }, ]; const findModelByPath = (path: string): ModelOption | undefined => { return MODEL_OPTIONS.find(m => m.modelPath === path || path?.includes(m.id)); }; interface ChatInputProps { sessionId?: string; onSend: (text: string) => void; onStop?: () => void; isProcessing?: boolean; disabled?: boolean; placeholder?: string; } export default function ChatInput({ sessionId, onSend, onStop, isProcessing = false, disabled = false, placeholder = 'Ask anything...' }: ChatInputProps) { const [input, setInput] = useState(''); const inputRef = useRef(null); const [selectedModelId, setSelectedModelId] = useState(MODEL_OPTIONS[0].id); const [modelAnchorEl, setModelAnchorEl] = useState(null); // Model is per-session: fetch this tab's current model every time the // session changes. Other tabs keep their own selections independently. useEffect(() => { if (!sessionId) return; let cancelled = false; apiFetch(`/api/session/${sessionId}`) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (cancelled) return; if (data?.model) { const model = findModelByPath(data.model); if (model) setSelectedModelId(model.id); } }) .catch(() => { /* ignore */ }); return () => { cancelled = true; }; }, [sessionId]); 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(); if (!sessionId) return; try { const res = await apiFetch(`/api/session/${sessionId}/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={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 ? ( ) : ( )} {/* 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)' } }} /> ))} ); }