mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
refactor(frontend): system message modal (#10969)
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import ReactJsonView from "@microlink/react-json-view";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { useState } from "react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { JSON_VIEW_THEME } from "#/utils/constants";
|
||||
import { SystemMessageHeader } from "./system-message-modal/system-message-header";
|
||||
import { TabNavigation } from "./system-message-modal/tab-navigation";
|
||||
import { TabContent } from "./system-message-modal/tab-content";
|
||||
|
||||
interface SystemMessageModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -19,26 +16,11 @@ interface SystemMessageModalProps {
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface FunctionData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ToolData {
|
||||
type?: string;
|
||||
function?: FunctionData;
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function SystemMessageModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
systemMessage,
|
||||
}: SystemMessageModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"system" | "tools">("system");
|
||||
const [expandedTools, setExpandedTools] = useState<Record<number, boolean>>(
|
||||
{},
|
||||
@@ -62,155 +44,27 @@ export function SystemMessageModal({
|
||||
width="medium"
|
||||
className="max-h-[80vh] flex flex-col items-start"
|
||||
>
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<BaseModalTitle title={t("SYSTEM_MESSAGE_MODAL$TITLE")} />
|
||||
<div className="flex flex-col gap-2">
|
||||
{systemMessage.agent_class && (
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$AGENT_CLASS")}
|
||||
</span>{" "}
|
||||
<span className="font-medium text-gray-100">
|
||||
{systemMessage.agent_class}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{systemMessage.openhands_version && (
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION")}
|
||||
</span>{" "}
|
||||
<span className="text-gray-100">
|
||||
{systemMessage.openhands_version}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SystemMessageHeader
|
||||
agentClass={systemMessage.agent_class}
|
||||
openhandsVersion={systemMessage.openhands_version}
|
||||
/>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex border-b mb-2">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 font-medium border-b-2 transition-colors",
|
||||
activeTab === "system"
|
||||
? "border-primary text-gray-100"
|
||||
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
onClick={() => setActiveTab("system")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB")}
|
||||
</button>
|
||||
{systemMessage.tools && systemMessage.tools.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 font-medium border-b-2 transition-colors",
|
||||
activeTab === "tools"
|
||||
? "border-primary text-gray-100"
|
||||
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
|
||||
)}
|
||||
onClick={() => setActiveTab("tools")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$TOOLS_TAB")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
hasTools={
|
||||
!!(systemMessage.tools && systemMessage.tools.length > 0)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="max-h-[51vh] overflow-auto rounded-md">
|
||||
{activeTab === "system" && (
|
||||
<div className="p-4 whitespace-pre-wrap font-mono text-sm leading-relaxed text-gray-300 shadow-inner">
|
||||
{systemMessage.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "tools" &&
|
||||
systemMessage.tools &&
|
||||
systemMessage.tools.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{systemMessage.tools.map((tool, index) => {
|
||||
// Extract function data from the nested structure
|
||||
const toolData = tool as ToolData;
|
||||
const functionData = toolData.function || toolData;
|
||||
const name =
|
||||
functionData.name ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.name) ||
|
||||
"";
|
||||
const description =
|
||||
functionData.description ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.description) ||
|
||||
"";
|
||||
const parameters =
|
||||
functionData.parameters ||
|
||||
(toolData.type === "function" &&
|
||||
toolData.function?.parameters) ||
|
||||
null;
|
||||
|
||||
const isExpanded = expandedTools[index] || false;
|
||||
|
||||
return (
|
||||
<div key={index} className="rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTool(index)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-bold text-gray-100">
|
||||
{String(name)}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
<div className="mt-2 mb-3">
|
||||
<p className="text-sm whitespace-pre-wrap text-gray-300 leading-relaxed">
|
||||
{String(description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Parameters section */}
|
||||
{parameters && (
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$PARAMETERS")}
|
||||
</h4>
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<ReactJsonView
|
||||
name={false}
|
||||
src={parameters}
|
||||
theme={JSON_VIEW_THEME}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "tools" &&
|
||||
(!systemMessage.tools || systemMessage.tools.length === 0) && (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<p className="text-gray-400">
|
||||
{t("SYSTEM_MESSAGE_MODAL$NO_TOOLS")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[51vh] overflow-auto rounded-md custom-scrollbar-always">
|
||||
<TabContent
|
||||
activeTab={activeTab}
|
||||
systemMessage={systemMessage}
|
||||
expandedTools={expandedTools}
|
||||
onToggleTool={toggleTool}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function EmptyToolsState() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<Typography.Text className="text-gray-400">
|
||||
{t("SYSTEM_MESSAGE_MODAL$NO_TOOLS")}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface SystemMessageContentProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function SystemMessageContent({ content }: SystemMessageContentProps) {
|
||||
return (
|
||||
<div className="p-4 shadow-inner">
|
||||
<Typography.CodeBlock>{content}</Typography.CodeBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface SystemMessageHeaderProps {
|
||||
agentClass: string | null;
|
||||
openhandsVersion: string | null;
|
||||
}
|
||||
|
||||
export function SystemMessageHeader({
|
||||
agentClass,
|
||||
openhandsVersion,
|
||||
}: SystemMessageHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<BaseModalTitle title={t("SYSTEM_MESSAGE_MODAL$TITLE")} />
|
||||
<div className="flex flex-col gap-2">
|
||||
{agentClass && (
|
||||
<div className="text-sm">
|
||||
<Typography.Text className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$AGENT_CLASS")}
|
||||
</Typography.Text>{" "}
|
||||
<Typography.Text className="font-medium text-gray-100">
|
||||
{agentClass}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{openhandsVersion && (
|
||||
<div className="text-sm">
|
||||
<Typography.Text className="font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION")}
|
||||
</Typography.Text>{" "}
|
||||
<Typography.Text className="text-gray-100">
|
||||
{openhandsVersion}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface TabButtonProps {
|
||||
isActive: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function TabButton({
|
||||
isActive,
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
disabled = false,
|
||||
}: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"px-4 py-2 font-medium border-b-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary text-gray-100"
|
||||
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
aria-selected={isActive}
|
||||
role="tab"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { SystemMessageContent } from "./system-message-content";
|
||||
import { ToolsList } from "./tools-list";
|
||||
import { EmptyToolsState } from "./empty-tools-state";
|
||||
|
||||
interface TabContentProps {
|
||||
activeTab: "system" | "tools";
|
||||
systemMessage: {
|
||||
content: string;
|
||||
tools: Array<Record<string, unknown>> | null;
|
||||
};
|
||||
expandedTools: Record<number, boolean>;
|
||||
onToggleTool: (index: number) => void;
|
||||
}
|
||||
|
||||
export function TabContent({
|
||||
activeTab,
|
||||
systemMessage,
|
||||
expandedTools,
|
||||
onToggleTool,
|
||||
}: TabContentProps) {
|
||||
if (activeTab === "system") {
|
||||
return <SystemMessageContent content={systemMessage.content} />;
|
||||
}
|
||||
|
||||
if (activeTab === "tools") {
|
||||
if (systemMessage.tools && systemMessage.tools.length > 0) {
|
||||
return (
|
||||
<ToolsList
|
||||
tools={systemMessage.tools}
|
||||
expandedTools={expandedTools}
|
||||
onToggleTool={onToggleTool}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyToolsState />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TabButton } from "./tab-button";
|
||||
|
||||
interface TabNavigationProps {
|
||||
activeTab: "system" | "tools";
|
||||
onTabChange: (tab: "system" | "tools") => void;
|
||||
hasTools: boolean;
|
||||
}
|
||||
|
||||
export function TabNavigation({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
hasTools,
|
||||
}: TabNavigationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex border-b mb-2" role="tablist">
|
||||
<TabButton
|
||||
isActive={activeTab === "system"}
|
||||
onClick={() => onTabChange("system")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB")}
|
||||
</TabButton>
|
||||
{hasTools && (
|
||||
<TabButton
|
||||
isActive={activeTab === "tools"}
|
||||
onClick={() => onTabChange("tools")}
|
||||
>
|
||||
{t("SYSTEM_MESSAGE_MODAL$TOOLS_TAB")}
|
||||
</TabButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface ToggleButtonProps {
|
||||
title: string;
|
||||
isExpanded: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ToggleButton({
|
||||
title,
|
||||
isExpanded,
|
||||
onClick,
|
||||
className,
|
||||
}: ToggleButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors ${className || ""}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Typography.Text className="font-bold text-gray-100">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className="text-gray-300">
|
||||
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
||||
</Typography.Text>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { ToolParameters } from "./tool-parameters";
|
||||
import { ToggleButton } from "./toggle-button";
|
||||
|
||||
interface FunctionData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ToolData {
|
||||
type?: string;
|
||||
function?: FunctionData;
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ToolItemProps {
|
||||
tool: Record<string, unknown>;
|
||||
index: number;
|
||||
isExpanded: boolean;
|
||||
onToggle: (index: number) => void;
|
||||
}
|
||||
|
||||
export function ToolItem({ tool, index, isExpanded, onToggle }: ToolItemProps) {
|
||||
// Extract function data from the nested structure
|
||||
const toolData = tool as ToolData;
|
||||
const functionData = toolData.function || toolData;
|
||||
const name =
|
||||
functionData.name ||
|
||||
(toolData.type === "function" && toolData.function?.name) ||
|
||||
"";
|
||||
const description =
|
||||
functionData.description ||
|
||||
(toolData.type === "function" && toolData.function?.description) ||
|
||||
"";
|
||||
const parameters =
|
||||
functionData.parameters ||
|
||||
(toolData.type === "function" && toolData.function?.parameters) ||
|
||||
null;
|
||||
|
||||
return (
|
||||
<div className="rounded-md overflow-hidden">
|
||||
<ToggleButton
|
||||
title={String(name)}
|
||||
isExpanded={isExpanded}
|
||||
onClick={() => onToggle(index)}
|
||||
/>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
<div className="mt-2 mb-3">
|
||||
<Typography.Text className="text-sm whitespace-pre-wrap text-gray-300 leading-relaxed">
|
||||
{String(description)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* Parameters section */}
|
||||
{parameters && <ToolParameters parameters={parameters} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReactJsonView from "@microlink/react-json-view";
|
||||
import { JSON_VIEW_THEME } from "#/utils/constants";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface ToolParametersProps {
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function ToolParameters({ parameters }: ToolParametersProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Typography.Text className="text-sm font-semibold text-gray-300">
|
||||
{t("SYSTEM_MESSAGE_MODAL$PARAMETERS")}
|
||||
</Typography.Text>
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<ReactJsonView name={false} src={parameters} theme={JSON_VIEW_THEME} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ToolItem } from "./tool-item";
|
||||
|
||||
interface ToolsListProps {
|
||||
tools: Array<Record<string, unknown>>;
|
||||
expandedTools: Record<number, boolean>;
|
||||
onToggleTool: (index: number) => void;
|
||||
}
|
||||
|
||||
export function ToolsList({
|
||||
tools,
|
||||
expandedTools,
|
||||
onToggleTool,
|
||||
}: ToolsListProps) {
|
||||
return (
|
||||
<div className="p-2 space-y-3">
|
||||
{tools.map((tool, index) => (
|
||||
<ToolItem
|
||||
key={index}
|
||||
tool={tool}
|
||||
index={index}
|
||||
isExpanded={expandedTools[index] || false}
|
||||
onToggle={onToggleTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ const typographyVariants = cva("", {
|
||||
variant: {
|
||||
h1: "text-[32px] text-white font-bold leading-5",
|
||||
span: "text-sm font-normal text-white leading-5.5",
|
||||
codeBlock:
|
||||
"font-mono text-sm leading-relaxed text-gray-300 whitespace-pre-wrap",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -62,6 +64,19 @@ export function Text({
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
className,
|
||||
testId,
|
||||
children,
|
||||
}: Omit<TypographyProps, "variant">) {
|
||||
return (
|
||||
<Typography variant="codeBlock" className={className} testId={testId}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// Attach components to Typography for the expected API
|
||||
Typography.H1 = H1;
|
||||
Typography.Text = Text;
|
||||
Typography.CodeBlock = CodeBlock;
|
||||
|
||||
Reference in New Issue
Block a user