1- import { Bell } from "lucide-react" ;
1+ import { useState } from "react" ;
2+ import { Bell , MessageSquare , Eye , EyeOff } from "lucide-react" ;
23import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "../../../components/ui/card" ;
34import { Label } from "../../../components/ui/label" ;
45import { Switch } from "../../../components/ui/switch" ;
6+ import { Button } from "../../../components/ui/button" ;
7+ import { RadioGroup , RadioGroupItem } from "../../../components/ui/radio-group" ;
58import type { NotificationPreferences } from "../../../lib/user-preferences" ;
69
710interface NotificationsSectionProps {
811 notifications : NotificationPreferences ;
912 onUpdate : ( updated : NotificationPreferences ) => void ;
13+ notificationChannel : "browser" | "discord" ;
14+ onChannelChange : ( channel : "browser" | "discord" ) => Promise < void > ;
15+ webhookUrl : string | null ;
16+ webhookSaveError : string | null ;
17+ webhookTestStatus : "idle" | "success" | "error" ;
18+ onWebhookChange : ( value : string ) => Promise < void > ;
19+ onWebhookTest : ( ) => Promise < void > ;
1020}
1121
12- export function NotificationsSection ( { notifications, onUpdate } : NotificationsSectionProps ) {
22+ export function NotificationsSection ( {
23+ notifications,
24+ onUpdate,
25+ notificationChannel,
26+ onChannelChange,
27+ webhookUrl,
28+ webhookSaveError,
29+ webhookTestStatus,
30+ onWebhookChange,
31+ onWebhookTest,
32+ } : NotificationsSectionProps ) {
33+ const [ showUrl , setShowUrl ] = useState ( false ) ;
34+
1335 function toggle < K extends keyof NotificationPreferences > ( key : K , value : boolean ) {
1436 onUpdate ( { ...notifications , [ key ] : value } ) ;
1537 }
@@ -23,40 +45,150 @@ export function NotificationsSection({ notifications, onUpdate }: NotificationsS
2345 </ div >
2446 < div >
2547 < CardTitle > Notifications</ CardTitle >
26- < CardDescription > Choose which agent events trigger browser notifications </ CardDescription >
48+ < CardDescription > Choose how you want to be notified about agent events </ CardDescription >
2749 </ div >
2850 </ div >
2951 </ CardHeader >
3052 < CardContent className = "space-y-6" >
31- < div className = "flex items-center justify-between gap-4" >
32- < div className = "space-y-1" >
33- < Label className = "text-base font-semibold" > Agent awaits your response</ Label >
34- < p className = "text-xs text-muted-foreground" >
35- Fires when an agent is waiting for your input. Debounced by 2s to avoid noise from
36- brief status flickers.
37- </ p >
38- </ div >
39- < Switch
40- checked = { notifications . awaitingInput }
41- onCheckedChange = { ( value ) => toggle ( "awaitingInput" , value ) }
42- />
53+ < div className = "space-y-3" >
54+ < Label className = "text-sm font-semibold" > Notification channel</ Label >
55+ < RadioGroup
56+ value = { notificationChannel }
57+ onValueChange = { ( v ) => void onChannelChange ( v as "browser" | "discord" ) }
58+ className = "grid grid-cols-2 gap-3"
59+ >
60+ < label
61+ htmlFor = "channel-browser"
62+ className = { `flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
63+ notificationChannel === "browser"
64+ ? "border-primary bg-primary/5"
65+ : "border-muted hover:border-muted-foreground/40"
66+ } `}
67+ >
68+ < RadioGroupItem value = "browser" id = "channel-browser" />
69+ < div className = "space-y-0.5" >
70+ < p className = "text-sm font-medium leading-none" > Browser</ p >
71+ < p className = "text-xs text-muted-foreground" > Desktop pop-ups</ p >
72+ </ div >
73+ </ label >
74+ < label
75+ htmlFor = "channel-discord"
76+ className = { `flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
77+ notificationChannel === "discord"
78+ ? "border-primary bg-primary/5"
79+ : "border-muted hover:border-muted-foreground/40"
80+ } `}
81+ >
82+ < RadioGroupItem value = "discord" id = "channel-discord" />
83+ < div className = "space-y-0.5" >
84+ < p className = "text-sm font-medium leading-none" > Discord</ p >
85+ < p className = "text-xs text-muted-foreground" > Webhook messages</ p >
86+ </ div >
87+ </ label >
88+ </ RadioGroup >
4389 </ div >
4490
4591 < div className = "h-px bg-gradient-to-r from-transparent via-border to-transparent" />
4692
47- < div className = "flex items-center justify-between gap-4" >
48- < div className = "space-y-1" >
49- < Label className = "text-base font-semibold" > Agent session finished</ Label >
50- < p className = "text-xs text-muted-foreground" >
51- Fires when an agent finishes or disconnects.
52- </ p >
93+ { notificationChannel === "browser" && (
94+ < div className = "space-y-6" >
95+ < div className = "flex items-center justify-between gap-4" >
96+ < div className = "space-y-1" >
97+ < Label className = "text-base font-semibold" > Agent awaits your response</ Label >
98+ < p className = "text-xs text-muted-foreground" >
99+ Fires when an agent is waiting for your input. Debounced by 2s to avoid noise from
100+ brief status flickers.
101+ </ p >
102+ </ div >
103+ < Switch
104+ checked = { notifications . awaitingInput }
105+ onCheckedChange = { ( value ) => toggle ( "awaitingInput" , value ) }
106+ />
107+ </ div >
108+
109+ < div className = "h-px bg-gradient-to-r from-transparent via-border to-transparent" />
110+
111+ < div className = "flex items-center justify-between gap-4" >
112+ < div className = "space-y-1" >
113+ < Label className = "text-base font-semibold" > Agent session finished</ Label >
114+ < p className = "text-xs text-muted-foreground" >
115+ Fires when an agent finishes or disconnects.
116+ </ p >
117+ </ div >
118+ < Switch
119+ checked = { notifications . agentFinished }
120+ onCheckedChange = { ( value ) => toggle ( "agentFinished" , value ) }
121+ />
122+ </ div >
53123 </ div >
54- < Switch
55- checked = { notifications . agentFinished }
56- onCheckedChange = { ( value ) => toggle ( "agentFinished" , value ) }
57- />
58- </ div >
124+ ) }
125+
126+ { notificationChannel === "discord" && (
127+ < div className = "space-y-4" >
128+ < div className = "flex items-center gap-2 text-sm font-medium text-muted-foreground" >
129+ < MessageSquare className = "h-4 w-4" />
130+ Discord webhook
131+ </ div >
132+ < div className = "space-y-2" >
133+ < Label htmlFor = "discord-webhook" > Webhook URL</ Label >
134+ < div className = "flex gap-2" >
135+ < div className = "relative flex-1" >
136+ < input
137+ id = "discord-webhook"
138+ type = { showUrl ? "text" : "password" }
139+ placeholder = "https://discord.com/api/webhooks/..."
140+ value = { webhookUrl ?? "" }
141+ onChange = { ( e ) => void onWebhookChange ( e . target . value ) }
142+ className = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm pr-9 focus:outline-none focus:ring-1 focus:ring-ring"
143+ />
144+ < button
145+ type = "button"
146+ onClick = { ( ) => setShowUrl ( ( v ) => ! v ) }
147+ className = "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
148+ tabIndex = { - 1 }
149+ >
150+ { showUrl ? < EyeOff className = "h-4 w-4" /> : < Eye className = "h-4 w-4" /> }
151+ </ button >
152+ </ div >
153+ < Button
154+ variant = "outline"
155+ size = "sm"
156+ disabled = { ! webhookUrl }
157+ onClick = { onWebhookTest }
158+ >
159+ Test
160+ </ Button >
161+ </ div >
162+ { webhookTestStatus === "success" && (
163+ < p className = "text-xs text-green-600 dark:text-green-400" > Test message sent! Check your Discord channel.</ p >
164+ ) }
165+ { webhookTestStatus === "error" && (
166+ < p className = "text-xs text-destructive" > Test failed. Double-check the webhook URL.</ p >
167+ ) }
168+ { webhookSaveError && (
169+ < p className = "text-xs text-destructive" > { webhookSaveError } </ p >
170+ ) }
171+ </ div >
59172
173+ < div className = "rounded-md border border-muted bg-muted/30 p-3 space-y-1" >
174+ < p className = "text-xs font-semibold text-muted-foreground" > How to get a webhook URL</ p >
175+ < ol className = "text-xs text-muted-foreground space-y-0.5 list-decimal list-inside" >
176+ < li > Open your Discord server → go to the channel you want</ li >
177+ < li > Channel Settings → Integrations → Webhooks → New Webhook</ li >
178+ < li > Copy the webhook URL and paste it above</ li >
179+ </ ol >
180+ </ div >
181+
182+ < div className = "text-xs text-muted-foreground space-y-1" >
183+ < p className = "font-semibold" > You'll be notified when:</ p >
184+ < ul className = "list-disc list-inside space-y-0.5" >
185+ < li > A session finishes — with task name, duration, and token count</ li >
186+ < li > An agent crashes or hits an error</ li >
187+ < li > An agent is waiting for your input</ li >
188+ </ ul >
189+ </ div >
190+ </ div >
191+ ) }
60192 </ CardContent >
61193 </ Card >
62194 ) ;
0 commit comments