Merge branch 'main' into 'feature/mermaid-integration'

This commit is contained in:
ManishMadan2882
2025-05-07 03:44:47 +05:30
49 changed files with 1500 additions and 593 deletions

View File

@@ -1,58 +1,136 @@
import clsx from 'clsx';
import copy from 'copy-to-clipboard';
import { useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CheckMark from '../assets/checkmark.svg?react';
import Copy from '../assets/copy.svg?react';
import CopyIcon from '../assets/copy.svg?react';
type CopyButtonProps = {
textToCopy: string;
bgColorLight?: string;
bgColorDark?: string;
hoverBgColorLight?: string;
hoverBgColorDark?: string;
iconSize?: string;
padding?: string;
showText?: boolean;
copiedDuration?: number;
className?: string;
iconWrapperClassName?: string;
textClassName?: string;
};
const DEFAULT_ICON_SIZE = 'w-4 h-4';
const DEFAULT_PADDING = 'p-2';
const DEFAULT_COPIED_DURATION = 2000;
const DEFAULT_BG_LIGHT = '#FFFFFF';
const DEFAULT_BG_DARK = 'transparent';
const DEFAULT_HOVER_BG_LIGHT = '#EEEEEE';
const DEFAULT_HOVER_BG_DARK = '#4A4A4A';
export default function CopyButton({
text,
colorLight,
colorDark,
textToCopy,
bgColorLight = DEFAULT_BG_LIGHT,
bgColorDark = DEFAULT_BG_DARK,
hoverBgColorLight = DEFAULT_HOVER_BG_LIGHT,
hoverBgColorDark = DEFAULT_HOVER_BG_DARK,
iconSize = DEFAULT_ICON_SIZE,
padding = DEFAULT_PADDING,
showText = false,
}: {
text: string;
colorLight?: string;
colorDark?: string;
showText?: boolean;
}) {
copiedDuration = DEFAULT_COPIED_DURATION,
className,
iconWrapperClassName,
textClassName,
}: CopyButtonProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const [isCopyHovered, setIsCopyHovered] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
};
const iconWrapperClasses = clsx(
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
padding,
`bg-[${bgColorLight}] dark:bg-[${bgColorDark}]`,
`hover:bg-[${hoverBgColorLight}] dark:hover:bg-[${hoverBgColorDark}]`,
{
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
isCopied,
},
iconWrapperClassName,
);
const rootButtonClasses = clsx(
'flex items-center gap-2 group',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 rounded-full',
className,
);
const textSpanClasses = clsx(
'text-xs text-gray-600 dark:text-gray-400 transition-opacity duration-150 ease-in-out',
{ 'opacity-75': isCopied },
textClassName,
);
const IconComponent = isCopied ? CheckMark : CopyIcon;
const iconClasses = clsx(iconSize, {
'stroke-green-600 dark:stroke-green-400': isCopied,
'fill-none text-gray-700 dark:text-gray-300': !isCopied,
});
const buttonTitle = isCopied
? t('conversation.copied')
: t('conversation.copy');
const displayedText = isCopied
? t('conversation.copied')
: t('conversation.copy');
const handleCopy = useCallback(() => {
if (isCopied) return;
try {
const success = copy(textToCopy);
if (success) {
setIsCopied(true);
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = setTimeout(() => {
setIsCopied(false);
timeoutIdRef.current = null;
}, copiedDuration);
} else {
console.warn('Copy command failed.');
}
} catch (error) {
console.error('Failed to copy text:', error);
}
}, [textToCopy, copiedDuration, isCopied]);
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
return (
<button
onClick={() => handleCopyClick(text)}
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
className="flex items-center gap-2"
type="button"
onClick={handleCopy}
className={rootButtonClasses}
title={buttonTitle}
aria-label={buttonTitle}
disabled={isCopied}
>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isCopyHovered
? `bg-[#EEEEEE] dark:bg-purple-taupe`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
}`}
>
{copied ? (
<CheckMark className="cursor-pointer stroke-green-2000" />
) : (
<Copy className="w-4 cursor-pointer fill-none" />
)}
<div className={iconWrapperClasses}>
<IconComponent className={iconClasses} aria-hidden="true" />
</div>
{showText && (
<span className="text-xs text-gray-600 dark:text-gray-400">
{copied ? t('conversation.copied') : t('conversation.copy')}
</span>
)}
{showText && <span className={textSpanClasses}>{displayedText}</span>}
<span className="sr-only" aria-live="polite" aria-atomic="true">
{isCopied ? t('conversation.copied', 'Copied to clipboard') : ''}
</span>
</button>
);
}

View File

@@ -261,7 +261,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
mermaid
</span>
<div className="flex items-center gap-2">
<CopyButton text={String(code).replace(/\n$/, '')} />
<CopyButton textToCopy={String(code).replace(/\n$/, '')} />
{showDiagramOptions && (
<div className="relative" ref={downloadMenuRef}>

View File

@@ -36,15 +36,7 @@ type MessageInputProps = {
loading: boolean;
showSourceButton?: boolean;
showToolButton?: boolean;
};
type UploadState = {
taskId: string;
fileName: string;
progress: number;
attachment_id?: string;
token_count?: number;
status: 'uploading' | 'processing' | 'completed' | 'failed';
autoFocus?: boolean;
};
export default function MessageInput({
@@ -54,6 +46,7 @@ export default function MessageInput({
loading,
showSourceButton = true,
showToolButton = true,
autoFocus = true,
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
@@ -235,7 +228,7 @@ export default function MessageInput({
};
useEffect(() => {
inputRef.current?.focus();
if (autoFocus) inputRef.current?.focus();
handleInput();
}, []);