mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-03 23:00:18 +01:00
✨ feat: Enhance Artifact Management with Version Control and UI Improvements
This commit is contained in:
parent
114deecc4e
commit
0ee5712df1
10 changed files with 194 additions and 109 deletions
|
|
@ -13,8 +13,9 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const setVisible = useSetRecoilState(store.artifactsVisibility);
|
const setVisible = useSetRecoilState(store.artifactsVisibility);
|
||||||
const [artifacts, setArtifacts] = useRecoilState(store.artifactsState);
|
const [artifacts, setArtifacts] = useRecoilState(store.artifactsState);
|
||||||
const setCurrentArtifactId = useSetRecoilState(store.currentArtifactId);
|
const [currentArtifactId, setCurrentArtifactId] = useRecoilState(store.currentArtifactId);
|
||||||
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
|
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
|
||||||
|
const isSelected = artifact?.id === currentArtifactId;
|
||||||
const [visibleArtifacts, setVisibleArtifacts] = useRecoilState(store.visibleArtifacts);
|
const [visibleArtifacts, setVisibleArtifacts] = useRecoilState(store.visibleArtifacts);
|
||||||
|
|
||||||
const debouncedSetVisibleRef = useRef(
|
const debouncedSetVisibleRef = useRef(
|
||||||
|
|
@ -69,9 +70,14 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
|
||||||
setCurrentArtifactId(artifact.id);
|
setCurrentArtifactId(artifact.id);
|
||||||
}, 15);
|
}, 15);
|
||||||
}}
|
}}
|
||||||
className="relative overflow-hidden rounded-xl border border-border-medium transition-all duration-300 hover:border-border-xheavy hover:shadow-lg"
|
className={
|
||||||
|
`relative overflow-hidden rounded-xl transition-all duration-200 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg ` +
|
||||||
|
(isSelected
|
||||||
|
? 'border-border-medium bg-surface-hover shadow-lg'
|
||||||
|
: 'border-border-light bg-surface-tertiary shadow-sm')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="w-fit bg-surface-tertiary p-2">
|
<div className="w-fit p-2">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<FilePreview fileType={fileType} className="relative" />
|
<FilePreview fileType={fileType} className="relative" />
|
||||||
<div className="overflow-hidden text-left">
|
<div className="overflow-hidden text-left">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
||||||
import {
|
import {
|
||||||
useSandpack,
|
useSandpack,
|
||||||
SandpackCodeEditor,
|
SandpackCodeEditor,
|
||||||
|
|
@ -116,6 +117,9 @@ const CodeEditor = ({
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
showInlineErrors={true}
|
showInlineErrors={true}
|
||||||
readOnly={readOnly === true}
|
readOnly={readOnly === true}
|
||||||
|
extensions={[autocompletion()]}
|
||||||
|
// @ts-ignore
|
||||||
|
extensionsKeymap={[completionKeymap]}
|
||||||
className="hljs language-javascript bg-black"
|
className="hljs language-javascript bg-black"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo, type MutableRefObject } from 'react';
|
||||||
import {
|
import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled';
|
||||||
SandpackPreview,
|
import type { SandpackProviderProps, SandpackPreviewRef, PreviewProps } from '@codesandbox/sandpack-react/unstyled';
|
||||||
SandpackProvider,
|
|
||||||
SandpackProviderProps,
|
|
||||||
} from '@codesandbox/sandpack-react/unstyled';
|
|
||||||
import type { SandpackPreviewRef, PreviewProps } from '@codesandbox/sandpack-react/unstyled';
|
|
||||||
import type { TStartupConfig } from 'librechat-data-provider';
|
import type { TStartupConfig } from 'librechat-data-provider';
|
||||||
import type { ArtifactFiles } from '~/common';
|
import type { ArtifactFiles } from '~/common';
|
||||||
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
||||||
|
|
@ -22,7 +18,7 @@ export const ArtifactPreview = memo(function ({
|
||||||
fileKey: string;
|
fileKey: string;
|
||||||
template: SandpackProviderProps['template'];
|
template: SandpackProviderProps['template'];
|
||||||
sharedProps: Partial<SandpackProviderProps>;
|
sharedProps: Partial<SandpackProviderProps>;
|
||||||
previewRef: React.MutableRefObject<SandpackPreviewRef>;
|
previewRef: MutableRefObject<SandpackPreviewRef>;
|
||||||
currentCode?: string;
|
currentCode?: string;
|
||||||
startupConfig?: TStartupConfig;
|
startupConfig?: TStartupConfig;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -36,9 +32,7 @@ export const ArtifactPreview = memo(function ({
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...files,
|
...files,
|
||||||
[fileKey]: {
|
[fileKey]: { code },
|
||||||
code,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}, [currentCode, files, fileKey]);
|
}, [currentCode, files, fileKey]);
|
||||||
|
|
||||||
|
|
@ -46,12 +40,10 @@ export const ArtifactPreview = memo(function ({
|
||||||
if (!startupConfig) {
|
if (!startupConfig) {
|
||||||
return sharedOptions;
|
return sharedOptions;
|
||||||
}
|
}
|
||||||
const _options: typeof sharedOptions = {
|
return {
|
||||||
...sharedOptions,
|
...sharedOptions,
|
||||||
bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL,
|
bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL,
|
||||||
};
|
};
|
||||||
|
|
||||||
return _options;
|
|
||||||
}, [startupConfig, template]);
|
}, [startupConfig, template]);
|
||||||
|
|
||||||
if (Object.keys(artifactFiles).length === 0) {
|
if (Object.keys(artifactFiles).length === 0) {
|
||||||
|
|
@ -60,10 +52,7 @@ export const ArtifactPreview = memo(function ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SandpackProvider
|
<SandpackProvider
|
||||||
files={{
|
files={{ ...artifactFiles, ...sharedFiles }}
|
||||||
...artifactFiles,
|
|
||||||
...sharedFiles,
|
|
||||||
}}
|
|
||||||
options={options}
|
options={options}
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
template={template}
|
template={template}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useRef, useEffect } from 'react';
|
import { useRef, useEffect } from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
|
import type { SandpackPreviewRef } from '@codesandbox/sandpack-react/unstyled';
|
||||||
|
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||||
import type { Artifact } from '~/common';
|
import type { Artifact } from '~/common';
|
||||||
import { useEditorContext, useArtifactsContext } from '~/Providers';
|
import { useEditorContext, useArtifactsContext } from '~/Providers';
|
||||||
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
|
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
|
||||||
|
|
@ -18,11 +19,13 @@ export default function ArtifactTabs({
|
||||||
artifact: Artifact;
|
artifact: Artifact;
|
||||||
editorRef: React.MutableRefObject<CodeEditorRef>;
|
editorRef: React.MutableRefObject<CodeEditorRef>;
|
||||||
previewRef: React.MutableRefObject<SandpackPreviewRef>;
|
previewRef: React.MutableRefObject<SandpackPreviewRef>;
|
||||||
|
isSubmitting: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { isSubmitting } = useArtifactsContext();
|
const { isSubmitting } = useArtifactsContext();
|
||||||
const { currentCode, setCurrentCode } = useEditorContext();
|
const { currentCode, setCurrentCode } = useEditorContext();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const lastIdRef = useRef<string | null>(null);
|
const lastIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (artifact.id !== lastIdRef.current) {
|
if (artifact.id !== lastIdRef.current) {
|
||||||
setCurrentCode(undefined);
|
setCurrentCode(undefined);
|
||||||
|
|
@ -33,7 +36,9 @@ export default function ArtifactTabs({
|
||||||
const content = artifact.content ?? '';
|
const content = artifact.content ?? '';
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
useAutoScroll({ ref: contentRef, content, isSubmitting });
|
useAutoScroll({ ref: contentRef, content, isSubmitting });
|
||||||
|
|
||||||
const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact });
|
const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs.Content
|
<Tabs.Content
|
||||||
|
|
|
||||||
78
client/src/components/Artifacts/ArtifactVersion.tsx
Normal file
78
client/src/components/Artifacts/ArtifactVersion.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { MenuButton } from '@ariakit/react';
|
||||||
|
import { History, Check } from 'lucide-react';
|
||||||
|
import { DropdownPopup, TooltipAnchor, Button } from '~/components';
|
||||||
|
import { useLocalize, useMediaQuery } from '~/hooks';
|
||||||
|
|
||||||
|
interface ArtifactVersionProps {
|
||||||
|
currentIndex: number;
|
||||||
|
totalVersions: number;
|
||||||
|
onVersionChange: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArtifactVersion({
|
||||||
|
currentIndex,
|
||||||
|
totalVersions,
|
||||||
|
onVersionChange,
|
||||||
|
}: ArtifactVersionProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
const menuId = 'version-dropdown-menu';
|
||||||
|
|
||||||
|
const handleValueChange = (value: string) => {
|
||||||
|
const index = parseInt(value, 10);
|
||||||
|
onVersionChange(index);
|
||||||
|
setIsPopoverActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (totalVersions <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = Array.from({ length: totalVersions }, (_, index) => ({
|
||||||
|
value: index.toString(),
|
||||||
|
label: localize('com_ui_version_var', { 0: String(index + 1) }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dropdownItems = options.map((option) => {
|
||||||
|
const isSelected = option.value === String(currentIndex);
|
||||||
|
return {
|
||||||
|
label: option.label,
|
||||||
|
onClick: () => handleValueChange(option.value),
|
||||||
|
value: option.value,
|
||||||
|
icon: isSelected ? (
|
||||||
|
<Check size={16} className="text-text-primary" aria-hidden="true" />
|
||||||
|
) : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownPopup
|
||||||
|
menuId={menuId}
|
||||||
|
focusLoop
|
||||||
|
unmountOnHide
|
||||||
|
isOpen={isPopoverActive}
|
||||||
|
setIsOpen={setIsPopoverActive}
|
||||||
|
trigger={
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize('com_ui_change_version')}
|
||||||
|
render={
|
||||||
|
<Button size="icon" variant="ghost" asChild>
|
||||||
|
<MenuButton>
|
||||||
|
<History
|
||||||
|
size={18}
|
||||||
|
className="text-text-secondary"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
/>
|
||||||
|
</MenuButton>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
items={dropdownItems}
|
||||||
|
className={isSmallScreen ? '' : 'absolute right-0 top-0 mt-2'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useRef, useState, useEffect } from 'react';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
import { ArrowLeft, ChevronLeft, ChevronRight, RefreshCw, X } from 'lucide-react';
|
import { Button, Spinner } from '@librechat/client';
|
||||||
|
import { Code, Play, RefreshCw, X } from 'lucide-react';
|
||||||
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
|
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||||
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
|
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
|
||||||
import DownloadArtifact from './DownloadArtifact';
|
import DownloadArtifact from './DownloadArtifact';
|
||||||
|
import ArtifactVersion from './ArtifactVersion';
|
||||||
import { useEditorContext } from '~/Providers';
|
import { useEditorContext } from '~/Providers';
|
||||||
import ArtifactTabs from './ArtifactTabs';
|
import ArtifactTabs from './ArtifactTabs';
|
||||||
import { CopyCodeButton } from './Code';
|
import { CopyCodeButton } from './Code';
|
||||||
|
|
@ -29,19 +31,19 @@ export default function Artifacts() {
|
||||||
activeTab,
|
activeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
cycleArtifact,
|
|
||||||
currentArtifact,
|
currentArtifact,
|
||||||
orderedArtifactIds,
|
orderedArtifactIds,
|
||||||
|
setCurrentArtifactId,
|
||||||
} = useArtifacts();
|
} = useArtifacts();
|
||||||
|
|
||||||
if (currentArtifact === null || currentArtifact === undefined) {
|
if (!currentArtifact) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
const client = previewRef.current?.getClient();
|
const client = previewRef.current?.getClient();
|
||||||
if (client != null) {
|
if (client) {
|
||||||
client.dispatch({ type: 'refresh' });
|
client.dispatch({ type: 'refresh' });
|
||||||
}
|
}
|
||||||
setTimeout(() => setIsRefreshing(false), 750);
|
setTimeout(() => setIsRefreshing(false), 750);
|
||||||
|
|
@ -54,94 +56,96 @@ export default function Artifacts() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
|
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
|
||||||
{/* Main Parent */}
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
{/* Main Container */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex h-full w-full flex-col overflow-hidden border border-border-medium bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-500 ease-in-out`,
|
`flex h-full w-full flex-col overflow-hidden bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-500 ease-in-out`,
|
||||||
isVisible ? 'scale-100 opacity-100 blur-0' : 'scale-105 opacity-0 blur-sm',
|
isVisible ? 'opacity-100 blur-0' : 'opacity-0 blur-sm'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<div className="flex items-center justify-between bg-surface-primary-alt p-2">
|
||||||
<div className="flex items-center justify-between border-b border-border-medium bg-surface-primary-alt p-2">
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button className="mr-2 text-text-secondary" onClick={closeArtifacts}>
|
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<div
|
||||||
</button>
|
className={`absolute top-0.5 h-8 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out ${
|
||||||
<h3 className="truncate text-sm text-text-primary">{currentArtifact.title}</h3>
|
activeTab === 'code'
|
||||||
|
? 'w-[42%] translate-x-0'
|
||||||
|
: 'w-[50%] translate-x-[calc(100%-0.250rem)]'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="code"
|
||||||
|
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent py-1 pl-2.5 pr-2.5 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||||
|
>
|
||||||
|
<Code className="size-3" />
|
||||||
|
<span className="transition-all duration-200 ease-out">
|
||||||
|
{localize('com_ui_code')}
|
||||||
|
</span>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger
|
||||||
|
value="preview"
|
||||||
|
disabled={isMutating}
|
||||||
|
className="relative z-10 flex items-center gap-2 rounded-xl border-transparent py-1 pl-2.5 pr-2.5 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
|
||||||
|
>
|
||||||
|
<Play className="size-3" />
|
||||||
|
<span className="transition-all duration-200 ease-out">
|
||||||
|
{localize('com_ui_preview')}
|
||||||
|
</span>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
|
||||||
{/* Refresh button */}
|
<div className="flex items-center gap-2">
|
||||||
{activeTab === 'preview' && (
|
{activeTab === 'preview' && (
|
||||||
<button
|
<Button
|
||||||
className={cn(
|
size="icon"
|
||||||
'mr-2 text-text-secondary transition-transform duration-500 ease-in-out',
|
variant="ghost"
|
||||||
isRefreshing ? 'rotate-180' : '',
|
|
||||||
)}
|
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
aria-label="Refresh"
|
aria-label="Refresh"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
{isRefreshing ? (
|
||||||
size={16}
|
<Spinner size={16} />
|
||||||
className={cn('transform', isRefreshing ? 'animate-spin' : '')}
|
) : (
|
||||||
/>
|
<RefreshCw
|
||||||
</button>
|
size={16}
|
||||||
|
className={cn('transform', isRefreshing ? 'animate-spin' : '')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
{activeTab !== 'preview' && isMutating && (
|
{activeTab !== 'preview' && isMutating && (
|
||||||
<RefreshCw size={16} className="mr-2 animate-spin text-text-secondary" />
|
<RefreshCw size={16} className="mr-2 animate-spin text-text-secondary" />
|
||||||
)}
|
)}
|
||||||
{/* Tabs */}
|
{orderedArtifactIds.length > 1 && (
|
||||||
<Tabs.List className="mr-2 inline-flex h-7 rounded-full border border-border-medium bg-surface-tertiary">
|
<ArtifactVersion
|
||||||
<Tabs.Trigger
|
currentIndex={currentIndex}
|
||||||
value="preview"
|
totalVersions={orderedArtifactIds.length}
|
||||||
disabled={isMutating}
|
onVersionChange={(index) => {
|
||||||
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"
|
const target = orderedArtifactIds[index];
|
||||||
>
|
if (target) setCurrentArtifactId(target);
|
||||||
{localize('com_ui_preview')}
|
}}
|
||||||
</Tabs.Trigger>
|
/>
|
||||||
<Tabs.Trigger
|
)}
|
||||||
value="code"
|
<CopyCodeButton content={currentArtifact.content ?? ''} />
|
||||||
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"
|
<DownloadArtifact artifact={currentArtifact} />
|
||||||
>
|
<Button
|
||||||
{localize('com_ui_code')}
|
size="icon"
|
||||||
</Tabs.Trigger>
|
variant="ghost"
|
||||||
</Tabs.List>
|
onClick={closeArtifacts}
|
||||||
<button className="text-text-secondary" onClick={closeArtifacts}>
|
disabled={isRefreshing}
|
||||||
<X className="h-4 w-4" />
|
aria-label="Close Artifacts"
|
||||||
</button>
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Content */}
|
|
||||||
<ArtifactTabs
|
<ArtifactTabs
|
||||||
artifact={currentArtifact}
|
artifact={currentArtifact}
|
||||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||||
/>
|
/>
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between border-t border-border-medium bg-surface-primary-alt p-2 text-sm text-text-secondary">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button onClick={() => cycleArtifact('prev')} className="mr-2 text-text-secondary">
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<span className="text-xs">{`${currentIndex + 1} / ${
|
|
||||||
orderedArtifactIds.length
|
|
||||||
}`}</span>
|
|
||||||
<button onClick={() => cycleArtifact('next')} className="ml-2 text-text-secondary">
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CopyCodeButton content={currentArtifact.content ?? ''} />
|
|
||||||
{/* Download Button */}
|
|
||||||
<DownloadArtifact artifact={currentArtifact} />
|
|
||||||
{/* Publish button */}
|
|
||||||
{/* <button className="border-0.5 min-w-[4rem] whitespace-nowrap rounded-md border-border-medium bg-[radial-gradient(ellipse,_var(--tw-gradient-stops))] from-surface-active from-50% to-surface-active px-3 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-surface-active hover:text-text-primary active:scale-[0.985] active:bg-surface-active">
|
|
||||||
Publish
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import React, { memo, useEffect, useRef, useState } from 'react';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Button } from '@librechat/client';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
import { Clipboard, CheckMark } from '@librechat/client';
|
import { Copy, CircleCheckBig } from 'lucide-react';
|
||||||
import { handleDoubleClick, langSubset } from '~/utils';
|
import { handleDoubleClick, langSubset } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
|
@ -107,12 +108,13 @@ export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className="mr-2 text-text-secondary"
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||||
>
|
>
|
||||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard />}
|
{isCopied ? <CircleCheckBig size={16} /> : <Copy size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Download } from 'lucide-react';
|
import { Download, CircleCheckBig } from 'lucide-react';
|
||||||
import type { Artifact } from '~/common';
|
import type { Artifact } from '~/common';
|
||||||
import { CheckMark } from '@librechat/client';
|
import { Button } from '@librechat/client';
|
||||||
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
|
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
|
||||||
import { useEditorContext } from '~/Providers';
|
import { useEditorContext } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const DownloadArtifact = ({
|
const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => {
|
||||||
artifact,
|
|
||||||
className = '',
|
|
||||||
}: {
|
|
||||||
artifact: Artifact;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { currentCode } = useEditorContext();
|
const { currentCode } = useEditorContext();
|
||||||
const [isDownloaded, setIsDownloaded] = useState(false);
|
const [isDownloaded, setIsDownloaded] = useState(false);
|
||||||
|
|
@ -41,13 +35,14 @@ const DownloadArtifact = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className={`mr-2 text-text-secondary ${className}`}
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
aria-label={localize('com_ui_download_artifact')}
|
aria-label={localize('com_ui_download_artifact')}
|
||||||
>
|
>
|
||||||
{isDownloaded ? <CheckMark className="h-4 w-4" /> : <Download className="h-4 w-4" />}
|
{isDownloaded ? <CircleCheckBig size={16} /> : <Download size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,5 +129,6 @@ export default function useArtifacts() {
|
||||||
cycleArtifact,
|
cycleArtifact,
|
||||||
currentArtifact,
|
currentArtifact,
|
||||||
orderedArtifactIds,
|
orderedArtifactIds,
|
||||||
|
setCurrentArtifactId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1263,6 +1263,7 @@
|
||||||
"com_ui_verify": "Verify",
|
"com_ui_verify": "Verify",
|
||||||
"com_ui_version_var": "Version {{0}}",
|
"com_ui_version_var": "Version {{0}}",
|
||||||
"com_ui_versions": "Versions",
|
"com_ui_versions": "Versions",
|
||||||
|
"com_ui_change_version": "Change Version",
|
||||||
"com_ui_view_memory": "View Memory",
|
"com_ui_view_memory": "View Memory",
|
||||||
"com_ui_view_source": "View source chat",
|
"com_ui_view_source": "View source chat",
|
||||||
"com_ui_web_search": "Web Search",
|
"com_ui_web_search": "Web Search",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue