/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; import { TextInput } from './TextInput.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { PROVIDER_OPTIONS } from './constants.js'; import { getStepPosition } from './utils.js'; import type { WizardState } from './types.js'; import { useRuntimeApi } from '../../contexts/RuntimeContext.js'; import type { HydratedModel } from '@vybestack/llxprt-code-core'; const buildModelItems = (models: string[]) => [ ...models.map((m, idx) => ({ label: idx === 0 ? `${m} (Recommended)` : m, value: m, key: m, })), { label: 'Enter custom model name...', value: '__custom__', key: '__custom__', }, { label: '← Back', value: '__back__', key: '__back__', }, ]; const NoModelsManualInput: React.FC<{ customModelInput: string; onCustomModelChange: (value: string) => void; handleCustomModelSubmit: () => void; }> = ({ customModelInput, onCustomModelChange, handleCustomModelSubmit }) => ( <> This provider doesn't have a predefined model list. Enter the model name manually: Model name: ← → Move cursor Enter Continue Esc Back ); const CustomModelInput: React.FC<{ customModelInput: string; onCustomModelChange: (value: string) => void; handleCustomModelSubmit: () => void; }> = ({ customModelInput, onCustomModelChange, handleCustomModelSubmit }) => ( <> Model name: For Ollama: Run 'ollama list' to see available models For custom providers: Check provider documentation ← → Move cursor Enter Continue Esc Back to list ); const useProviderModels = (provider: string | null) => { const runtime = useRuntimeApi(); const [models, setModels] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const loadModels = async () => { if (!provider) { setIsLoading(false); return; } setIsLoading(true); try { const hydratedModels: HydratedModel[] = await runtime.listAvailableModels(provider); const modelIds = hydratedModels .filter((m) => m.metadata?.status !== 'deprecated') .map((m) => m.id); setModels(modelIds); } catch { setModels([]); } setIsLoading(false); }; void loadModels(); }, [runtime, provider]); return { models, isLoading }; }; const LoadingView: React.FC<{ current: number; total: number }> = ({ current, total, }) => ( Create New Profile - Step {current} of {total} Loading models... ); const ModelSelectListView: React.FC<{ modelItems: Array<{ label: string; value: string; key: string }>; initialIndex: number | undefined; handleModelSelect: (value: string) => void; }> = ({ modelItems, initialIndex, handleModelSelect }) => ( <> Esc Back ); const useModelSelectEscape = ( focusedComponent: 'select' | 'input', setFocusedComponent: (v: 'select' | 'input') => void, onBack: () => void, ) => { useKeypress( (key) => { if (key.name === 'escape') { if (focusedComponent === 'input') { setFocusedComponent('select'); } else { onBack(); } } }, { isActive: true }, ); }; const applyModelSelection = ( value: string, setFocusedComponent: (v: 'select' | 'input') => void, onUpdateModel: (model: string) => void, onContinue: () => void, onBack: () => void, ) => { if (value === '__custom__') { setFocusedComponent('input'); } else if (value === '__back__') { onBack(); } else { onUpdateModel(value); onContinue(); } }; const ModelSelectHeaderView: React.FC<{ focusedComponent: 'select' | 'input'; providerOption: { label?: string } | undefined; provider: string | null; }> = ({ focusedComponent, providerOption, provider }) => ( <> {focusedComponent === 'input' ? 'Enter Model Name:' : 'Select Model:'} {focusedComponent === 'input' ? "Enter the model name exactly as it appears in your provider's documentation" : `Choose the AI model for ${providerOption?.label ?? provider}`} ); interface ModelSelectStepProps { state: WizardState; onUpdateModel: (model: string) => void; onContinue: () => void; onBack: () => void; onCancel: () => void; } export const ModelSelectStep: React.FC = ({ state, onUpdateModel, onContinue, onBack, onCancel: _onCancel, }) => { const [focusedComponent, setFocusedComponent] = useState<'select' | 'input'>( 'select', ); const [customModelInput, setCustomModelInput] = useState(''); useModelSelectEscape(focusedComponent, setFocusedComponent, onBack); const handleModelSelect = useCallback( (value: string) => { applyModelSelection( value, setFocusedComponent, onUpdateModel, onContinue, onBack, ); }, [onUpdateModel, onContinue, onBack], ); const handleCustomModelSubmit = useCallback(() => { if (customModelInput.trim()) { onUpdateModel(customModelInput.trim()); onContinue(); } }, [customModelInput, onUpdateModel, onContinue]); const { models, isLoading } = useProviderModels(state.config.provider); const hasKnownModels = models.length > 0; const providerOption = PROVIDER_OPTIONS.find( (p) => p.value === state.config.provider, ); const modelItems = hasKnownModels ? buildModelItems(models) : []; const selectedModel = state.config.model ?? undefined; const initialIndex = selectedModel && hasKnownModels ? models.findIndex((m) => m === selectedModel) : undefined; const { current, total } = getStepPosition(state); if (isLoading) { return ; } return ( ); }; const ModelSelectContent: React.FC<{ focusedComponent: 'select' | 'input'; hasKnownModels: boolean; modelItems: Array<{ label: string; value: string; key: string }>; initialIndex: number | undefined; handleModelSelect: (value: string) => void; customModelInput: string; onCustomModelChange: (value: string) => void; handleCustomModelSubmit: () => void; current: number; total: number; providerOption: { label?: string } | undefined; provider: string | null; }> = ({ focusedComponent, hasKnownModels, modelItems, initialIndex, handleModelSelect, customModelInput, onCustomModelChange, handleCustomModelSubmit, current, total, providerOption, provider, }) => ( Create New Profile - Step {current} of {total} {focusedComponent === 'select' && hasKnownModels && ( )} {focusedComponent === 'select' && !hasKnownModels && ( )} {focusedComponent === 'input' && ( )} );