diff --git a/client/src/components/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx index c125889c88..8257f76887 100644 --- a/client/src/components/Artifacts/ArtifactPreview.tsx +++ b/client/src/components/Artifacts/ArtifactPreview.tsx @@ -6,7 +6,7 @@ import type { } from '@codesandbox/sandpack-react/unstyled'; import type { TStartupConfig } from 'librechat-data-provider'; import type { ArtifactFiles } from '~/common'; -import { sharedFiles, sharedOptions } from '~/utils/artifacts'; +import { sharedFiles, buildSandpackOptions } from '~/utils/artifacts'; export const ArtifactPreview = memo(function ({ files, @@ -39,15 +39,10 @@ export const ArtifactPreview = memo(function ({ }; }, [currentCode, files, fileKey]); - const options: typeof sharedOptions = useMemo(() => { - if (!startupConfig) { - return sharedOptions; - } - return { - ...sharedOptions, - bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL, - }; - }, [startupConfig, template]); + const options: SandpackProviderProps['options'] = useMemo( + () => buildSandpackOptions(template, startupConfig), + [startupConfig, template], + ); if (Object.keys(artifactFiles).length === 0) { return null; diff --git a/client/src/utils/__tests__/artifacts.test.ts b/client/src/utils/__tests__/artifacts.test.ts new file mode 100644 index 0000000000..bf5c1919c7 --- /dev/null +++ b/client/src/utils/__tests__/artifacts.test.ts @@ -0,0 +1,38 @@ +import { buildSandpackOptions } from '../artifacts'; + +const TAILWIND_CDN = 'https://cdn.tailwindcss.com/3.4.17#tailwind.js'; + +describe('buildSandpackOptions', () => { + it('includes externalResources with .js fragment hint for static template', () => { + const options = buildSandpackOptions('static'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('includes externalResources for react-ts template', () => { + const options = buildSandpackOptions('react-ts'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('uses staticBundlerURL when template is static and config is provided', () => { + const config = { staticBundlerURL: 'https://static.example.com' } as Parameters< + typeof buildSandpackOptions + >[1]; + const options = buildSandpackOptions('static', config); + expect(options?.bundlerURL).toBe('https://static.example.com'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('uses bundlerURL when template is react-ts and config is provided', () => { + const config = { bundlerURL: 'https://bundler.example.com' } as Parameters< + typeof buildSandpackOptions + >[1]; + const options = buildSandpackOptions('react-ts', config); + expect(options?.bundlerURL).toBe('https://bundler.example.com'); + expect(options?.externalResources).toEqual([TAILWIND_CDN]); + }); + + it('returns base options without bundlerURL when no config is provided', () => { + const options = buildSandpackOptions('react-ts'); + expect(options?.bundlerURL).toBeUndefined(); + }); +}); diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 793bc484bc..2ca02422a2 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -4,6 +4,7 @@ import type { SandpackProviderProps, SandpackPredefinedTemplate, } from '@codesandbox/sandpack-react'; +import type { TStartupConfig } from 'librechat-data-provider'; const artifactFilename = { 'application/vnd.react': 'App.tsx', @@ -138,10 +139,29 @@ export function getProps(type: string): Partial { }; } +/** Fragment hint lets Sandpack's static-template regex detect `.js` from the URL; + * without it, the versioned CDN path (`/3.4.17`) has no recognised extension and + * `injectExternalResources` throws "Unable to determine file type". */ +const TAILWIND_CDN = 'https://cdn.tailwindcss.com/3.4.17#tailwind.js'; + export const sharedOptions: SandpackProviderProps['options'] = { - externalResources: ['https://cdn.tailwindcss.com/3.4.17'], + externalResources: [TAILWIND_CDN], }; +export function buildSandpackOptions( + template: SandpackProviderProps['template'], + startupConfig?: TStartupConfig, +): SandpackProviderProps['options'] { + if (!startupConfig) { + return sharedOptions; + } + + return { + ...sharedOptions, + bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL, + }; +} + export const sharedFiles = { '/lib/utils.ts': shadcnComponents.utils, '/components/ui/accordion.tsx': shadcnComponents.accordian,