mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-10 15:55:53 +08:00
feat: signal when chat runs complete
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.
This commit is contained in:
83
src/utils/notificationSound.ts
Normal file
83
src/utils/notificationSound.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user