Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Markdown and Code artifacts #4985

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
573 changes: 171 additions & 402 deletions api/app/clients/prompts/artifacts.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/src/components/Artifacts/Artifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function Artifact({
const title = props.title ?? 'Untitled Artifact';
const type = props.type ?? 'unknown';
const identifier = props.identifier ?? 'no-identifier';
const language = props.language;
const artifactKey = `${identifier}_${type}_${title}`.replace(/\s+/g, '_').toLowerCase();

throttledUpdateRef.current(() => {
Expand All @@ -74,6 +75,7 @@ export function Artifact({
identifier,
title,
type,
language,
content,
lastUpdateTime: now,
};
Expand Down
34 changes: 17 additions & 17 deletions client/src/components/Artifacts/Artifacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as Tabs from '@radix-ui/react-tabs';
import { SandpackPreviewRef } from '@codesandbox/sandpack-react';
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
import { CodeMarkdown, CopyCodeButton } from './Code';
import { getFileExtension } from '~/utils/artifacts';
import { formatContent, isProse, needsPreview } from '~/utils/artifacts';
import { ArtifactPreview } from './ArtifactPreview';
import { cn } from '~/utils';
import store from '~/store';
Expand Down Expand Up @@ -44,21 +44,23 @@ export default function Artifacts() {
setTimeout(() => setIsRefreshing(false), 750);
};

const hasPreview = needsPreview(currentArtifact.type ?? '');

return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
{/* Main Parent */}
<div className="flex h-full w-full items-center justify-center py-2">
{/* Main Container */}
<div
className={`flex h-[97%] w-[97%] flex-col overflow-hidden rounded-xl border border-border-medium bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-300 ease-in-out ${
isVisible
className={`flex h-[97%] w-[97%] flex-col overflow-hidden rounded-xl border border-border-medium bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-300 ease-in-out ${isVisible
? 'translate-x-0 scale-100 opacity-100'
: 'translate-x-full scale-95 opacity-0'
}`}
}`}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-border-medium bg-surface-primary-alt p-2">
<div className="flex items-center">
{/* Back button*/}
<button
className="mr-2 text-text-secondary"
onClick={() => {
Expand All @@ -76,15 +78,15 @@ export default function Artifacts() {
<path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z" />
</svg>
</button>
{/* Artifact title*/}
<h3 className="truncate text-sm text-text-primary">{currentArtifact.title}</h3>
</div>
<div className="flex items-center">
{/* Refresh button */}
{activeTab === 'preview' && (
{hasPreview && activeTab === 'preview' && (
<button
className={`mr-2 text-text-secondary transition-transform duration-500 ease-in-out ${
isRefreshing ? 'rotate-180' : ''
}`}
className={`mr-2 text-text-secondary transition-transform duration-500 ease-in-out ${isRefreshing ? 'rotate-180' : ''
}`}
onClick={handleRefresh}
disabled={isRefreshing}
aria-label="Refresh"
Expand All @@ -95,7 +97,7 @@ export default function Artifacts() {
/>
</button>
)}
<Tabs.List className="mr-2 inline-flex h-7 rounded-full border border-border-medium bg-surface-tertiary">
{hasPreview && <Tabs.List className="mr-2 inline-flex h-7 rounded-full border border-border-medium bg-surface-tertiary">
<Tabs.Trigger
value="preview"
className="border-0.5 flex items-center gap-1 rounded-full border-transparent py-1 pl-2.5 pr-2.5 text-xs font-medium text-text-secondary data-[state=active]:border-border-light data-[state=active]:bg-surface-primary-alt data-[state=active]:text-text-primary"
Expand All @@ -108,7 +110,7 @@ export default function Artifacts() {
>
Code
</Tabs.Trigger>
</Tabs.List>
</Tabs.List>}
<button
className="text-text-secondary"
onClick={() => {
Expand All @@ -131,12 +133,11 @@ export default function Artifacts() {
{/* Content */}
<Tabs.Content
value="code"
className={cn('flex-grow overflow-x-auto overflow-y-scroll bg-gray-900 p-4')}
className={cn('flex-grow overflow-x-auto overflow-y-scroll p-4', isProse(currentArtifact.type) ? 'prose dark:prose-invert light' : 'bg-gray-900')}
>
<CodeMarkdown
content={`\`\`\`${getFileExtension(currentArtifact.type)}\n${
currentArtifact.content ?? ''
}\`\`\``}
content={formatContent(currentArtifact.content ?? '', currentArtifact.type, currentArtifact.language)}
isCode={isProse(currentArtifact.type)}
isSubmitting={isSubmitting}
/>
</Tabs.Content>
Expand All @@ -163,9 +164,8 @@ export default function Artifacts() {
<path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z" />
</svg>
</button>
<span className="text-xs">{`${currentIndex + 1} / ${
orderedArtifactIds.length
}`}</span>
<span className="text-xs">{`${currentIndex + 1} / ${orderedArtifactIds.length
}`}</span>
<button onClick={() => cycleArtifact('next')} className="ml-2 text-text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Artifacts/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC
});

export const CodeMarkdown = memo(
({ content = '', isSubmitting }: { content: string; isSubmitting: boolean }) => {
({ content = '', isSubmitting }: { content: string; isSubmitting: boolean, isCode: boolean }) => {
const scrollRef = useRef<HTMLDivElement>(null);
const [userScrolled, setUserScrolled] = useState(false);
const currentContent = content;
Expand Down
12 changes: 10 additions & 2 deletions client/src/hooks/Artifacts/useArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo, useState, useEffect, useRef } from 'react';
import { Constants } from 'librechat-data-provider';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { useChatContext } from '~/Providers';
import { getKey } from '~/utils/artifacts';
import { getKey, needsPreview } from '~/utils/artifacts';
import { getLatestText } from '~/utils';
import store from '~/store';

Expand Down Expand Up @@ -70,7 +70,7 @@ export default function useArtifacts() {
);

if (hasEnclosedArtifact && !hasEnclosedArtifactRef.current) {
setActiveTab('preview');
setActiveTab(needsPreview(latestArtifact?.type) ? 'preview' : 'code');
hasEnclosedArtifactRef.current = true;
hasAutoSwitchedToCodeRef.current = false;
} else if (!hasEnclosedArtifactRef.current && !hasAutoSwitchedToCodeRef.current) {
Expand All @@ -94,6 +94,14 @@ export default function useArtifacts() {

const currentArtifact = currentArtifactId != null ? artifacts?.[currentArtifactId] : null;

// Prevent non-previewable artifacts from using the preview tab
useEffect(() => {
if(!needsPreview(currentArtifact?.type) && activeTab == "preview") {
setActiveTab("code");
}
}, [currentArtifact])


const currentIndex = orderedArtifactIds.indexOf(currentArtifactId ?? '');
const cycleArtifact = (direction: 'next' | 'prev') => {
let newIndex: number;
Expand Down
31 changes: 29 additions & 2 deletions client/src/utils/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ const artifactFilename = {
// 'tsx': 'tsx',
};

// Artifacts that need to render an HTML preview window
const previewableArtifacts = {
'application/vnd.mermaid': true,
'application/vnd.react': true,
'text/html': true,
default: false,
};

const artifactTemplate: Record<
keyof typeof artifactFilename,
SandpackPredefinedTemplate | undefined
Expand All @@ -53,14 +61,16 @@ const artifactTemplate: Record<
// 'tsx': 'tsx',
};

export function getFileExtension(language?: string): string {
switch (language) {
export function getArtifactLanguage(type?: string, language?: string): string {
switch (type) {
case 'application/vnd.react':
return 'tsx';
case 'application/vnd.mermaid':
return 'mermaid';
case 'text/html':
return 'html';
case 'application/vnd.code':
return language ?? 'txt';
// case 'jsx':
// return 'jsx';
// case 'tsx':
Expand All @@ -83,11 +93,28 @@ export function getArtifactFilename(type: string, language?: string): string {
return artifactFilename[key] ?? artifactFilename.default;
}

export function needsPreview(type?: string): boolean {
return (type && previewableArtifacts[type]) ?? previewableArtifacts.default;
}

export function getTemplate(type: string, language?: string): SandpackPredefinedTemplate {
const key = getKey(type, language);
return artifactTemplate[key] ?? (artifactTemplate.default as SandpackPredefinedTemplate);
}

export function isProse(type?: string) {
return type === 'text/markdown';
}

export function formatContent(content: string, type?: string, language?: string) {
if (isProse(type)) {
// If rendering markdown, no need to add a code block
return content;
}
// Else return a markdown code block
return `\`\`\`${getArtifactLanguage(type, language)}\n${content}\`\`\``;
}

const standardDependencies = {
three: '^0.167.1',
'lucide-react': '^0.394.0',
Expand Down
Loading