mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-01 10:02:57 +08:00
Users can miss chat completions while the app is in the background. They can also miss completions when their attention is elsewhere. Add opt-out sound notifications and a temporary title marker. This makes completion noticeable without external audio assets or persistent browser notifications.
197 lines
7.2 KiB
TypeScript
197 lines
7.2 KiB
TypeScript
import { Bell, BellOff, BellRing, Loader2, Play, Volume2 } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { Button } from '../../../../shared/view/ui';
|
|
import { playChatCompletionSound } from '../../../../utils/notificationSound';
|
|
import type { NotificationPreferencesState } from '../../types/types';
|
|
|
|
type NotificationsSettingsTabProps = {
|
|
notificationPreferences: NotificationPreferencesState;
|
|
onNotificationPreferencesChange: (value: NotificationPreferencesState) => void;
|
|
pushPermission: NotificationPermission | 'unsupported';
|
|
isPushSubscribed: boolean;
|
|
isPushLoading: boolean;
|
|
onEnablePush: () => void;
|
|
onDisablePush: () => void;
|
|
};
|
|
|
|
export default function NotificationsSettingsTab({
|
|
notificationPreferences,
|
|
onNotificationPreferencesChange,
|
|
pushPermission,
|
|
isPushSubscribed,
|
|
isPushLoading,
|
|
onEnablePush,
|
|
onDisablePush,
|
|
}: NotificationsSettingsTabProps) {
|
|
const { t } = useTranslation('settings');
|
|
|
|
const pushSupported = pushPermission !== 'unsupported';
|
|
const pushDenied = pushPermission === 'denied';
|
|
|
|
return (
|
|
<div className="space-y-6 md:space-y-8">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<Bell className="w-5 h-5 text-blue-600" />
|
|
<h3 className="text-lg font-medium text-foreground">{t('notifications.title')}</h3>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{t('notifications.description')}</p>
|
|
</div>
|
|
|
|
<div className="space-y-4 bg-card border border-border rounded-lg p-4">
|
|
<h4 className="font-medium text-foreground">{t('notifications.webPush.title')}</h4>
|
|
{!pushSupported ? (
|
|
<p className="text-sm text-muted-foreground">{t('notifications.webPush.unsupported')}</p>
|
|
) : pushDenied ? (
|
|
<p className="text-sm text-muted-foreground">{t('notifications.webPush.denied')}</p>
|
|
) : (
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
disabled={isPushLoading}
|
|
onClick={() => {
|
|
if (isPushSubscribed) {
|
|
onDisablePush();
|
|
} else {
|
|
onEnablePush();
|
|
}
|
|
}}
|
|
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
isPushSubscribed
|
|
? 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
|
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600'
|
|
}`}
|
|
>
|
|
{isPushLoading ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : isPushSubscribed ? (
|
|
<BellOff className="w-4 h-4" />
|
|
) : (
|
|
<BellRing className="w-4 h-4" />
|
|
)}
|
|
{isPushLoading
|
|
? t('notifications.webPush.loading')
|
|
: isPushSubscribed
|
|
? t('notifications.webPush.disable')
|
|
: t('notifications.webPush.enable')}
|
|
</button>
|
|
{isPushSubscribed && (
|
|
<span className="text-sm text-green-600 dark:text-green-400">
|
|
{t('notifications.webPush.enabled')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<Volume2 className="h-4 w-4 text-blue-600" />
|
|
<h4 className="font-medium text-foreground">
|
|
{t('notifications.sound.title', { defaultValue: 'Sound' })}
|
|
</h4>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('notifications.sound.description', {
|
|
defaultValue: 'Play a short tone when a chat run finishes.',
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
<label className="flex shrink-0 items-center gap-2 text-sm text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={notificationPreferences.channels.sound}
|
|
onChange={(event) =>
|
|
onNotificationPreferencesChange({
|
|
...notificationPreferences,
|
|
channels: {
|
|
...notificationPreferences.channels,
|
|
sound: event.target.checked,
|
|
},
|
|
})
|
|
}
|
|
className="h-4 w-4"
|
|
/>
|
|
{t('notifications.sound.enabled', { defaultValue: 'Enabled' })}
|
|
</label>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
void playChatCompletionSound({ force: true });
|
|
}}
|
|
>
|
|
<Play className="h-4 w-4" />
|
|
{t('notifications.sound.test', { defaultValue: 'Test sound' })}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-4 bg-card border border-border rounded-lg p-4">
|
|
<h4 className="font-medium text-foreground">{t('notifications.events.title')}</h4>
|
|
<div className="space-y-3">
|
|
<label className="flex items-center gap-2 text-sm text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={notificationPreferences.events.actionRequired}
|
|
onChange={(event) =>
|
|
onNotificationPreferencesChange({
|
|
...notificationPreferences,
|
|
events: {
|
|
...notificationPreferences.events,
|
|
actionRequired: event.target.checked,
|
|
},
|
|
})
|
|
}
|
|
className="w-4 h-4"
|
|
/>
|
|
{t('notifications.events.actionRequired')}
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={notificationPreferences.events.stop}
|
|
onChange={(event) =>
|
|
onNotificationPreferencesChange({
|
|
...notificationPreferences,
|
|
events: {
|
|
...notificationPreferences.events,
|
|
stop: event.target.checked,
|
|
},
|
|
})
|
|
}
|
|
className="w-4 h-4"
|
|
/>
|
|
{t('notifications.events.stop')}
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 text-sm text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={notificationPreferences.events.error}
|
|
onChange={(event) =>
|
|
onNotificationPreferencesChange({
|
|
...notificationPreferences,
|
|
events: {
|
|
...notificationPreferences.events,
|
|
error: event.target.checked,
|
|
},
|
|
})
|
|
}
|
|
className="w-4 h-4"
|
|
/>
|
|
{t('notifications.events.error')}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|