| import React, { useRef, useState, useMemo, useEffect } from 'react'; |
| import copy from 'copy-to-clipboard'; |
| import { InfoIcon } from 'lucide-react'; |
| import { Tools } from 'librechat-data-provider'; |
| import { Clipboard, CheckMark } from '@librechat/client'; |
| import type { CodeBarProps } from '~/common'; |
| import ResultSwitcher from '~/components/Messages/Content/ResultSwitcher'; |
| import { useToolCallsMapContext, useMessageContext } from '~/Providers'; |
| import { LogContent } from '~/components/Chat/Messages/Content/Parts'; |
| import RunCode from '~/components/Messages/Content/RunCode'; |
| import { useLocalize } from '~/hooks'; |
| import cn from '~/utils/cn'; |
|
|
| type CodeBlockProps = Pick< |
| CodeBarProps, |
| 'lang' | 'plugin' | 'error' | 'allowExecution' | 'blockIndex' |
| > & { |
| codeChildren: React.ReactNode; |
| classProp?: string; |
| }; |
|
|
| const CodeBar: React.FC<CodeBarProps> = React.memo( |
| ({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => { |
| const localize = useLocalize(); |
| const [isCopied, setIsCopied] = useState(false); |
| return ( |
| <div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700"> |
| <span className="">{lang}</span> |
| {plugin === true ? ( |
| <InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" /> |
| ) : ( |
| <div className="flex items-center justify-center gap-4"> |
| {allowExecution === true && ( |
| <RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} /> |
| )} |
| <button |
| type="button" |
| className={cn( |
| 'ml-auto flex gap-2', |
| error === true ? 'h-4 w-4 items-start text-white/50' : '', |
| )} |
| onClick={async () => { |
| const codeString = codeRef.current?.textContent; |
| if (codeString != null) { |
| setIsCopied(true); |
| copy(codeString.trim(), { format: 'text/plain' }); |
| |
| setTimeout(() => { |
| setIsCopied(false); |
| }, 3000); |
| } |
| }} |
| > |
| {isCopied ? ( |
| <> |
| <CheckMark className="h-[18px] w-[18px]" /> |
| {error === true ? '' : localize('com_ui_copied')} |
| </> |
| ) : ( |
| <> |
| <Clipboard /> |
| {error === true ? '' : localize('com_ui_copy_code')} |
| </> |
| )} |
| </button> |
| </div> |
| )} |
| </div> |
| ); |
| }, |
| ); |
|
|
| const CodeBlock: React.FC<CodeBlockProps> = ({ |
| lang, |
| blockIndex, |
| codeChildren, |
| classProp = '', |
| allowExecution = true, |
| plugin = null, |
| error, |
| }) => { |
| const codeRef = useRef<HTMLElement>(null); |
| const toolCallsMap = useToolCallsMapContext(); |
| const { messageId, partIndex } = useMessageContext(); |
| const key = allowExecution |
| ? `${messageId}_${partIndex ?? 0}_${blockIndex ?? 0}_${Tools.execute_code}` |
| : ''; |
| const [currentIndex, setCurrentIndex] = useState(0); |
|
|
| const fetchedToolCalls = toolCallsMap?.[key]; |
| const [toolCalls, setToolCalls] = useState(toolCallsMap?.[key] ?? null); |
|
|
| useEffect(() => { |
| if (fetchedToolCalls) { |
| setToolCalls(fetchedToolCalls); |
| setCurrentIndex(fetchedToolCalls.length - 1); |
| } |
| }, [fetchedToolCalls]); |
|
|
| const currentToolCall = useMemo(() => toolCalls?.[currentIndex], [toolCalls, currentIndex]); |
|
|
| const next = () => { |
| if (!toolCalls) { |
| return; |
| } |
| if (currentIndex < toolCalls.length - 1) { |
| setCurrentIndex(currentIndex + 1); |
| } |
| }; |
|
|
| const previous = () => { |
| if (currentIndex > 0) { |
| setCurrentIndex(currentIndex - 1); |
| } |
| }; |
|
|
| const isNonCode = !!(plugin === true || error === true); |
| const language = isNonCode ? 'json' : lang; |
|
|
| return ( |
| <div className="w-full rounded-md bg-gray-900 text-xs text-white/80"> |
| <CodeBar |
| lang={lang} |
| error={error} |
| codeRef={codeRef} |
| blockIndex={blockIndex} |
| plugin={plugin === true} |
| allowExecution={allowExecution} |
| /> |
| <div className={cn(classProp, 'overflow-y-auto p-4')}> |
| <code |
| ref={codeRef} |
| className={cn( |
| isNonCode ? '!whitespace-pre-wrap' : `hljs language-${language} !whitespace-pre`, |
| )} |
| > |
| {codeChildren} |
| </code> |
| </div> |
| {allowExecution === true && toolCalls && toolCalls.length > 0 && ( |
| <> |
| <div className="bg-gray-700 p-4 text-xs"> |
| <div |
| className="prose flex flex-col-reverse text-white" |
| style={{ |
| color: 'white', |
| }} |
| > |
| <pre className="shrink-0"> |
| <LogContent |
| output={(currentToolCall?.result as string | undefined) ?? ''} |
| attachments={currentToolCall?.attachments ?? []} |
| renderImages={true} |
| /> |
| </pre> |
| </div> |
| </div> |
| {toolCalls.length > 1 && ( |
| <ResultSwitcher |
| currentIndex={currentIndex} |
| totalCount={toolCalls.length} |
| onPrevious={previous} |
| onNext={next} |
| /> |
| )} |
| </> |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default CodeBlock; |
|
|