Skip to content

Commit

Permalink
Fix issue #5716: [Feature Request]: Add the button to jump to file ed…
Browse files Browse the repository at this point in the history
…its view
  • Loading branch information
openhands-agent committed Dec 20, 2024
1 parent 7a0488c commit d1016c5
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 8 deletions.
22 changes: 15 additions & 7 deletions frontend/__tests__/components/chat-message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, test } from "vitest";
import { ChatMessage } from "#/components/features/chat/chat-message";
import { FilesProvider } from "#/context/files";

describe("ChatMessage", () => {
const renderWithProvider = (ui: React.ReactElement) => {
return render(
<FilesProvider>
{ui}
</FilesProvider>
);
};
it("should render a user message", () => {
render(<ChatMessage type="user" message="Hello, World!" />);
renderWithProvider(<ChatMessage type="user" message="Hello, World!" />);
expect(screen.getByTestId("user-message")).toBeInTheDocument();
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
});

it("should render an assistant message", () => {
render(<ChatMessage type="assistant" message="Hello, World!" />);
renderWithProvider(<ChatMessage type="assistant" message="Hello, World!" />);
expect(screen.getByTestId("assistant-message")).toBeInTheDocument();
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
});

it.skip("should support code syntax highlighting", () => {
const code = "```js\nconsole.log('Hello, World!')\n```";
render(<ChatMessage type="user" message={code} />);
renderWithProvider(<ChatMessage type="user" message={code} />);

// SyntaxHighlighter breaks the code blocks into "tokens"
expect(screen.getByText("console")).toBeInTheDocument();
Expand All @@ -30,7 +38,7 @@ describe("ChatMessage", () => {

it("should render the copy to clipboard button when the user hovers over the message", async () => {
const user = userEvent.setup();
render(<ChatMessage type="user" message="Hello, World!" />);
renderWithProvider(<ChatMessage type="user" message="Hello, World!" />);
const message = screen.getByText("Hello, World!");

expect(screen.getByTestId("copy-to-clipboard")).not.toBeVisible();
Expand All @@ -42,7 +50,7 @@ describe("ChatMessage", () => {

it("should copy content to clipboard", async () => {
const user = userEvent.setup();
render(<ChatMessage type="user" message="Hello, World!" />);
renderWithProvider(<ChatMessage type="user" message="Hello, World!" />);
const copyToClipboardButton = screen.getByTestId("copy-to-clipboard");

await user.click(copyToClipboardButton);
Expand All @@ -63,7 +71,7 @@ describe("ChatMessage", () => {
function Component() {
return <div data-testid="custom-component">Custom Component</div>;
}
render(
renderWithProvider(
<ChatMessage type="user" message="Hello, World">
<Component />
</ChatMessage>,
Expand All @@ -72,7 +80,7 @@ describe("ChatMessage", () => {
});

it("should apply correct styles to inline code", () => {
render(<ChatMessage type="assistant" message="Here is some `inline code` text" />);
renderWithProvider(<ChatMessage type="assistant" message="Here is some `inline code` text" />);
const codeElement = screen.getByText("inline code");

expect(codeElement.tagName.toLowerCase()).toBe("code");
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/features/chat/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import { code } from "../markdown/code";
import { cn } from "#/utils/utils";
import { ul, ol } from "../markdown/list";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
import { JumpToFileButton } from "#/components/shared/buttons/jump-to-file-button";
import { anchor } from "../markdown/anchor";
import { useFiles } from "#/context/files";

interface ChatMessageProps {
type: "user" | "assistant";
message: string;
filePath?: string;
}

export function ChatMessage({
type,
message,
filePath,
children,
}: React.PropsWithChildren<ChatMessageProps>) {
const [isHovering, setIsHovering] = React.useState(false);
const [isCopy, setIsCopy] = React.useState(false);
const { selectFile } = useFiles();

const handleCopyToClipboard = async () => {
await navigator.clipboard.writeText(message);
Expand Down Expand Up @@ -57,6 +62,13 @@ export function ChatMessage({
onClick={handleCopyToClipboard}
mode={isCopy ? "copied" : "copy"}
/>
{filePath && (
<JumpToFileButton
filePath={filePath}
onClick={() => selectFile(filePath)}
isDisabled={!filePath}
/>
)}
<Markdown
className="text-sm overflow-auto"
components={{
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/features/chat/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function Messages({
}

return (
<ChatMessage key={index} type={message.sender} message={message.content}>
<ChatMessage key={index} type={message.sender} message={message.content} filePath={message.filePath}>

Check failure on line 29 in frontend/src/components/features/chat/messages.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Replace `·key={index}·type={message.sender}·message={message.content}·filePath={message.filePath}` with `⏎········key={index}⏎········type={message.sender}⏎········message={message.content}⏎········filePath={message.filePath}⏎······`
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/shared/buttons/jump-to-file-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { VscGoToFile } from "react-icons/vsc";
import { I18nKey } from "#/i18n/declaration";
import { ActionTooltip } from "#/components/shared/action-tooltip";
import { cn } from "#/utils/utils";

interface JumpToFileButtonProps {
filePath: string;
onClick: () => void;
isDisabled?: boolean;
}

export function JumpToFileButton({
filePath,
onClick,
isDisabled = false,
}: JumpToFileButtonProps) {
const { t } = useTranslation();

return (
<ActionTooltip
content={t(I18nKey.CHAT$JUMP_TO_FILE_TOOLTIP, { path: filePath })}
side="top"
>
<button

Check failure on line 26 in frontend/src/components/shared/buttons/jump-to-file-button.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Missing an explicit type attribute for button
data-testid="jump-to-file-button"
onClick={onClick}
disabled={isDisabled}
className={cn(
"absolute top-2 right-12 p-2 rounded-lg",
"text-neutral-400 hover:text-neutral-200 hover:bg-neutral-700",
"transition-colors duration-200",
isDisabled && "opacity-50 cursor-not-allowed"

Check failure on line 34 in frontend/src/components/shared/buttons/jump-to-file-button.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Insert `,`
)}
>
<VscGoToFile size={16} />
</button>
</ActionTooltip>
);
}
14 changes: 14 additions & 0 deletions frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,20 @@
"fr": "Inconnu",
"tr": "Bilinmeyen"
},
"CHAT$JUMP_TO_FILE_TOOLTIP": {
"en": "Jump to file: {{path}}",
"zh-CN": "跳转到文件:{{path}}",
"de": "Zur Datei springen: {{path}}",
"ko-KR": "파일로 이동: {{path}}",
"no": "Hopp til fil: {{path}}",
"zh-TW": "跳轉到文件:{{path}}",
"it": "Vai al file: {{path}}",
"pt": "Ir para o arquivo: {{path}}",
"es": "Ir al archivo: {{path}}",
"ar": "انتقل إلى الملف: {{path}}",
"fr": "Aller au fichier: {{path}}",
"tr": "Dosyaya git: {{path}}"
},
"SECURITY_ANALYZER$UNKNOWN_RISK": {
"en": "Unknown Risk",
"de": "Unbekanntes Risiko",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type Message = {
pending?: boolean;
translationID?: string;
eventID?: number;
filePath?: string;
};
1 change: 1 addition & 0 deletions frontend/src/state/chat-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const chatSlice = createSlice({
} else if (observationID === "read" || observationID === "edit") {
const { content } = observation.payload;
causeMessage.content = `\`\`\`${observationID === "edit" ? "diff" : "python"}\n${content}\n\`\`\``; // Content is already truncated by the ACI
causeMessage.filePath = observation.payload.extras.path;
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {
Expand Down

0 comments on commit d1016c5

Please sign in to comment.