- Add ui/src/api/push.ts with getVapidPublicKey, subscribe, unsubscribe methods - Add ui/src/hooks/usePushNotifications.ts with SW pushManager subscription flow - urlBase64ToUint8Array utility converts VAPID key for applicationServerKey - NotificationPermissionPrompt shows after 3rd agent response (engagement gate) - Checks nexus.notifPromptDismissed localStorage key for dismiss state - ChatPanel tracks agentResponseCount from assistant messages and renders prompt - Install idb package (missing dependency from plan 26-00 prerequisites)
64 lines
2 KiB
TypeScript
64 lines
2 KiB
TypeScript
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { usePushNotifications } from "../hooks/usePushNotifications";
|
|
|
|
const DISMISS_KEY = "nexus.notifPromptDismissed";
|
|
|
|
interface NotificationPermissionPromptProps {
|
|
agentResponseCount: number;
|
|
}
|
|
|
|
/**
|
|
* Notification permission prompt banner.
|
|
*
|
|
* Shows when:
|
|
* - Push notifications are supported in this browser
|
|
* - Permission is still "default" (not granted or denied)
|
|
* - User has not previously dismissed this prompt
|
|
* - User has received at least 3 agent responses (engagement gate)
|
|
*/
|
|
export function NotificationPermissionPrompt({ agentResponseCount }: NotificationPermissionPromptProps) {
|
|
const { isSupported, permission, subscribe } = usePushNotifications();
|
|
const [dismissed, setDismissed] = useState(() => {
|
|
try {
|
|
return localStorage.getItem(DISMISS_KEY) === "true";
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (!isSupported) return null;
|
|
if (permission !== "default") return null;
|
|
if (dismissed) return null;
|
|
if (agentResponseCount < 3) return null;
|
|
|
|
const handleAllow = async () => {
|
|
await subscribe();
|
|
};
|
|
|
|
const handleDismiss = () => {
|
|
try {
|
|
localStorage.setItem(DISMISS_KEY, "true");
|
|
} catch {
|
|
// localStorage unavailable — dismiss for session only
|
|
}
|
|
setDismissed(true);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed bottom-20 left-4 right-4 md:bottom-auto md:top-16 md:left-auto md:right-4 md:max-w-sm z-50 bg-card border border-border rounded-lg shadow-lg p-4">
|
|
<p className="text-sm font-semibold">Stay in the loop</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Get notified when your agents complete tasks or need input.
|
|
</p>
|
|
<div className="flex gap-2 mt-3">
|
|
<Button size="sm" onClick={handleAllow}>
|
|
Allow notifications
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={handleDismiss}>
|
|
Not now
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|