From a9a3e7bfd823770f4e6b385be3a3cc5d1642eeac Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 17 Oct 2025 16:57:51 -0700 Subject: [PATCH] display download size + refactor (#221) --- src-tauri/src/managers/model.rs | 6 +- src-tauri/tauri.conf.json | 4 +- src/App.tsx | 2 +- .../model-selector/ModelDropdown.tsx | 6 +- src/components/onboarding/ModelCard.tsx | 80 +++++++++++++++++++ src/components/onboarding/Onboarding.tsx | 64 +++++---------- src/lib/utils/format.ts | 21 +++++ 7 files changed, 132 insertions(+), 51 deletions(-) create mode 100644 src/components/onboarding/ModelCard.tsx create mode 100644 src/lib/utils/format.ts diff --git a/src-tauri/src/managers/model.rs b/src-tauri/src/managers/model.rs index ba0a188..dc60dda 100644 --- a/src-tauri/src/managers/model.rs +++ b/src-tauri/src/managers/model.rs @@ -70,7 +70,7 @@ impl ModelManager { description: "Fast and fairly accurate.".to_string(), filename: "ggml-small.bin".to_string(), url: Some("https://blob.handy.computer/ggml-small.bin".to_string()), - size_mb: 244, + size_mb: 487, is_downloaded: false, is_downloading: false, partial_size: 0, @@ -88,7 +88,7 @@ impl ModelManager { description: "Good accuracy, medium speed".to_string(), filename: "whisper-medium-q4_1.bin".to_string(), url: Some("https://blob.handy.computer/whisper-medium-q4_1.bin".to_string()), - size_mb: 491, // Approximate size + size_mb: 492, // Approximate size is_downloaded: false, is_downloading: false, partial_size: 0, @@ -122,7 +122,7 @@ impl ModelManager { description: "Good accuracy, but slow.".to_string(), filename: "ggml-large-v3-q5_0.bin".to_string(), url: Some("https://blob.handy.computer/ggml-large-v3-q5_0.bin".to_string()), - size_mb: 1080, // Approximate size + size_mb: 1100, // Approximate size is_downloaded: false, is_downloading: false, partial_size: 0, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 30a45f8..6ea31d8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,9 +16,9 @@ "label": "main", "title": "Handy", "width": 680, - "height": 510, + "height": 520, "minWidth": 680, - "minHeight": 510, + "minHeight": 520, "resizable": true, "maximizable": false, "visible": false diff --git a/src/App.tsx b/src/App.tsx index 546440a..1a6754f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -69,7 +69,7 @@ function App() { if (showOnboarding) { return (
-
+
diff --git a/src/components/model-selector/ModelDropdown.tsx b/src/components/model-selector/ModelDropdown.tsx index e2752d4..d825fd6 100644 --- a/src/components/model-selector/ModelDropdown.tsx +++ b/src/components/model-selector/ModelDropdown.tsx @@ -1,6 +1,7 @@ import React from "react"; import { ModelInfo } from "../../lib/types"; -import { ProgressBar, ProgressData } from "../shared"; +import { formatModelSize } from "../../lib/utils/format"; +import { ProgressBar } from "../shared"; interface DownloadProgress { model_id: string; @@ -178,6 +179,9 @@ const ModelDropdown: React.FC = ({
{model.description}
+
+ Download size ยท {formatModelSize(model.size_mb)} +
{isDownloading && progress ? ( diff --git a/src/components/onboarding/ModelCard.tsx b/src/components/onboarding/ModelCard.tsx new file mode 100644 index 0000000..4dafd9a --- /dev/null +++ b/src/components/onboarding/ModelCard.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { Download } from "lucide-react"; +import { ModelInfo } from "../../lib/types"; +import { formatModelSize } from "../../lib/utils/format"; + +interface ModelCardProps { + model: ModelInfo; + variant?: "default" | "featured"; + disabled?: boolean; + badgeText?: string; + className?: string; + onSelect: (modelId: string) => void; +} + +const ModelCard: React.FC = ({ + model, + variant = "default", + disabled = false, + badgeText, + className = "", + onSelect, +}) => { + const isFeatured = variant === "featured"; + + const baseButtonClasses = + "relative rounded-xl p-3 p-4 text-left transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] cursor-pointer group"; + + const variantClasses = isFeatured + ? "border-2 border-logo-primary/40 bg-logo-primary/5 hover:border-logo-primary/60 hover:bg-logo-primary/10 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-logo-primary/40 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100" + : "border-2 border-mid-gray/20 hover:border-logo-primary/50 hover:bg-logo-primary/5 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-mid-gray/20 disabled:hover:bg-transparent disabled:hover:shadow-none disabled:hover:scale-100"; + + const titleClasses = + "text-lg font-semibold text-text group-hover:text-logo-primary transition-colors"; + + const descriptionClasses = "text-text/60 text-sm leading-relaxed"; + + const sizeRowClasses = + "mt-1 flex items-center gap-2 text-xs text-text/60 tabular-nums"; + + const containerSpacing = "space-y-3"; + + const sizeIconClasses = "h-3.5 w-3.5 text-text/45"; + + return ( + + ); +}; + +export default ModelCard; diff --git a/src/components/onboarding/Onboarding.tsx b/src/components/onboarding/Onboarding.tsx index c2f7988..92b3b32 100644 --- a/src/components/onboarding/Onboarding.tsx +++ b/src/components/onboarding/Onboarding.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { invoke } from "@tauri-apps/api/core"; import { ModelInfo } from "../../lib/types"; +import ModelCard from "./ModelCard"; interface OnboardingProps { onModelSelected: () => void; @@ -58,57 +59,32 @@ const Onboarding: React.FC = ({ onModelSelected }) => {
)} -
- {/* Recommended model - full width */} - {availableModels - .filter((model) => getRecommendedBadge(model.id)) - .map((model) => ( - - ))} - - {/* Other models - 2 column grid */} -
{availableModels .filter((model) => !getRecommendedBadge(model.id)) .sort((a, b) => a.size_mb - b.size_mb) .map((model) => ( - + onSelect={handleDownloadModel} + /> ))}
diff --git a/src/lib/utils/format.ts b/src/lib/utils/format.ts new file mode 100644 index 0000000..2a32bf8 --- /dev/null +++ b/src/lib/utils/format.ts @@ -0,0 +1,21 @@ +export const formatModelSize = (sizeMb: number | null | undefined): string => { + if (!sizeMb || !Number.isFinite(sizeMb) || sizeMb <= 0) { + return "Unknown size"; + } + + if (sizeMb >= 1024) { + const sizeGb = sizeMb / 1024; + const formatter = new Intl.NumberFormat(undefined, { + minimumFractionDigits: sizeGb >= 10 ? 0 : 1, + maximumFractionDigits: sizeGb >= 10 ? 0 : 1, + }); + return `${formatter.format(sizeGb)} GB`; + } + + const formatter = new Intl.NumberFormat(undefined, { + minimumFractionDigits: sizeMb >= 100 ? 0 : 1, + maximumFractionDigits: sizeMb >= 100 ? 0 : 1, + }); + + return `${formatter.format(sizeMb)} MB`; +};