- {feedbackShared !== messages.length && messages.length > 3 && (
-
- onClickShareFeedbackActionButton("positive")
- }
- onNegativeFeedback={() =>
- onClickShareFeedbackActionButton("negative")
- }
- />
- )}
+
+ onClickShareFeedbackActionButton("positive")
+ }
+ onNegativeFeedback={() =>
+ onClickShareFeedbackActionButton("negative")
+ }
+ />
{messages.length > 2 &&
curAgentState === AgentState.AWAITING_USER_INPUT && (
@@ -163,9 +128,8 @@ export function ChatInterface() {
setFeedbackModalIsOpen(false)}
- onSubmit={handleSubmitFeedback}
+ polarity={feedbackPolarity}
/>
);
diff --git a/frontend/src/components/feedback-form.tsx b/frontend/src/components/feedback-form.tsx
index 7ff03a44307..4e1ddde6354 100644
--- a/frontend/src/components/feedback-form.tsx
+++ b/frontend/src/components/feedback-form.tsx
@@ -1,27 +1,87 @@
+import React from "react";
+import hotToast from "react-hot-toast";
import ModalButton from "./buttons/ModalButton";
+import { request } from "#/services/api";
+import { Feedback } from "#/api/open-hands.types";
+
+const FEEDBACK_VERSION = "1.0";
+const VIEWER_PAGE = "https://www.all-hands.dev/share";
interface FeedbackFormProps {
- onSubmit: (permissions: "private" | "public", email: string) => void;
onClose: () => void;
- isSubmitting?: boolean;
+ polarity: "positive" | "negative";
}
-export function FeedbackForm({
- onSubmit,
- onClose,
- isSubmitting,
-}: FeedbackFormProps) {
- const handleSubmit = (event: React.FormEvent) => {
+export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+ const copiedToClipboardToast = () => {
+ hotToast("Password copied to clipboard", {
+ icon: "📋",
+ position: "bottom-right",
+ });
+ };
+
+ const onPressToast = (password: string) => {
+ navigator.clipboard.writeText(password);
+ copiedToClipboardToast();
+ };
+
+ const shareFeedbackToast = (
+ message: string,
+ link: string,
+ password: string,
+ ) => {
+ hotToast(
+ ,
+ { duration: 10000 },
+ );
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
event?.preventDefault();
const formData = new FormData(event.currentTarget);
+ setIsSubmitting(true);
+
+ const email = formData.get("email")?.toString() || "";
+ const permissions = (formData.get("permissions")?.toString() ||
+ "private") as "private" | "public";
- const email = formData.get("email")?.toString();
- const permissions = formData.get("permissions")?.toString() as
- | "private"
- | "public"
- | undefined;
+ const feedback: Feedback = {
+ version: FEEDBACK_VERSION,
+ email,
+ polarity,
+ permissions,
+ trajectory: [],
+ token: "",
+ };
- if (email) onSubmit(permissions || "private", email);
+ const response = await request("/api/submit-feedback", {
+ method: "POST",
+ body: JSON.stringify(feedback),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const { message, feedback_id, password } = response.body; // eslint-disable-line
+ const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
+ shareFeedbackToast(message, link, password);
+ setIsSubmitting(false);
};
return (
diff --git a/frontend/src/components/feedback-modal.tsx b/frontend/src/components/feedback-modal.tsx
index f9cf05f0789..96663135e89 100644
--- a/frontend/src/components/feedback-modal.tsx
+++ b/frontend/src/components/feedback-modal.tsx
@@ -1,6 +1,4 @@
import React from "react";
-import hotToast, { toast } from "react-hot-toast";
-import { useFetcher } from "@remix-run/react";
import { FeedbackForm } from "./feedback-form";
import {
BaseModalTitle,
@@ -8,82 +6,18 @@ import {
} from "./modals/confirmation-modals/BaseModal";
import { ModalBackdrop } from "./modals/modal-backdrop";
import ModalBody from "./modals/ModalBody";
-import { clientAction } from "#/routes/submit-feedback";
interface FeedbackModalProps {
- onSubmit: (permissions: "private" | "public", email: string) => void;
onClose: () => void;
isOpen: boolean;
- isSubmitting?: boolean;
+ polarity: "positive" | "negative";
}
export function FeedbackModal({
- onSubmit,
onClose,
isOpen,
- isSubmitting,
+ polarity,
}: FeedbackModalProps) {
- const fetcher = useFetcher({ key: "feedback" });
- const isInitialRender = React.useRef(true);
-
- const copiedToClipboardToast = () => {
- hotToast("Password copied to clipboard", {
- icon: "📋",
- position: "bottom-right",
- });
- };
-
- const onPressToast = (password: string) => {
- navigator.clipboard.writeText(password);
- copiedToClipboardToast();
- };
-
- const shareFeedbackToast = (
- message: string,
- link: string,
- password: string,
- ) => {
- hotToast(
- ,
- { duration: 10000 },
- );
- };
-
- React.useEffect(() => {
- if (isInitialRender.current) {
- isInitialRender.current = false;
- return;
- }
-
- // Handle feedback submission
- if (fetcher.state === "idle" && fetcher.data) {
- if (!fetcher.data.success) {
- toast.error("Error submitting feedback");
- } else if (fetcher.data.data) {
- const { data } = fetcher.data;
- const { message, link, password } = data;
- shareFeedbackToast(message, link, password);
- }
-
- onClose();
- }
- }, [fetcher.state, fetcher.data?.success]);
-
if (!isOpen) return null;
return (
@@ -91,11 +25,7 @@ export function FeedbackModal({
-
+
);
diff --git a/frontend/src/routes/submit-feedback.ts b/frontend/src/routes/submit-feedback.ts
deleted file mode 100644
index 19281eea7ad..00000000000
--- a/frontend/src/routes/submit-feedback.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ClientActionFunctionArgs, json } from "@remix-run/react";
-import { Feedback } from "#/api/open-hands.types";
-import OpenHands from "#/api/open-hands";
-
-const VIEWER_PAGE = "https://www.all-hands.dev/share";
-
-const isFeedback = (feedback: unknown): feedback is Feedback => {
- if (typeof feedback !== "object" || feedback === null) {
- return false;
- }
-
- return (
- "version" in feedback &&
- "email" in feedback &&
- "token" in feedback &&
- "feedback" in feedback &&
- "permissions" in feedback &&
- "trajectory" in feedback
- );
-};
-
-export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
- const formData = await request.formData();
- const feedback = formData.get("feedback")?.toString();
- const token = localStorage.getItem("token");
-
- if (token && feedback) {
- const parsed = JSON.parse(feedback);
- if (isFeedback(parsed)) {
- try {
- const response = await OpenHands.sendFeedback(token, parsed);
- if (response.statusCode === 200) {
- const { message, feedback_id: feedbackId, password } = response.body;
- const link = `${VIEWER_PAGE}?share_id=${feedbackId}`;
- return json({
- success: true,
- data: { message, link, password },
- });
- }
- } catch (error) {
- return json({ success: false, data: null });
- }
- }
- }
-
- return json({ success: false, data: null });
-};
diff --git a/openhands/events/stream.py b/openhands/events/stream.py
index 1e4c3b9d539..aafbcc2fc87 100644
--- a/openhands/events/stream.py
+++ b/openhands/events/stream.py
@@ -71,7 +71,15 @@ def get_events(
end_id=None,
reverse=False,
filter_out_type: tuple[type[Event], ...] | None = None,
+ filter_hidden=False,
) -> Iterable[Event]:
+ def should_filter(event: Event):
+ if filter_hidden and hasattr(event, 'hidden') and event.hidden:
+ return True
+ if filter_out_type is not None and isinstance(event, filter_out_type):
+ return True
+ return False
+
if reverse:
if end_id is None:
end_id = self._cur_id - 1
@@ -79,9 +87,7 @@ def get_events(
while event_id >= start_id:
try:
event = self.get_event(event_id)
- if filter_out_type is None or not isinstance(
- event, filter_out_type
- ):
+ if not should_filter(event):
yield event
except FileNotFoundError:
logger.debug(f'No event found for ID {event_id}')
@@ -93,9 +99,7 @@ def get_events(
break
try:
event = self.get_event(event_id)
- if filter_out_type is None or not isinstance(
- event, filter_out_type
- ):
+ if not should_filter(event):
yield event
except FileNotFoundError:
break
diff --git a/openhands/runtime/impl/e2b/sandbox.py b/openhands/runtime/impl/e2b/sandbox.py
index 666dc43f701..d145dac3511 100644
--- a/openhands/runtime/impl/e2b/sandbox.py
+++ b/openhands/runtime/impl/e2b/sandbox.py
@@ -4,9 +4,7 @@
from glob import glob
from e2b import Sandbox as E2BSandbox
-from e2b.sandbox.exception import (
- TimeoutException,
-)
+from e2b.sandbox.exception import TimeoutException
from openhands.core.config import SandboxConfig
from openhands.core.logger import openhands_logger as logger
diff --git a/openhands/server/data_models/feedback.py b/openhands/server/data_models/feedback.py
index cbdd8744807..59f32008b52 100644
--- a/openhands/server/data_models/feedback.py
+++ b/openhands/server/data_models/feedback.py
@@ -1,5 +1,5 @@
import json
-from typing import Any, Literal
+from typing import Any, Literal, Optional
import requests
from pydantic import BaseModel
@@ -10,10 +10,12 @@
class FeedbackDataModel(BaseModel):
version: str
email: str
- token: str
- feedback: Literal['positive', 'negative']
+ polarity: Literal['positive', 'negative']
+ feedback: Literal[
+ 'positive', 'negative'
+ ] # TODO: remove this, its here for backward compatibility
permissions: Literal['public', 'private']
- trajectory: list[dict[str, Any]]
+ trajectory: Optional[list[dict[str, Any]]]
FEEDBACK_URL = 'https://share-od-trajectory-3u9bw9tx.uc.gateway.dev/share_od_trajectory'
@@ -21,6 +23,7 @@ class FeedbackDataModel(BaseModel):
def store_feedback(feedback: FeedbackDataModel) -> dict[str, str]:
# Start logging
+ feedback.feedback = feedback.polarity
display_feedback = feedback.model_dump()
if 'trajectory' in display_feedback:
display_feedback['trajectory'] = (
diff --git a/openhands/server/listen.py b/openhands/server/listen.py
index fc740e80293..cc74d5ba736 100644
--- a/openhands/server/listen.py
+++ b/openhands/server/listen.py
@@ -634,14 +634,14 @@ async def upload_file(request: Request, files: list[UploadFile]):
@app.post('/api/submit-feedback')
-async def submit_feedback(request: Request, feedback: FeedbackDataModel):
+async def submit_feedback(request: Request):
"""Submit user feedback.
This function stores the provided feedback data.
To submit feedback:
```sh
- curl -X POST -F "email=test@example.com" -F "token=abc" -F "feedback=positive" -F "permissions=private" -F "trajectory={}" http://localhost:3000/api/submit-feedback
+ curl -X POST -d '{"email": "test@example.com"}' -H "Authorization:"
```
Args:
@@ -656,6 +656,19 @@ async def submit_feedback(request: Request, feedback: FeedbackDataModel):
"""
# Assuming the storage service is already configured in the backend
# and there is a function to handle the storage.
+ body = await request.json()
+ events = request.state.conversation.event_stream.get_events(filter_hidden=True)
+ trajectory = []
+ for event in events:
+ trajectory.append(event_to_dict(event))
+ feedback = FeedbackDataModel(
+ email=body.get('email', ''),
+ version=body.get('version', ''),
+ permissions=body.get('permissions', 'private'),
+ polarity=body.get('polarity', ''),
+ feedback=body.get('polarity', ''),
+ trajectory=trajectory,
+ )
try:
feedback_data = store_feedback(feedback)
return JSONResponse(status_code=200, content=feedback_data)