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: ( +
+
Downloading the large language model...
+ { + toast({ + title: "Large Language Model", + content: "Download complete!", + dismissible: true, + }); + }} + /> +
+ ), + dismissible: false, + }, + ); + } + }, + primary: true, + }, + ], + dismissible: false, + }); }, [checkForModelDownload.data]); return null; } + +interface ProgressPayload { + progress: number; +} + +const ModelDownloadProgress = ({ + channel, + onComplete, +}: { + channel: Channel; + 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