mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(frontend): add change agent button (#11675)
This commit is contained in:
parent
f4dcc136d0
commit
9b4f1c365b
@ -0,0 +1,92 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Typography } from "#/ui/typography";
|
||||||
|
import { I18nKey } from "#/i18n/declaration";
|
||||||
|
import CodeTagIcon from "#/icons/code-tag.svg?react";
|
||||||
|
import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react";
|
||||||
|
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||||
|
import { useConversationStore } from "#/state/conversation-store";
|
||||||
|
import { ChangeAgentContextMenu } from "./change-agent-context-menu";
|
||||||
|
import { cn } from "#/utils/utils";
|
||||||
|
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||||
|
|
||||||
|
export function ChangeAgentButton() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [contextMenuOpen, setContextMenuOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const conversationMode = useConversationStore(
|
||||||
|
(state) => state.conversationMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setConversationMode = useConversationStore(
|
||||||
|
(state) => state.setConversationMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
|
||||||
|
|
||||||
|
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setContextMenuOpen(!contextMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setConversationMode("code");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlanClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setConversationMode("plan");
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExecutionAgent = conversationMode === "code";
|
||||||
|
|
||||||
|
const buttonLabel = useMemo(() => {
|
||||||
|
if (isExecutionAgent) {
|
||||||
|
return t(I18nKey.COMMON$CODE);
|
||||||
|
}
|
||||||
|
return t(I18nKey.COMMON$PLAN);
|
||||||
|
}, [isExecutionAgent, t]);
|
||||||
|
|
||||||
|
const buttonIcon = useMemo(() => {
|
||||||
|
if (isExecutionAgent) {
|
||||||
|
return <CodeTagIcon width={18} height={18} color="#737373" />;
|
||||||
|
}
|
||||||
|
return <LessonPlanIcon width={18} height={18} color="#ffffff" />;
|
||||||
|
}, [isExecutionAgent]);
|
||||||
|
|
||||||
|
if (!shouldUsePlanningAgent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center border border-[#4B505F] rounded-[100px] cursor-pointer hover:opacity-80",
|
||||||
|
!isExecutionAgent && "border-[#597FF4] bg-[#4A67BD]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1 pl-1.5">
|
||||||
|
{buttonIcon}
|
||||||
|
<Typography.Text className="text-white text-2.75 not-italic font-normal leading-5">
|
||||||
|
{buttonLabel}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<ChevronDownSmallIcon width={24} height={24} color="#ffffff" />
|
||||||
|
</button>
|
||||||
|
{contextMenuOpen && (
|
||||||
|
<ChangeAgentContextMenu
|
||||||
|
onClose={() => setContextMenuOpen(false)}
|
||||||
|
onCodeClick={handleCodeClick}
|
||||||
|
onPlanClick={handlePlanClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { I18nKey } from "#/i18n/declaration";
|
||||||
|
import CodeTagIcon from "#/icons/code-tag.svg?react";
|
||||||
|
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||||
|
import { ContextMenu } from "#/ui/context-menu";
|
||||||
|
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||||
|
import { ContextMenuIconText } from "../context-menu/context-menu-icon-text";
|
||||||
|
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||||
|
import { cn } from "#/utils/utils";
|
||||||
|
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||||
|
|
||||||
|
const contextMenuListItemClassName = cn(
|
||||||
|
"cursor-pointer p-0 h-auto hover:bg-transparent",
|
||||||
|
CONTEXT_MENU_ICON_TEXT_CLASSNAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextMenuIconTextClassName =
|
||||||
|
"gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]";
|
||||||
|
|
||||||
|
interface ChangeAgentContextMenuProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onCodeClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onPlanClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangeAgentContextMenu({
|
||||||
|
onClose,
|
||||||
|
onCodeClick,
|
||||||
|
onPlanClick,
|
||||||
|
}: ChangeAgentContextMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const menuRef = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||||
|
|
||||||
|
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onCodeClick?.(event);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlanClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onPlanClick?.(event);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu
|
||||||
|
ref={menuRef}
|
||||||
|
testId="change-agent-context-menu"
|
||||||
|
position="top"
|
||||||
|
alignment="left"
|
||||||
|
className="min-h-fit min-w-[195px] mb-2"
|
||||||
|
>
|
||||||
|
<ContextMenuListItem
|
||||||
|
testId="code-option"
|
||||||
|
onClick={handleCodeClick}
|
||||||
|
className={contextMenuListItemClassName}
|
||||||
|
>
|
||||||
|
<ContextMenuIconText
|
||||||
|
icon={CodeTagIcon}
|
||||||
|
text={t(I18nKey.COMMON$CODE)}
|
||||||
|
className={contextMenuIconTextClassName}
|
||||||
|
/>
|
||||||
|
</ContextMenuListItem>
|
||||||
|
<ContextMenuListItem
|
||||||
|
testId="plan-option"
|
||||||
|
onClick={handlePlanClick}
|
||||||
|
className={contextMenuListItemClassName}
|
||||||
|
>
|
||||||
|
<ContextMenuIconText
|
||||||
|
icon={LessonPlanIcon}
|
||||||
|
text={t(I18nKey.COMMON$PLAN)}
|
||||||
|
className={contextMenuIconTextClassName}
|
||||||
|
/>
|
||||||
|
</ContextMenuListItem>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
|||||||
import { AgentState } from "#/types/agent-state";
|
import { AgentState } from "#/types/agent-state";
|
||||||
import { useV1PauseConversation } from "#/hooks/mutation/use-v1-pause-conversation";
|
import { useV1PauseConversation } from "#/hooks/mutation/use-v1-pause-conversation";
|
||||||
import { useV1ResumeConversation } from "#/hooks/mutation/use-v1-resume-conversation";
|
import { useV1ResumeConversation } from "#/hooks/mutation/use-v1-resume-conversation";
|
||||||
|
import { ChangeAgentButton } from "../change-agent-button";
|
||||||
|
|
||||||
interface ChatInputActionsProps {
|
interface ChatInputActionsProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -56,7 +57,10 @@ export function ChatInputActions({
|
|||||||
return (
|
return (
|
||||||
<div className="w-full flex items-center justify-between">
|
<div className="w-full flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Tools />
|
<div className="flex items-center gap-4">
|
||||||
|
<Tools />
|
||||||
|
<ChangeAgentButton />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AgentStatus
|
<AgentStatus
|
||||||
className="ml-2 md:ml-3"
|
className="ml-2 md:ml-3"
|
||||||
|
|||||||
@ -937,5 +937,7 @@ export enum I18nKey {
|
|||||||
AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION = "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION",
|
AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION = "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION",
|
||||||
COMMON$MORE_OPTIONS = "COMMON$MORE_OPTIONS",
|
COMMON$MORE_OPTIONS = "COMMON$MORE_OPTIONS",
|
||||||
COMMON$CREATE_A_PLAN = "COMMON$CREATE_A_PLAN",
|
COMMON$CREATE_A_PLAN = "COMMON$CREATE_A_PLAN",
|
||||||
|
COMMON$ASK = "COMMON$ASK",
|
||||||
|
COMMON$PLAN = "COMMON$PLAN",
|
||||||
COMMON$LET_S_WORK_ON_A_PLAN = "COMMON$LET_S_WORK_ON_A_PLAN",
|
COMMON$LET_S_WORK_ON_A_PLAN = "COMMON$LET_S_WORK_ON_A_PLAN",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14991,6 +14991,38 @@
|
|||||||
"de": "Einen Plan erstellen",
|
"de": "Einen Plan erstellen",
|
||||||
"uk": "Створити план"
|
"uk": "Створити план"
|
||||||
},
|
},
|
||||||
|
"COMMON$ASK": {
|
||||||
|
"en": "Ask",
|
||||||
|
"ja": "質問する",
|
||||||
|
"zh-CN": "提问",
|
||||||
|
"zh-TW": "詢問",
|
||||||
|
"ko-KR": "질문",
|
||||||
|
"no": "Spør",
|
||||||
|
"it": "Chiedi",
|
||||||
|
"pt": "Perguntar",
|
||||||
|
"es": "Preguntar",
|
||||||
|
"ar": "اسأل",
|
||||||
|
"fr": "Demander",
|
||||||
|
"tr": "Sor",
|
||||||
|
"de": "Fragen",
|
||||||
|
"uk": "Запитати"
|
||||||
|
},
|
||||||
|
"COMMON$PLAN": {
|
||||||
|
"en": "Plan",
|
||||||
|
"ja": "計画",
|
||||||
|
"zh-CN": "计划",
|
||||||
|
"zh-TW": "計劃",
|
||||||
|
"ko-KR": "계획",
|
||||||
|
"no": "Plan",
|
||||||
|
"it": "Piano",
|
||||||
|
"pt": "Plano",
|
||||||
|
"es": "Plan",
|
||||||
|
"ar": "خطة",
|
||||||
|
"fr": "Planifier",
|
||||||
|
"tr": "Plan",
|
||||||
|
"de": "Plan",
|
||||||
|
"uk": "План"
|
||||||
|
},
|
||||||
"COMMON$LET_S_WORK_ON_A_PLAN": {
|
"COMMON$LET_S_WORK_ON_A_PLAN": {
|
||||||
"en": "Let’s work on a plan",
|
"en": "Let’s work on a plan",
|
||||||
"ja": "プランに取り組みましょう",
|
"ja": "プランに取り組みましょう",
|
||||||
|
|||||||
3
frontend/src/icons/code-tag.svg
Normal file
3
frontend/src/icons/code-tag.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7.062 8.367L3.0915 12.336L7.062 16.305L6 17.367L1.5 12.867V11.805L6 7.305L7.062 8.367ZM17.562 7.305L16.5 8.367L20.4705 12.336L16.5 16.305L17.562 17.367L22.062 12.867V11.805L17.562 7.305ZM7.362 19.5L8.703 20.172L16.203 5.172L14.862 4.5L7.362 19.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
Loading…
x
Reference in New Issue
Block a user