diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index 1954b80e3..7ade77564 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo, useRef, useEffect } from 'react'; +import React, { memo, useMemo } from 'react'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import supersub from 'remark-supersub'; @@ -7,168 +7,16 @@ import { useRecoilValue } from 'recoil'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import remarkDirective from 'remark-directive'; -import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { Pluggable } from 'unified'; -import { - useToastContext, - ArtifactProvider, - CodeBlockProvider, - useCodeBlockContext, -} from '~/Providers'; import { Citation, CompositeCitation, HighlightedText } from '~/components/Web/Citation'; import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact'; -import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils'; -import CodeBlock from '~/components/Messages/Content/CodeBlock'; +import { ArtifactProvider, CodeBlockProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; -import useHasAccess from '~/hooks/Roles/useHasAccess'; +import { langSubset, preprocessLaTeX } from '~/utils'; import { unicodeCitation } from '~/components/Web'; -import { useFileDownload } from '~/data-provider'; -import useLocalize from '~/hooks/useLocalize'; +import { code, a, p } from './MarkdownComponents'; import store from '~/store'; -type TCodeProps = { - inline?: boolean; - className?: string; - children: React.ReactNode; -}; - -export const code: React.ElementType = memo(({ className, children }: TCodeProps) => { - const canRunCode = useHasAccess({ - permissionType: PermissionTypes.RUN_CODE, - permission: Permissions.USE, - }); - const match = /language-(\w+)/.exec(className ?? ''); - const lang = match && match[1]; - const isMath = lang === 'math'; - const isSingleLine = typeof children === 'string' && children.split('\n').length === 1; - - const { getNextIndex, resetCounter } = useCodeBlockContext(); - const blockIndex = useRef(getNextIndex(isMath || isSingleLine)).current; - - useEffect(() => { - resetCounter(); - }, [children, resetCounter]); - - if (isMath) { - return <>{children}; - } else if (isSingleLine) { - return ( - - {children} - - ); - } else { - return ( - - ); - } -}); - -export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => { - const match = /language-(\w+)/.exec(className ?? ''); - const lang = match && match[1]; - - if (lang === 'math') { - return children; - } else if (typeof children === 'string' && children.split('\n').length === 1) { - return ( - - {children} - - ); - } else { - return ; - } -}); - -type TAnchorProps = { - href: string; - children: React.ReactNode; -}; - -export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { - 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 }; - } - 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} - - ); - } - - 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'; - - return ( - - {children} - - ); -}); - -type TParagraphProps = { - children: React.ReactNode; -}; - -export const p: React.ElementType = memo(({ children }: TParagraphProps) => { - return

{children}

; -}); - type TContentProps = { content: string; isLatestMessage: boolean; diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx new file mode 100644 index 000000000..e0a381ff5 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -0,0 +1,153 @@ +import React, { memo, useMemo, useRef, useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; +import { useToastContext, useCodeBlockContext } from '~/Providers'; +import CodeBlock from '~/components/Messages/Content/CodeBlock'; +import useHasAccess from '~/hooks/Roles/useHasAccess'; +import { useFileDownload } from '~/data-provider'; +import useLocalize from '~/hooks/useLocalize'; +import { handleDoubleClick } from '~/utils'; +import store from '~/store'; + +type TCodeProps = { + inline?: boolean; + className?: string; + children: React.ReactNode; +}; + +export const code: React.ElementType = memo(({ className, children }: TCodeProps) => { + const canRunCode = useHasAccess({ + permissionType: PermissionTypes.RUN_CODE, + permission: Permissions.USE, + }); + const match = /language-(\w+)/.exec(className ?? ''); + const lang = match && match[1]; + const isMath = lang === 'math'; + const isSingleLine = typeof children === 'string' && children.split('\n').length === 1; + + const { getNextIndex, resetCounter } = useCodeBlockContext(); + const blockIndex = useRef(getNextIndex(isMath || isSingleLine)).current; + + useEffect(() => { + resetCounter(); + }, [children, resetCounter]); + + if (isMath) { + return <>{children}; + } else if (isSingleLine) { + return ( + + {children} + + ); + } else { + return ( + + ); + } +}); + +export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => { + const match = /language-(\w+)/.exec(className ?? ''); + const lang = match && match[1]; + + if (lang === 'math') { + return children; + } else if (typeof children === 'string' && children.split('\n').length === 1) { + return ( + + {children} + + ); + } else { + return ; + } +}); + +type TAnchorProps = { + href: string; + children: React.ReactNode; +}; + +export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { + 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 }; + } + 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} + + ); + } + + 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'; + + return ( + + {children} + + ); +}); + +type TParagraphProps = { + children: React.ReactNode; +}; + +export const p: React.ElementType = memo(({ children }: TParagraphProps) => { + return

