diff --git a/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx b/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx
index e256384bf78f..855c2b0e9c61 100644
--- a/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx
+++ b/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx
@@ -3,6 +3,7 @@ import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax/browser";
import remarkGfm from "remark-gfm";
import type { ContentType } from "@/types/chat";
+import { extractLanguage, isCodeBlock } from "@/utils/codeBlockUtils";
import ForwardedIconComponent from "../../common/genericIconComponent";
import SimplifiedCodeTabComponent from "../codeTabsComponent";
import DurationDisplay from "./DurationDisplay";
@@ -74,7 +75,6 @@ export default function ContentDisplay({
return <>{props.children}>;
},
code: ({ node, className, children, ...props }) => {
- const inline = !(props as any).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@@ -90,14 +90,16 @@ export default function ContentDisplay({
}
}
- const match = /language-(\w+)/.exec(className || "");
+ if (isCodeBlock(className, props, content)) {
+ return (
+
{content}
@@ -171,16 +173,16 @@ export default function ContentDisplay({
return
{children}
diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/content-view.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/content-view.tsx
index 31ac6be427df..dc78b85cc372 100644
--- a/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/content-view.tsx
+++ b/src/frontend/src/modals/IOModal/components/chatView/chatMessage/components/content-view.tsx
@@ -3,6 +3,7 @@ import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import { TextShimmer } from "@/components/ui/TextShimmer";
+import { extractLanguage, isCodeBlock } from "@/utils/codeBlockUtils";
import { cn } from "@/utils/utils";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent";
import LogoIcon from "./chat-logo-icon";
@@ -121,9 +122,6 @@ export const ErrorView = ({
children,
...props
}) => {
- const inline = !(
- props as any
- ).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@@ -141,19 +139,23 @@ export const ErrorView = ({
}
}
- const match = /language-(\w+)/.exec(
- className || "",
- );
+ if (
+ isCodeBlock(className, props, content)
+ ) {
+ return (
+ {
- const inline = !(props as any).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@@ -88,14 +88,16 @@ export const MarkdownField = ({
}
}
- const match = /language-(\w+)/.exec(className || "");
+ if (isCodeBlock(className, props, content)) {
+ return (
+
+ );
+ }
- return !inline ? (
-
- ) : (
+ return (
{content}
diff --git a/src/frontend/src/utils/__tests__/codeBlockUtils.test.ts b/src/frontend/src/utils/__tests__/codeBlockUtils.test.ts
new file mode 100644
index 000000000000..7e5045197987
--- /dev/null
+++ b/src/frontend/src/utils/__tests__/codeBlockUtils.test.ts
@@ -0,0 +1,172 @@
+import { extractLanguage, isCodeBlock } from "../codeBlockUtils";
+
+describe("codeBlockUtils", () => {
+ describe("isCodeBlock", () => {
+ describe("should return true (block code)", () => {
+ it("should_return_true_when_className_has_language_identifier", () => {
+ const result = isCodeBlock("language-python", {}, "print('hello')");
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_className_has_language_javascript", () => {
+ const result = isCodeBlock("language-javascript", {}, "const x = 1");
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_className_has_language_with_other_classes", () => {
+ const result = isCodeBlock(
+ "hljs language-typescript some-other-class",
+ {},
+ "const x: number = 1",
+ );
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_props_has_data_language", () => {
+ const result = isCodeBlock(
+ undefined,
+ { "data-language": "python" },
+ "print('hello')",
+ );
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_props_has_data_language_empty_string", () => {
+ const result = isCodeBlock(undefined, { "data-language": "" }, "code");
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_content_has_newlines", () => {
+ const result = isCodeBlock(undefined, {}, "line1\nline2\nline3");
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_content_has_single_newline", () => {
+ const result = isCodeBlock(undefined, {}, "line1\nline2");
+ expect(result).toBe(true);
+ });
+
+ it("should_return_true_when_multiple_conditions_are_met", () => {
+ const result = isCodeBlock(
+ "language-python",
+ { "data-language": "python" },
+ "def hello():\n print('world')",
+ );
+ expect(result).toBe(true);
+ });
+ });
+
+ describe("should return false (inline code)", () => {
+ it("should_return_false_when_no_language_class_no_data_language_no_newlines", () => {
+ const result = isCodeBlock(undefined, {}, "inline code");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_className_is_empty_string", () => {
+ const result = isCodeBlock("", {}, "simple code");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_className_has_no_language_prefix", () => {
+ const result = isCodeBlock("hljs some-class", {}, "code");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_props_is_undefined", () => {
+ const result = isCodeBlock(undefined, undefined, "inline");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_props_is_empty_object", () => {
+ const result = isCodeBlock(undefined, {}, "x = 1");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_content_is_single_line", () => {
+ const result = isCodeBlock(undefined, {}, "print('hello')");
+ expect(result).toBe(false);
+ });
+
+ it("should_return_false_when_content_is_empty", () => {
+ const result = isCodeBlock(undefined, {}, "");
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should_handle_language_class_with_numbers", () => {
+ const result = isCodeBlock("language-es2020", {}, "code");
+ expect(result).toBe(true);
+ });
+
+ it("should_handle_content_with_carriage_return_only", () => {
+ // \r alone should not be treated as newline for code block
+ const result = isCodeBlock(undefined, {}, "line1\rline2");
+ expect(result).toBe(false);
+ });
+
+ it("should_handle_content_with_crlf", () => {
+ const result = isCodeBlock(undefined, {}, "line1\r\nline2");
+ expect(result).toBe(true);
+ });
+
+ it("should_handle_content_with_trailing_newline_only", () => {
+ const result = isCodeBlock(undefined, {}, "single line\n");
+ expect(result).toBe(true);
+ });
+
+ it("should_handle_content_with_leading_newline_only", () => {
+ const result = isCodeBlock(undefined, {}, "\nsingle line");
+ expect(result).toBe(true);
+ });
+
+ it("should_handle_null_props_gracefully", () => {
+ // TypeScript would prevent this, but testing runtime safety
+ const result = isCodeBlock(undefined, null as any, "code");
+ expect(result).toBe(false);
+ });
+ });
+ });
+
+ describe("extractLanguage", () => {
+ it("should_extract_python_from_language_class", () => {
+ const result = extractLanguage("language-python");
+ expect(result).toBe("python");
+ });
+
+ it("should_extract_javascript_from_language_class", () => {
+ const result = extractLanguage("language-javascript");
+ expect(result).toBe("javascript");
+ });
+
+ it("should_extract_language_when_mixed_with_other_classes", () => {
+ const result = extractLanguage("hljs language-typescript highlight");
+ expect(result).toBe("typescript");
+ });
+
+ it("should_return_empty_string_when_no_language_class", () => {
+ const result = extractLanguage("hljs some-class");
+ expect(result).toBe("");
+ });
+
+ it("should_return_empty_string_when_className_is_undefined", () => {
+ const result = extractLanguage(undefined);
+ expect(result).toBe("");
+ });
+
+ it("should_return_empty_string_when_className_is_empty", () => {
+ const result = extractLanguage("");
+ expect(result).toBe("");
+ });
+
+ it("should_handle_language_with_numbers", () => {
+ const result = extractLanguage("language-es6");
+ expect(result).toBe("es6");
+ });
+
+ it("should_extract_first_language_when_multiple_present", () => {
+ const result = extractLanguage("language-python language-javascript");
+ expect(result).toBe("python");
+ });
+ });
+});
diff --git a/src/frontend/src/utils/codeBlockUtils.ts b/src/frontend/src/utils/codeBlockUtils.ts
new file mode 100644
index 000000000000..f8284050e3b0
--- /dev/null
+++ b/src/frontend/src/utils/codeBlockUtils.ts
@@ -0,0 +1,30 @@
+/**
+ * Determines if a code element should be rendered as a block (with copy button)
+ * or as inline code.
+ *
+ * A code element is considered a block if any of the following conditions are met:
+ * 1. It has a language class (e.g., "language-python")
+ * 2. It has the "data-language" attribute (from some markdown parsers)
+ * 3. The content contains newlines (multi-line code)
+ */
+export function isCodeBlock(
+ className: string | undefined,
+ props: Record | undefined,
+ content: string,
+): boolean {
+ const languageMatch = /language-(\w+)/.exec(className ?? "");
+ const hasLanguageClass = !!languageMatch;
+ const hasDataLanguage = "data-language" in (props ?? {});
+ const hasNewlines = content.includes("\n");
+
+ return hasLanguageClass || hasDataLanguage || hasNewlines;
+}
+
+/**
+ * Extracts the language identifier from a className.
+ * Returns empty string if no language is found.
+ */
+export function extractLanguage(className: string | undefined): string {
+ const match = /language-(\w+)/.exec(className ?? "");
+ return match?.[1] ?? "";
+}
diff --git a/src/frontend/tests/core/features/freeze-path.spec.ts b/src/frontend/tests/core/features/freeze-path.spec.ts
index 5d3513813d9a..26f34f8c6990 100644
--- a/src/frontend/tests/core/features/freeze-path.spec.ts
+++ b/src/frontend/tests/core/features/freeze-path.spec.ts
@@ -1,4 +1,3 @@
-import { type Page } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { expect, test } from "../../fixtures";
@@ -6,7 +5,6 @@ import { addFlowToTestOnEmptyLangflow } from "../../utils/add-flow-to-test-on-em
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
-import { selectGptModel } from "../../utils/select-gpt-model";
test(
"user must be able to freeze a path",
@@ -37,11 +35,13 @@ test(
await initialGPTsetup(page);
+ // Use unique prompts to avoid OpenAI caching returning identical responses
+ const timestamp = Date.now();
await page
.getByTestId("textarea_str_input_value")
.first()
.fill(
- "say a random number between 1 and 300000 and a random animal that lives in the sea",
+ `say a random number between 1 and 300000 and a random animal that lives in the sea. Request ID: ${timestamp}-1`,
);
await adjustScreenView(page);
@@ -66,8 +66,13 @@ test(
await page.getByText("Close").last().click();
- // Change model to force different output
- await selectGptModel(page);
+ // Change the prompt to ensure different output (avoid OpenAI caching)
+ await page
+ .getByTestId("textarea_str_input_value")
+ .first()
+ .fill(
+ `say a random number between 1 and 300000 and a random animal that lives in the sea. Request ID: ${timestamp}-2`,
+ );
await page.waitForSelector('[data-testid="button_run_chat output"]', {
timeout: 3000,
@@ -133,27 +138,3 @@ test(
expect(secondRandomTextGeneratedByAI).toEqual(thirdRandomTextGeneratedByAI);
},
);
-
-async function _moveSlider(
- page: Page,
- side: "left" | "right",
- advanced: boolean = false,
-) {
- const thumbSelector = `slider_thumb${advanced ? "_advanced" : ""}`;
- const trackSelector = `slider_track${advanced ? "_advanced" : ""}`;
-
- await page.getByTestId(thumbSelector).click();
-
- const trackBoundingBox = await page.getByTestId(trackSelector).boundingBox();
-
- if (trackBoundingBox) {
- const moveDistance =
- trackBoundingBox.width * 0.1 * (side === "left" ? -1 : 1);
- const centerX = trackBoundingBox.x + trackBoundingBox.width / 2;
- const centerY = trackBoundingBox.y + trackBoundingBox.height / 2;
-
- await page.mouse.move(centerX + moveDistance, centerY);
- await page.mouse.down();
- await page.mouse.up();
- }
-}
diff --git a/src/frontend/tests/core/integrations/Research Translation Loop.spec.ts b/src/frontend/tests/core/integrations/Research Translation Loop.spec.ts
index e899c30b2ccd..e9a510cf7e0b 100644
--- a/src/frontend/tests/core/integrations/Research Translation Loop.spec.ts
+++ b/src/frontend/tests/core/integrations/Research Translation Loop.spec.ts
@@ -4,6 +4,7 @@ import { expect, test } from "../../fixtures";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
+import { selectGptModel } from "../../utils/select-gpt-model";
withEventDeliveryModes(
"Research Translation Loop.spec",
@@ -31,25 +32,13 @@ withEventDeliveryModes(
await initialGPTsetup(page, {
skipAdjustScreenView: true,
- skipAddNewApiKeys: true,
skipSelectGptModel: true,
});
// TODO: Uncomment this when we have a way to test Anthropic
// await page.getByTestId("dropdown_str_provider").click();
// await page.getByTestId("Anthropic-1-option").click();
- await page
- .getByTestId("popover-anchor-input-api_key")
- .last()
- .fill(process.env.OPENAI_API_KEY ?? "");
-
- await page.waitForSelector('[data-testid="dropdown_str_model_name"]', {
- timeout: 5000,
- });
-
- await page.getByTestId("dropdown_str_model_name").click();
-
- await page.keyboard.press("Enter");
+ await selectGptModel(page);
await page.getByTestId("playground-btn-flow-io").click();
diff --git a/src/frontend/tests/extended/regression/generalBugs-shard-3.spec.ts b/src/frontend/tests/extended/regression/generalBugs-shard-3.spec.ts
index 996199872aa4..1cebbad2d061 100644
--- a/src/frontend/tests/extended/regression/generalBugs-shard-3.spec.ts
+++ b/src/frontend/tests/extended/regression/generalBugs-shard-3.spec.ts
@@ -59,6 +59,10 @@ test(
await initialGPTsetup(page);
await adjustScreenView(page);
+ await page
+ .getByTestId("popover-anchor-input-api_key")
+ .fill(process.env.OPENAI_API_KEY || "");
+
await page
.getByTestId("handle-chatinput-noshownode-chat message-source")
.click();
diff --git a/src/frontend/tests/utils/add-open-ai-input-key.ts b/src/frontend/tests/utils/add-open-ai-input-key.ts
new file mode 100644
index 000000000000..413c443b2b83
--- /dev/null
+++ b/src/frontend/tests/utils/add-open-ai-input-key.ts
@@ -0,0 +1,20 @@
+import type { Page } from "@playwright/test";
+
+export const addOpenAiInputKey = async (page: Page) => {
+ const numberOfOpenAiFields = await page
+ .getByTestId("popover-anchor-input-openai_api_key")
+ .count();
+
+ for (let i = 0; i < numberOfOpenAiFields; i++) {
+ const openAiInput = page
+ .getByTestId("popover-anchor-input-openai_api_key")
+ .nth(i);
+ const inputValue = await openAiInput.inputValue();
+
+ if (!inputValue) {
+ await openAiInput.fill(process.env.OPENAI_API_KEY!);
+ }
+
+ await page.waitForTimeout(500);
+ }
+};
diff --git a/src/frontend/tests/utils/adjust-screen-view.ts b/src/frontend/tests/utils/adjust-screen-view.ts
index 009be25fe888..2a05da5ebb69 100644
--- a/src/frontend/tests/utils/adjust-screen-view.ts
+++ b/src/frontend/tests/utils/adjust-screen-view.ts
@@ -9,7 +9,7 @@ export async function adjustScreenView(
} = {},
) {
await page.waitForSelector('[data-testid="canvas_controls_dropdown"]', {
- timeout: 5000,
+ timeout: 30000,
});
let fitViewButton = await page.getByTestId("fit_view").count();
diff --git a/src/frontend/tests/utils/initialGPTsetup.ts b/src/frontend/tests/utils/initialGPTsetup.ts
index 4b01606360ab..cf7ffa5e9e7d 100644
--- a/src/frontend/tests/utils/initialGPTsetup.ts
+++ b/src/frontend/tests/utils/initialGPTsetup.ts
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import { adjustScreenView } from "./adjust-screen-view";
import { selectGptModel } from "./select-gpt-model";
import { updateOldComponents } from "./update-old-components";
+import { addOpenAiInputKey } from "./add-open-ai-input-key";
export async function initialGPTsetup(
page: Page,
@@ -9,6 +10,7 @@ export async function initialGPTsetup(
skipAdjustScreenView?: boolean;
skipUpdateOldComponents?: boolean;
skipSelectGptModel?: boolean;
+ skipAddOpenAiInputKey?: boolean;
},
) {
if (!options?.skipAdjustScreenView) {
@@ -20,4 +22,7 @@ export async function initialGPTsetup(
if (!options?.skipSelectGptModel) {
await selectGptModel(page);
}
+ if (!options?.skipAddOpenAiInputKey) {
+ await addOpenAiInputKey(page);
+ }
}
diff --git a/src/frontend/tests/utils/select-gpt-model.ts b/src/frontend/tests/utils/select-gpt-model.ts
index c64a4e0d53e2..783c322aaa90 100644
--- a/src/frontend/tests/utils/select-gpt-model.ts
+++ b/src/frontend/tests/utils/select-gpt-model.ts
@@ -9,11 +9,14 @@ export const selectGptModel = async (page: Page) => {
const gptOMiniOption = await page.getByTestId("gpt-4o-mini-option").count();
+ await page.waitForTimeout(500);
+
if (gptOMiniOption === 0) {
await page.getByTestId("manage-model-providers").click();
await page.waitForSelector("text=Model providers", { timeout: 30000 });
await page.getByTestId("provider-item-OpenAI").click();
+ await page.waitForTimeout(500);
const checkExistingKey = await page.getByTestId("input-end-icon").count();
if (checkExistingKey === 0) {
@@ -26,6 +29,8 @@ export const selectGptModel = async (page: Page) => {
await page.getByTestId("llm-toggle-gpt-4o-mini").click();
await page.getByText("Close").last().click();
} else {
+ await page.waitForTimeout(500);
+
const isChecked = await page
.getByTestId("llm-toggle-gpt-4o-mini")
.isChecked();
@@ -36,7 +41,7 @@ export const selectGptModel = async (page: Page) => {
await page.getByTestId("model_model").nth(i).click();
}
}
-
+ await page.waitForTimeout(500);
await page.getByTestId("gpt-4o-mini-option").click();
}
};