mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
4 Commits
openhands/
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe92626a24 | ||
|
|
f08f934349 | ||
|
|
09b0d4dfcb | ||
|
|
66db570079 |
@@ -14,6 +14,8 @@ import { renderWithProviders } from "test-utils";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-trajectory";
|
||||
|
||||
// We'll use the actual i18next implementation but override the translation function
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
@@ -29,7 +31,9 @@ vi.mock("react-i18next", async () => {
|
||||
const translations: Record<string, string> = {
|
||||
"CONVERSATION$CREATED": "Created",
|
||||
"CONVERSATION$AGO": "ago",
|
||||
"CONVERSATION$UPDATED": "Updated"
|
||||
"CONVERSATION$UPDATED": "Updated",
|
||||
"CONVERSATION$DOWNLOAD_ERROR": "Download Error",
|
||||
"CONVERSATION$UNKNOWN": "Unknown"
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -45,6 +49,29 @@ describe("ConversationCard", () => {
|
||||
const onDelete = vi.fn();
|
||||
const onChangeTitle = vi.fn();
|
||||
|
||||
// Mock the useGetTrajectory hook
|
||||
vi.mock("#/hooks/mutation/use-get-trajectory", () => ({
|
||||
useGetTrajectory: vi.fn().mockReturnValue({
|
||||
mutate: vi.fn((id, options) => {
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess({ trajectory: [{ test: "data" }] });
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the downloadTrajectory function
|
||||
vi.mock("#/utils/download-trajectory", () => ({
|
||||
downloadTrajectory: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock posthog
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
capture: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal("window", {
|
||||
open: vi.fn(),
|
||||
@@ -348,6 +375,52 @@ describe("ConversationCard", () => {
|
||||
const newMenu = await screen.findByTestId("context-menu");
|
||||
within(newMenu).getByTestId("display-cost-button");
|
||||
});
|
||||
|
||||
it("should show export trajectory button only when showOptions and conversationId are true", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = renderWithProviders(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
isActive
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
// Wait for context menu to appear
|
||||
const menu = await screen.findByTestId("context-menu");
|
||||
expect(
|
||||
within(menu).queryByTestId("export-trajectory-button"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Close menu
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
showOptions
|
||||
isActive
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
conversationId="test-conversation-id"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open menu again
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
// Wait for context menu to appear and check for export trajectory button
|
||||
const newMenu = await screen.findByTestId("context-menu");
|
||||
within(newMenu).getByTestId("export-trajectory-button");
|
||||
});
|
||||
|
||||
it("should show metrics modal when clicking the display cost button", async () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -374,6 +447,46 @@ describe("ConversationCard", () => {
|
||||
// Verify if metrics modal is displayed by checking for the modal content
|
||||
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call export trajectory when clicking the export trajectory button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getTrajectoryMock = useGetTrajectory as unknown as {
|
||||
(): { mutate: ReturnType<typeof vi.fn> };
|
||||
};
|
||||
const downloadTrajectoryMock = downloadTrajectory as ReturnType<typeof vi.fn>;
|
||||
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
showOptions
|
||||
conversationId="test-conversation-id"
|
||||
/>,
|
||||
);
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const exportTrajectoryButton = within(menu).getByTestId("export-trajectory-button");
|
||||
|
||||
await user.click(exportTrajectoryButton);
|
||||
|
||||
// Verify that the trajectory was fetched and downloaded
|
||||
expect(getTrajectoryMock().mutate).toHaveBeenCalledWith(
|
||||
"test-conversation-id",
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
})
|
||||
);
|
||||
|
||||
// Check that downloadTrajectory was called
|
||||
expect(downloadTrajectoryMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not display the edit or delete options if the handler is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ConversationCardContextMenuProps {
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onExportTrajectory?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ export function ConversationCardContextMenu({
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
onDownloadViaVSCode,
|
||||
onExportTrajectory,
|
||||
position = "bottom",
|
||||
}: ConversationCardContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
@@ -68,6 +70,14 @@ export function ConversationCardContextMenu({
|
||||
Show Agent Tools & Metadata
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onExportTrajectory && (
|
||||
<ContextMenuListItem
|
||||
testId="export-trajectory-button"
|
||||
onClick={onExportTrajectory}
|
||||
>
|
||||
Export Trajectory
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { isSystemMessage } from "#/types/core/guards";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-trajectory";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
@@ -60,6 +63,7 @@ export function ConversationCard({
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const { mutate: getTrajectory } = useGetTrajectory();
|
||||
|
||||
const systemMessage = parsedEvents.find(isSystemMessage);
|
||||
|
||||
@@ -142,6 +146,33 @@ export function ConversationCard({
|
||||
setSystemModalVisible(true);
|
||||
};
|
||||
|
||||
const handleExportTrajectory = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
posthog.capture("export_trajectory_button_clicked");
|
||||
|
||||
if (!conversationId) {
|
||||
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
getTrajectory(conversationId, {
|
||||
onSuccess: async (data) => {
|
||||
await downloadTrajectory(
|
||||
conversationId ?? t(I18nKey.CONVERSATION$UNKNOWN),
|
||||
data.trajectory,
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
|
||||
},
|
||||
});
|
||||
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (titleMode === "edit") {
|
||||
inputRef.current?.focus();
|
||||
@@ -225,6 +256,9 @@ export function ConversationCard({
|
||||
? handleShowAgentTools
|
||||
: undefined
|
||||
}
|
||||
onExportTrajectory={
|
||||
conversationId ? handleExportTrajectory : undefined
|
||||
}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user