diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index bfc7a2bb1..a11fb3126 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -13,6 +13,7 @@ import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import { commands as connectorCommands } from "@hypr/plugin-connector"; import { commands as dbCommands } from "@hypr/plugin-db"; import { commands as miscCommands } from "@hypr/plugin-misc"; +import { commands as notificationCommands } from "@hypr/plugin-notification"; import { commands as templateCommands, type Grammar } from "@hypr/plugin-template"; import Editor, { type TiptapEditor } from "@hypr/tiptap/editor"; import Renderer from "@hypr/tiptap/renderer"; @@ -458,6 +459,15 @@ export function useEnhanceMutation({ if (actualIsLocalLlm) { setProgress(0); + + // Show native notification for local AI model summarization completion + if (sessionId !== onboardingSessionId) { + notificationCommands.getSummarizationNotification().then(enabled => { + if (enabled) { + notificationCommands.showSummarizationCompleteNotification(); + } + }); + } } setEnhanceController(null); diff --git a/apps/desktop/src/components/settings/views/notifications.tsx b/apps/desktop/src/components/settings/views/notifications.tsx index 7e65ba6e9..403ba2721 100644 --- a/apps/desktop/src/components/settings/views/notifications.tsx +++ b/apps/desktop/src/components/settings/views/notifications.tsx @@ -12,6 +12,7 @@ import { Switch } from "@hypr/ui/components/ui/switch"; const schema = z.object({ detect: z.boolean().optional(), event: z.boolean().optional(), + summarization: z.boolean().optional(), }); type Schema = z.infer; @@ -27,11 +28,17 @@ export default function NotificationsComponent() { queryFn: () => notificationCommands.getDetectNotification(), }); + const summarizationNotification = useQuery({ + queryKey: ["notification", "summarization"], + queryFn: () => notificationCommands.getSummarizationNotification(), + }); + const form = useForm({ resolver: zodResolver(schema), values: { detect: detectNotification.data ?? false, event: eventNotification.data ?? false, + summarization: summarizationNotification.data ?? true, }, }); @@ -77,6 +84,16 @@ export default function NotificationsComponent() { }, }); + const summarizationMutation = useMutation({ + mutationFn: async (v: Schema) => { + notificationCommands.setSummarizationNotification(v.summarization ?? true); + return v.summarization; + }, + onSuccess: () => { + summarizationNotification.refetch(); + }, + }); + useEffect(() => { const subscription = form.watch((value, { name }) => { if (name === "detect") { @@ -85,10 +102,13 @@ export default function NotificationsComponent() { if (name === "event") { eventMutation.mutate(value); } + if (name === "summarization") { + summarizationMutation.mutate(value); + } }); return () => subscription.unsubscribe(); - }, [eventMutation, detectMutation]); + }, [eventMutation, detectMutation, summarizationMutation]); return (
@@ -138,6 +158,33 @@ export default function NotificationsComponent() {
+ + + + + + )} + /> + ( + +
+
+ + Summarization complete notifications + + + + Show notifications when local AI finishes summarizing your meetings. + + +
+ { }, async stopEventNotification() : Promise { return await TAURI_INVOKE("plugin:notification|stop_event_notification"); +}, +async showSummarizationCompleteNotification() : Promise { + return await TAURI_INVOKE("plugin:notification|show_summarization_complete_notification"); +}, +async getSummarizationNotification() : Promise { + return await TAURI_INVOKE("plugin:notification|get_summarization_notification"); +}, +async setSummarizationNotification(enabled: boolean) : Promise { + return await TAURI_INVOKE("plugin:notification|set_summarization_notification", { enabled }); } } diff --git a/plugins/notification/src/commands.rs b/plugins/notification/src/commands.rs index 23fd5a835..a871e4780 100644 --- a/plugins/notification/src/commands.rs +++ b/plugins/notification/src/commands.rs @@ -98,3 +98,31 @@ pub(crate) async fn stop_event_notification( ) -> Result<(), String> { app.stop_event_notification().map_err(|e| e.to_string()) } + +#[tauri::command] +#[specta::specta] +pub(crate) async fn show_summarization_complete_notification( + app: tauri::AppHandle, +) -> Result<(), String> { + app.show_summarization_complete_notification() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn get_summarization_notification( + app: tauri::AppHandle, +) -> Result { + app.get_summarization_notification() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn set_summarization_notification( + app: tauri::AppHandle, + enabled: bool, +) -> Result<(), String> { + app.set_summarization_notification(enabled) + .map_err(|e| e.to_string()) +} diff --git a/plugins/notification/src/ext.rs b/plugins/notification/src/ext.rs index 0d41de5f3..125a7ccca 100644 --- a/plugins/notification/src/ext.rs +++ b/plugins/notification/src/ext.rs @@ -13,6 +13,9 @@ pub trait NotificationPluginExt { fn get_detect_notification(&self) -> Result; fn set_detect_notification(&self, enabled: bool) -> Result<(), Error>; + fn get_summarization_notification(&self) -> Result; + fn set_summarization_notification(&self, enabled: bool) -> Result<(), Error>; + fn start_event_notification(&self) -> impl Future>; fn stop_event_notification(&self) -> Result<(), Error>; @@ -24,6 +27,8 @@ pub trait NotificationPluginExt { fn check_notification_permission( &self, ) -> impl Future>; + + fn show_summarization_complete_notification(&self) -> Result<(), Error>; } impl> NotificationPluginExt for T { @@ -65,6 +70,23 @@ impl> NotificationPluginExt for T { .map_err(Error::Store) } + #[tracing::instrument(skip(self))] + fn get_summarization_notification(&self) -> Result { + let store = self.notification_store(); + store + .get(crate::StoreKey::SummarizationNotification) + .map_err(Error::Store) + .map(|v| v.unwrap_or(true)) // Default to enabled + } + + #[tracing::instrument(skip(self))] + fn set_summarization_notification(&self, enabled: bool) -> Result<(), Error> { + let store = self.notification_store(); + store + .set(crate::StoreKey::SummarizationNotification, enabled) + .map_err(Error::Store) + } + #[tracing::instrument(skip(self))] async fn start_event_notification(&self) -> Result<(), Error> { let db_state = self.state::(); @@ -162,4 +184,17 @@ impl> NotificationPluginExt for T { .await .map_err(|_| Error::PermissionTimeout)? } + + #[tracing::instrument(skip(self))] + fn show_summarization_complete_notification(&self) -> Result<(), Error> { + let notif = hypr_notification2::Notification { + title: "Meeting Summary Complete".to_string(), + message: "Your meeting has been summarized by local AI".to_string(), + url: Some("hypr://hyprnote.com/notification".to_string()), + timeout: Some(std::time::Duration::from_secs(5)), + }; + + hypr_notification2::show(notif); + Ok(()) + } } diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 6ca9b3ae1..f89c149da 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -36,6 +36,9 @@ fn make_specta_builder() -> tauri_specta::Builder { commands::stop_detect_notification::, commands::start_event_notification::, commands::stop_event_notification::, + commands::show_summarization_complete_notification::, + commands::get_summarization_notification::, + commands::set_summarization_notification::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Throw) } diff --git a/plugins/notification/src/store.rs b/plugins/notification/src/store.rs index 479cf174f..e9ffa4220 100644 --- a/plugins/notification/src/store.rs +++ b/plugins/notification/src/store.rs @@ -4,6 +4,7 @@ use tauri_plugin_store2::ScopedStoreKey; pub enum StoreKey { EventNotification, DetectNotification, + SummarizationNotification, } impl ScopedStoreKey for StoreKey {}