Merge pull request #1538 from Chaitanya045/image-not-rendering-in-md-files

fix : MD Renderer not rendering the local images on the sandbox
This commit is contained in:
Marko Kraemer 2025-09-03 16:10:18 -07:00 committed by GitHub
commit a97bf8a739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 96 additions and 6 deletions

View File

@ -257,7 +257,7 @@ export function FileRenderer({
) : fileType === 'pdf' && binaryUrl ? (
<PdfRenderer url={binaryUrl} />
) : fileType === 'markdown' ? (
<MarkdownRenderer content={content || ''} ref={markdownRef} project={project} />
<MarkdownRenderer content={content || ''} ref={markdownRef} project={project} basePath={filePath} />
) : fileType === 'csv' ? (
<CsvRenderer content={content || ''} />
) : fileType === 'xlsx' ? (

View File

@ -37,18 +37,86 @@ interface AuthenticatedImageProps {
alt?: string;
className?: string;
project?: FileRendererProject;
basePath?: string;
}
function AuthenticatedImage({ src, alt, className, project }: AuthenticatedImageProps) {
// Join and normalize paths like a simplified path.resolve for URLs
function joinAndNormalize(baseDir: string, relativePath: string): string {
// Ensure baseDir starts with a leading slash
let base = baseDir.startsWith('/') ? baseDir : `/${baseDir}`;
// If base appears to be a file path, remove the file name to get the directory
if (base.includes('.')) {
base = base.substring(0, base.lastIndexOf('/')) || '/';
}
// Ensure we are rooted at /workspace
if (!base.startsWith('/workspace')) {
base = `/workspace/${base.replace(/^\//, '')}`;
}
const stack: string[] = base.split('/').filter(Boolean);
const segments = relativePath.split('/');
for (const segment of segments) {
if (!segment || segment === '.') continue;
if (segment === '..') {
if (stack.length > 1) stack.pop(); // keep at least 'workspace'
} else {
stack.push(segment);
}
}
return `/${stack.join('/')}`;
}
function normalizeWorkspacePath(path: string): string {
if (!path) return '/workspace';
let normalized = path;
if (!normalized.startsWith('/workspace')) {
normalized = `/workspace/${normalized.replace(/^\//, '')}`;
}
return normalized;
}
function resolveImagePath(src: string, basePath?: string): string {
if (!src) return src;
const lower = src.toLowerCase();
// External or data/blob URLs should pass through
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('data:') || lower.startsWith('blob:')) {
return src;
}
// Already absolute workspace path
if (src.startsWith('/workspace/')) {
return src;
}
// Root-relative to workspace or absolute without /workspace
if (src.startsWith('/')) {
return normalizeWorkspacePath(src);
}
// Relative path -> resolve against basePath directory
if (basePath) {
return joinAndNormalize(basePath, src);
}
// Fallback: treat as under workspace root
return normalizeWorkspacePath(src);
}
function AuthenticatedImage({ src, alt, className, project, basePath }: AuthenticatedImageProps) {
// For sandbox files, use the existing useImageContent hook
const sandboxId = typeof project?.sandbox === 'string'
? project.sandbox
: project?.sandbox?.id;
const { data: imageUrl, isLoading, error } = useImageContent(sandboxId, src);
const resolvedSrc = resolveImagePath(src, basePath);
const { data: imageUrl, isLoading, error } = useImageContent(sandboxId, resolvedSrc);
// If it's already a URL or data URL, use regular img
if (src.startsWith('http') || src.startsWith('data:')) {
if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('blob:')) {
return <img src={src} alt={alt || ''} className={className} />;
}
@ -81,12 +149,13 @@ interface MarkdownRendererProps {
content: string;
className?: string;
project?: FileRendererProject;
basePath?: string; // used to resolve relative image sources inside markdown
}
export const MarkdownRenderer = forwardRef<
HTMLDivElement,
MarkdownRendererProps
>(({ content, className, project }, ref) => {
>(({ content, className, project, basePath }, ref) => {
// Process Unicode escape sequences in the content
const processedContent = processUnicodeContent(content);
@ -157,6 +226,7 @@ export const MarkdownRenderer = forwardRef<
alt={props.alt || ''}
className="max-w-full h-auto rounded-md my-2"
project={project}
basePath={basePath}
/>
),
pre: ({ node, ...props }) => (

View File

@ -10,6 +10,8 @@ interface MarkdownRendererProps {
content: string;
className?: string;
project?: Project;
previewUrl?: string;
basePath?: string;
}
/**
@ -19,8 +21,23 @@ interface MarkdownRendererProps {
export function MarkdownRenderer({
content,
className,
project
project,
previewUrl,
basePath
}: MarkdownRendererProps) {
// Derive basePath from previewUrl if not explicitly provided
let derivedBasePath = basePath;
try {
if (!derivedBasePath && previewUrl) {
if (previewUrl.includes('/api/sandboxes/')) {
const u = new URL(previewUrl);
const p = u.searchParams.get('path');
if (p) derivedBasePath = p;
} else {
derivedBasePath = previewUrl;
}
}
} catch {}
return (
<div className={cn('w-full h-full overflow-hidden', className)}>
<ScrollArea className="w-full h-full">
@ -29,6 +46,7 @@ export function MarkdownRenderer({
content={content}
className="prose prose-sm dark:prose-invert max-w-none [&>:first-child]:mt-0"
project={project}
basePath={derivedBasePath}
/>
</div>
</ScrollArea>

View File

@ -244,6 +244,7 @@ export function FileEditToolView({
<MarkdownRenderer
content={processUnicodeContent(updatedContent)}
project={project}
basePath={processedFilePath || undefined}
/>
</div>
);

View File

@ -200,6 +200,7 @@ export function FileOperationToolView({
<MarkdownRenderer
content={processUnicodeContent(fileContent)}
project={project}
basePath={processedFilePath || undefined}
/>
</div>
);