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:
Graham Neubig
2024-06-03 10:33:53 -04:00
committed by GitHub
parent c9c5d71e5c
commit 4476c250c5
6 changed files with 221 additions and 34 deletions

View File

@@ -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>
);
}

View 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;

View 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;

View File

@@ -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 秒以上时间)",

View File

@@ -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",

View File

@@ -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'},