import { useState, useCallback, useEffect, 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 KeyIcon from '@mui/icons-material/Key'; import { useSessionStore } from '@/store/sessionStore'; import { useAuthStore } from '@/store/authStore'; import { useAgentStore } from '@/store/agentStore'; import { AnthropicKeyModal } from '@/components/Auth/AnthropicKeyModal'; // Model configuration interface ModelOption { id: string; name: string; description: string; provider: 'huggingface' | 'anthropic'; modelPath: string; // Full path for API avatarUrl: string; // Logo URL recommended?: boolean; requiresApiKey?: boolean; } // Helper to get HF avatar URL from model ID const getHfAvatarUrl = (modelId: string) => { const org = modelId.split('/')[0]; return `https://huggingface.co/api/avatars/${org}`; }; // Curated model list const MODEL_OPTIONS: ModelOption[] = [ { id: 'minimax-m2.1', name: 'MiniMax M2.1', description: 'Via Novita', provider: 'huggingface', modelPath: 'huggingface/novita/MiniMaxAI/MiniMax-M2.1', avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.1'), recommended: true, }, { id: 'kimi-k2.5', name: 'Kimi K2.5', description: 'Via Novita', provider: 'huggingface', modelPath: 'huggingface/novita/moonshotai/Kimi-K2.5', avatarUrl: getHfAvatarUrl('moonshotai/Kimi-K2.5'), }, { id: 'glm-4.7', name: 'GLM 4.7', description: 'Via Novita', provider: 'huggingface', modelPath: 'huggingface/novita/zai-org/GLM-4.7', avatarUrl: getHfAvatarUrl('zai-org/GLM-4.7'), }, { id: 'deepseek-v3.2', name: 'DeepSeek V3.2', description: 'Via Novita', provider: 'huggingface', modelPath: 'huggingface/novita/deepseek-ai/DeepSeek-V3.2', avatarUrl: getHfAvatarUrl('deepseek-ai/DeepSeek-V3.2'), }, { id: 'qwen3-coder-480b', name: 'Qwen3 Coder 480B', description: 'Via Nebius', provider: 'huggingface', modelPath: 'huggingface/nebius/Qwen/Qwen3-Coder-480B-A35B-Instruct', avatarUrl: getHfAvatarUrl('Qwen/Qwen3-Coder-480B-A35B-Instruct'), }, { id: 'claude-opus', name: 'Claude Opus 4.5', description: 'Requires API Key', provider: 'anthropic', modelPath: 'anthropic/claude-opus-4-5-20251101', avatarUrl: '/claude-logo.png', recommended: true, requiresApiKey: true, }, ]; // Find model by path (for syncing with backend) 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; } export default function ChatInput({ onSend, disabled = false }: ChatInputProps) { const [input, setInput] = useState(''); const [modelAnchorEl, setModelAnchorEl] = useState(null); const [showApiKeyModal, setShowApiKeyModal] = useState(false); const [pendingModel, setPendingModel] = useState(null); const { switchModel, createSession, activeModelName } = useSessionStore(); const { user } = useAuthStore(); const { clearMessages, setPlan, setPanelContent } = useAgentStore(); // Track selected model by ID const [selectedModelId, setSelectedModelId] = useState('minimax-m2.1'); // Sync selectedModelId with the active session's model useEffect(() => { if (activeModelName) { const model = findModelByPath(activeModelName); if (model) { setSelectedModelId(model.id); } } }, [activeModelName]); const selectedModel = MODEL_OPTIONS.find(m => m.id === selectedModelId) || MODEL_OPTIONS[0]; const handleSend = useCallback(() => { if (input.trim() && !disabled) { // Check if current model requires API key and user doesn't have one if (selectedModel.requiresApiKey && !user?.has_anthropic_key) { setShowApiKeyModal(true); return; } onSend(input); setInput(''); } }, [input, disabled, onSend, selectedModel, user?.has_anthropic_key]); 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 handleSwitchModel = async (model: ModelOption) => { // Check if user has Anthropic API key when switching to Claude if (model.requiresApiKey && !user?.has_anthropic_key) { setPendingModel(model); setShowApiKeyModal(true); handleModelClose(); return; } await completeSwitchModel(model); handleModelClose(); }; const completeSwitchModel = async (model: ModelOption) => { const success = await switchModel(model.modelPath); if (success) { setSelectedModelId(model.id); // Create a new session when switching models const sessionId = await createSession(); if (sessionId) { clearMessages(); setPlan([]); setPanelContent(null); } } }; const handleApiKeyModalClose = async () => { setShowApiKeyModal(false); // Get current user state (not the captured closure value) const currentUser = useAuthStore.getState().user; if (currentUser?.has_anthropic_key) { if (pendingModel) { // User was trying to switch to a model that required a key await completeSwitchModel(pendingModel); } else if (selectedModel.requiresApiKey) { // User added key while already having a model selected that requires it // Create a new session so they can use it right away await completeSwitchModel(selectedModel); } } setPendingModel(null); }; return ( setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Ask anything..." disabled={disabled} variant="standard" slotProps={{ input: { disableUnderline: true, sx: { color: 'var(--text)', fontSize: '15px', fontFamily: 'inherit', padding: 0, lineHeight: 1.5, minHeight: '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} {/* API Key prompt - shown below model switcher when needed */} {selectedModel.requiresApiKey && !user?.has_anthropic_key && ( setShowApiKeyModal(true)} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', mt: 1, gap: 1, py: 0.5, px: 1.5, borderRadius: '6px', bgcolor: 'rgba(255, 193, 7, 0.1)', border: '1px solid rgba(255, 193, 7, 0.2)', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { bgcolor: 'rgba(255, 193, 7, 0.15)', borderColor: 'rgba(255, 193, 7, 0.4)', } }} > Add API key to use this model )} {MODEL_OPTIONS.map((model) => { const needsKey = model.requiresApiKey && !user?.has_anthropic_key; return ( handleSwitchModel(model)} selected={selectedModelId === model.id} sx={{ py: 1.5, '&.Mui-selected': { bgcolor: 'rgba(255,255,255,0.05)', } }} > {model.name} {model.name} {model.recommended && ( )} {needsKey && ( } label="API Key Required" size="small" sx={{ height: '18px', fontSize: '10px', bgcolor: 'rgba(255,255,255,0.1)', color: 'var(--muted-text)', fontWeight: 500, '& .MuiChip-icon': { color: 'var(--muted-text)', marginLeft: '4px', } }} /> )} } secondary={model.description} secondaryTypographyProps={{ sx: { fontSize: '12px', color: 'var(--muted-text)' } }} /> ); })} {/* API Key Modal */} ); }