{children}

; +}); diff --git a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx index 15c68f7e9..0342c60f8 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx @@ -4,7 +4,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p } from './Markdown'; +import { code, codeNoExecution, a, p } from './MarkdownComponents'; import { CodeBlockProvider } from '~/Providers'; import { langSubset } from '~/utils'; diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx index c3b302d0d..d553e6b70 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx @@ -6,7 +6,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p } from './Markdown'; +import { code, codeNoExecution, a, p } from './MarkdownComponents'; import { CodeBlockProvider, ArtifactProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset } from '~/utils'; diff --git a/client/src/components/Prompts/Groups/VariableForm.tsx b/client/src/components/Prompts/Groups/VariableForm.tsx index 09bdcd40d..48ad704f8 100644 --- a/client/src/components/Prompts/Groups/VariableForm.tsx +++ b/client/src/components/Prompts/Groups/VariableForm.tsx @@ -8,8 +8,8 @@ import rehypeHighlight from 'rehype-highlight'; import { replaceSpecialVars } from 'librechat-data-provider'; import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form'; import type { TPromptGroup } from 'librechat-data-provider'; +import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; import { cn, wrapVariable, defaultTextProps, extractVariableInfo } from '~/utils'; -import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown'; import { TextareaAutosize, InputCombobox, Button } from '~/components/ui'; import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks'; import { PromptVariableGfm } from '../Markdown'; diff --git a/client/src/components/Prompts/PromptDetails.tsx b/client/src/components/Prompts/PromptDetails.tsx index 0bfd8e993..62e9d02e3 100644 --- a/client/src/components/Prompts/PromptDetails.tsx +++ b/client/src/components/Prompts/PromptDetails.tsx @@ -7,7 +7,7 @@ import supersub from 'remark-supersub'; import rehypeHighlight from 'rehype-highlight'; import { replaceSpecialVars } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider'; -import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown'; +import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; import { useLocalize, useAuthContext } from '~/hooks'; import CategoryIcon from './Groups/CategoryIcon'; import PromptVariables from './PromptVariables'; diff --git a/client/src/components/Prompts/PromptEditor.tsx b/client/src/components/Prompts/PromptEditor.tsx index f10a94c11..75584f849 100644 --- a/client/src/components/Prompts/PromptEditor.tsx +++ b/client/src/components/Prompts/PromptEditor.tsx @@ -9,7 +9,7 @@ import rehypeKatex from 'rehype-katex'; import remarkMath from 'remark-math'; import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; -import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown'; +import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd'; import { SaveIcon, CrossIcon } from '~/components/svg'; import VariablesDropdown from './VariablesDropdown'; diff --git a/client/src/components/SidePanel/Agents/Version/VersionContent.tsx b/client/src/components/SidePanel/Agents/Version/VersionContent.tsx index 25b8f5bd5..0aa1fc4af 100644 --- a/client/src/components/SidePanel/Agents/Version/VersionContent.tsx +++ b/client/src/components/SidePanel/Agents/Version/VersionContent.tsx @@ -1,7 +1,7 @@ import { Spinner } from '~/components/svg'; import { useLocalize } from '~/hooks'; import VersionItem from './VersionItem'; -import { VersionContext } from './VersionPanel'; +import type { VersionContext } from './types'; type VersionContentProps = { selectedAgentId: string; diff --git a/client/src/components/SidePanel/Agents/Version/VersionItem.tsx b/client/src/components/SidePanel/Agents/Version/VersionItem.tsx index c1d27cada..931eb09a8 100644 --- a/client/src/components/SidePanel/Agents/Version/VersionItem.tsx +++ b/client/src/components/SidePanel/Agents/Version/VersionItem.tsx @@ -1,5 +1,5 @@ import { useLocalize } from '~/hooks'; -import { VersionRecord } from './VersionPanel'; +import type { VersionRecord } from './types'; type VersionItemProps = { version: VersionRecord; diff --git a/client/src/components/SidePanel/Agents/Version/VersionPanel.tsx b/client/src/components/SidePanel/Agents/Version/VersionPanel.tsx index 0f8919921..57048f66f 100644 --- a/client/src/components/SidePanel/Agents/Version/VersionPanel.tsx +++ b/client/src/components/SidePanel/Agents/Version/VersionPanel.tsx @@ -1,44 +1,13 @@ import { ChevronLeft } from 'lucide-react'; import { useCallback, useMemo } from 'react'; import { useGetAgentByIdQuery, useRevertAgentVersionMutation } from '~/data-provider'; -import type { Agent } from 'librechat-data-provider'; +import type { AgentWithVersions, VersionContext } from './types'; import { isActiveVersion } from './isActiveVersion'; import { useAgentPanelContext } from '~/Providers'; import { useLocalize, useToast } from '~/hooks'; import VersionContent from './VersionContent'; import { Panel } from '~/common'; -export type VersionRecord = Record; - -export type AgentState = { - name: string | null; - description: string | null; - instructions: string | null; - artifacts?: string | null; - capabilities?: string[]; - tools?: string[]; -} | null; - -export type VersionWithId = { - id: number; - originalIndex: number; - version: VersionRecord; - isActive: boolean; -}; - -export type VersionContext = { - versions: VersionRecord[]; - versionIds: VersionWithId[]; - currentAgent: AgentState; - selectedAgentId: string; - activeVersion: VersionRecord | null; -}; - -export interface AgentWithVersions extends Agent { - capabilities?: string[]; - versions?: Array; -} - export default function VersionPanel() { const localize = useLocalize(); const { showToast } = useToast(); diff --git a/client/src/components/SidePanel/Agents/Version/isActiveVersion.ts b/client/src/components/SidePanel/Agents/Version/isActiveVersion.ts index 61919953d..e0eb5f66d 100644 --- a/client/src/components/SidePanel/Agents/Version/isActiveVersion.ts +++ b/client/src/components/SidePanel/Agents/Version/isActiveVersion.ts @@ -1,4 +1,4 @@ -import { AgentState, VersionRecord } from './VersionPanel'; +import type { AgentState, VersionRecord } from './types'; export const isActiveVersion = ( version: VersionRecord, diff --git a/client/src/components/SidePanel/Agents/Version/types.ts b/client/src/components/SidePanel/Agents/Version/types.ts new file mode 100644 index 000000000..210e4d32b --- /dev/null +++ b/client/src/components/SidePanel/Agents/Version/types.ts @@ -0,0 +1,35 @@ +export type VersionRecord = Record; + +export type AgentState = { + name: string | null; + description: string | null; + instructions: string | null; + artifacts?: string | null; + capabilities?: string[]; + tools?: string[]; +} | null; + +export type VersionWithId = { + id: number; + originalIndex: number; + version: VersionRecord; + isActive: boolean; +}; + +export type VersionContext = { + versions: VersionRecord[]; + versionIds: VersionWithId[]; + currentAgent: AgentState; + selectedAgentId: string; + activeVersion: VersionRecord | null; +}; + +export interface AgentWithVersions { + name: string; + description: string | null; + instructions: string | null; + artifacts?: string | null; + capabilities?: string[]; + tools?: string[]; + versions?: Array; +}