This is the content of my blog post. It's short and sweet!
+Count: {count}
+ +This is the content of my blog post. It's short and sweet!
+Count: {count}
+ +This is the content of my blog post. It's short and sweet!
+Card Content
+Card Footer
+Add to library
+
+ {children}
+
+ );
+ }
+
+ return {children};
+});
+
+export const CodeMarkdown = memo(
+ ({ content = '', isSubmitting }: { content: string; isSubmitting: boolean }) => {
+ const scrollRef = useRef
{children}
@@ -35,73 +39,75 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC
}
});
-export const a = memo(({ href, children }: { href: string; children: React.ReactNode }) => {
- const user = useRecoilValue(store.user);
- const { showToast } = useToastContext();
- const localize = useLocalize();
+export const a: React.ElementType = memo(
+ ({ href, children }: { href: string; children: React.ReactNode }) => {
+ const user = useRecoilValue(store.user);
+ const { showToast } = useToastContext();
+ const localize = useLocalize();
- const { file_id, filename, filepath } = useMemo(() => {
- const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
- const match = href.match(pattern);
- if (match && match[0]) {
- const path = match[0];
- const parts = path.split('/');
- const name = parts.pop();
- const file_id = parts.pop();
- return { file_id, filename: name, filepath: path };
+ const { file_id, filename, filepath } = useMemo(() => {
+ const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
+ const match = href.match(pattern);
+ if (match && match[0]) {
+ const path = match[0];
+ const parts = path.split('/');
+ const name = parts.pop();
+ const file_id = parts.pop();
+ return { file_id, filename: name, filepath: path };
+ }
+ return { file_id: '', filename: '', filepath: '' };
+ }, [user?.id, href]);
+
+ const { refetch: downloadFile } = useFileDownload(user?.id ?? '', file_id);
+ const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_new' };
+
+ if (!file_id || !filename) {
+ return (
+
+ {children}
+
+ );
}
- return { file_id: '', filename: '', filepath: '' };
- }, [user?.id, href]);
- const { refetch: downloadFile } = useFileDownload(user?.id ?? '', file_id);
- const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_new' };
+ const handleDownload = async (event: React.MouseEvent) => {
+ event.preventDefault();
+ try {
+ const stream = await downloadFile();
+ if (stream.data == null || stream.data === '') {
+ console.error('Error downloading file: No data found');
+ showToast({
+ status: 'error',
+ message: localize('com_ui_download_error'),
+ });
+ return;
+ }
+ const link = document.createElement('a');
+ link.href = stream.data;
+ link.setAttribute('download', filename);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(stream.data);
+ } catch (error) {
+ console.error('Error downloading file:', error);
+ }
+ };
+
+ props.onClick = handleDownload;
+ props.target = '_blank';
- if (!file_id || !filename) {
return (
-
+
{children}
);
- }
+ },
+);
- const handleDownload = async (event: React.MouseEvent) => {
- event.preventDefault();
- try {
- const stream = await downloadFile();
- if (!stream.data) {
- console.error('Error downloading file: No data found');
- showToast({
- status: 'error',
- message: localize('com_ui_download_error'),
- });
- return;
- }
- const link = document.createElement('a');
- link.href = stream.data;
- link.setAttribute('download', filename);
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(stream.data);
- } catch (error) {
- console.error('Error downloading file:', error);
- }
- };
-
- props.onClick = handleDownload;
- props.target = '_blank';
-
- return (
-
- {children}
-
- );
-});
-
-export const p = memo(({ children }: { children: React.ReactNode }) => {
+export const p: React.ElementType = memo(({ children }: { children: React.ReactNode }) => {
return {children}
;
});
@@ -115,6 +121,7 @@ type TContentProps = {
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
const LaTeXParsing = useRecoilValue(store.LaTeXParsing);
+ const codeArtifacts = useRecoilValue(store.codeArtifacts);
const isInitializing = content === '';
@@ -124,7 +131,7 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
currentContent = LaTeXParsing ? preprocessLaTeX(currentContent) : currentContent;
}
- const rehypePlugins: PluggableList = [
+ const rehypePlugins = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
@@ -146,16 +153,29 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
);
}
+ const remarkPlugins: Pluggable[] = codeArtifacts
+ ? [
+ supersub,
+ remarkGfm,
+ [remarkMath, { singleDollarTextMath: true }],
+ remarkDirective,
+ artifactPlugin,
+ ]
+ : [supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]];
+
return (
{
startupConfig?.interface ?? defaultInterface,
[startupConfig],
@@ -44,12 +49,15 @@ export default function Presentation({
const filesToDelete = localStorage.getItem(LocalStorageKeys.FILES_TO_DELETE);
const map = JSON.parse(filesToDelete ?? '{}') as Record;
const files = Object.values(map)
- .filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
+ .filter(
+ (file) =>
+ file.filepath != null && file.source && !(file.embedded ?? false) && file.temp_file_id,
+ )
.map((file) => ({
file_id: file.file_id,
filepath: file.filepath as string,
source: file.source as FileSources,
- embedded: !!file.embedded,
+ embedded: !!(file.embedded ?? false),
}));
if (files.length === 0) {
@@ -89,6 +97,13 @@ export default function Presentation({
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
+ artifacts={
+ artifactsVisible === true &&
+ codeArtifacts === true &&
+ Object.keys(artifacts ?? {}).length > 0 ? (
+
+ ) : null
+ }
>
{children}
@@ -102,7 +117,7 @@ export default function Presentation({
return (
{layout()}
- {panel && panel}
+ {panel != null && panel}
);
}
diff --git a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx
index 7faa55d4d1..e4aae59dd6 100644
--- a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx
+++ b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx
@@ -1,8 +1,7 @@
import { memo } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { SettingsTabValues } from 'librechat-data-provider';
-import LaTeXParsing from './LaTeXParsing';
-import ModularChat from './ModularChat';
+import CodeArtifacts from './CodeArtifacts';
function Beta() {
return (
@@ -13,10 +12,7 @@ function Beta() {
>
-
-
-
-
+
diff --git a/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx b/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx
new file mode 100644
index 0000000000..804899cedd
--- /dev/null
+++ b/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx
@@ -0,0 +1,95 @@
+import { useRecoilState } from 'recoil';
+import HoverCardSettings from '../HoverCardSettings';
+import { Switch } from '~/components/ui';
+import { useLocalize } from '~/hooks';
+import store from '~/store';
+
+export default function CodeArtifacts() {
+ const [codeArtifacts, setCodeArtifacts] = useRecoilState(store.codeArtifacts);
+ const [includeShadcnui, setIncludeShadcnui] = useRecoilState(store.includeShadcnui);
+ const [customPromptMode, setCustomPromptMode] = useRecoilState(store.customPromptMode);
+ const localize = useLocalize();
+
+ const handleCodeArtifactsChange = (value: boolean) => {
+ setCodeArtifacts(value);
+ if (!value) {
+ setIncludeShadcnui(false);
+ setCustomPromptMode(false);
+ }
+ };
+
+ const handleIncludeShadcnuiChange = (value: boolean) => {
+ setIncludeShadcnui(value);
+ };
+
+ const handleCustomPromptModeChange = (value: boolean) => {
+ setCustomPromptMode(value);
+ if (value) {
+ setIncludeShadcnui(false);
+ }
+ };
+
+ return (
+
+ {localize('com_ui_artifacts')}
+
+
+
+
+
+
+ );
+}
+
+function SwitchItem({
+ id,
+ label,
+ checked,
+ onCheckedChange,
+ hoverCardText,
+ disabled = false,
+}: {
+ id: string;
+ label: string;
+ checked: boolean;
+ onCheckedChange: (value: boolean) => void;
+ hoverCardText: string;
+ disabled?: boolean;
+}) {
+ return (
+
+
+ {label}
+
+
+
+
+ );
+}
diff --git a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx
index acfe581d33..d7278e6c3c 100644
--- a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx
+++ b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx
@@ -6,6 +6,8 @@ import SendMessageKeyEnter from './EnterToSend';
import ShowCodeSwitch from './ShowCodeSwitch';
import { ForkSettings } from './ForkSettings';
import ChatDirection from './ChatDirection';
+import LaTeXParsing from './LaTeXParsing';
+import ModularChat from './ModularChat';
import SaveDraft from './SaveDraft';
function Chat() {
@@ -28,6 +30,12 @@ function Chat() {
+
+
+
+
+
+
);
diff --git a/client/src/components/Nav/SettingsTabs/Beta/LaTeXParsing.tsx b/client/src/components/Nav/SettingsTabs/Chat/LaTeXParsing.tsx
similarity index 100%
rename from client/src/components/Nav/SettingsTabs/Beta/LaTeXParsing.tsx
rename to client/src/components/Nav/SettingsTabs/Chat/LaTeXParsing.tsx
diff --git a/client/src/components/Nav/SettingsTabs/Beta/ModularChat.tsx b/client/src/components/Nav/SettingsTabs/Chat/ModularChat.tsx
similarity index 100%
rename from client/src/components/Nav/SettingsTabs/Beta/ModularChat.tsx
rename to client/src/components/Nav/SettingsTabs/Chat/ModularChat.tsx
diff --git a/client/src/components/SidePanel/SidePanel.tsx b/client/src/components/SidePanel/SidePanel.tsx
index 8f3fb24f39..ef3f868d6a 100644
--- a/client/src/components/SidePanel/SidePanel.tsx
+++ b/client/src/components/SidePanel/SidePanel.tsx
@@ -23,17 +23,37 @@ interface SidePanelProps {
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
+ artifacts?: React.ReactNode;
children: React.ReactNode;
}
const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
+const normalizeLayout = (layout: number[]) => {
+ const sum = layout.reduce((acc, size) => acc + size, 0);
+ if (Math.abs(sum - 100) < 0.01) {
+ return layout.map((size) => Number(size.toFixed(2)));
+ }
+
+ const factor = 100 / sum;
+ const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
+
+ const adjustedSum = normalizedLayout.reduce(
+ (acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
+ 0,
+ );
+ normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
+
+ return normalizedLayout;
+};
+
const SidePanel = ({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
+ artifacts,
children,
}: SidePanelProps) => {
const localize = useLocalize();
@@ -64,11 +84,11 @@ const SidePanel = ({
const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
const userProvidesKey = useMemo(
- () => !!endpointsConfig?.[endpoint ?? '']?.userProvide,
+ () => !!(endpointsConfig?.[endpoint ?? '']?.userProvide ?? false),
[endpointsConfig, endpoint],
);
const keyProvided = useMemo(
- () => (userProvidesKey ? !!keyExpiry.expiresAt : true),
+ () => (userProvidesKey ? !!(keyExpiry.expiresAt ?? '') : true),
[keyExpiry.expiresAt, userProvidesKey],
);
@@ -89,10 +109,26 @@ const SidePanel = ({
interfaceConfig,
});
+ const calculateLayout = useCallback(() => {
+ if (!artifacts) {
+ const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
+ return [100 - navSize, navSize];
+ } else {
+ const navSize = Math.max(minSize, navCollapsedSize);
+ const remainingSpace = 100 - navSize;
+ const newMainSize = Math.floor(remainingSpace / 2);
+ const artifactsSize = remainingSpace - newMainSize;
+ return [newMainSize, artifactsSize, navSize];
+ }
+ }, [artifacts, defaultLayout, minSize, navCollapsedSize]);
+
+ const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
+
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledSaveLayout = useCallback(
throttle((sizes: number[]) => {
- localStorage.setItem('react-resizable-panels:layout', JSON.stringify(sizes));
+ const normalizedSizes = normalizeLayout(sizes);
+ localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
@@ -133,17 +169,37 @@ const SidePanel = ({
}
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
+ const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
+
return (
<>
throttledSaveLayout(sizes)}
+ onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-white dark:bg-gray-800"
>
-
+
{children}
+ {artifacts != null && (
+ <>
+
+
+ {artifacts}
+
+ >
+ )}