Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sound and browser notifications for agent state changes #6530

Merged
merged 74 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4e3b039
feat: add sound and browser notifications for agent state changes
openhands-agent Jan 29, 2025
2ed9329
feat: add sound notification toggle in settings
openhands-agent Jan 29, 2025
a47fd6d
style: fix frontend linting
openhands-agent Jan 29, 2025
e819298
refactor: move sound toggle to toolbar
openhands-agent Jan 29, 2025
594b6be
style: fix linting issues in sound-toggle-button
openhands-agent Jan 29, 2025
2da899d
chore: use MIT-licensed notification sound from freeCodeCamp
openhands-agent Jan 29, 2025
7369085
chore: add programmatically generated notification sound
openhands-agent Jan 29, 2025
32fba3b
fix: typescript errors in settings
openhands-agent Jan 29, 2025
7912da0
fix: replace @mui/icons-material with react-icons to fix build error
openhands-agent Jan 30, 2025
ac4dd32
fix: update node and npm versions in Dockerfile
openhands-agent Jan 30, 2025
73fdfb2
merge: resolve conflicts with main
openhands-agent Jan 30, 2025
657d79e
style: fix linting issues
openhands-agent Jan 30, 2025
ca6d52d
Merge branch 'main' into feature/notifications
xingyaoww Jan 30, 2025
d793877
fix: update build script to use vite directly for ES module compatibi…
openhands-agent Jan 30, 2025
74e6213
chore: remove unnecessary get-docker.sh file
openhands-agent Jan 30, 2025
ba3cf92
fix: add enable_sound_notifications to backend settings and improve f…
openhands-agent Jan 30, 2025
ad4c818
refactor: move sound toggle button to trajectory actions
openhands-agent Jan 30, 2025
95a102e
Merge main into feature/notifications: resolve settings.ts conflict
openhands-agent Jan 30, 2025
00937cf
Fix sound toggle button visibility and default state
openhands-agent Jan 30, 2025
8acfb09
Fix sound toggle button to handle loading state and undefined settings
openhands-agent Jan 30, 2025
a8e2298
Fix Vite build configuration for ES modules and proper asset loading
openhands-agent Jan 30, 2025
eb594d9
Fix build configuration to properly handle React Router and ES modules
openhands-agent Jan 30, 2025
affbda5
Fix build configuration to use Vite directly instead of react-router …
openhands-agent Jan 30, 2025
c8f1062
Switch to standard React Router setup with Vite
openhands-agent Jan 30, 2025
a3a6ba1
Add Makefile for building and serving frontend on specific host and port
openhands-agent Jan 30, 2025
21fbde2
Update volta Node.js version to match engines requirement
openhands-agent Jan 30, 2025
90b2699
Merge branch 'main' into feature/notifications
xingyaoww Jan 31, 2025
129dc80
Merge branch 'main' into feature/notifications
xingyaoww Feb 2, 2025
cf72ea7
revert: restore dependency versions and configurations from main branch
openhands-agent Feb 2, 2025
1881e5c
fix: add missing type declarations
openhands-agent Feb 2, 2025
540fe80
fix: add use-sound package for notification sounds
openhands-agent Feb 2, 2025
42932d4
fix: update use-sound and add howler dependency
openhands-agent Feb 2, 2025
a095a9a
cleanup: remove unnecessary files
openhands-agent Feb 2, 2025
c9bf019
revert: restore original test structure
openhands-agent Feb 2, 2025
e2273bc
refactor: integrate notifications into AgentStatusBar
openhands-agent Feb 2, 2025
5bc4f4d
refactor: simplify notification hook
openhands-agent Feb 2, 2025
d651055
cleanup: revert root.tsx changes
openhands-agent Feb 2, 2025
bd33834
fix: sound toggle button issues
openhands-agent Feb 2, 2025
a1ff96c
fix: tooltip implementation
openhands-agent Feb 2, 2025
03ac1d3
fix: replace use-sound with direct howler usage
openhands-agent Feb 2, 2025
af7516b
fix: add howler type declarations
openhands-agent Feb 2, 2025
de98a44
refactor: simplify notification sound implementation
openhands-agent Feb 2, 2025
c96c963
fix: handle server-side rendering for notifications
openhands-agent Feb 2, 2025
053c7fd
fix: TypeScript error in useRef initialization
openhands-agent Feb 2, 2025
91a1587
fix: sound toggle settings update
openhands-agent Feb 2, 2025
77b5068
style: fix linting issues
openhands-agent Feb 2, 2025
bb24100
refactor: simplify tooltip implementation
openhands-agent Feb 2, 2025
be06c3d
fix: sound notification behavior
openhands-agent Feb 2, 2025
de201be
feat: add browser tab notifications
openhands-agent Feb 2, 2025
fdffa01
fix: handle SSR for browser tab notifications
openhands-agent Feb 2, 2025
5f26f2f
feat: add tooltips for all trajectory buttons
openhands-agent Feb 2, 2025
97cc4d1
fix: update translation keys and fix linting
openhands-agent Feb 2, 2025
9ce560b
revert: remove unrelated style changes
openhands-agent Feb 2, 2025
84baa78
fix: settings update and error handling
openhands-agent Feb 2, 2025
623f0cf
fix: sound toggle behavior
openhands-agent Feb 2, 2025
7278510
fix: i18n keys and remove toast
openhands-agent Feb 2, 2025
fe00dee
fix: prevent sound on settings toggle
openhands-agent Feb 2, 2025
00e7bc7
Merge branch 'main' into feature/notifications
xingyaoww Feb 2, 2025
5f34e20
Merge branch 'main' into feature/notifications
xingyaoww Feb 3, 2025
746c825
Merge branch 'main' into feature/notifications
xingyaoww Feb 6, 2025
12c9e40
Merge branch 'main' into feature/notifications
xingyaoww Feb 10, 2025
bb1b366
Merge branch 'main' into feature/notifications
xingyaoww Feb 26, 2025
4736f4a
Add sound notifications toggle in account settings
openhands-agent Feb 26, 2025
03efb9a
Fix TrajectoryActions tests by adding proper providers
openhands-agent Feb 26, 2025
3771bdb
update notification sound
xingyaoww Feb 26, 2025
5a3269a
Move sound notification toggle from trajectory actions to settings page
openhands-agent Feb 27, 2025
50e7bf7
Add i18n declaration file to fix test failures
openhands-agent Feb 27, 2025
099da27
Simplify and lint test
amanape Feb 27, 2025
e563422
Remove unnecessary packages
amanape Feb 27, 2025
d94fcd6
Update frontend/src/services/settings.ts
xingyaoww Feb 27, 2025
e4b2f8d
Update openhands/server/settings.py
xingyaoww Feb 27, 2025
206abe1
Update frontend/src/i18n/translation.json
xingyaoww Feb 27, 2025
3870ee1
Apply suggestions from code review
xingyaoww Feb 27, 2025
5b95c3c
Merge branch 'main' into feature/notifications
xingyaoww Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions frontend/__tests__/components/feedback-actions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen, within } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { TrajectoryActions } from "#/components/features/trajectory/trajectory-actions";

