mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-10 07:35:56 +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.
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
const NOTIFICATION_SOUND_ENABLED_STORAGE_KEY = 'notificationSoundEnabled';
|
|
const AudioContextConstructor =
|
|
typeof window !== 'undefined'
|
|
? window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
|
|
: undefined;
|
|
|
|
let audioContext: AudioContext | null = null;
|
|
|
|
export const isNotificationSoundEnabled = (): boolean => {
|
|
if (typeof localStorage === 'undefined') {
|
|
return true;
|
|
}
|
|
|
|
return localStorage.getItem(NOTIFICATION_SOUND_ENABLED_STORAGE_KEY) !== 'false';
|
|
};
|
|
|
|
export const setNotificationSoundEnabled = (enabled: boolean): void => {
|
|
if (typeof localStorage === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem(NOTIFICATION_SOUND_ENABLED_STORAGE_KEY, String(enabled));
|
|
};
|
|
|
|
const getAudioContext = (): AudioContext | null => {
|
|
if (!AudioContextConstructor) {
|
|
return null;
|
|
}
|
|
|
|
if (!audioContext) {
|
|
audioContext = new AudioContextConstructor();
|
|
}
|
|
|
|
return audioContext;
|
|
};
|
|
|
|
const playTone = (
|
|
context: AudioContext,
|
|
frequency: number,
|
|
startsAt: number,
|
|
duration: number,
|
|
peakVolume: number,
|
|
): void => {
|
|
const oscillator = context.createOscillator();
|
|
const gain = context.createGain();
|
|
|
|
oscillator.type = 'sine';
|
|
oscillator.frequency.setValueAtTime(frequency, startsAt);
|
|
|
|
// Shape the volume so the synthesized tone starts and stops cleanly.
|
|
gain.gain.setValueAtTime(0.0001, startsAt);
|
|
gain.gain.exponentialRampToValueAtTime(peakVolume, startsAt + 0.015);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, startsAt + duration);
|
|
|
|
oscillator.connect(gain);
|
|
gain.connect(context.destination);
|
|
oscillator.start(startsAt);
|
|
oscillator.stop(startsAt + duration + 0.02);
|
|
};
|
|
|
|
export const playChatCompletionSound = async ({ force = false } = {}): Promise<void> => {
|
|
if (!force && !isNotificationSoundEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const context = getAudioContext();
|
|
if (!context) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (context.state === 'suspended') {
|
|
await context.resume();
|
|
}
|
|
|
|
const now = context.currentTime;
|
|
playTone(context, 740, now, 0.12, 0.075);
|
|
playTone(context, 988, now + 0.11, 0.16, 0.06);
|
|
} catch (error) {
|
|
// Browsers may block audio until the page receives a user gesture.
|
|
console.warn('Unable to play notification sound:', error);
|
|
}
|
|
};
|