mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
💄 style(workflow): inset partial warning badge (#15773)
* 💄 style(workflow): inset partial warning badge * ✨ feat(portal): support preview for local markdown images * 🐛 fix(portal): narrow markdown image src
This commit is contained in:
@@ -363,8 +363,8 @@ const WorkflowCollapse = memo<WorkflowCollapseProps>(
|
||||
}
|
||||
case 'partial': {
|
||||
// Mix of success + failure: show success as the primary state and
|
||||
// surface a small warning badge at the bottom-right so the overall
|
||||
// turn still reads as "done" rather than "broken".
|
||||
// surface a small warning badge slightly inset from the bottom-right
|
||||
// so the overall turn still reads as "done" rather than "broken".
|
||||
return (
|
||||
<div style={{ flex: 'none', position: 'relative' }}>
|
||||
{wrapInBlock(<Icon color={cssVar.colorSuccess} icon={Check} />)}
|
||||
@@ -373,12 +373,12 @@ const WorkflowCollapse = memo<WorkflowCollapseProps>(
|
||||
alignItems: 'center',
|
||||
background: cssVar.colorBgContainer,
|
||||
borderRadius: '50%',
|
||||
bottom: 0,
|
||||
bottom: 2,
|
||||
display: 'flex',
|
||||
height: 10,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
right: 2,
|
||||
width: 10,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -34,6 +34,7 @@ vi.mock('@lobehub/ui', () => ({
|
||||
Empty: ({ description }: { description?: ReactNode }) => <div>{description}</div>,
|
||||
Flexbox: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
Icon: () => null,
|
||||
Image: ({ alt, src }: { alt?: string; src?: string }) => <img alt={alt} src={src} />,
|
||||
Markdown: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
Segmented: () => null,
|
||||
Text: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { isDesktop } from '@lobechat/const';
|
||||
import type { MarkdownProps } from '@lobehub/ui';
|
||||
import { ActionIcon, Center, Empty, Flexbox, Icon, Markdown, Segmented, Text } from '@lobehub/ui';
|
||||
import {
|
||||
ActionIcon,
|
||||
Center,
|
||||
Empty,
|
||||
Flexbox,
|
||||
Icon,
|
||||
Image,
|
||||
Markdown,
|
||||
Segmented,
|
||||
Text,
|
||||
} from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { CodeIcon, EyeIcon, RefreshCwIcon } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -44,7 +54,13 @@ const ImagePreview = memo<ImagePreviewProps>(({ blob, filename }) => {
|
||||
|
||||
return (
|
||||
<Center height={'100%'} style={{ overflow: 'auto' }} width={'100%'}>
|
||||
<img alt={filename} src={imageSrc} style={{ maxWidth: '100%', objectFit: 'contain' }} />
|
||||
<Image
|
||||
alt={filename}
|
||||
objectFit={'contain'}
|
||||
src={imageSrc}
|
||||
style={{ maxWidth: '100%' }}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import MarkdownImage from './MarkdownImage';
|
||||
|
||||
const mockImage = vi.hoisted(() => vi.fn());
|
||||
const mockUseClientDataSWR = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@lobehub/ui', () => ({
|
||||
Image: ({
|
||||
alt,
|
||||
classNames,
|
||||
objectFit,
|
||||
src,
|
||||
styles,
|
||||
variant,
|
||||
}: {
|
||||
alt?: string;
|
||||
classNames?: { image?: string };
|
||||
objectFit?: string;
|
||||
src?: string;
|
||||
styles?: { image?: CSSProperties };
|
||||
variant?: string;
|
||||
}) => {
|
||||
mockImage({ alt, classNames, objectFit, src, styles, variant });
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={alt}
|
||||
className={classNames?.image}
|
||||
data-object-fit={objectFit}
|
||||
data-testid="lobe-image"
|
||||
data-variant={variant}
|
||||
src={src}
|
||||
style={styles?.image}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('antd-style', () => ({
|
||||
cssVar: {
|
||||
colorFillQuaternary: 'var(--color-fill-quaternary)',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/libs/swr', () => ({
|
||||
useClientDataSWR: mockUseClientDataSWR,
|
||||
}));
|
||||
|
||||
vi.mock('@/services/projectFile', () => ({
|
||||
projectFileService: {
|
||||
getLocalFilePreview: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MarkdownImage', () => {
|
||||
beforeEach(() => {
|
||||
mockImage.mockClear();
|
||||
mockUseClientDataSWR.mockReset();
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: vi.fn(() => 'blob:markdown-image'),
|
||||
revokeObjectURL: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remote markdown images with the LobeHub Image component', () => {
|
||||
mockUseClientDataSWR.mockReturnValue({});
|
||||
|
||||
render(
|
||||
<MarkdownImage
|
||||
alt="remote"
|
||||
className="markdown-img"
|
||||
markdownFilePath="/repo/report.md"
|
||||
src="https://example.com/screenshot.png"
|
||||
style={{ width: 320 }}
|
||||
workingDirectory="/repo"
|
||||
/>,
|
||||
);
|
||||
|
||||
const image = screen.getByTestId('lobe-image');
|
||||
expect(image).toHaveAttribute('src', 'https://example.com/screenshot.png');
|
||||
expect(image).toHaveClass('markdown-img');
|
||||
expect(image).toHaveStyle({ maxWidth: '100%', width: '320px' });
|
||||
expect(mockImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
objectFit: 'contain',
|
||||
src: 'https://example.com/screenshot.png',
|
||||
variant: 'borderless',
|
||||
}),
|
||||
);
|
||||
expect(mockUseClientDataSWR).toHaveBeenCalledWith(null, expect.any(Function), {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves relative markdown images and renders the loaded blob through LobeHub Image', () => {
|
||||
mockUseClientDataSWR.mockReturnValue({
|
||||
data: {
|
||||
blob: new Blob(['image']),
|
||||
type: 'image',
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<MarkdownImage
|
||||
alt="local"
|
||||
deviceId="device-1"
|
||||
markdownFilePath="/repo/.records/report.md"
|
||||
src="assets/screenshot.png"
|
||||
workingDirectory="/repo"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('lobe-image')).toHaveAttribute('src', 'blob:markdown-image');
|
||||
expect(mockUseClientDataSWR).toHaveBeenCalledWith(
|
||||
['local-markdown-image-preview', 'device-1', '/repo/.records/assets/screenshot.png', '/repo'],
|
||||
expect.any(Function),
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a stable placeholder while a relative markdown image is loading', () => {
|
||||
mockUseClientDataSWR.mockReturnValue({});
|
||||
|
||||
render(
|
||||
<MarkdownImage
|
||||
alt="local"
|
||||
markdownFilePath="/repo/report.md"
|
||||
src="assets/screenshot.png"
|
||||
workingDirectory="/repo"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('lobe-image')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: 'local' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Image } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
@@ -15,7 +16,7 @@ interface MarkdownImageProps extends ComponentProps<'img'> {
|
||||
}
|
||||
|
||||
const MarkdownImage = memo<MarkdownImageProps>(
|
||||
({ alt, deviceId, markdownFilePath, node, src, style, workingDirectory, ...rest }) => {
|
||||
({ alt, className, deviceId, markdownFilePath, node, src, style, workingDirectory }) => {
|
||||
void node;
|
||||
|
||||
const markdownSrc = typeof src === 'string' ? src : undefined;
|
||||
@@ -71,12 +72,16 @@ const MarkdownImage = memo<MarkdownImageProps>(
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedSrc = imageSrc ?? markdownSrc;
|
||||
|
||||
return (
|
||||
<img
|
||||
<Image
|
||||
alt={alt}
|
||||
src={imageSrc ?? src}
|
||||
style={{ maxWidth: '100%', ...style }}
|
||||
{...rest}
|
||||
classNames={className ? { image: className } : undefined}
|
||||
objectFit={'contain'}
|
||||
src={resolvedSrc}
|
||||
styles={{ image: { maxWidth: '100%', ...style } }}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user