mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-26 20:34:10 +01:00
📋 feat: Add Floating Copy Button to Code Blocks (#11113)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* feat: Add MermaidErrorBoundary for handling rendering errors in Mermaid diagrams * feat: Implement FloatingCodeBar for enhanced code block interaction and copy functionality * feat: Add zoom-level bar copy functionality to Mermaid component * feat: Enhance button styles in FloatingCodeBar and RunCode components for improved user interaction * refactor: copy button rendering in CodeBar and FloatingCodeBar for improved accessibility and clarity * chore: linting * chore: import order
This commit is contained in:
parent
3503b7caeb
commit
daed6d9c0e
5 changed files with 244 additions and 14 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useState, useMemo, useEffect } from 'react';
|
||||
import React, { useRef, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
|
|
@ -19,6 +19,10 @@ type CodeBlockProps = Pick<
|
|||
classProp?: string;
|
||||
};
|
||||
|
||||
interface FloatingCodeBarProps extends CodeBarProps {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const CodeBar: React.FC<CodeBarProps> = React.memo(
|
||||
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -51,16 +55,14 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(
|
|||
}
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckMark className="h-[18px] w-[18px]" />
|
||||
{error === true ? '' : localize('com_ui_copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard />
|
||||
{error === true ? '' : localize('com_ui_copy_code')}
|
||||
</>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard />}
|
||||
{error !== true && (
|
||||
<span className="relative">
|
||||
<span className="invisible">{localize('com_ui_copy_code')}</span>
|
||||
<span className="absolute inset-0 flex items-center">
|
||||
{isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -70,6 +72,75 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(
|
|||
},
|
||||
);
|
||||
|
||||
const FloatingCodeBar: React.FC<FloatingCodeBarProps> = React.memo(
|
||||
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true, isVisible }) => {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
const wasFocused = document.activeElement === copyButtonRef.current;
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
if (wasFocused) {
|
||||
requestAnimationFrame(() => {
|
||||
copyButtonRef.current?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const focusedElement = document.activeElement as HTMLElement | null;
|
||||
setIsCopied(false);
|
||||
requestAnimationFrame(() => {
|
||||
focusedElement?.focus();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
}, [codeRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-2 right-2 flex items-center gap-2 font-sans text-xs text-gray-200 transition-opacity duration-150',
|
||||
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<>
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
|
||||
)}
|
||||
<button
|
||||
ref={copyButtonRef}
|
||||
type="button"
|
||||
tabIndex={isVisible ? 0 : -1}
|
||||
className={cn(
|
||||
'flex gap-2 rounded px-2 py-1 hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white',
|
||||
error === true ? 'h-4 w-4 items-start text-white/50' : '',
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard />}
|
||||
{error !== true && (
|
||||
<span className="relative">
|
||||
<span className="invisible">{localize('com_ui_copy_code')}</span>
|
||||
<span className="absolute inset-0 flex items-center">
|
||||
{isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
lang,
|
||||
blockIndex,
|
||||
|
|
@ -80,6 +151,8 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
|
|||
error,
|
||||
}) => {
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isBarVisible, setIsBarVisible] = useState(false);
|
||||
const toolCallsMap = useToolCallsMapContext();
|
||||
const { messageId, partIndex } = useMessageContext();
|
||||
const key = allowExecution
|
||||
|
|
@ -97,6 +170,29 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
|
|||
}
|
||||
}, [fetchedToolCalls]);
|
||||
|
||||
// Handle focus within the container (for keyboard navigation)
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsBarVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback((e: React.FocusEvent) => {
|
||||
// Check if focus is moving to another element within the container
|
||||
if (!containerRef.current?.contains(e.relatedTarget as Node)) {
|
||||
setIsBarVisible(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsBarVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// Only hide if no element inside has focus
|
||||
if (!containerRef.current?.contains(document.activeElement)) {
|
||||
setIsBarVisible(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const currentToolCall = useMemo(() => toolCalls?.[currentIndex], [toolCalls, currentIndex]);
|
||||
|
||||
const next = () => {
|
||||
|
|
@ -118,7 +214,14 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
|
|||
const language = isNonCode ? 'json' : lang;
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full rounded-md bg-gray-900 text-xs text-white/80"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<CodeBar
|
||||
lang={lang}
|
||||
error={error}
|
||||
|
|
@ -137,6 +240,15 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
|
|||
{codeChildren}
|
||||
</code>
|
||||
</div>
|
||||
<FloatingCodeBar
|
||||
lang={lang}
|
||||
error={error}
|
||||
codeRef={codeRef}
|
||||
blockIndex={blockIndex}
|
||||
plugin={plugin === true}
|
||||
allowExecution={allowExecution}
|
||||
isVisible={isBarVisible}
|
||||
/>
|
||||
{allowExecution === true && toolCalls && toolCalls.length > 0 && (
|
||||
<>
|
||||
<div className="bg-gray-700 p-4 text-xs">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue