refactor(frontend): microagents modal (#10970)

This commit is contained in:
Hiep Le 2025-09-16 22:32:23 +07:00 committed by GitHub
parent 0f1780728e
commit 3c2acad28d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 323 additions and 101 deletions

View File

@ -0,0 +1,35 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
import { Pre } from "#/ui/pre";
interface MicroagentContentProps {
content: string;
}
export function MicroagentContent({ content }: MicroagentContentProps) {
const { t } = useTranslation();
return (
<div className="mt-2">
<Typography.Text className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</Typography.Text>
<Pre
size="default"
font="mono"
lineHeight="relaxed"
background="dark"
textColor="light"
padding="medium"
borderRadius="medium"
shadow="inner"
maxHeight="small"
overflow="auto"
className="mt-2"
>
{content || t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</Pre>
</div>
);
}

View File

@ -0,0 +1,52 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import { Microagent } from "#/api/open-hands.types";
import { Typography } from "#/ui/typography";
import { MicroagentTriggers } from "./microagent-triggers";
import { MicroagentContent } from "./microagent-content";
interface MicroagentItemProps {
agent: Microagent;
isExpanded: boolean;
onToggle: (agentName: string) => void;
}
export function MicroagentItem({
agent,
isExpanded,
onToggle,
}: MicroagentItemProps) {
return (
<div className="rounded-md overflow-hidden">
<button
type="button"
onClick={() => onToggle(agent.name)}
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">
<Typography.Text className="font-bold text-gray-100">
{agent.name}
</Typography.Text>
</div>
<div className="flex items-center">
<Typography.Text className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</Typography.Text>
<Typography.Text className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</Typography.Text>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
<MicroagentTriggers triggers={agent.triggers} />
<MicroagentContent content={agent.content} />
</div>
)}
</div>
);
}

View File

