mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Add consent dialog (#2169)
* Add consent dialog for sharing conversation histories * Update * Update to nextui modals * Update * More fixes to modal * Updates * Revert most changes to ChatInterface * Update form * Cleanup * Update consent dialog * Lint * Fix toast * Fix to be a select * prettier * Update frontend/src/components/chat/ChatInterface.tsx Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> * Update frontend/src/components/modals/feedback/FeedbackModal.tsx Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> * Update frontend/src/components/modals/feedback/FeedbackModal.tsx Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> * Update frontend/src/components/chat/ChatInterface.tsx Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> * Fix --------- Co-authored-by: OpenDevin <opendevin@opendevin.ai> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { VscArrowDown } from "react-icons/vsc";
|
||||
import { FaRegThumbsDown, FaRegThumbsUp } from "react-icons/fa";
|
||||
import { useDisclosure } from "@nextui-org/react";
|
||||
import ChatInput from "./ChatInput";
|
||||
import Chat from "./Chat";
|
||||
import { RootState } from "#/store";
|
||||
@@ -14,11 +15,11 @@ import { sendChatMessage } from "#/services/chatService";
|
||||
import { addUserMessage, addAssistantMessage } from "#/state/chatSlice";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useScrollToBottom } from "#/hooks/useScrollToBottom";
|
||||
import { Feedback } from "#/services/feedbackService";
|
||||
import FeedbackModal from "../modals/feedback/FeedbackModal";
|
||||
import { removeApiKey } from "#/utils/utils";
|
||||
import Session from "#/services/session";
|
||||
import { getToken } from "#/services/auth";
|
||||
import toast from "#/utils/toast";
|
||||
import { removeApiKey } from "#/utils/utils";
|
||||
import { FeedbackData, sendFeedback } from "#/services/feedbackService";
|
||||
|
||||
interface ScrollButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -53,31 +54,32 @@ function ChatInterface() {
|
||||
const { messages } = useSelector((state: RootState) => state.chat);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const [feedbackShared, setFeedbackShared] = React.useState(false);
|
||||
const [feedbackLoading, setFeedbackLoading] = React.useState(false);
|
||||
const feedbackVersion = "1.0";
|
||||
const [feedback, setFeedback] = React.useState<Feedback>({
|
||||
email: "",
|
||||
feedback: "positive",
|
||||
permissions: "private",
|
||||
trajectory: [],
|
||||
token: "",
|
||||
version: feedbackVersion,
|
||||
});
|
||||
const [feedbackShared, setFeedbackShared] = React.useState(0);
|
||||
|
||||
const shareFeedback = async (feedback: "positive" | "negative") => {
|
||||
// TODO: implement email and permissions
|
||||
// https://github.com/OpenDevin/OpenDevin/issues/2033
|
||||
const data: FeedbackData = {
|
||||
email: "NOT_PROVIDED",
|
||||
token: getToken(),
|
||||
feedback,
|
||||
permissions: "private",
|
||||
const {
|
||||
isOpen: feedbackModalIsOpen,
|
||||
onOpen: onFeedbackModalOpen,
|
||||
onOpenChange: onFeedbackModalOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
const shareFeedback = async (polarity: "positive" | "negative") => {
|
||||
setFeedbackShared(messages.length);
|
||||
setFeedback((prev) => ({
|
||||
...prev,
|
||||
feedback: polarity,
|
||||
trajectory: removeApiKey(Session._history),
|
||||
};
|
||||
|
||||
try {
|
||||
setFeedbackLoading(true);
|
||||
await sendFeedback(data);
|
||||
toast.info("Feedback shared successfully.");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("share-error", "Failed to share, see console for details.");
|
||||
} finally {
|
||||
setFeedbackShared(true);
|
||||
setFeedbackLoading(false);
|
||||
}
|
||||
token: getToken(),
|
||||
}));
|
||||
onFeedbackModalOpen();
|
||||
};
|
||||
|
||||
const handleSendMessage = (content: string) => {
|
||||
@@ -85,6 +87,14 @@ function ChatInterface() {
|
||||
sendChatMessage(content);
|
||||
};
|
||||
|
||||
const handleEmailChange = (key: string) => {
|
||||
setFeedback({ ...feedback, email: key } as Feedback);
|
||||
};
|
||||
|
||||
const handlePermissionsChange = (permissions: "public" | "private") => {
|
||||
setFeedback({ ...feedback, permissions } as Feedback);
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleSendContinueMsg = () => {
|
||||
handleSendMessage(t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE));
|
||||
@@ -99,7 +109,7 @@ function ChatInterface() {
|
||||
if (curAgentState === AgentState.INIT && messages.length === 0) {
|
||||
dispatch(addAssistantMessage(t(I18nKey.CHAT_INTERFACE$INITIAL_MESSAGE)));
|
||||
}
|
||||
}, [curAgentState]);
|
||||
}, [curAgentState, dispatch, messages.length, t]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-neutral-800">
|
||||
@@ -142,16 +152,14 @@ function ChatInterface() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!feedbackShared && messages.length > 3 && (
|
||||
{feedbackShared !== messages.length && messages.length > 3 && (
|
||||
<div className="flex justify-start gap-2 p-2">
|
||||
<ScrollButton
|
||||
disabled={feedbackLoading}
|
||||
onClick={() => shareFeedback("positive")}
|
||||
icon={<FaRegThumbsUp className="inline mr-2 w-3 h-3" />}
|
||||
label=""
|
||||
/>
|
||||
<ScrollButton
|
||||
disabled={feedbackLoading}
|
||||
onClick={() => shareFeedback("negative")}
|
||||
icon={<FaRegThumbsDown className="inline mr-2 w-3 h-3" />}
|
||||
label=""
|
||||
@@ -164,6 +172,13 @@ function ChatInterface() {
|
||||
disabled={curAgentState === AgentState.LOADING}
|
||||
onSendMessage={handleSendMessage}
|
||||
/>
|
||||
<FeedbackModal
|
||||
feedback={feedback}
|
||||
handleEmailChange={handleEmailChange}
|
||||
handlePermissionsChange={handlePermissionsChange}
|
||||
isOpen={feedbackModalIsOpen}
|
||||
onOpenChange={onFeedbackModalOpenChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
63
frontend/src/components/modals/feedback/FeedbackForm.tsx
Normal file
63
frontend/src/components/modals/feedback/FeedbackForm.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Input, Select, SelectItem } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "../../../i18n/declaration";
|
||||
import { Feedback } from "#/services/feedbackService";
|
||||
|
||||
interface FeedbackFormProps {
|
||||
feedback: Feedback;
|
||||
|
||||
onEmailChange: (email: string) => void;
|
||||
onPermissionsChange: (permissions: "public" | "private") => void;
|
||||
}
|
||||
|
||||
function FeedbackForm({
|
||||
feedback,
|
||||
onEmailChange,
|
||||
onPermissionsChange,
|
||||
}: FeedbackFormProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isEmailValid = (email: string) => {
|
||||
// Regular expression to validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Email"
|
||||
aria-label="email"
|
||||
data-testid="email"
|
||||
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
|
||||
type="text"
|
||||
value={feedback.email || ""}
|
||||
onChange={(e) => {
|
||||
onEmailChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Sharing settings"
|
||||
aria-label="permissions"
|
||||
data-testid="permissions"
|
||||
value={feedback.permissions}
|
||||
onChange={(e) => {
|
||||
onPermissionsChange(e.target.value as "public" | "private");
|
||||
}}
|
||||
>
|
||||
<SelectItem key="public" value="public">
|
||||
Public
|
||||
</SelectItem>
|
||||
<SelectItem key="private" value="private">
|
||||
Private
|
||||
</SelectItem>
|
||||
</Select>
|
||||
{isEmailValid(feedback.email) ? null : (
|
||||
<p className="text-red-500">Invalid email format</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedbackForm;
|
||||
77
frontend/src/components/modals/feedback/FeedbackModal.tsx
Normal file
77
frontend/src/components/modals/feedback/FeedbackModal.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BaseModal from "../base-modal/BaseModal";
|
||||
import { Feedback, sendFeedback } from "#/services/feedbackService";
|
||||
import FeedbackForm from "./FeedbackForm";
|
||||
import toast from "#/utils/toast";
|
||||
|
||||
interface FeedbackModalProps {
|
||||
feedback: Feedback;
|
||||
handleEmailChange: (key: string) => void;
|
||||
handlePermissionsChange: (permissions: "public" | "private") => void;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function FeedbackModal({
|
||||
feedback,
|
||||
handleEmailChange,
|
||||
handlePermissionsChange,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: FeedbackModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSendFeedback = () => {
|
||||
sendFeedback(feedback)
|
||||
.then((response) => {
|
||||
if (response.message === "Feedback submitted successfully") {
|
||||
toast.info(response.message);
|
||||
} else {
|
||||
toast.error(
|
||||
"share-error",
|
||||
`Failed to share, please contact the developers: ${response.message}`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
"share-error",
|
||||
`Failed to share, please contact the developers: ${error}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
title={t(I18nKey.FEEDBACK$MODAL_TITLE)}
|
||||
onOpenChange={onOpenChange}
|
||||
isDismissable={false} // prevent unnecessary messages from being stored (issue #1285)
|
||||
actions={[
|
||||
{
|
||||
label: t(I18nKey.FEEDBACK$SHARE_LABEL),
|
||||
className: "bg-primary rounded-lg",
|
||||
action: handleSendFeedback,
|
||||
closeAfterAction: true,
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.FEEDBACK$CANCEL_LABEL),
|
||||
className: "bg-neutral-500 rounded-lg",
|
||||
action() {},
|
||||
closeAfterAction: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>{t(I18nKey.FEEDBACK$MODAL_CONTENT)}</p>
|
||||
<FeedbackForm
|
||||
feedback={feedback}
|
||||
onEmailChange={handleEmailChange}
|
||||
onPermissionsChange={handlePermissionsChange}
|
||||
/>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedbackModal;
|
||||
@@ -292,6 +292,27 @@
|
||||
"zh-CN": "开始新会话",
|
||||
"zh-TW": "開始新會話"
|
||||
},
|
||||
"FEEDBACK$MODAL_TITLE": {
|
||||
"en": "Share feedback"
|
||||
},
|
||||
"FEEDBACK$MODAL_CONTENT": {
|
||||
"en": "Thank you for your feedback! The history of this conversation, together with your positive/negative feedback will be shared with the OpenDevin developers. Additionally, you can choose to contribute this feedback to our public dataset that will be shared with the community for model improvements."
|
||||
},
|
||||
"FEEDBACK$EMAIL_LABEL": {
|
||||
"en": "Your email"
|
||||
},
|
||||
"FEEDBACK$CONTRIBUTE_LABEL": {
|
||||
"en": "Contribute to public dataset"
|
||||
},
|
||||
"FEEDBACK$SHARE_LABEL": {
|
||||
"en": "Share"
|
||||
},
|
||||
"FEEDBACK$CANCEL_LABEL": {
|
||||
"en": "Cancel"
|
||||
},
|
||||
"FEEDBACK$EMAIL_PLACEHOLDER": {
|
||||
"en": "Enter your email address."
|
||||
},
|
||||
"CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
|
||||
"en": "Initializing agent (may take up to 10 seconds)...",
|
||||
"zh-CN": "正在初始化智能体(可能需要 10 秒以上时间)",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { request } from "./api";
|
||||
|
||||
export interface FeedbackData {
|
||||
export interface Feedback {
|
||||
version: string;
|
||||
email: string;
|
||||
token: string;
|
||||
feedback: "positive" | "negative";
|
||||
@@ -8,8 +9,8 @@ export interface FeedbackData {
|
||||
trajectory: unknown[];
|
||||
}
|
||||
|
||||
export async function sendFeedback(data: FeedbackData) {
|
||||
await request("/api/submit-feedback", {
|
||||
export async function sendFeedback(data: Feedback) {
|
||||
return request("/api/submit-feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -7,6 +7,7 @@ from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
class FeedbackDataModel(BaseModel):
|
||||
version: str
|
||||
email: str
|
||||
token: str
|
||||
feedback: Literal['positive', 'negative']
|
||||
@@ -20,7 +21,16 @@ FEEDBACK_URL = (
|
||||
|
||||
|
||||
def store_feedback(feedback: FeedbackDataModel):
|
||||
logger.info(f'Got feedback: {feedback.model_dump_json()}')
|
||||
# Start logging
|
||||
display_feedback = feedback.model_dump()
|
||||
if 'trajectory' in display_feedback:
|
||||
display_feedback['trajectory'] = (
|
||||
f"elided [length: {len(display_feedback['trajectory'])}"
|
||||
)
|
||||
if 'token' in display_feedback:
|
||||
display_feedback['token'] = 'elided'
|
||||
logger.info(f'Got feedback: {display_feedback}')
|
||||
# Start actual request
|
||||
response = requests.post(
|
||||
FEEDBACK_URL,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
|
||||
Reference in New Issue
Block a user