diff --git a/apps/desktop/src/components/toast/model.tsx b/apps/desktop/src/components/toast/model.tsx
index 78a596b31a..942c170f93 100644
--- a/apps/desktop/src/components/toast/model.tsx
+++ b/apps/desktop/src/components/toast/model.tsx
@@ -1,10 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { Channel } from "@tauri-apps/api/core";
-import { useEffect } from "react";
-import { toast } from "sonner";
+import { useEffect, useState } from "react";
import { commands as localLlmCommands } from "@hypr/plugin-local-llm";
import { commands as localSttCommands } from "@hypr/plugin-local-stt";
+import { Progress } from "@hypr/ui/components/ui/progress";
+import { toast } from "@hypr/ui/components/ui/toast";
export default function ModelDownloadNotification() {
const checkForModelDownload = useQuery({
@@ -27,41 +28,103 @@ export default function ModelDownloadNotification() {
const sttChannel = new Channel();
const llmChannel = new Channel();
- toast.custom(
- (id) => (
-
-
Model Download Needed
+ toast({
+ title: "Model Download Needed",
+ content: "Local models are required for offline functionality.",
+ buttons: [
+ {
+ label: "Download Models",
+ onClick: () => {
+ if (!checkForModelDownload.data?.stt) {
+ localSttCommands.downloadModel(sttChannel);
- {!checkForModelDownload.data?.stt && (
-
-
-
- )}
+ toast(
+ {
+ title: "Speech-to-Text Model",
+ content: (
+
+
Downloading the speech-to-text model...
+
{
+ toast({
+ title: "Speech-to-Text Model",
+ content: "Download complete!",
+ dismissible: true,
+ });
+ }}
+ />
+
+ ),
+ dismissible: false,
+ },
+ );
+ }
- {!checkForModelDownload.data?.llm && (
-
-
-
- )}
+ if (!checkForModelDownload.data?.llm) {
+ localLlmCommands.downloadModel(llmChannel);
-
-
- ),
- {
- id: "model-download-notification",
- duration: Infinity,
- },
- );
+ toast(
+ {
+ title: "Large Language Model",
+ content: (
+ ;
+ onComplete?: () => void;
+}) => {
+ const [progress, setProgress] = useState(0);
+
+ useEffect(() => {
+ channel.onmessage = (response) => {
+ const data = response as unknown as ProgressPayload;
+ setProgress(data.progress);
+
+ if (data.progress >= 100 && onComplete) {
+ onComplete();
+ }
+ };
+ }, [channel, onComplete]);
+
+ return (
+
+
+
{Math.round(progress)}%
+
+ );
+};
diff --git a/apps/desktop/src/components/toast/ota.tsx b/apps/desktop/src/components/toast/ota.tsx
index 65968fd10c..0d939469ae 100644
--- a/apps/desktop/src/components/toast/ota.tsx
+++ b/apps/desktop/src/components/toast/ota.tsx
@@ -3,7 +3,8 @@ import { ask } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
import { check } from "@tauri-apps/plugin-updater";
import { useEffect } from "react";
-import { toast } from "sonner";
+
+import { toast } from "@hypr/ui/components/ui/toast";
export default function OtaNotification() {
const checkForUpdate = useQuery({
@@ -29,52 +30,36 @@ export default function OtaNotification() {
const update = checkForUpdate.data;
- toast.custom(
- (id) => (
-
-
Update Available
-
- Version {update.version} is available to install
-
-
-
-
-
-
- ),
- {
- id: "update-notification",
- duration: Infinity,
- },
- );
+ if (yes && process.env.NODE_ENV === "production") {
+ await update.downloadAndInstall();
+ await relaunch();
+ }
+ },
+ primary: true,
+ },
+ ],
+ dismissible: true,
+ });
}, [checkForUpdate.data?.available]);
return null;
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b2939642bf..bcdfbd5dc6 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -25,6 +25,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
+ "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
diff --git a/packages/ui/src/components/ui/progress.tsx b/packages/ui/src/components/ui/progress.tsx
new file mode 100644
index 0000000000..fbed8ef517
--- /dev/null
+++ b/packages/ui/src/components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+import * as React from "react";
+
+import { cn } from "@hypr/ui/lib/utils";
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+));
+Progress.displayName = ProgressPrimitive.Root.displayName;
+
+export { Progress };
diff --git a/packages/ui/src/components/ui/sonner.tsx b/packages/ui/src/components/ui/sonner.tsx
deleted file mode 100644
index 5add2ce9d3..0000000000
--- a/packages/ui/src/components/ui/sonner.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useTheme } from "next-themes";
-import { Toaster as Sonner } from "sonner";
-
-type ToasterProps = React.ComponentProps;
-
-const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme();
-
- return (
-
- );
-};
-
-export { Toaster };
diff --git a/packages/ui/src/components/ui/toast.tsx b/packages/ui/src/components/ui/toast.tsx
new file mode 100644
index 0000000000..b5c562dfd1
--- /dev/null
+++ b/packages/ui/src/components/ui/toast.tsx
@@ -0,0 +1,107 @@
+import { X } from "lucide-react";
+import { useTheme } from "next-themes";
+import React from "react";
+import { toast as sonnerToast, Toaster as Sonner } from "sonner";
+
+export interface ToastButtonProps {
+ label: string;
+ onClick: () => void;
+ primary?: boolean;
+}
+
+export interface CustomToastProps {
+ id: string | number;
+ title: string;
+ content?: React.ReactNode;
+ buttons?: ToastButtonProps[];
+ dismissible?: boolean;
+ children?: React.ReactNode;
+}
+
+export function CustomToast(props: CustomToastProps) {
+ const { id, title, content, buttons = [], dismissible, children } = props;
+
+ return (
+
+ {dismissible && (
+
+ )}
+
+
{title}
+
+ {content &&
{content}
}
+
+ {children}
+
+ {buttons.length > 0 && (
+
+ {buttons.map((button, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export function toast(toast: Omit) {
+ return sonnerToast.custom(
+ (id) => (
+
+
+
+ ),
+ {
+ duration: toast.dismissible === false ? Infinity : undefined,
+ },
+ );
+}
+
+type ToasterProps = React.ComponentProps;
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme();
+
+ return (
+
+ );
+};
+
+export { Toaster };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f019658ab0..f53a32b08b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1232,6 +1232,9 @@ importers:
'@radix-ui/react-popover':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-progress':
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-scroll-area':
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -2801,6 +2804,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-progress@1.1.2':
+ resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-radio-group@1.2.3':
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
peerDependencies:
@@ -9942,6 +9958,16 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
+ '@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.18
+ '@types/react-dom': 18.3.5(@types/react@18.3.18)
+
'@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.1