import React from "react"; import { useTranslation } from "react-i18next"; import type { ModelInfo } from "@/bindings"; import { formatModelSize } from "../../lib/utils/format"; import { getTranslatedModelName, getTranslatedModelDescription, } from "../../lib/utils/modelTranslation"; import { ProgressBar } from "../shared"; interface DownloadProgress { model_id: string; downloaded: number; total: number; percentage: number; } interface ModelDropdownProps { models: ModelInfo[]; currentModelId: string; downloadProgress: Record; onModelSelect: (modelId: string) => void; onModelDownload: (modelId: string) => void; onModelDelete: (modelId: string) => Promise; onError?: (error: string) => void; } const ModelDropdown: React.FC = ({ models, currentModelId, downloadProgress, onModelSelect, onModelDownload, onModelDelete, onError, }) => { const { t } = useTranslation(); const availableModels = models.filter((m) => m.is_downloaded); const downloadableModels = models.filter((m) => !m.is_downloaded); const isFirstRun = availableModels.length === 0 && models.length > 0; const handleDeleteClick = async (e: React.MouseEvent, modelId: string) => { e.preventDefault(); e.stopPropagation(); try { await onModelDelete(modelId); } catch (err) { const errorMsg = `Failed to delete model: ${err}`; onError?.(errorMsg); } }; const handleModelClick = (modelId: string) => { if (modelId in downloadProgress) { return; // Don't allow interaction while downloading } onModelSelect(modelId); }; const handleDownloadClick = (modelId: string) => { if (modelId in downloadProgress) { return; // Don't allow interaction while downloading } onModelDownload(modelId); }; return (
{/* First Run Welcome */} {isFirstRun && (
{t("modelSelector.welcome")}
{t("modelSelector.downloadPrompt")}
)} {/* Available Models */} {availableModels.length > 0 && (
{t("modelSelector.availableModels")}
{availableModels.map((model) => (
handleModelClick(model.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleModelClick(model.id); } }} tabIndex={0} role="button" className={`w-full px-3 py-2 text-left hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${ currentModelId === model.id ? "bg-logo-primary/10 text-logo-primary" : "" }`} >
{getTranslatedModelName(model, t)}
{getTranslatedModelDescription(model, t)}
{currentModelId === model.id && (
{t("modelSelector.active")}
)} {currentModelId !== model.id && ( )}
))}
)} {/* Downloadable Models */} {downloadableModels.length > 0 && (
{(availableModels.length > 0 || isFirstRun) && (
)}
{isFirstRun ? t("modelSelector.chooseModel") : t("modelSelector.downloadModels")}
{downloadableModels.map((model) => { const isDownloading = model.id in downloadProgress; const progress = downloadProgress[model.id]; return (
handleDownloadClick(model.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleDownloadClick(model.id); } }} tabIndex={0} role="button" aria-disabled={isDownloading} className={`w-full px-3 py-2 text-left hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${ isDownloading ? "opacity-50 cursor-not-allowed hover:bg-transparent" : "" }`} >
{getTranslatedModelName(model, t)} {model.id === "parakeet-tdt-0.6b-v3" && isFirstRun && ( {t("onboarding.recommended")} )}
{getTranslatedModelDescription(model, t)}
{t("modelSelector.downloadSize")} ยท{" "} {formatModelSize(Number(model.size_mb))}
{isDownloading && progress ? `${Math.max(0, Math.min(100, Math.round(progress.percentage)))}%` : t("modelSelector.download")}
{isDownloading && progress && (
)}
); })}
)} {/* No Models Available */} {availableModels.length === 0 && downloadableModels.length === 0 && (
{t("modelSelector.noModelsAvailable")}
)}
); }; export default ModelDropdown;