describe("TrajectoryActions", () => {
Expand All @@ -14,7 +15,7 @@ describe("TrajectoryActions", () => {
});

it("should render correctly", () => {
render(
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
Expand All @@ -25,10 +26,11 @@ describe("TrajectoryActions", () => {
const actions = screen.getByTestId("feedback-actions");
within(actions).getByTestId("positive-feedback");
within(actions).getByTestId("negative-feedback");
within(actions).getByTestId("export-trajectory");
});

it("should call onPositiveFeedback when positive feedback is clicked", async () => {
render(
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
Expand All @@ -43,7 +45,7 @@ describe("TrajectoryActions", () => {
});

it("should call onNegativeFeedback when negative feedback is clicked", async () => {
render(
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
Expand All @@ -58,7 +60,7 @@ describe("TrajectoryActions", () => {
});

it("should call onExportTrajectory when negative feedback is clicked", async () => {
render(
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
Expand Down
Binary file added frontend/src/assets/notification.mp3
Binary file not shown.
38 changes: 37 additions & 1 deletion frontend/src/components/features/controls/agent-status-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";

const notificationStates = [
AgentState.AWAITING_USER_INPUT,
AgentState.FINISHED,
AgentState.AWAITING_USER_CONFIRMATION,
];

export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status } = useWsClient();
const { notify } = useNotification();

const [statusMessage, setStatusMessage] = React.useState<string>("");

Expand Down Expand Up @@ -45,13 +54,40 @@ export function AgentStatusBar() {
updateStatusMessage();
}, [curStatusMessage.id]);

// Handle window focus/blur
React.useEffect(() => {
if (typeof window === "undefined") return undefined;

const handleFocus = () => {
browserTab.stopNotification();
};

window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
browserTab.stopNotification();
};
}, []);

React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage("Connecting...");
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
if (notificationStates.includes(curAgentState)) {
const message = t(AGENT_STATUS_MAP[curAgentState].message);
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
body: t(`Agent state changed to ${curAgentState}`),
playSound: true,
});

// Update browser tab if window exists and is not focused
if (typeof document !== "undefined" && !document.hasFocus()) {
browserTab.startNotification(message);
}
}
}
}, [curAgentState]);
}, [curAgentState, notify, t]);

return (
<div className="flex flex-col items-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import ExportIcon from "#/icons/export.svg?react";
Expand All @@ -14,22 +15,27 @@ export function TrajectoryActions({
onNegativeFeedback,
onExportTrajectory,
}: TrajectoryActionsProps) {
const { t } = useTranslation();

return (
<div data-testid="feedback-actions" className="flex gap-1">
<TrajectoryActionButton
testId="positive-feedback"
onClick={onPositiveFeedback}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_HELPFUL")}
/>
<TrajectoryActionButton
testId="negative-feedback"
onClick={onNegativeFeedback}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t("BUTTON$MARK_NOT_HELPFUL")}
/>
<TrajectoryActionButton
testId="export-trajectory"
onClick={onExportTrajectory}
icon={<ExportIcon width={15} height={15} />}
tooltip={t("BUTTON$EXPORT_CONVERSATION")}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Tooltip } from "@heroui/react";

interface TrajectoryActionButtonProps {
testId?: string;
onClick: () => void;
icon: React.ReactNode;
tooltip?: string;
}

export function TrajectoryActionButton({
testId,
onClick,
icon,
tooltip,
}: TrajectoryActionButtonProps) {
return (
const button = (
<button
type="button"
data-testid={testId}
Expand All @@ -19,4 +23,14 @@ export function TrajectoryActionButton({
{icon}
</button>
);

if (tooltip) {
return (
<Tooltip content={tooltip} closeDelay={100}>
{button}
</Tooltip>
);
}

return button;
}
1 change: 1 addition & 0 deletions frontend/src/hooks/mutation/use-save-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
github_token: settings.github_token,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/query/use-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const getSettingsQueryFn = async () => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
GITHUB_TOKEN_IS_SET: apiSettings.github_token_is_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
};
};
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/hooks/useNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useCallback, useRef } from "react";
import notificationSound from "../assets/notification.mp3";
import { useCurrentSettings } from "../context/settings-context";

export const useNotification = () => {
const { settings } = useCurrentSettings();
const audioRef = useRef<HTMLAudioElement | undefined>(undefined);

// Initialize audio only in browser environment
if (typeof window !== "undefined" && !audioRef.current) {
audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5;
}

const notify = useCallback(
async (
title: string,
options?: NotificationOptions & { playSound?: boolean },
): Promise<Notification | undefined> => {
if (typeof window === "undefined") return undefined;

// Only play sound if:
// 1. Explicitly requested via playSound option
// 2. Sound notifications are enabled in settings
// 3. Audio is available
// 4. Not a settings-related notification
if (
options?.playSound === true && // Must be explicitly true
settings?.ENABLE_SOUND_NOTIFICATIONS &&
audioRef.current &&
!title.includes("BUTTON$") // Don't play for button/settings actions
) {
// Reset and play sound
audioRef.current.currentTime = 0;
audioRef.current.play().catch(() => {
// Ignore autoplay errors
});
}

if (Notification.permission === "default") {
await Notification.requestPermission();
}

if (Notification.permission === "granted") {
// Remove playSound from options before passing to Notification
const { playSound, ...notificationOptions } = options || {};
return new Notification(title, notificationOptions);
}

return undefined;
},
[settings?.ENABLE_SOUND_NOTIFICATIONS],
);

return { notify };
};
Loading
Loading