Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 100 additions & 94 deletions src/components/TranscriptionModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,100 @@ interface LocalModelCardProps {
// Backwards compatibility alias
type WhisperModel = LocalModel;

function LocalModelCard({
modelId,
name,
description,
size,
actualSizeMb,
isSelected,
isDownloaded,
isDownloading,
isCancelling,
recommended,
provider,
languageLabel,
onSelect,
onDelete,
onDownload,
onCancel,
styles: cardStyles,
}: LocalModelCardProps) {
return (
<div
className={`p-3 rounded-lg border-2 transition-all ${
isSelected ? cardStyles.modelCard.selected : cardStyles.modelCard.default
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<ProviderIcon provider={provider} className="w-4 h-4" />
<span className="font-medium text-gray-900">{name}</span>
{isSelected && <span className={cardStyles.badges.selected}>✓ Selected</span>}
{recommended && <span className={cardStyles.badges.recommended}>Recommended</span>}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-600">{description}</span>
<span className="text-xs text-gray-500">
• {actualSizeMb ? `${actualSizeMb}MB` : size}
</span>
{languageLabel && <span className="text-xs text-blue-600">{languageLabel}</span>}
{isDownloaded && (
<span className={cardStyles.badges.downloaded}>
<Check className="inline w-3 h-3 mr-1" />
Downloaded
</span>
)}
</div>
</div>

<div className="flex gap-2">
{isDownloaded ? (
<>
{!isSelected && (
<Button
onClick={onSelect}
size="sm"
variant="outline"
className={cardStyles.buttons.select}
>
Select
</Button>
)}
<Button
onClick={onDelete}
size="sm"
variant="outline"
className={cardStyles.buttons.delete}
>
<Trash2 size={14} />
<span className="ml-1">Delete</span>
</Button>
</>
) : isDownloading ? (
<Button
onClick={onCancel}
disabled={isCancelling}
size="sm"
variant="outline"
className="text-red-600 border-red-300 hover:bg-red-50"
>
<X size={14} />
<span className="ml-1">{isCancelling ? "..." : "Cancel"}</span>
</Button>
) : (
<Button onClick={onDownload} size="sm" className={cardStyles.buttons.download}>
<Download size={14} />
<span className="ml-1">Download</span>
</Button>
)}
</div>
</div>
</div>
);
}

interface TranscriptionModelPickerProps {
selectedCloudProvider: string;
onCloudProviderSelect: (providerId: string) => void;
Expand Down Expand Up @@ -258,6 +352,7 @@ export default function TranscriptionModelPicker({
downloadModel,
deleteModel,
isDownloadingModel,
isInstalling,
cancelDownload,
isCancelling,
} = useModelDownload({
Expand All @@ -271,6 +366,7 @@ export default function TranscriptionModelPicker({
downloadModel: downloadParakeetModel,
deleteModel: deleteParakeetModel,
isDownloadingModel: isDownloadingParakeetModel,
isInstalling: isInstallingParakeet,
cancelDownload: cancelParakeetDownload,
isCancelling: isCancellingParakeet,
} = useModelDownload({
Expand Down Expand Up @@ -418,6 +514,7 @@ export default function TranscriptionModelPicker({
<DownloadProgressBar
modelName={modelInfo?.name || downloadingModel}
progress={downloadProgress}
isInstalling={isInstalling}
styles={styles}
/>
);
Expand All @@ -429,6 +526,7 @@ export default function TranscriptionModelPicker({
<DownloadProgressBar
modelName={modelInfo?.name || downloadingParakeetModel}
progress={parakeetDownloadProgress}
isInstalling={isInstallingParakeet}
styles={styles}
/>
);
Expand All @@ -438,107 +536,15 @@ export default function TranscriptionModelPicker({
}, [
downloadingModel,
downloadProgress,
isInstalling,
downloadingParakeetModel,
parakeetDownloadProgress,
isInstallingParakeet,
useLocalWhisper,
internalLocalProvider,
styles,
]);

// Shared component for rendering local model cards (Whisper and Parakeet)
const LocalModelCard = ({
modelId,
name,
description,
size,
actualSizeMb,
isSelected,
isDownloaded,
isDownloading,
isCancelling,
recommended,
provider,
languageLabel,
onSelect,
onDelete,
onDownload,
onCancel,
styles: cardStyles,
}: LocalModelCardProps) => (
<div
key={modelId}
className={`p-3 rounded-lg border-2 transition-all ${
isSelected ? cardStyles.modelCard.selected : cardStyles.modelCard.default
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<ProviderIcon provider={provider} className="w-4 h-4" />
<span className="font-medium text-gray-900">{name}</span>
{isSelected && <span className={cardStyles.badges.selected}>✓ Selected</span>}
{recommended && <span className={cardStyles.badges.recommended}>Recommended</span>}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-600">{description}</span>
<span className="text-xs text-gray-500">
• {actualSizeMb ? `${actualSizeMb}MB` : size}
</span>
{languageLabel && <span className="text-xs text-blue-600">{languageLabel}</span>}
{isDownloaded && (
<span className={cardStyles.badges.downloaded}>
<Check className="inline w-3 h-3 mr-1" />
Downloaded
</span>
)}
</div>
</div>

<div className="flex gap-2">
{isDownloaded ? (
<>
{!isSelected && (
<Button
onClick={onSelect}
size="sm"
variant="outline"
className={cardStyles.buttons.select}
>
Select
</Button>
)}
<Button
onClick={onDelete}
size="sm"
variant="outline"
className={cardStyles.buttons.delete}
>
<Trash2 size={14} />
<span className="ml-1">Delete</span>
</Button>
</>
) : isDownloading ? (
<Button
onClick={onCancel}
disabled={isCancelling}
size="sm"
variant="outline"
className="text-red-600 border-red-300 hover:bg-red-50"
>
<X size={14} />
<span className="ml-1">{isCancelling ? "..." : "Cancel"}</span>
</Button>
) : (
<Button onClick={onDownload} size="sm" className={cardStyles.buttons.download}>
<Download size={14} />
<span className="ml-1">Download</span>
</Button>
)}
</div>
</div>
</div>
);

const renderLocalModels = () => (
<div className="space-y-2">
{localModels.map((model) => {
Expand Down
30 changes: 19 additions & 11 deletions src/components/ui/DownloadProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,38 @@ interface DownloadProgressBarProps {
modelName: string;
progress: DownloadProgress;
styles: ModelPickerStyles;
isInstalling?: boolean;
}

export function DownloadProgressBar({ modelName, progress, styles }: DownloadProgressBarProps) {
export function DownloadProgressBar({
modelName,
progress,
styles,
isInstalling,
}: DownloadProgressBarProps) {
const { percentage, speed, eta } = progress;
const progressText = `${Math.round(percentage)}%`;
const speedText = speed ? ` • ${speed.toFixed(1)} MB/s` : "";
const etaText = eta ? ` • ETA: ${formatETA(eta)}` : "";

const label = isInstalling ? `Installing ${modelName}...` : `Downloading ${modelName}...`;

return (
<div className={`${styles.progress} p-3`}>
<div className="flex items-center justify-between mb-2">
<span className={`text-sm font-medium ${styles.progressText}`}>
Downloading {modelName}...
</span>
<span className={`text-xs ${styles.progressText}`}>
{progressText}
{speedText}
{etaText}
</span>
<span className={`text-sm font-medium ${styles.progressText}`}>{label}</span>
{!isInstalling && (
<span className={`text-xs ${styles.progressText}`}>
{progressText}
{speedText}
{etaText}
</span>
)}
</div>
<div className={`w-full ${styles.progressBar} rounded-full h-2`}>
<div
className={`${styles.progressFill} h-2 rounded-full transition-all duration-300 ease-out`}
style={{ width: `${Math.min(percentage, 100)}%` }}
className={`${styles.progressFill} h-2 rounded-full transition-all duration-300 ease-out ${isInstalling ? "animate-pulse" : ""}`}
style={{ width: `${isInstalling ? 100 : Math.min(percentage, 100)}%` }}
/>
</div>
</div>
Expand Down
Loading