mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
✨ feat(file-preview): render HTML files inline (#15671)
✨ feat(file-preview): render html files inline
This commit is contained in:
@@ -1009,6 +1009,7 @@
|
||||
"workingPanel.localFile.error": "Couldn't load this file",
|
||||
"workingPanel.localFile.preview.raw": "Raw",
|
||||
"workingPanel.localFile.preview.render": "Preview",
|
||||
"workingPanel.localFile.preview.source": "Source",
|
||||
"workingPanel.localFile.truncated": "File preview truncated to {{limit}} characters",
|
||||
"workingPanel.progress": "Progress",
|
||||
"workingPanel.progress.allCompleted": "All tasks completed",
|
||||
|
||||
@@ -1009,6 +1009,7 @@
|
||||
"workingPanel.localFile.error": "无法加载此文件",
|
||||
"workingPanel.localFile.preview.raw": "原文",
|
||||
"workingPanel.localFile.preview.render": "预览",
|
||||
"workingPanel.localFile.preview.source": "源码",
|
||||
"workingPanel.localFile.truncated": "文件预览被截断至 {{limit}} 个字符",
|
||||
"workingPanel.progress": "进度",
|
||||
"workingPanel.progress.allCompleted": "所有任务已完成",
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FileIcon from '@/components/FileIcon';
|
||||
import { InlineHtmlPreview, isHtmlFile } from '@/components/HtmlPreview';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
actions: css`
|
||||
@@ -100,6 +101,7 @@ const ReadFileView = memo<ReadFileState>(
|
||||
const { t } = useTranslation('tool');
|
||||
const { openFile, openFolder, displayRelativePath } = useToolRenderCapabilities();
|
||||
const filename = filenameProp || path.split('/').pop() || path;
|
||||
const isHtml = isHtmlFile({ fileName: filename, fileType, path });
|
||||
|
||||
const handleOpenFile = openFile
|
||||
? (e: React.MouseEvent) => {
|
||||
@@ -192,8 +194,13 @@ const ReadFileView = memo<ReadFileState>(
|
||||
</Text>
|
||||
</Flexbox>
|
||||
|
||||
<Flexbox className={styles.previewBox} style={{ maxHeight: 240 }}>
|
||||
{fileType === 'md' ? (
|
||||
<Flexbox
|
||||
className={styles.previewBox}
|
||||
style={{ height: isHtml ? 240 : undefined, maxHeight: 240 }}
|
||||
>
|
||||
{isHtml ? (
|
||||
<InlineHtmlPreview content={content} />
|
||||
) : fileType === 'md' ? (
|
||||
<Markdown style={{ overflow: 'auto' }}>{content}</Markdown>
|
||||
) : (
|
||||
<div className={styles.previewText} style={{ width: '100%' }}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChevronRight } from 'lucide-react';
|
||||
import path from 'path-browserify-esm';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InlineHtmlPreview, isHtmlFile } from '@/components/HtmlPreview';
|
||||
import { LocalFile, LocalFolder } from '@/features/LocalFile';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
@@ -36,12 +37,13 @@ const WriteFile = memo<BuiltinRenderProps<WriteLocalFileParams>>(({ args }) => {
|
||||
|
||||
const { base, dir } = path.parse(args.path);
|
||||
const ext = path.extname(args.path).slice(1).toLowerCase();
|
||||
const isHtml = isHtmlFile({ path: args.path });
|
||||
const isMarkdown = ext === 'md' || ext === 'mdx';
|
||||
|
||||
// Code-type files render as a "new file" unified diff so the visual is
|
||||
// consistent with EditLocalFile's PatchDiff. Markdown keeps its rendered
|
||||
// preview because a rendered doc reads better than an all-green diff.
|
||||
if (!isMarkdown && args.content) {
|
||||
if (!isMarkdown && !isHtml && args.content) {
|
||||
return (
|
||||
<PatchDiff
|
||||
fileName={base}
|
||||
@@ -63,10 +65,17 @@ const WriteFile = memo<BuiltinRenderProps<WriteLocalFileParams>>(({ args }) => {
|
||||
</Flexbox>
|
||||
|
||||
{args.content && (
|
||||
<Flexbox className={styles.previewBox}>
|
||||
<Markdown style={{ maxHeight: 240, overflow: 'auto', padding: '0 8px' }} variant={'chat'}>
|
||||
{args.content}
|
||||
</Markdown>
|
||||
<Flexbox className={styles.previewBox} style={{ height: isHtml ? 260 : undefined }}>
|
||||
{isHtml ? (
|
||||
<InlineHtmlPreview content={args.content} />
|
||||
) : (
|
||||
<Markdown
|
||||
style={{ maxHeight: 240, overflow: 'auto', padding: '0 8px' }}
|
||||
variant={'chat'}
|
||||
>
|
||||
{args.content}
|
||||
</Markdown>
|
||||
)}
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
|
||||
@@ -1111,6 +1111,7 @@ export default {
|
||||
'workingPanel.localFile.error': "Couldn't load this file",
|
||||
'workingPanel.localFile.preview.raw': 'Raw',
|
||||
'workingPanel.localFile.preview.render': 'Preview',
|
||||
'workingPanel.localFile.preview.source': 'Source',
|
||||
'workingPanel.localFile.truncated': 'File preview truncated to {{limit}} characters',
|
||||
'workingPanel.skills.empty': 'No skills available',
|
||||
'workingPanel.skills.section.agent': 'Agent skills',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { HtmlPreview } from '@lobehub/ui';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const hideHtmlPreviewActions = () => null;
|
||||
|
||||
interface InlineHtmlPreviewProps {
|
||||
animated?: boolean;
|
||||
className?: string;
|
||||
content: string;
|
||||
height?: CSSProperties['height'];
|
||||
style?: CSSProperties;
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
const InlineHtmlPreview = memo<InlineHtmlPreviewProps>(
|
||||
({ animated, className, content, height = '100%', style, width = '100%' }) => (
|
||||
<HtmlPreview
|
||||
actionsRender={hideHtmlPreviewActions}
|
||||
animated={animated}
|
||||
className={className}
|
||||
copyable={false}
|
||||
downloadable={false}
|
||||
shadow={false}
|
||||
style={{ height, minHeight: 0, overflow: 'hidden', width, ...style }}
|
||||
variant={'borderless'}
|
||||
styles={{
|
||||
content: { height: '100%' },
|
||||
iframe: { height: '100%' },
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</HtmlPreview>
|
||||
),
|
||||
);
|
||||
|
||||
InlineHtmlPreview.displayName = 'InlineHtmlPreview';
|
||||
|
||||
export default InlineHtmlPreview;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isHtmlFile } from './fileType';
|
||||
|
||||
describe('isHtmlFile', () => {
|
||||
it('detects HTML files by MIME type', () => {
|
||||
expect(isHtmlFile({ fileType: 'text/html' })).toBe(true);
|
||||
expect(isHtmlFile({ fileType: 'text/html; charset=utf-8' })).toBe(true);
|
||||
expect(isHtmlFile({ fileType: 'application/xhtml+xml' })).toBe(true);
|
||||
});
|
||||
|
||||
it('detects HTML files by filename or path extension', () => {
|
||||
expect(isHtmlFile({ fileName: 'preview.HTML' })).toBe(true);
|
||||
expect(isHtmlFile({ path: '/tmp/demo.htm' })).toBe(true);
|
||||
expect(isHtmlFile({ fileName: 'preview', path: '/tmp/demo.html' })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects non-HTML files', () => {
|
||||
expect(isHtmlFile({ fileName: 'preview.tsx', fileType: 'text/plain' })).toBe(false);
|
||||
expect(isHtmlFile({ path: '/tmp/html-preview.ts' })).toBe(false);
|
||||
expect(isHtmlFile({})).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const HTML_FILE_EXTENSIONS = ['.html', '.htm'];
|
||||
const HTML_FILE_TYPES = new Set(['html', 'htm', 'text/html', 'application/xhtml+xml']);
|
||||
|
||||
const normalizeFileType = (fileType?: string | null) =>
|
||||
fileType?.split(';')[0].trim().toLowerCase();
|
||||
|
||||
interface HtmlFileFields {
|
||||
fileName?: string | null;
|
||||
fileType?: string | null;
|
||||
path?: string | null;
|
||||
}
|
||||
|
||||
export const isHtmlFile = ({ fileName, fileType, path }: HtmlFileFields): boolean => {
|
||||
const normalizedFileType = normalizeFileType(fileType);
|
||||
|
||||
if (normalizedFileType && HTML_FILE_TYPES.has(normalizedFileType)) return true;
|
||||
|
||||
const candidates = [fileName, path].flatMap((candidate) =>
|
||||
candidate ? [candidate.toLowerCase()] : [],
|
||||
);
|
||||
|
||||
if (candidates.length === 0) return false;
|
||||
|
||||
return candidates.some((candidate) =>
|
||||
HTML_FILE_EXTENSIONS.some((extension) => candidate.endsWith(extension)),
|
||||
);
|
||||
};
|
||||
@@ -1 +1,3 @@
|
||||
export { isHtmlFile } from './fileType';
|
||||
export { default as InlineHtmlPreview } from './InlinePreview';
|
||||
export { default as HtmlPreviewDrawer } from './PreviewDrawer';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { Center, Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InlineHtmlPreview } from '@/components/HtmlPreview';
|
||||
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
||||
|
||||
import { useTextFileLoader } from '../../hooks/useTextFileLoader';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
page: css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface HTMLViewerProps {
|
||||
fileId: string;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
const HTMLViewer = memo<HTMLViewerProps>(({ url }) => {
|
||||
const { fileData, loading } = useTextFileLoader(url);
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.page}>
|
||||
{!loading && fileData !== null ? (
|
||||
<InlineHtmlPreview content={fileData} />
|
||||
) : (
|
||||
<Center height={'100%'}>
|
||||
<NeuralNetworkLoading size={36} />
|
||||
</Center>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
HTMLViewer.displayName = 'HTMLViewer';
|
||||
|
||||
export default HTMLViewer;
|
||||
@@ -1,13 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { MARKDOWN_MIME_TYPES } from '@lobechat/const';
|
||||
import { type CSSProperties } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { isHtmlFile } from '@/components/HtmlPreview';
|
||||
import { type FileListItem } from '@/types/files';
|
||||
|
||||
import NotSupport from './NotSupport';
|
||||
import CodeViewer from './Renderer/Code';
|
||||
import HTMLViewer from './Renderer/HTML';
|
||||
import ImageViewer from './Renderer/Image';
|
||||
import MSDocViewer from './Renderer/MSDoc';
|
||||
import PDFViewer from './Renderer/PDF';
|
||||
@@ -267,6 +269,11 @@ const FileViewer = memo<FileViewerProps>(({ id, style, fileType, url, name }) =>
|
||||
return <MSDocViewer fileId={id} url={url} />;
|
||||
}
|
||||
|
||||
// HTML files should render as a sandboxed preview before the broader code-file fallback.
|
||||
if (isHtmlFile({ fileName: name, fileType })) {
|
||||
return <HTMLViewer fileId={id} url={url} />;
|
||||
}
|
||||
|
||||
// Code files (JavaScript, TypeScript, Python, Java, C++, Go, Rust, Markdown, etc.)
|
||||
if (matchesFileType(fileType, name, CODE_EXTENSIONS, CODE_MIME_TYPES)) {
|
||||
return <CodeViewer fileId={id} fileName={name} url={url} />;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HtmlPreview } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InlineHtmlPreview } from '@/components/HtmlPreview';
|
||||
|
||||
interface HTMLRendererProps {
|
||||
animated?: boolean;
|
||||
height?: string;
|
||||
@@ -8,26 +9,10 @@ interface HTMLRendererProps {
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const hideHtmlPreviewActions = () => null;
|
||||
|
||||
const HTMLRenderer = memo<HTMLRendererProps>(
|
||||
({ animated, htmlContent, width = '100%', height = '100%' }) => {
|
||||
return (
|
||||
<HtmlPreview
|
||||
actionsRender={hideHtmlPreviewActions}
|
||||
animated={animated}
|
||||
copyable={false}
|
||||
downloadable={false}
|
||||
shadow={false}
|
||||
style={{ height, minHeight: 0, overflow: 'hidden', width }}
|
||||
variant={'borderless'}
|
||||
styles={{
|
||||
content: { height: '100%' },
|
||||
iframe: { height: '100%' },
|
||||
}}
|
||||
>
|
||||
{htmlContent}
|
||||
</HtmlPreview>
|
||||
<InlineHtmlPreview animated={animated} content={htmlContent} height={height} width={width} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CodeEditorPane from '@/components/CodeEditorPane';
|
||||
import { InlineHtmlPreview, isHtmlFile } from '@/components/HtmlPreview';
|
||||
import Loading from '@/components/Loading/CircleLoading';
|
||||
import { useClientDataSWR } from '@/libs/swr';
|
||||
import { localFileService } from '@/services/electron/localFileService';
|
||||
@@ -166,116 +167,134 @@ type TextPreviewMode = 'render' | 'raw';
|
||||
|
||||
interface TextPreviewPaneProps {
|
||||
content: string;
|
||||
contentType?: string;
|
||||
ext: string;
|
||||
filePath: string;
|
||||
onSaved?: (savedContent: string) => void;
|
||||
}
|
||||
|
||||
const TextPreviewPane = memo<TextPreviewPaneProps>(({ content, ext, filePath, onSaved }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const isMarkdown = useMemo(() => MARKDOWN_EXTS.has(ext.toLowerCase()), [ext]);
|
||||
const buffer = useChatStore(chatPortalSelectors.localFileBuffer(filePath));
|
||||
const setLocalFileBuffer = useChatStore((s) => s.setLocalFileBuffer);
|
||||
const saveLocalFile = useChatStore((s) => s.saveLocalFile);
|
||||
const TextPreviewPane = memo<TextPreviewPaneProps>(
|
||||
({ content, contentType, ext, filePath, onSaved }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const isMarkdown = useMemo(() => MARKDOWN_EXTS.has(ext.toLowerCase()), [ext]);
|
||||
const isHtml = useMemo(
|
||||
() => isHtmlFile({ fileType: contentType, path: filePath }),
|
||||
[contentType, filePath],
|
||||
);
|
||||
const canRender = isMarkdown || isHtml;
|
||||
const buffer = useChatStore(chatPortalSelectors.localFileBuffer(filePath));
|
||||
const setLocalFileBuffer = useChatStore((s) => s.setLocalFileBuffer);
|
||||
const saveLocalFile = useChatStore((s) => s.saveLocalFile);
|
||||
|
||||
const editingValue = buffer ?? content;
|
||||
const editingValue = buffer ?? content;
|
||||
|
||||
const handleCodeChange = useCallback(
|
||||
(next: string) => {
|
||||
if (next === content) {
|
||||
const handleCodeChange = useCallback(
|
||||
(next: string) => {
|
||||
if (next === content) {
|
||||
setLocalFileBuffer(filePath, undefined);
|
||||
} else {
|
||||
setLocalFileBuffer(filePath, next);
|
||||
}
|
||||
},
|
||||
[content, filePath, setLocalFileBuffer],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
const saved = await saveLocalFile(filePath);
|
||||
if (saved === undefined) return;
|
||||
// Update SWR cache BEFORE clearing the buffer, otherwise React will
|
||||
// briefly render with buffer cleared but content still stale, causing
|
||||
// CodeMirror to setValue and reset the cursor.
|
||||
onSaved?.(saved);
|
||||
setLocalFileBuffer(filePath, undefined);
|
||||
} else {
|
||||
setLocalFileBuffer(filePath, next);
|
||||
} catch {
|
||||
/* swallow — surfacing handled elsewhere if needed */
|
||||
}
|
||||
},
|
||||
[content, filePath, setLocalFileBuffer],
|
||||
);
|
||||
}, [filePath, onSaved, saveLocalFile, setLocalFileBuffer]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
const saved = await saveLocalFile(filePath);
|
||||
if (saved === undefined) return;
|
||||
// Update SWR cache BEFORE clearing the buffer, otherwise React will
|
||||
// briefly render with buffer cleared but content still stale, causing
|
||||
// CodeMirror to setValue and reset the cursor.
|
||||
onSaved?.(saved);
|
||||
setLocalFileBuffer(filePath, undefined);
|
||||
} catch {
|
||||
/* swallow — surfacing handled elsewhere if needed */
|
||||
}
|
||||
}, [filePath, onSaved, saveLocalFile, setLocalFileBuffer]);
|
||||
const { body, frontmatter } = useMemo(
|
||||
() => (isMarkdown ? parseSkillMarkdownFrontmatter(editingValue) : { body: editingValue }),
|
||||
[isMarkdown, editingValue],
|
||||
);
|
||||
const frontmatterFields = useMemo(
|
||||
() => (frontmatter ? parseSkillMarkdownFrontmatterFields(frontmatter) : {}),
|
||||
[frontmatter],
|
||||
);
|
||||
const frontmatterMetadata = useMemo(
|
||||
() => (frontmatter ? parseSkillMarkdownMetadata(frontmatter) : []),
|
||||
[frontmatter],
|
||||
);
|
||||
const previewTitle = isMarkdown
|
||||
? (frontmatterFields.name ?? '')
|
||||
: (filePath.split('/').at(-1) ?? filePath);
|
||||
|
||||
const { body, frontmatter } = useMemo(
|
||||
() => (isMarkdown ? parseSkillMarkdownFrontmatter(editingValue) : { body: editingValue }),
|
||||
[isMarkdown, editingValue],
|
||||
);
|
||||
const frontmatterFields = useMemo(
|
||||
() => (frontmatter ? parseSkillMarkdownFrontmatterFields(frontmatter) : {}),
|
||||
[frontmatter],
|
||||
);
|
||||
const frontmatterMetadata = useMemo(
|
||||
() => (frontmatter ? parseSkillMarkdownMetadata(frontmatter) : []),
|
||||
[frontmatter],
|
||||
);
|
||||
const [mode, setMode] = useState<TextPreviewMode>(canRender ? 'render' : 'raw');
|
||||
const showHtmlPreview = isHtml && mode === 'render';
|
||||
|
||||
const [mode, setMode] = useState<TextPreviewMode>(isMarkdown ? 'render' : 'raw');
|
||||
useEffect(() => {
|
||||
setMode(canRender ? 'render' : 'raw');
|
||||
}, [canRender, filePath]);
|
||||
|
||||
useEffect(() => {
|
||||
setMode(isMarkdown ? 'render' : 'raw');
|
||||
}, [isMarkdown]);
|
||||
|
||||
return (
|
||||
<Flexbox flex={1} height={'100%'} style={{ minHeight: 0, overflow: 'hidden' }}>
|
||||
{isMarkdown && (
|
||||
<Flexbox
|
||||
horizontal
|
||||
align={'center'}
|
||||
gap={8}
|
||||
paddingBlock={6}
|
||||
paddingInline={12}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text ellipsis style={{ flex: 1, fontSize: 13, fontWeight: 500, minWidth: 0 }}>
|
||||
{frontmatterFields.name ?? ''}
|
||||
</Text>
|
||||
<Segmented
|
||||
size={'small'}
|
||||
value={mode}
|
||||
options={[
|
||||
{
|
||||
icon: <Icon icon={EyeIcon} />,
|
||||
label: t('workingPanel.localFile.preview.render'),
|
||||
value: 'render',
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={CodeIcon} />,
|
||||
label: t('workingPanel.localFile.preview.raw'),
|
||||
value: 'raw',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => setMode(v as TextPreviewMode)}
|
||||
/>
|
||||
</Flexbox>
|
||||
)}
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||
{isMarkdown && mode === 'render' ? (
|
||||
<>
|
||||
<SkillFrontmatterPreviewCard metadata={frontmatterMetadata} />
|
||||
<Markdown style={{ paddingBlock: 8, paddingInline: 12 }}>{body}</Markdown>
|
||||
</>
|
||||
) : (
|
||||
<CodeEditorPane
|
||||
language={extensionToLanguage(ext)}
|
||||
style={{ fontSize: 12, minHeight: '100%' }}
|
||||
value={editingValue}
|
||||
onChange={handleCodeChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
return (
|
||||
<Flexbox flex={1} height={'100%'} style={{ minHeight: 0, overflow: 'hidden' }}>
|
||||
{canRender && (
|
||||
<Flexbox
|
||||
horizontal
|
||||
align={'center'}
|
||||
gap={8}
|
||||
paddingBlock={6}
|
||||
paddingInline={12}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text ellipsis style={{ flex: 1, fontSize: 13, fontWeight: 500, minWidth: 0 }}>
|
||||
{previewTitle}
|
||||
</Text>
|
||||
<Segmented
|
||||
size={'small'}
|
||||
value={mode}
|
||||
options={[
|
||||
{
|
||||
icon: <Icon icon={EyeIcon} />,
|
||||
label: t('workingPanel.localFile.preview.render'),
|
||||
value: 'render',
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={CodeIcon} />,
|
||||
label: t(
|
||||
isHtml
|
||||
? 'workingPanel.localFile.preview.source'
|
||||
: 'workingPanel.localFile.preview.raw',
|
||||
),
|
||||
value: 'raw',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => setMode(v as TextPreviewMode)}
|
||||
/>
|
||||
</Flexbox>
|
||||
)}
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: showHtmlPreview ? 'hidden' : 'auto' }}>
|
||||
{isMarkdown && mode === 'render' ? (
|
||||
<>
|
||||
<SkillFrontmatterPreviewCard metadata={frontmatterMetadata} />
|
||||
<Markdown style={{ paddingBlock: 8, paddingInline: 12 }}>{body}</Markdown>
|
||||
</>
|
||||
) : showHtmlPreview ? (
|
||||
<InlineHtmlPreview content={editingValue} />
|
||||
) : (
|
||||
<CodeEditorPane
|
||||
language={extensionToLanguage(ext)}
|
||||
style={{ fontSize: 12, minHeight: '100%' }}
|
||||
value={editingValue}
|
||||
onChange={handleCodeChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TextPreviewPane.displayName = 'TextPreviewPane';
|
||||
|
||||
@@ -358,6 +377,7 @@ const ActiveFileView = memo<ActiveFileViewProps>(({ filePath, workingDirectory }
|
||||
return (
|
||||
<TextPreviewPane
|
||||
content={preview.content}
|
||||
contentType={preview.contentType}
|
||||
ext={ext}
|
||||
filePath={filePath}
|
||||
onSaved={handleSavedContent}
|
||||
|
||||
Reference in New Issue
Block a user