@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
interface MicroagentTriggersProps {
triggers: string[];
}
export function MicroagentTriggers({ triggers }: MicroagentTriggersProps) {
const { t } = useTranslation();
if (!triggers || triggers.length === 0) {
return null;
}
return (
<div className="mt-2 mb-3">
<Typography.Text className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</Typography.Text>
<div className="flex flex-wrap gap-1">
{triggers.map((trigger) => (
<Typography.Text
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</Typography.Text>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
interface MicroagentsEmptyStateProps {
isError: boolean;
}
export function MicroagentsEmptyState({ isError }: MicroagentsEmptyStateProps) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center h-full p-4">
<Typography.Text className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</Typography.Text>
</div>
);
}

View File

@ -0,0 +1,7 @@
export function MicroagentsLoadingState() {
return (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
);
}

View File

@ -0,0 +1,45 @@
import { useTranslation } from "react-i18next";
import { RefreshCw } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "../settings/brand-button";
interface MicroagentsModalHeaderProps {
isAgentReady: boolean;
isLoading: boolean;
isRefetching: boolean;
onRefresh: () => void;
}
export function MicroagentsModalHeader({
isAgentReady,
isLoading,
isRefetching,
onRefresh,
}: MicroagentsModalHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6 w-full">
<div className="flex items-center justify-between w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
{isAgentReady && (
<BrandButton
testId="refresh-microagents"
type="button"
variant="primary"
className="flex items-center gap-2"
onClick={onRefresh}
isDisabled={isLoading || isRefetching}
>
<RefreshCw
size={16}
className={`${isRefetching ? "animate-spin" : ""}`}
/>
{t(I18nKey.BUTTON$REFRESH)}
</BrandButton>
)}
</div>
</div>
);
}

View File

@ -1,15 +1,17 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { I18nKey } from "#/i18n/declaration";
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { BrandButton } from "../settings/brand-button";
import { Typography } from "#/ui/typography";
import { MicroagentsModalHeader } from "./microagents-modal-header";
import { MicroagentsLoadingState } from "./microagents-loading-state";
import { MicroagentsEmptyState } from "./microagents-empty-state";
import { MicroagentItem } from "./microagent-item";
interface MicroagentsModalProps {
onClose: () => void;
@ -47,57 +49,34 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
className="max-h-[80vh] flex flex-col items-start"
testID="microagents-modal"
>
<div className="flex flex-col gap-6 w-full">
<div className="flex items-center justify-between w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
{isAgentReady && (
<BrandButton
testId="refresh-microagents"
type="button"
variant="primary"
className="flex items-center gap-2"
onClick={refetch}
isDisabled={isLoading || isRefetching}
>
<RefreshCw
size={16}
className={`${isRefetching ? "animate-spin" : ""}`}
/>
{t(I18nKey.BUTTON$REFRESH)}
</BrandButton>
)}
</div>
</div>
<MicroagentsModalHeader
isAgentReady={isAgentReady}
isLoading={isLoading}
isRefetching={isRefetching}
onRefresh={refetch}
/>
{isAgentReady && (
<span className="text-sm text-gray-400">
<Typography.Text className="text-sm text-gray-400">
{t(I18nKey.MICROAGENTS_MODAL$WARNING)}
</span>
</Typography.Text>
)}
<div className="w-full h-[60vh] overflow-auto rounded-md">
<div className="w-full h-[60vh] overflow-auto rounded-md custom-scrollbar-always">
{!isAgentReady && (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
<Typography.Text>
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
</Typography.Text>
</div>
)}
{isLoading && (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
)}
{isLoading && <MicroagentsLoadingState />}
{!isLoading &&
isAgentReady &&
(isError || !microagents || microagents.length === 0) && (
<div className="flex items-center justify-center h-full p-4">
<p className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</p>
</div>
<MicroagentsEmptyState isError={isError} />
)}
{!isLoading &&
@ -109,68 +88,12 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
const isExpanded = expandedAgents[agent.name] || false;
return (
<div
<MicroagentItem
key={agent.name}
className="rounded-md overflow-hidden"
>
<button
type="button"
onClick={() => toggleAgent(agent.name)}
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">
{agent.name}
</h3>
</div>
<div className="flex items-center">
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</span>
<span className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</span>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
{agent.triggers && agent.triggers.length > 0 && (
<div className="mt-2 mb-3">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</h4>
<div className="flex flex-wrap gap-1">
{agent.triggers.map((trigger) => (
<span
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</span>
))}
</div>
</div>
)}
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
{agent.content ||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</pre>
</div>
</div>
</div>
)}
</div>
agent={agent}
isExpanded={isExpanded}
onToggle={toggleAgent}
/>
);
})}
</div>

106
frontend/src/ui/pre.tsx Normal file
View File

@ -0,0 +1,106 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "#/utils/utils";
const preVariants = cva("whitespace-pre-wrap", {
variants: {
size: {
default: "text-sm",
small: "text-xs",
},
font: {
default: "",
mono: "font-mono",
},
lineHeight: {
default: "",
relaxed: "leading-relaxed",
},
background: {
default: "",
dark: "bg-gray-900",
},
textColor: {
default: "",
light: "text-gray-300",
},
padding: {
default: "",
medium: "p-3",
large: "px-5",
},
borderRadius: {
default: "",
medium: "rounded-md",
},
shadow: {
default: "",
inner: "shadow-inner",
},
maxHeight: {
default: "",
small: "max-h-[400px]",
large: "max-h-[60vh]",
},
overflow: {
default: "",
auto: "overflow-auto",
},
},
defaultVariants: {
size: "default",
font: "default",
lineHeight: "default",
background: "default",
textColor: "default",
padding: "default",
borderRadius: "default",
shadow: "default",
maxHeight: "default",
overflow: "default",
},
});
interface PreProps extends VariantProps<typeof preVariants> {
className?: string;
testId?: string;
children: React.ReactNode;
}
export function Pre({
size,
font,
lineHeight,
background,
textColor,
padding,
borderRadius,
shadow,
maxHeight,
overflow,
className,
testId,
children,
}: PreProps) {
return (
<pre
data-testid={testId}
className={cn(
preVariants({
size,
font,
lineHeight,
background,
textColor,
padding,
borderRadius,
shadow,
maxHeight,
overflow,
}),
className,
)}
>
{children}
</pre>
);
}