fix: Fix image preview for Windows paths in playground (#12083)

* fix image sent on windows playground

* [autofix.ci] apply automated fixes

* fix jest

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa
2026-03-10 14:29:02 -03:00
committed by GitHub
parent 351b943a1b
commit 9ce2b894fe
5 changed files with 593 additions and 86 deletions

View File

@@ -180,13 +180,13 @@ describe("ChatMessage Component", () => {
chat: {
...mockChat,
message: "",
files: ["/path/to/file.jpg"],
files: ["/path/to/file.pdf"],
},
};
render(<ChatMessage {...propsWithFiles} />);
// Should render UserMessage with file preview (shows loading icon for files)
expect(screen.getByTestId("loading-icon")).toBeInTheDocument();
// Should render UserMessage with file preview (non-image files show File icon)
expect(screen.getByTestId("forwarded-icon")).toBeInTheDocument();
});
it("renders bot message when no text and no files", () => {

View File

@@ -0,0 +1,549 @@
import { getBaseUrl } from "@/customization/utils/urls";
import {
extractFileInfo,
formatFileName,
getFileDisplayName,
getFilePreviewUrl,
isImageFile,
} from "../file-utils";
// Mock the getBaseUrl function
jest.mock("@/customization/utils/urls", () => ({
getBaseUrl: jest.fn(() => "http://localhost:3000/api/v1/"),
}));
const mockGetBaseUrl = getBaseUrl as jest.MockedFunction<typeof getBaseUrl>;
beforeEach(() => {
mockGetBaseUrl.mockReturnValue("http://localhost:3000/api/v1/");
});
describe("file-utils", () => {
describe("isImageFile", () => {
describe("File object detection", () => {
it("should_return_true_for_browser_File_with_image_mime_type", () => {
const file = new File(["content"], "test.jpg", {
type: "image/jpeg",
});
expect(isImageFile(file)).toBe(true);
});
it("should_return_true_for_browser_File_with_image_prefix", () => {
const file = new File(["content"], "test.png", {
type: "image/png",
});
expect(isImageFile(file)).toBe(true);
});
it("should_return_false_for_browser_File_with_text_type", () => {
const file = new File(["content"], "test.txt", {
type: "text/plain",
});
expect(isImageFile(file)).toBe(false);
});
});
describe("Windows path normalization", () => {
it("should_detect_image_from_Windows_backslash_path", () => {
const windowsPath = "C:\\Users\\test\\image.jpg";
expect(isImageFile(windowsPath)).toBe(true);
});
it("should_detect_image_from_nested_Windows_path", () => {
const windowsPath = "flow123\\subfolder\\image.png";
expect(isImageFile(windowsPath)).toBe(true);
});
it("should_detect_image_from_mixed_path_separators", () => {
const mixedPath = "flow123/subfolder\\image.gif";
expect(isImageFile(mixedPath)).toBe(true);
});
it("should_reject_non_image_from_Windows_path", () => {
const windowsPath = "C:\\Users\\test\\document.pdf";
expect(isImageFile(windowsPath)).toBe(false);
});
});
describe("Unix path handling", () => {
it("should_detect_image_from_Unix_forward_slash_path", () => {
const unixPath = "/home/user/image.jpg";
expect(isImageFile(unixPath)).toBe(true);
});
it("should_detect_image_from_flow_path", () => {
const flowPath = "flow123/image.webp";
expect(isImageFile(flowPath)).toBe(true);
});
});
describe("Object with path and type properties", () => {
it("should_prioritize_path_extension_over_type_property", () => {
const fileObj = {
path: "flow123\\image.jpg",
type: "text/plain", // Conflicting type
};
expect(isImageFile(fileObj)).toBe(true);
});
it("should_fall_back_to_type_property_when_path_extension_invalid", () => {
const fileObj = {
path: "flow123\\unknown",
type: "image/png",
};
expect(isImageFile(fileObj)).toBe(true);
});
it("should_normalize_Windows_paths_in_object", () => {
const fileObj = {
path: "C:\\temp\\folder\\image.bmp",
type: "application/octet-stream",
};
expect(isImageFile(fileObj)).toBe(true);
});
});
describe("Edge cases", () => {
it("should_handle_empty_string_path", () => {
expect(isImageFile("")).toBe(false);
});
it("should_handle_path_without_extension", () => {
expect(isImageFile("just-a-filename")).toBe(false);
});
it("should_handle_path_with_multiple_dots", () => {
expect(isImageFile("file.backup.jpg")).toBe(true);
});
it("should_handle_uppercase_extension", () => {
expect(isImageFile("image.JPG")).toBe(true);
expect(isImageFile("C:\\Images\\photo.PNG")).toBe(true);
});
it("should_handle_null_or_undefined_input", () => {
expect(isImageFile(null as any)).toBe(false);
expect(isImageFile(undefined as any)).toBe(false);
});
});
});
describe("getFileDisplayName", () => {
describe("File object handling", () => {
it("should_return_file_name_from_browser_File", () => {
const file = new File(["content"], "test-image.jpg");
expect(getFileDisplayName(file)).toBe("test-image.jpg");
});
});
describe("Windows path normalization", () => {
it("should_extract_filename_from_Windows_absolute_path", () => {
const windowsPath = "C:\\Users\\username\\Documents\\report.pdf";
expect(getFileDisplayName(windowsPath)).toBe("report.pdf");
});
it("should_extract_filename_from_Windows_relative_path", () => {
const windowsPath = "folder\\subfolder\\image.png";
expect(getFileDisplayName(windowsPath)).toBe("image.png");
});
it("should_handle_mixed_path_separators", () => {
const mixedPath = "flow/data\\files\\document.txt";
expect(getFileDisplayName(mixedPath)).toBe("document.txt");
});
it("should_handle_single_backslash_path", () => {
const path = "flow123\\image.jpg";
expect(getFileDisplayName(path)).toBe("image.jpg");
});
});
describe("Unix path handling", () => {
it("should_extract_filename_from_Unix_path", () => {
const unixPath = "/home/user/documents/file.txt";
expect(getFileDisplayName(unixPath)).toBe("file.txt");
});
it("should_extract_filename_from_relative_Unix_path", () => {
const unixPath = "folder/subfolder/image.png";
expect(getFileDisplayName(unixPath)).toBe("image.png");
});
});
describe("Object with name and path properties", () => {
it("should_prefer_name_property_when_available", () => {
const fileObj = {
name: "custom-name.jpg",
path: "C:\\long\\path\\different-name.jpg",
};
expect(getFileDisplayName(fileObj)).toBe("custom-name.jpg");
});
it("should_extract_from_path_when_no_name_property", () => {
const fileObj = {
path: "flow123\\folder\\image.png",
};
expect(getFileDisplayName(fileObj)).toBe("image.png");
});
});
describe("Edge cases", () => {
it("should_handle_empty_string", () => {
expect(getFileDisplayName("")).toBe("");
});
it("should_return_original_string_when_no_separators", () => {
expect(getFileDisplayName("filename")).toBe("filename");
});
it("should_handle_path_ending_with_separator", () => {
// When path ends with \, it gets normalized to / and split creates empty string at end
expect(getFileDisplayName("folder\\")).toBe("folder\\"); // Falls back to original
expect(getFileDisplayName("folder/")).toBe("folder/"); // Falls back to original
});
});
});
describe("getFilePreviewUrl", () => {
describe("File object handling", () => {
it("should_return_object_URL_for_browser_File", () => {
// Mock URL.createObjectURL
const mockObjectURL = "blob:http://localhost/123-456";
global.URL.createObjectURL = jest.fn(() => mockObjectURL);
const file = new File(["content"], "test.jpg", {
type: "image/jpeg",
});
expect(getFilePreviewUrl(file)).toBe(mockObjectURL);
expect(URL.createObjectURL).toHaveBeenCalledWith(file);
});
it("should_return_null_for_non_image_File", () => {
const file = new File(["content"], "test.txt", {
type: "text/plain",
});
expect(getFilePreviewUrl(file)).toBeNull();
});
});
describe("Windows path normalization and URL construction", () => {
it("should_normalize_Windows_path_and_create_URL", () => {
const windowsPath = "flow123\\subfolder\\image.jpg";
const expected =
"http://localhost:3000/api/v1/files/images/flow123/subfolder/image.jpg";
expect(getFilePreviewUrl(windowsPath)).toBe(expected);
});
it("should_handle_absolute_Windows_path", () => {
// Note: This tests the current behavior, but absolute paths shouldn't typically be used
const windowsPath = "C:\\temp\\flow123\\image.png";
const expected =
"http://localhost:3000/api/v1/files/images/C%3A/temp/flow123/image.png";
expect(getFilePreviewUrl(windowsPath)).toBe(expected);
});
it("should_encode_special_characters_in_path_segments", () => {
const pathWithSpaces = "flow 123\\folder name\\image file.jpg";
const expected =
"http://localhost:3000/api/v1/files/images/flow%20123/folder%20name/image%20file.jpg";
expect(getFilePreviewUrl(pathWithSpaces)).toBe(expected);
});
it("should_handle_mixed_path_separators", () => {
const mixedPath = "flow123/data\\images\\photo.png";
const expected =
"http://localhost:3000/api/v1/files/images/flow123/data/images/photo.png";
expect(getFilePreviewUrl(mixedPath)).toBe(expected);
});
});
describe("Unix path handling", () => {
it("should_create_URL_for_Unix_path", () => {
const unixPath = "flow123/images/photo.jpg";
const expected =
"http://localhost:3000/api/v1/files/images/flow123/images/photo.jpg";
expect(getFilePreviewUrl(unixPath)).toBe(expected);
});
});
describe("Object with path property", () => {
it("should_normalize_Windows_path_in_object", () => {
const fileObj = {
path: "flow123\\images\\photo.jpg",
type: "image/jpeg",
};
const expected =
"http://localhost:3000/api/v1/files/images/flow123/images/photo.jpg";
expect(getFilePreviewUrl(fileObj)).toBe(expected);
});
it("should_return_null_for_non_image_object", () => {
const fileObj = {
path: "flow123\\documents\\file.txt",
type: "text/plain",
};
expect(getFilePreviewUrl(fileObj)).toBeNull();
});
});
describe("Edge cases", () => {
it("should_return_null_for_empty_path_string", () => {
expect(getFilePreviewUrl("")).toBeNull();
});
it("should_return_null_for_whitespace_only_path", () => {
expect(getFilePreviewUrl(" ")).toBeNull();
});
it("should_handle_path_with_only_spaces", () => {
expect(getFilePreviewUrl(" ")).toBeNull();
});
it("should_return_null_for_object_with_empty_path", () => {
const fileObj = {
path: "",
type: "image/jpeg",
};
expect(getFilePreviewUrl(fileObj)).toBeNull();
});
});
describe("Base URL variations", () => {
it("should_work_with_different_base_URLs", () => {
mockGetBaseUrl.mockReturnValue("https://example.com/api/");
const path = "flow123\\image.jpg";
const expected =
"https://example.com/api/files/images/flow123/image.jpg";
expect(getFilePreviewUrl(path)).toBe(expected);
});
it("should_handle_base_URL_without_trailing_slash", () => {
mockGetBaseUrl.mockReturnValue("http://localhost:8000/api");
const path = "flow123\\image.jpg";
const expected =
"http://localhost:8000/apifiles/images/flow123/image.jpg";
expect(getFilePreviewUrl(path)).toBe(expected);
});
});
});
describe("formatFileName", () => {
it("should_return_unchanged_when_under_limit", () => {
const name = "short.jpg";
expect(formatFileName(name)).toBe("short.jpg");
});
it("should_truncate_long_basename_with_ellipsis", () => {
const name = "this-is-a-very-long-filename-that-should-be-truncated.jpg";
const result = formatFileName(name, 25);
expect(result).toContain("...");
expect(result).toContain(".jpg");
expect(result).toContain("this-is-a-very-long-filen");
});
it("should_preserve_short_basename_even_when_over_total_limit", () => {
const name = "short.extension";
expect(formatFileName(name, 10)).toBe("short.extension");
});
it("should_handle_files_without_extension", () => {
const name = "filename-without-extension-very-long";
const result = formatFileName(name, 25);
// When no extension is found, lastIndexOf('.') returns -1,
// so baseName becomes entire string, fileExtension becomes the last part
expect(result).toContain("...");
expect(result.length).toBeGreaterThan(25);
});
it("should_use_custom_max_length", () => {
const name = "moderately-long-filename.jpg";
const result = formatFileName(name, 15);
expect(result).toContain("...");
expect(result).toContain(".jpg");
});
});
describe("extractFileInfo", () => {
describe("File object handling", () => {
it("should_extract_info_from_browser_File", () => {
const file = new File(["content"], "test.jpg", {
type: "image/jpeg",
});
const result = extractFileInfo(file);
expect(result).toEqual({
name: "test.jpg",
type: "image/jpeg",
path: "test.jpg",
});
});
});
describe("Windows path normalization", () => {
it("should_extract_info_from_Windows_path_string", () => {
const windowsPath = "C:\\Users\\test\\image.jpg";
const result = extractFileInfo(windowsPath);
expect(result).toEqual({
name: "image.jpg",
type: "jpg",
path: "C:\\Users\\test\\image.jpg",
});
});
it("should_handle_relative_Windows_path", () => {
const windowsPath = "flow123\\subfolder\\document.pdf";
const result = extractFileInfo(windowsPath);
expect(result).toEqual({
name: "document.pdf",
type: "pdf",
path: "flow123\\subfolder\\document.pdf",
});
});
});
describe("Unix path handling", () => {
it("should_extract_info_from_Unix_path", () => {
const unixPath = "flow123/folder/image.png";
const result = extractFileInfo(unixPath);
expect(result).toEqual({
name: "image.png",
type: "png",
path: "flow123/folder/image.png",
});
});
});
describe("Object with properties", () => {
it("should_extract_info_from_file_object", () => {
const fileObj = {
path: "flow123\\image.jpg",
type: "image/jpeg",
name: "my-image.jpg",
};
const result = extractFileInfo(fileObj);
expect(result).toEqual({
name: "my-image.jpg",
type: "image/jpeg",
path: "flow123\\image.jpg",
});
});
});
describe("Edge cases", () => {
it("should_handle_path_without_extension", () => {
const result = extractFileInfo("just-filename");
expect(result).toEqual({
name: "just-filename",
type: "just-filename", // split(".").pop() returns the whole string when no dots
path: "just-filename",
});
});
it("should_handle_empty_path", () => {
const result = extractFileInfo("");
expect(result).toEqual({
name: "",
type: "",
path: "",
});
});
it("should_handle_path_with_multiple_dots", () => {
const result = extractFileInfo("file.backup.tar.gz");
expect(result).toEqual({
name: "file.backup.tar.gz",
type: "gz",
path: "file.backup.tar.gz",
});
});
});
});
describe("Cross-platform compatibility tests", () => {
describe("Mixed environment scenarios", () => {
it("should_consistently_handle_paths_from_Windows_and_Unix", () => {
const windowsPath = "flow123\\images\\photo.jpg";
const unixPath = "flow123/images/photo.jpg";
// Both should be treated as images
expect(isImageFile(windowsPath)).toBe(true);
expect(isImageFile(unixPath)).toBe(true);
// Both should extract the same filename
expect(getFileDisplayName(windowsPath)).toBe("photo.jpg");
expect(getFileDisplayName(unixPath)).toBe("photo.jpg");
// Both should generate equivalent URLs (normalized to forward slashes)
const windowsUrl = getFilePreviewUrl(windowsPath);
const unixUrl = getFilePreviewUrl(unixPath);
expect(windowsUrl).toBe(unixUrl);
});
it("should_handle_complex_nested_paths_consistently", () => {
const complexWindowsPath =
"project\\data\\2024\\Q1\\reports\\image.png";
const complexUnixPath = "project/data/2024/Q1/reports/image.png";
expect(isImageFile(complexWindowsPath)).toBe(true);
expect(isImageFile(complexUnixPath)).toBe(true);
const windowsUrl = getFilePreviewUrl(complexWindowsPath);
const unixUrl = getFilePreviewUrl(complexUnixPath);
expect(windowsUrl).toBe(unixUrl);
});
});
describe("CI environment compatibility", () => {
it("should_work_in_Linux_CI_environment", () => {
// Simulate Linux environment test
const unixStylePath = "flow123/uploads/test-image.jpg";
expect(isImageFile(unixStylePath)).toBe(true);
expect(getFileDisplayName(unixStylePath)).toBe("test-image.jpg");
expect(getFilePreviewUrl(unixStylePath)).toContain(
"flow123/uploads/test-image.jpg",
);
});
it("should_work_in_Windows_CI_environment", () => {
// Simulate Windows environment test
const windowsStylePath = "flow123\\uploads\\test-image.jpg";
expect(isImageFile(windowsStylePath)).toBe(true);
expect(getFileDisplayName(windowsStylePath)).toBe("test-image.jpg");
expect(getFilePreviewUrl(windowsStylePath)).toContain(
"flow123/uploads/test-image.jpg",
);
});
});
});
});

View File

@@ -1,8 +1,6 @@
import { useEffect, useState } from "react";
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import Loading from "@/components/ui/loading";
import { customGetAccessToken } from "@/customization/utils/custom-get-access-token";
import { getFetchCredentials } from "@/customization/utils/get-fetch-credentials";
import { cn } from "@/utils/utils";
import {
extractFileInfo,
@@ -59,68 +57,27 @@ export default function FilePreviewDisplay({
className,
}: FilePreviewDisplayProps) {
const [imageError, setImageError] = useState(false);
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [isLoadingBlob, setIsLoadingBlob] = useState(false);
const previewUrl = getFilePreviewUrl(file);
const fileInfo = extractFileInfo(file);
// Reset error state when file changes
useEffect(() => {
setImageError(false);
setBlobUrl(null);
}, [file]);
// For server file paths (not File objects), fetch and convert to blob URL
// Cleanup blob URLs for File objects
useEffect(() => {
if (
previewUrl &&
!(file instanceof File) &&
!blobUrl &&
!isLoadingBlob &&
!imageError
) {
setIsLoadingBlob(true);
// Prepare fetch options with credentials and auth headers
const accessToken = customGetAccessToken();
const fetchOptions: RequestInit = {
credentials: getFetchCredentials(),
};
if (accessToken) {
fetchOptions.headers = {
Authorization: `Bearer ${accessToken}`,
};
}
fetch(previewUrl, fetchOptions)
.then((response) => {
if (!response.ok) {
throw new Error(
`Failed to fetch image: ${response.status} ${response.statusText}`,
);
}
return response.blob();
})
.then((blob) => {
const url = URL.createObjectURL(blob);
setBlobUrl(url);
setIsLoadingBlob(false);
})
.catch((err) => {
console.error("Failed to load image as blob:", err);
setImageError(true);
setIsLoadingBlob(false);
});
}
// Cleanup blob URL on unmount or when file changes
return () => {
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
// Only cleanup blob URLs from File objects
if (
file instanceof File &&
previewUrl &&
previewUrl.startsWith("blob:")
) {
URL.revokeObjectURL(previewUrl);
}
};
}, [file, previewUrl, blobUrl, isLoadingBlob, imageError]);
}, [file, previewUrl]);
// Compact variant (for input-wrapper)
if (variant === "compact") {
@@ -132,21 +89,17 @@ export default function FilePreviewDisplay({
className,
)}
>
{loading || isLoadingBlob ? (
{loading ? (
<Loading className="h-4 w-4" />
) : previewUrl &&
(file instanceof File ? previewUrl : blobUrl) &&
!imageError ? (
) : previewUrl && !imageError ? (
<img
src={(file instanceof File ? previewUrl : blobUrl) ?? undefined}
src={previewUrl}
alt={fileInfo.name}
className="h-full w-full rounded-md object-cover"
crossOrigin={file instanceof File ? undefined : "use-credentials"}
onError={() => {
setImageError(true);
console.error(
"Failed to load image:",
file instanceof File ? previewUrl : blobUrl,
);
console.error("Failed to load image:", previewUrl);
}}
/>
) : (
@@ -176,21 +129,17 @@ export default function FilePreviewDisplay({
className,
)}
>
{loading || isLoadingBlob ? (
{loading ? (
<Loading className="h-4 w-4" />
) : previewUrl &&
(file instanceof File ? previewUrl : blobUrl) &&
!imageError ? (
) : previewUrl && !imageError ? (
<img
src={(file instanceof File ? previewUrl : blobUrl) ?? undefined}
src={previewUrl}
alt={fileInfo.name}
className="h-full w-full rounded-md object-cover"
crossOrigin={file instanceof File ? undefined : "use-credentials"}
onError={() => {
setImageError(true);
console.error(
"Failed to load image:",
file instanceof File ? previewUrl : blobUrl,
);
console.error("Failed to load image:", previewUrl);
}}
/>
) : (

View File

@@ -30,13 +30,17 @@ export function isImageFile(
);
} else if (typeof file === "string") {
// File path string - extract extension
const extension = file.split(".").pop()?.toLowerCase() || "";
// Normalize Windows paths first
const normalizedPath = file.replace(/\\/g, "/");
const extension = normalizedPath.split(".").pop()?.toLowerCase() || "";
return IMAGE_TYPES.has(extension);
} else if (file && typeof file === "object") {
// Object with type or path property
// For server files, check path extension first (most reliable)
if ("path" in file && file.path) {
const extension = file.path.split(".").pop()?.toLowerCase() || "";
// Normalize Windows paths first
const normalizedPath = file.path.replace(/\\/g, "/");
const extension = normalizedPath.split(".").pop()?.toLowerCase() || "";
if (IMAGE_TYPES.has(extension)) {
return true;
}
@@ -64,12 +68,14 @@ export function getFileDisplayName(
if (file instanceof File) {
return file.name;
} else if (typeof file === "string") {
// Extract name from path
return file.split("/").pop() || file;
// Extract name from path (normalize Windows paths first)
const normalizedPath = file.replace(/\\/g, "/");
return normalizedPath.split("/").pop() || file;
} else if ("name" in file) {
return file.name;
} else if ("path" in file) {
return file.path.split("/").pop() || file.path;
const normalizedPath = file.path.replace(/\\/g, "/");
return normalizedPath.split("/").pop() || file.path;
}
return "";
}
@@ -108,9 +114,9 @@ export function getFilePreviewUrl(
// Browser File object - create object URL
return URL.createObjectURL(file);
} else if (typeof file === "string") {
// Server file path string - path format is "flow_id/filename"
// Encode each path segment to handle spaces and special characters
const path = file.trim();
// Server file path string - path format is "flow_id/filename" or "flow_id\filename" on Windows
// Normalize Windows backslashes to forward slashes
const path = file.trim().replace(/\\/g, "/");
if (!path) return null;
const encodedPath = path
.split("/")
@@ -119,9 +125,9 @@ export function getFilePreviewUrl(
// Explicitly use /api/v1/files/images/ prefix for server file paths
return `${getBaseUrl()}files/images/${encodedPath}`;
} else if ("path" in file) {
// Server file path object - path format is "flow_id/filename"
// Encode each path segment to handle spaces and special characters
const path = file.path.trim();
// Server file path object - path format is "flow_id/filename" or "flow_id\filename" on Windows
// Normalize Windows backslashes to forward slashes
const path = file.path.trim().replace(/\\/g, "/");
if (!path) return null;
const encodedPath = path
.split("/")
@@ -149,8 +155,9 @@ export function extractFileInfo(
path: file.name, // For File objects, path is just the name
};
} else if (typeof file === "string") {
const name = file.split("/").pop() || file;
const type = file.split(".").pop() || "";
const normalizedPath = file.replace(/\\/g, "/");
const name = normalizedPath.split("/").pop() || file;
const type = normalizedPath.split(".").pop() || "";
return { name, type, path: file };
} else {
return {

View File

@@ -29,6 +29,7 @@ export default function FileCard({
const fileWrapperClasses = getClasses(isHovered);
// Use direct URL like in v1.7.2 - the server handles authentication via cookies
const imgSrc = `${getBaseUrl()}files/images/${path}`;
if (showFile) {
@@ -45,6 +46,7 @@ export default function FileCard({
src={imgSrc}
alt="generated image"
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
crossOrigin="use-credentials"
/>
<DownloadButton
isHovered={isHovered}