From 05170cd827c543fc794eb3a66db95200219043db Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Fri, 27 Mar 2026 22:17:41 +0000 Subject: [PATCH 01/10] feat: add image preview support on /files page When clicking an image file on the /files tab, show the image using a signed URL instead of "Preview not available for this file type". Co-Authored-By: Paperclip --- components/Files/FileInfoDialogContent.tsx | 23 ++++----- components/Files/FilePreview.tsx | 57 +++++++++++++++++----- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/components/Files/FileInfoDialogContent.tsx b/components/Files/FileInfoDialogContent.tsx index 41cffed09..c3374f685 100644 --- a/components/Files/FileInfoDialogContent.tsx +++ b/components/Files/FileInfoDialogContent.tsx @@ -13,33 +13,34 @@ type FileInfoDialogContentProps = { onContentChange: (value: string) => void; }; -export default function FileInfoDialogContent({ - isEditing, - fileName, +export default function FileInfoDialogContent({ + isEditing, + fileName, storageKey, accountId, editedContent, - onContentChange + onContentChange, }: FileInfoDialogContentProps) { - const { content, loading, error, isTextFile } = useFileContent(fileName, storageKey, accountId); + const { content, loading, error, isTextFile } = useFileContent( + fileName, + storageKey, + accountId, + ); return (
{isEditing ? ( - + ) : ( - )}
); } - diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index bb0df4696..b41082266 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -1,6 +1,7 @@ import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; +import isImagePath from "@/utils/isImagePath"; type FilePreviewProps = { content: string | null; @@ -8,13 +9,39 @@ type FilePreviewProps = { error: string | null; isTextFile: boolean; fileName?: string; + storageKey?: string; }; -export default function FilePreview({ content, loading, error, isTextFile, fileName }: FilePreviewProps) { +export default function FilePreview({ + content, + loading, + error, + isTextFile, + fileName, + storageKey, +}: FilePreviewProps) { + const isImage = fileName ? isImagePath(fileName) : false; + + if (isImage && storageKey) { + const signedUrl = `/api/files/signed-url?key=${encodeURIComponent(storageKey)}`; + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {fileName +
+ ); + } + if (!isTextFile) { return (
-

Preview not available for this file type

+

+ Preview not available for this file type +

); } @@ -36,20 +63,24 @@ export default function FilePreview({ content, loading, error, isTextFile, fileN } // Check if file is markdown - const lowerFileName = fileName?.toLowerCase() ?? ''; - const isMarkdown = lowerFileName.endsWith('.md') || lowerFileName.endsWith('.markdown'); - + const lowerFileName = fileName?.toLowerCase() ?? ""; + const isMarkdown = + lowerFileName.endsWith(".md") || lowerFileName.endsWith(".markdown"); + // Limit preview size for performance (10MB - matches upload limit) const MAX_PREVIEW_SIZE = 10 * 1024 * 1024; - const contentToShow = content && content.length > MAX_PREVIEW_SIZE - ? content.substring(0, MAX_PREVIEW_SIZE) + '\n\n...(content truncated for preview)' - : content; + const contentToShow = + content && content.length > MAX_PREVIEW_SIZE + ? content.substring(0, MAX_PREVIEW_SIZE) + + "\n\n...(content truncated for preview)" + : content; return (
{isMarkdown ? ( -
); } - From cc222897749b9f88a15010e6796b891e204c90b3 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Fri, 27 Mar 2026 23:08:17 +0000 Subject: [PATCH 02/10] feat: add sandbox image preview on /files page Add image preview support for sandbox (GitHub repo) files on the /files page. The previous commit only handled Supabase-stored files via FileInfoDialog. This adds support for the SandboxFilePreview path by: - Adding getImageContent() to fetch base64 image data via API format=base64 - Extending useSandboxFileContent hook to detect images and return data URLs - Adding imageUrl prop to FilePreview as alternative to storageKey - Passing imageUrl through SandboxFilePreview and SandboxFileTree Depends on API PR #364 (base64 format support for sandbox file endpoint). Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- components/Files/FilePreview.tsx | 12 +++-- components/Sandboxes/SandboxFilePreview.tsx | 3 ++ components/Sandboxes/SandboxFileTree.tsx | 6 ++- hooks/useSandboxFileContent.ts | 14 +++++- lib/sandboxes/getImageContent.ts | 50 +++++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 lib/sandboxes/getImageContent.ts diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index b41082266..34cd71e37 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -10,6 +10,7 @@ type FilePreviewProps = { isTextFile: boolean; fileName?: string; storageKey?: string; + imageUrl?: string; }; export default function FilePreview({ @@ -19,16 +20,21 @@ export default function FilePreview({ isTextFile, fileName, storageKey, + imageUrl, }: FilePreviewProps) { const isImage = fileName ? isImagePath(fileName) : false; + const resolvedImageUrl = imageUrl + ? imageUrl + : isImage && storageKey + ? `/api/files/signed-url?key=${encodeURIComponent(storageKey)}` + : null; - if (isImage && storageKey) { - const signedUrl = `/api/files/signed-url?key=${encodeURIComponent(storageKey)}`; + if (isImage && resolvedImageUrl) { return (
{/* eslint-disable-next-line @next/next/no-img-element */} {fileName diff --git a/components/Sandboxes/SandboxFilePreview.tsx b/components/Sandboxes/SandboxFilePreview.tsx index add33c973..55507da9f 100644 --- a/components/Sandboxes/SandboxFilePreview.tsx +++ b/components/Sandboxes/SandboxFilePreview.tsx @@ -6,6 +6,7 @@ import { isTextFile } from "@/utils/isTextFile"; interface SandboxFilePreviewProps { selectedPath: string; content: string | null; + imageUrl: string | null; loading: boolean; error: string | null; } @@ -13,6 +14,7 @@ interface SandboxFilePreviewProps { export default function SandboxFilePreview({ selectedPath, content, + imageUrl, loading, error, }: SandboxFilePreviewProps) { @@ -27,6 +29,7 @@ export default function SandboxFilePreview({ error={error} isTextFile={isTextFile(fileName)} fileName={fileName} + imageUrl={imageUrl ?? undefined} />
); diff --git a/components/Sandboxes/SandboxFileTree.tsx b/components/Sandboxes/SandboxFileTree.tsx index cd9d246d3..4a9a10cb4 100644 --- a/components/Sandboxes/SandboxFileTree.tsx +++ b/components/Sandboxes/SandboxFileTree.tsx @@ -46,7 +46,10 @@ export default function SandboxFileTree() {

Repository Files

- + {filetree.map((node) => ( ))} @@ -56,6 +59,7 @@ export default function SandboxFileTree() { diff --git a/hooks/useSandboxFileContent.ts b/hooks/useSandboxFileContent.ts index 1f820cf93..0be6d6638 100644 --- a/hooks/useSandboxFileContent.ts +++ b/hooks/useSandboxFileContent.ts @@ -2,10 +2,13 @@ import { useCallback, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { usePrivy } from "@privy-io/react-auth"; import { getFileContents } from "@/lib/sandboxes/getFileContents"; +import { getImageContent } from "@/lib/sandboxes/getImageContent"; +import isImagePath from "@/utils/isImagePath"; interface UseSandboxFileContentReturn { selectedPath: string | undefined; content: string | null; + imageUrl: string | null; loading: boolean; error: string | null; select: (path: string) => void; @@ -21,7 +24,15 @@ export default function useSandboxFileContent(): UseSandboxFileContentReturn { if (!accessToken) { throw new Error("Please sign in to view file contents"); } - return getFileContents(accessToken, path); + + const fileName = path.split("/").pop() ?? ""; + if (isImagePath(fileName)) { + const dataUrl = await getImageContent(accessToken, path, fileName); + return { content: null, imageUrl: dataUrl }; + } + + const result = await getFileContents(accessToken, path); + return { content: result.content, imageUrl: null }; }, }); @@ -36,6 +47,7 @@ export default function useSandboxFileContent(): UseSandboxFileContentReturn { return { selectedPath, content: mutation.data?.content ?? null, + imageUrl: mutation.data?.imageUrl ?? null, loading: mutation.isPending, error: mutation.error?.message ?? null, select, diff --git a/lib/sandboxes/getImageContent.ts b/lib/sandboxes/getImageContent.ts new file mode 100644 index 000000000..9f0fbd584 --- /dev/null +++ b/lib/sandboxes/getImageContent.ts @@ -0,0 +1,50 @@ +import { NEW_API_BASE_URL } from "@/lib/consts"; + +interface GetImageContentResponse { + status: "success" | "error"; + content?: string; + encoding?: string; + error?: string; +} + +/** + * Fetches image content as base64 from the sandbox file API. + * Returns a data URL that can be used as an img src. + */ +export async function getImageContent( + accessToken: string, + path: string, + fileName: string, +): Promise { + const response = await fetch( + `${NEW_API_BASE_URL}/api/sandboxes/file?path=${encodeURIComponent(path)}&format=base64`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + const data: GetImageContentResponse = await response.json(); + + if (!response.ok || data.status === "error") { + throw new Error(data.error || "Failed to fetch image"); + } + + const mimeType = getMimeType(fileName); + return `data:${mimeType};base64,${data.content}`; +} + +function getMimeType(fileName: string): string { + const ext = fileName.toLowerCase().split(".").pop(); + const mimeTypes: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + }; + return mimeTypes[ext ?? ""] ?? "image/png"; +} From 4d755a4f527969d764f7eaa53ae5f45c629b3362 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:04:17 -0500 Subject: [PATCH 03/10] refactor: remove format=base64 query param, API auto-detects binary files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API now auto-detects binary files by extension and returns base64 encoding automatically — no need to pass format=base64 in the request. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/sandboxes/getImageContent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sandboxes/getImageContent.ts b/lib/sandboxes/getImageContent.ts index 9f0fbd584..2e2906e9c 100644 --- a/lib/sandboxes/getImageContent.ts +++ b/lib/sandboxes/getImageContent.ts @@ -17,7 +17,7 @@ export async function getImageContent( fileName: string, ): Promise { const response = await fetch( - `${NEW_API_BASE_URL}/api/sandboxes/file?path=${encodeURIComponent(path)}&format=base64`, + `${NEW_API_BASE_URL}/api/sandboxes/file?path=${encodeURIComponent(path)}`, { method: "GET", headers: { From 6730e11ab90b5f9ee373389533dd388c591cbf32 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:10:02 -0500 Subject: [PATCH 04/10] refactor: consolidate getImageContent into getFileContents, fix loading UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: - Eliminate getImageContent.ts — the API auto-detects binary files, so getFileContents now handles base64 responses and builds data URLs - Validate data.content before constructing data URLs - Fix loading state showing false "Preview not available" for images by moving loading/error checks before image/isTextFile checks - Add a spinner to the loading state Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Files/FilePreview.tsx | 35 ++++++++++++---------- hooks/useSandboxFileContent.ts | 11 +------ lib/sandboxes/getFileContents.ts | 39 +++++++++++++++++++++++-- lib/sandboxes/getImageContent.ts | 50 -------------------------------- 4 files changed, 56 insertions(+), 79 deletions(-) delete mode 100644 lib/sandboxes/getImageContent.ts diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index 34cd71e37..34bf29079 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -22,6 +22,25 @@ export default function FilePreview({ storageKey, imageUrl, }: FilePreviewProps) { + if (loading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + const isImage = fileName ? isImagePath(fileName) : false; const resolvedImageUrl = imageUrl ? imageUrl @@ -52,22 +71,6 @@ export default function FilePreview({ ); } - if (loading) { - return ( -
-

Loading...

-
- ); - } - - if (error) { - return ( -
-

{error}

-
- ); - } - // Check if file is markdown const lowerFileName = fileName?.toLowerCase() ?? ""; const isMarkdown = diff --git a/hooks/useSandboxFileContent.ts b/hooks/useSandboxFileContent.ts index 0be6d6638..7835ea88b 100644 --- a/hooks/useSandboxFileContent.ts +++ b/hooks/useSandboxFileContent.ts @@ -2,8 +2,6 @@ import { useCallback, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { usePrivy } from "@privy-io/react-auth"; import { getFileContents } from "@/lib/sandboxes/getFileContents"; -import { getImageContent } from "@/lib/sandboxes/getImageContent"; -import isImagePath from "@/utils/isImagePath"; interface UseSandboxFileContentReturn { selectedPath: string | undefined; @@ -25,14 +23,7 @@ export default function useSandboxFileContent(): UseSandboxFileContentReturn { throw new Error("Please sign in to view file contents"); } - const fileName = path.split("/").pop() ?? ""; - if (isImagePath(fileName)) { - const dataUrl = await getImageContent(accessToken, path, fileName); - return { content: null, imageUrl: dataUrl }; - } - - const result = await getFileContents(accessToken, path); - return { content: result.content, imageUrl: null }; + return getFileContents(accessToken, path); }, }); diff --git a/lib/sandboxes/getFileContents.ts b/lib/sandboxes/getFileContents.ts index 97bd6654c..ad15450a8 100644 --- a/lib/sandboxes/getFileContents.ts +++ b/lib/sandboxes/getFileContents.ts @@ -3,13 +3,37 @@ import { NEW_API_BASE_URL } from "@/lib/consts"; interface GetFileContentsResponse { status: "success" | "error"; content?: string; + encoding?: "base64"; error?: string; } +interface FileContentsResult { + content: string | null; + imageUrl: string | null; +} + +function getMimeType(fileName: string): string { + const ext = fileName.toLowerCase().split(".").pop(); + const mimeTypes: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + }; + return mimeTypes[ext ?? ""] ?? "application/octet-stream"; +} + +/** + * Fetches file content from the sandbox file API. + * The API auto-detects binary files and returns base64-encoded content. + * When base64 content is returned, this builds a data URL for image preview. + */ export async function getFileContents( accessToken: string, path: string, -): Promise<{ content: string }> { +): Promise { const response = await fetch( `${NEW_API_BASE_URL}/api/sandboxes/file?path=${encodeURIComponent(path)}`, { @@ -22,9 +46,18 @@ export async function getFileContents( const data: GetFileContentsResponse = await response.json(); - if (!response.ok || data.status === "error") { + if (!response.ok || data.status === "error" || !data.content) { throw new Error(data.error || "Failed to fetch file contents"); } - return { content: data.content || "" }; + if (data.encoding === "base64") { + const fileName = path.split("/").pop() ?? ""; + const mimeType = getMimeType(fileName); + return { + content: null, + imageUrl: `data:${mimeType};base64,${data.content}`, + }; + } + + return { content: data.content, imageUrl: null }; } diff --git a/lib/sandboxes/getImageContent.ts b/lib/sandboxes/getImageContent.ts deleted file mode 100644 index 2e2906e9c..000000000 --- a/lib/sandboxes/getImageContent.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NEW_API_BASE_URL } from "@/lib/consts"; - -interface GetImageContentResponse { - status: "success" | "error"; - content?: string; - encoding?: string; - error?: string; -} - -/** - * Fetches image content as base64 from the sandbox file API. - * Returns a data URL that can be used as an img src. - */ -export async function getImageContent( - accessToken: string, - path: string, - fileName: string, -): Promise { - const response = await fetch( - `${NEW_API_BASE_URL}/api/sandboxes/file?path=${encodeURIComponent(path)}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - const data: GetImageContentResponse = await response.json(); - - if (!response.ok || data.status === "error") { - throw new Error(data.error || "Failed to fetch image"); - } - - const mimeType = getMimeType(fileName); - return `data:${mimeType};base64,${data.content}`; -} - -function getMimeType(fileName: string): string { - const ext = fileName.toLowerCase().split(".").pop(); - const mimeTypes: Record = { - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - svg: "image/svg+xml", - }; - return mimeTypes[ext ?? ""] ?? "image/png"; -} From 70bc39ac97c503e53296afa8668cb837a2fddfb9 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:13:37 -0500 Subject: [PATCH 05/10] refactor: remove storageKey from FilePreview (YAGNI) storageKey/signed-URL image previews for Supabase-stored files are not part of this PR's scope. Remove the prop to keep the change focused on sandbox GitHub image previews only. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Files/FileInfoDialogContent.tsx | 1 - components/Files/FilePreview.tsx | 14 ++------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/components/Files/FileInfoDialogContent.tsx b/components/Files/FileInfoDialogContent.tsx index c3374f685..3e160ecda 100644 --- a/components/Files/FileInfoDialogContent.tsx +++ b/components/Files/FileInfoDialogContent.tsx @@ -38,7 +38,6 @@ export default function FileInfoDialogContent({ error={error} isTextFile={isTextFile} fileName={fileName} - storageKey={storageKey} /> )}
diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index 34bf29079..4bf6abafd 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -1,7 +1,6 @@ import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; -import isImagePath from "@/utils/isImagePath"; type FilePreviewProps = { content: string | null; @@ -9,7 +8,6 @@ type FilePreviewProps = { error: string | null; isTextFile: boolean; fileName?: string; - storageKey?: string; imageUrl?: string; }; @@ -19,7 +17,6 @@ export default function FilePreview({ error, isTextFile, fileName, - storageKey, imageUrl, }: FilePreviewProps) { if (loading) { @@ -41,19 +38,12 @@ export default function FilePreview({ ); } - const isImage = fileName ? isImagePath(fileName) : false; - const resolvedImageUrl = imageUrl - ? imageUrl - : isImage && storageKey - ? `/api/files/signed-url?key=${encodeURIComponent(storageKey)}` - : null; - - if (isImage && resolvedImageUrl) { + if (imageUrl) { return (
{/* eslint-disable-next-line @next/next/no-img-element */} {fileName From b434f33b1f834e064ca7ca74a9fa02f34dce1077 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:16:21 -0500 Subject: [PATCH 06/10] revert: drop formatting-only changes to FileInfoDialogContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not part of this PR's scope — keeps the diff focused. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Files/FileInfoDialogContent.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/components/Files/FileInfoDialogContent.tsx b/components/Files/FileInfoDialogContent.tsx index 3e160ecda..41cffed09 100644 --- a/components/Files/FileInfoDialogContent.tsx +++ b/components/Files/FileInfoDialogContent.tsx @@ -13,26 +13,25 @@ type FileInfoDialogContentProps = { onContentChange: (value: string) => void; }; -export default function FileInfoDialogContent({ - isEditing, - fileName, +export default function FileInfoDialogContent({ + isEditing, + fileName, storageKey, accountId, editedContent, - onContentChange, + onContentChange }: FileInfoDialogContentProps) { - const { content, loading, error, isTextFile } = useFileContent( - fileName, - storageKey, - accountId, - ); + const { content, loading, error, isTextFile } = useFileContent(fileName, storageKey, accountId); return (
{isEditing ? ( - + ) : ( - ); } + From 8a23bd6c47939f599f2e0fd00e7f299e92c6dec0 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:20:16 -0500 Subject: [PATCH 07/10] refactor: extract getMimeType into existing getMimeFromPath utility (SRP) Add image MIME types to the existing getMimeFromPath utility and reuse it in getFileContents instead of an inline function. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/sandboxes/getFileContents.ts | 17 ++--------------- utils/getMimeFromPath.ts | 6 ++++++ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/sandboxes/getFileContents.ts b/lib/sandboxes/getFileContents.ts index ad15450a8..efa63edb5 100644 --- a/lib/sandboxes/getFileContents.ts +++ b/lib/sandboxes/getFileContents.ts @@ -1,4 +1,5 @@ import { NEW_API_BASE_URL } from "@/lib/consts"; +import { getMimeFromPath } from "@/utils/getMimeFromPath"; interface GetFileContentsResponse { status: "success" | "error"; @@ -12,19 +13,6 @@ interface FileContentsResult { imageUrl: string | null; } -function getMimeType(fileName: string): string { - const ext = fileName.toLowerCase().split(".").pop(); - const mimeTypes: Record = { - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - svg: "image/svg+xml", - }; - return mimeTypes[ext ?? ""] ?? "application/octet-stream"; -} - /** * Fetches file content from the sandbox file API. * The API auto-detects binary files and returns base64-encoded content. @@ -51,8 +39,7 @@ export async function getFileContents( } if (data.encoding === "base64") { - const fileName = path.split("/").pop() ?? ""; - const mimeType = getMimeType(fileName); + const mimeType = getMimeFromPath(path); return { content: null, imageUrl: `data:${mimeType};base64,${data.content}`, diff --git a/utils/getMimeFromPath.ts b/utils/getMimeFromPath.ts index 841394c3d..08a4ce95d 100644 --- a/utils/getMimeFromPath.ts +++ b/utils/getMimeFromPath.ts @@ -7,6 +7,12 @@ const mimeByExt: Record = { xml: "application/xml", yml: "application/x-yaml", yaml: "application/x-yaml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", }; export const getMimeFromPath = (path: string): string => { From 4affa5113efbcf16ab91e4cdc4b15822c1aba308 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:22:54 -0500 Subject: [PATCH 08/10] refactor: use Skeleton for loading state, move MIME util to lib/files - Replace loading spinner with shadcn Skeleton component in FilePreview - Create lib/files/getImageMimeType.ts instead of modifying utils/ (SRP) - Revert utils/getMimeFromPath.ts to base branch state Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Files/FilePreview.tsx | 12 ++++++++---- lib/files/getImageMimeType.ts | 16 ++++++++++++++++ lib/sandboxes/getFileContents.ts | 4 ++-- utils/getMimeFromPath.ts | 6 ------ 4 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 lib/files/getImageMimeType.ts diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index 4bf6abafd..4a57ddca4 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -1,6 +1,7 @@ import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; +import { Skeleton } from "@/components/ui/skeleton"; type FilePreviewProps = { content: string | null; @@ -21,10 +22,13 @@ export default function FilePreview({ }: FilePreviewProps) { if (loading) { return ( -
-
-
-

Loading...

+
+
+ + + + +
); diff --git a/lib/files/getImageMimeType.ts b/lib/files/getImageMimeType.ts new file mode 100644 index 000000000..95e509209 --- /dev/null +++ b/lib/files/getImageMimeType.ts @@ -0,0 +1,16 @@ +const imageMimeTypes: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", +}; + +/** + * Returns the MIME type for an image file based on its path extension. + */ +export function getImageMimeType(path: string): string { + const ext = path.toLowerCase().split(".").pop() ?? ""; + return imageMimeTypes[ext] ?? "application/octet-stream"; +} diff --git a/lib/sandboxes/getFileContents.ts b/lib/sandboxes/getFileContents.ts index efa63edb5..c5bb0abe6 100644 --- a/lib/sandboxes/getFileContents.ts +++ b/lib/sandboxes/getFileContents.ts @@ -1,5 +1,5 @@ import { NEW_API_BASE_URL } from "@/lib/consts"; -import { getMimeFromPath } from "@/utils/getMimeFromPath"; +import { getImageMimeType } from "@/lib/files/getImageMimeType"; interface GetFileContentsResponse { status: "success" | "error"; @@ -39,7 +39,7 @@ export async function getFileContents( } if (data.encoding === "base64") { - const mimeType = getMimeFromPath(path); + const mimeType = getImageMimeType(path); return { content: null, imageUrl: `data:${mimeType};base64,${data.content}`, diff --git a/utils/getMimeFromPath.ts b/utils/getMimeFromPath.ts index 08a4ce95d..841394c3d 100644 --- a/utils/getMimeFromPath.ts +++ b/utils/getMimeFromPath.ts @@ -7,12 +7,6 @@ const mimeByExt: Record = { xml: "application/xml", yml: "application/x-yaml", yaml: "application/x-yaml", - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - svg: "image/svg+xml", }; export const getMimeFromPath = (path: string): string => { From 077d7145d15bb6ba54e318d5d159b7b2cf57b837 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:23:59 -0500 Subject: [PATCH 09/10] refactor: move getMimeFromPath from utils/ to lib/files/, add image MIME types Move getMimeFromPath to lib/files/ per project conventions (no utils/ path). Add image MIME types (png, jpg, jpeg, gif, webp, svg) to the existing map so it can be reused for both text and image MIME resolution (DRY). Co-Authored-By: Claude Opus 4.6 (1M context) --- hooks/useSaveKnowledgeEdit.ts | 2 +- lib/files/getImageMimeType.ts | 16 ---------------- {utils => lib/files}/getMimeFromPath.ts | 8 +++++++- lib/sandboxes/getFileContents.ts | 4 ++-- 4 files changed, 10 insertions(+), 20 deletions(-) delete mode 100644 lib/files/getImageMimeType.ts rename {utils => lib/files}/getMimeFromPath.ts (75%) diff --git a/hooks/useSaveKnowledgeEdit.ts b/hooks/useSaveKnowledgeEdit.ts index a12bcfcff..9c035ef16 100644 --- a/hooks/useSaveKnowledgeEdit.ts +++ b/hooks/useSaveKnowledgeEdit.ts @@ -1,5 +1,5 @@ import { useArtistProvider } from "@/providers/ArtistProvider"; -import getMimeFromPath from "@/utils/getMimeFromPath"; +import getMimeFromPath from "@/lib/files/getMimeFromPath"; import { uploadFile } from "@/lib/arweave/uploadFile"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; diff --git a/lib/files/getImageMimeType.ts b/lib/files/getImageMimeType.ts deleted file mode 100644 index 95e509209..000000000 --- a/lib/files/getImageMimeType.ts +++ /dev/null @@ -1,16 +0,0 @@ -const imageMimeTypes: Record = { - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - webp: "image/webp", - svg: "image/svg+xml", -}; - -/** - * Returns the MIME type for an image file based on its path extension. - */ -export function getImageMimeType(path: string): string { - const ext = path.toLowerCase().split(".").pop() ?? ""; - return imageMimeTypes[ext] ?? "application/octet-stream"; -} diff --git a/utils/getMimeFromPath.ts b/lib/files/getMimeFromPath.ts similarity index 75% rename from utils/getMimeFromPath.ts rename to lib/files/getMimeFromPath.ts index 841394c3d..8557d4dff 100644 --- a/utils/getMimeFromPath.ts +++ b/lib/files/getMimeFromPath.ts @@ -7,6 +7,12 @@ const mimeByExt: Record = { xml: "application/xml", yml: "application/x-yaml", yaml: "application/x-yaml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", }; export const getMimeFromPath = (path: string): string => { @@ -15,4 +21,4 @@ export const getMimeFromPath = (path: string): string => { return (ext && mimeByExt[ext]) || "text/plain"; }; -export default getMimeFromPath; \ No newline at end of file +export default getMimeFromPath; diff --git a/lib/sandboxes/getFileContents.ts b/lib/sandboxes/getFileContents.ts index c5bb0abe6..04bb69cf8 100644 --- a/lib/sandboxes/getFileContents.ts +++ b/lib/sandboxes/getFileContents.ts @@ -1,5 +1,5 @@ import { NEW_API_BASE_URL } from "@/lib/consts"; -import { getImageMimeType } from "@/lib/files/getImageMimeType"; +import { getMimeFromPath } from "@/lib/files/getMimeFromPath"; interface GetFileContentsResponse { status: "success" | "error"; @@ -39,7 +39,7 @@ export async function getFileContents( } if (data.encoding === "base64") { - const mimeType = getImageMimeType(path); + const mimeType = getMimeFromPath(path); return { content: null, imageUrl: `data:${mimeType};base64,${data.content}`, From 4e2e26c9ab5510bce92ab859806b7e613f22719a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 28 Mar 2026 17:29:44 -0500 Subject: [PATCH 10/10] refactor: extract FilePreviewSkeleton into its own component (SRP) Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Files/FilePreview.tsx | 14 ++------------ components/Files/FilePreviewSkeleton.tsx | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 components/Files/FilePreviewSkeleton.tsx diff --git a/components/Files/FilePreview.tsx b/components/Files/FilePreview.tsx index 4a57ddca4..9586220d0 100644 --- a/components/Files/FilePreview.tsx +++ b/components/Files/FilePreview.tsx @@ -1,7 +1,7 @@ import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; -import { Skeleton } from "@/components/ui/skeleton"; +import FilePreviewSkeleton from "./FilePreviewSkeleton"; type FilePreviewProps = { content: string | null; @@ -21,17 +21,7 @@ export default function FilePreview({ imageUrl, }: FilePreviewProps) { if (loading) { - return ( -
-
- - - - - -
-
- ); + return ; } if (error) { diff --git a/components/Files/FilePreviewSkeleton.tsx b/components/Files/FilePreviewSkeleton.tsx new file mode 100644 index 000000000..1fd80d776 --- /dev/null +++ b/components/Files/FilePreviewSkeleton.tsx @@ -0,0 +1,15 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function FilePreviewSkeleton() { + return ( +
+
+ + + + + +
+
+ ); +}