mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-07 00:30:19 +01:00
🖼️ feat: Tool Call and Loading UI Refresh, Image Resize Config (#7086)
* ✨ feat: Enhance Spinner component with customizable properties and improved animation * 🔧 fix: Replace Loader with Spinner in RunCode component and update FilePreview to use Spinner for progress indication * ✨ feat: Refactor icons in CodeProgress and CancelledIcon components; enhance animation and styling in ExecuteCode and ProgressText components * ✨ feat: Refactor attachment handling in ExecuteCode component; replace individual attachment rendering with AttachmentGroup for improved structure * ✨ feat: Refactor dialog components for improved accessibility and styling; integrate Skeleton loading state in Image component * ✨ feat: Refactor ToolCall component to use ToolCallInfo for better structure; replace ToolPopover with AttachmentGroup; enhance ProgressText with error handling and improved UI elements * 🔧 fix: Remove unnecessary whitespace in ProgressText * 🔧 fix: Remove unnecessary margin from AgentFooter and AgentPanel components; clean up SidePanel imports * ✨ feat: Enhance ToolCall and ToolCallInfo components with improved styling; update translations and add warning text color to Tailwind config * 🔧 fix: Update import statement for useLocalize in ToolCallInfo component; fix: chatform transition * ✨ feat: Refactor ToolCall and ToolCallInfo components for improved structure and styling; add optimized code block for better output display * ✨ feat: Implement OpenAI image generation component; add progress tracking and localization for user feedback * 🔧 fix: Adjust base duration values for image generation; optimize timing for quality settings * chore: remove unnecessary space * ✨ feat: Enhance OpenAI image generation with editing capabilities; update localization for progress feedback * ✨ feat: Add download functionality to images; enhance DialogImage component with download button * ✨ feat: Enhance image resizing functionality; support custom percentage and pixel dimensions in resizeImageBuffer
This commit is contained in:
parent
739b0d3012
commit
c79ee32006
44 changed files with 1452 additions and 527 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import { memo } from 'react';
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
import { imageExtRegex } from 'librechat-data-provider';
|
||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
import { useAttachmentLink } from './LogLink';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
||||
const { handleDownload } = useAttachmentLink({
|
||||
|
|
@ -11,15 +12,68 @@ const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
|||
filename: attachment.filename,
|
||||
});
|
||||
const extension = attachment.filename.split('.').pop();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FileContainer
|
||||
file={attachment}
|
||||
onClick={handleDownload}
|
||||
overrideType={extension}
|
||||
containerClassName="max-w-fit"
|
||||
buttonClassName="hover:cursor-pointer hover:bg-surface-secondary active:bg-surface-secondary focus:bg-surface-secondary hover:border-border-heavy active:border-border-heavy"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'file-attachment-container',
|
||||
'transition-all duration-300 ease-out',
|
||||
isVisible ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0',
|
||||
)}
|
||||
style={{
|
||||
transformOrigin: 'center top',
|
||||
willChange: 'opacity, transform',
|
||||
WebkitFontSmoothing: 'subpixel-antialiased',
|
||||
}}
|
||||
>
|
||||
<FileContainer
|
||||
file={attachment}
|
||||
onClick={handleDownload}
|
||||
overrideType={extension}
|
||||
containerClassName="max-w-fit"
|
||||
buttonClassName="bg-surface-secondary hover:cursor-pointer hover:bg-surface-hover active:bg-surface-secondary focus:bg-surface-hover hover:border-border-heavy active:border-border-heavy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoaded(false);
|
||||
const timer = setTimeout(() => setIsLoaded(true), 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [attachment]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'image-attachment-container',
|
||||
'transition-all duration-500 ease-out',
|
||||
isLoaded ? 'scale-100 opacity-100' : 'scale-[0.98] opacity-0',
|
||||
)}
|
||||
style={{
|
||||
transformOrigin: 'center top',
|
||||
willChange: 'opacity, transform',
|
||||
WebkitFontSmoothing: 'subpixel-antialiased',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
altText={attachment.filename}
|
||||
imagePath={filepath ?? ''}
|
||||
height={height ?? 0}
|
||||
width={width ?? 0}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -27,20 +81,56 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
|
|||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
||||
const isImage =
|
||||
imageExtRegex.test(attachment.filename) && width != null && height != null && filepath != null;
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<Image
|
||||
altText={attachment.filename}
|
||||
imagePath={filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
className="mb-4"
|
||||
/>
|
||||
);
|
||||
return <ImageAttachment attachment={attachment} />;
|
||||
}
|
||||
return <FileAttachment attachment={attachment} />;
|
||||
}
|
||||
|
||||
export function AttachmentGroup({ attachments }: { attachments?: TAttachment[] }) {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileAttachments: TAttachment[] = [];
|
||||
const imageAttachments: TAttachment[] = [];
|
||||
|
||||
attachments.forEach((attachment) => {
|
||||
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
||||
const isImage =
|
||||
imageExtRegex.test(attachment.filename) &&
|
||||
width != null &&
|
||||
height != null &&
|
||||
filepath != null;
|
||||
|
||||
if (isImage) {
|
||||
imageAttachments.push(attachment);
|
||||
} else {
|
||||
fileAttachments.push(attachment);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileAttachments.length > 0 && (
|
||||
<div className="my-2 flex flex-wrap items-center gap-2.5">
|
||||
{fileAttachments.map((attachment, index) => (
|
||||
<FileAttachment attachment={attachment} key={`file-${index}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{imageAttachments.length > 0 && (
|
||||
<div className="mb-2 flex flex-wrap items-center">
|
||||
{imageAttachments.map((attachment, index) => (
|
||||
<ImageAttachment attachment={attachment} key={`image-${index}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import ProgressCircle from '~/components/Chat/Messages/Content/ProgressCircle';
|
||||
import CancelledIcon from '~/components/Chat/Messages/Content/CancelledIcon';
|
||||
|
||||
export const CodeInProgress = ({
|
||||
offset,
|
||||
circumference,
|
||||
radius,
|
||||
isSubmitting,
|
||||
progress,
|
||||
}: {
|
||||
progress: number;
|
||||
offset: number;
|
||||
circumference: number;
|
||||
radius: number;
|
||||
isSubmitting: boolean;
|
||||
}) => {
|
||||
if (progress < 1 && !isSubmitting) {
|
||||
return <CancelledIcon />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
|
||||
style={{ opacity: 1, transform: 'none' }}
|
||||
data-projection-id="77"
|
||||
>
|
||||
<div className="absolute bottom-[1.5px] right-[1.5px]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
height="20"
|
||||
style={{ transform: 'translate3d(0px, 0px, 0px)' }}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="__lottie_element_11">
|
||||
<rect width="20" height="20" x="0" y="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clipPath="url(#__lottie_element_11)">
|
||||
<g
|
||||
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
|
||||
className="slide-from-left"
|
||||
>
|
||||
<g opacity="1" transform="matrix(1,0,0,1,7.026679992675781,8.834091186523438)">
|
||||
<path
|
||||
fill="rgb(177,98,253)"
|
||||
fillOpacity="1"
|
||||
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fillOpacity="0"
|
||||
stroke="rgb(177,98,253)"
|
||||
strokeOpacity="1"
|
||||
strokeWidth="0.201031"
|
||||
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
|
||||
className="slide-to-down"
|
||||
>
|
||||
<g opacity="1" transform="matrix(1,0,0,1,11.79640007019043,13.512199401855469)">
|
||||
<path
|
||||
fill="rgb(177,98,253)"
|
||||
fillOpacity="1"
|
||||
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fillOpacity="0"
|
||||
stroke="rgb(177,98,253)"
|
||||
strokeOpacity="1"
|
||||
strokeWidth="0.100515"
|
||||
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useRef, useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
|
||||
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
|
||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||
import { useProgress, useLocalize } from '~/hooks';
|
||||
import { CodeInProgress } from './CodeProgress';
|
||||
import Attachment from './Attachment';
|
||||
import { AttachmentGroup } from './Attachment';
|
||||
import Stdout from './Stdout';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
interface ParsedArgs {
|
||||
|
|
@ -45,46 +44,101 @@ export function useParseArgs(args: string): ParsedArgs {
|
|||
}, [args]);
|
||||
}
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
export default function ExecuteCode({
|
||||
initialProgress = 0.1,
|
||||
args,
|
||||
output = '',
|
||||
isSubmitting,
|
||||
attachments,
|
||||
}: {
|
||||
initialProgress: number;
|
||||
args: string;
|
||||
output?: string;
|
||||
isSubmitting: boolean;
|
||||
attachments?: TAttachment[];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||
const codeContentRef = useRef<HTMLDivElement>(null);
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const hasOutput = output.length > 0;
|
||||
const outputRef = useRef<string>(output);
|
||||
const prevShowCodeRef = useRef<boolean>(showCode);
|
||||
|
||||
const { lang, code } = useParseArgs(args);
|
||||
const progress = useProgress(initialProgress);
|
||||
const offset = circumference - progress * circumference;
|
||||
|
||||
useEffect(() => {
|
||||
if (output !== outputRef.current) {
|
||||
outputRef.current = output;
|
||||
|
||||
if (showCode && codeContentRef.current) {
|
||||
setTimeout(() => {
|
||||
if (codeContentRef.current) {
|
||||
const newHeight = codeContentRef.current.scrollHeight;
|
||||
setContentHeight(newHeight);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}, [output, showCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCode !== prevShowCodeRef.current) {
|
||||
prevShowCodeRef.current = showCode;
|
||||
|
||||
if (showCode && codeContentRef.current) {
|
||||
setIsAnimating(true);
|
||||
requestAnimationFrame(() => {
|
||||
if (codeContentRef.current) {
|
||||
const height = codeContentRef.current.scrollHeight;
|
||||
setContentHeight(height);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
} else if (!showCode) {
|
||||
setIsAnimating(true);
|
||||
setContentHeight(0);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [showCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!codeContentRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (showCode && !isAnimating) {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === codeContentRef.current) {
|
||||
setContentHeight(entry.contentRect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(codeContentRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [showCode, isAnimating]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-2.5 flex items-center gap-2.5">
|
||||
<div className="relative h-5 w-5 shrink-0">
|
||||
{progress < 1 ? (
|
||||
<CodeInProgress
|
||||
offset={offset}
|
||||
radius={radius}
|
||||
progress={progress}
|
||||
isSubmitting={isSubmitting}
|
||||
circumference={circumference}
|
||||
/>
|
||||
) : (
|
||||
<FinishedIcon />
|
||||
)}
|
||||
</div>
|
||||
<div className="relative my-2.5 flex size-5 shrink-0 items-center gap-2.5">
|
||||
<ProgressText
|
||||
progress={progress}
|
||||
onClick={() => setShowCode((prev) => !prev)}
|
||||
|
|
@ -94,31 +148,71 @@ export default function ExecuteCode({
|
|||
isExpanded={showCode}
|
||||
/>
|
||||
</div>
|
||||
{showCode && (
|
||||
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
|
||||
<MarkdownLite
|
||||
content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''}
|
||||
codeExecution={false}
|
||||
/>
|
||||
{output.length > 0 && (
|
||||
<div className="bg-gray-700 p-4 text-xs">
|
||||
<div
|
||||
className="prose flex flex-col-reverse text-white"
|
||||
style={{
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative mb-2"
|
||||
style={{
|
||||
height: showCode ? contentHeight : 0,
|
||||
overflow: 'hidden',
|
||||
transition:
|
||||
'height 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
opacity: showCode ? 1 : 0,
|
||||
transformOrigin: 'top',
|
||||
willChange: 'height, opacity',
|
||||
perspective: '1000px',
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitFontSmoothing: 'subpixel-antialiased',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'code-analyze-block mt-0.5 overflow-hidden rounded-xl bg-surface-primary',
|
||||
showCode && 'shadow-lg',
|
||||
)}
|
||||
ref={codeContentRef}
|
||||
style={{
|
||||
transform: showCode ? 'translateY(0) scale(1)' : 'translateY(-8px) scale(0.98)',
|
||||
opacity: showCode ? 1 : 0,
|
||||
transition:
|
||||
'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
>
|
||||
{showCode && (
|
||||
<div
|
||||
style={{
|
||||
transform: showCode ? 'translateY(0)' : 'translateY(-4px)',
|
||||
opacity: showCode ? 1 : 0,
|
||||
transition:
|
||||
'transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
>
|
||||
<MarkdownLite
|
||||
content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''}
|
||||
codeExecution={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasOutput && (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-surface-tertiary p-4 text-xs',
|
||||
showCode ? 'border-t border-surface-primary-contrast' : '',
|
||||
)}
|
||||
style={{
|
||||
transform: showCode ? 'translateY(0)' : 'translateY(-6px)',
|
||||
opacity: showCode ? 1 : 0,
|
||||
transition:
|
||||
'transform 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s, opacity 0.45s cubic-bezier(0.19, 1, 0.22, 1) 0.05s',
|
||||
boxShadow: showCode ? '0 -1px 0 rgba(0,0,0,0.05)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div className="prose flex flex-col-reverse">
|
||||
<Stdout output={output} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2.5">
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
</div>
|
||||
{attachments && attachments.length > 0 && <AttachmentGroup attachments={attachments} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
import ProgressText from './ProgressText';
|
||||
import { PixelCard } from '~/components';
|
||||
import { scaleImage } from '~/utils';
|
||||
|
||||
export default function OpenAIImageGen({
|
||||
initialProgress = 0.1,
|
||||
isSubmitting,
|
||||
toolName,
|
||||
args: _args = '',
|
||||
output,
|
||||
attachments,
|
||||
}: {
|
||||
initialProgress: number;
|
||||
isSubmitting: boolean;
|
||||
toolName: string;
|
||||
args: string | Record<string, unknown>;
|
||||
output?: string | null;
|
||||
attachments?: TAttachment[];
|
||||
}) {
|
||||
const [progress, setProgress] = useState(initialProgress);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const error =
|
||||
typeof output === 'string' && output.toLowerCase().includes('error processing tool');
|
||||
|
||||
const cancelled = (!isSubmitting && initialProgress < 1) || error === true;
|
||||
|
||||
let width: number | undefined;
|
||||
let height: number | undefined;
|
||||
let quality: 'low' | 'medium' | 'high' = 'high';
|
||||
|
||||
try {
|
||||
const argsObj = typeof _args === 'string' ? JSON.parse(_args) : _args;
|
||||
|
||||
if (argsObj && typeof argsObj.size === 'string') {
|
||||
const [w, h] = argsObj.size.split('x').map((v: string) => parseInt(v, 10));
|
||||
if (!isNaN(w) && !isNaN(h)) {
|
||||
width = w;
|
||||
height = h;
|
||||
}
|
||||
} else if (argsObj && (typeof argsObj.size !== 'string' || !argsObj.size)) {
|
||||
width = undefined;
|
||||
height = undefined;
|
||||
}
|
||||
|
||||
if (argsObj && typeof argsObj.quality === 'string') {
|
||||
const q = argsObj.quality.toLowerCase();
|
||||
if (q === 'low' || q === 'medium' || q === 'high') {
|
||||
quality = q;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
width = undefined;
|
||||
height = undefined;
|
||||
}
|
||||
|
||||
// Default to 1024x1024 if width and height are still undefined after parsing args and attachment metadata
|
||||
const attachment = attachments?.[0];
|
||||
const {
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
filepath = null,
|
||||
filename = '',
|
||||
} = (attachment as TFile & TAttachmentMetadata) || {};
|
||||
|
||||
let origWidth = width ?? imageWidth;
|
||||
let origHeight = height ?? imageHeight;
|
||||
|
||||
if (origWidth === undefined || origHeight === undefined) {
|
||||
origWidth = 1024;
|
||||
origHeight = 1024;
|
||||
}
|
||||
|
||||
const [dimensions, setDimensions] = useState({ width: 'auto', height: 'auto' });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateDimensions = useCallback(() => {
|
||||
if (origWidth && origHeight && containerRef.current) {
|
||||
const scaled = scaleImage({
|
||||
originalWidth: origWidth,
|
||||
originalHeight: origHeight,
|
||||
containerRef,
|
||||
});
|
||||
setDimensions(scaled);
|
||||
}
|
||||
}, [origWidth, origHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting) {
|
||||
setProgress(initialProgress);
|
||||
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
|
||||
let baseDuration = 20000;
|
||||
if (quality === 'low') {
|
||||
baseDuration = 10000;
|
||||
} else if (quality === 'high') {
|
||||
baseDuration = 50000;
|
||||
}
|
||||
// adding some jitter (±30% of base)
|
||||
const jitter = Math.floor(baseDuration * 0.3);
|
||||
const totalDuration = Math.floor(Math.random() * jitter) + baseDuration;
|
||||
const updateInterval = 200;
|
||||
const totalSteps = totalDuration / updateInterval;
|
||||
let currentStep = 0;
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
currentStep++;
|
||||
|
||||
if (currentStep >= totalSteps) {
|
||||
clearInterval(intervalRef.current as NodeJS.Timeout);
|
||||
setProgress(0.9);
|
||||
} else {
|
||||
const progressRatio = currentStep / totalSteps;
|
||||
let mapRatio: number;
|
||||
if (progressRatio < 0.8) {
|
||||
mapRatio = Math.pow(progressRatio, 1.1);
|
||||
} else {
|
||||
const sub = (progressRatio - 0.8) / 0.2;
|
||||
mapRatio = 0.8 + (1 - Math.pow(1 - sub, 2)) * 0.2;
|
||||
}
|
||||
const scaledProgress = 0.1 + mapRatio * 0.8;
|
||||
|
||||
setProgress(scaledProgress);
|
||||
}
|
||||
}, updateInterval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialProgress, quality]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialProgress >= 1 || cancelled) {
|
||||
setProgress(initialProgress);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
}
|
||||
}, [initialProgress, cancelled]);
|
||||
|
||||
useEffect(() => {
|
||||
updateDimensions();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateDimensions();
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [updateDimensions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative my-2.5 flex size-5 shrink-0 items-center gap-2.5">
|
||||
<ProgressText progress={progress} error={cancelled} toolName={toolName} />
|
||||
</div>
|
||||
|
||||
{/* {showInfo && hasInfo && (
|
||||
<ToolCallInfo
|
||||
key="tool-call-info"
|
||||
input={args ?? ''}
|
||||
output={output}
|
||||
function_name={function_name}
|
||||
pendingAuth={authDomain.length > 0 && !cancelled && initialProgress < 1}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
<div className="relative mb-2 flex w-full justify-start">
|
||||
<div ref={containerRef} className="w-full max-w-lg">
|
||||
{dimensions.width !== 'auto' && progress < 1 && (
|
||||
<PixelCard
|
||||
variant="default"
|
||||
progress={progress}
|
||||
randomness={0.6}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
altText={filename}
|
||||
imagePath={filepath ?? ''}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
placeholderDimensions={{ width: dimensions.width, height: dimensions.height }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ProgressText({
|
||||
progress,
|
||||
error,
|
||||
toolName = 'image_gen_oai',
|
||||
}: {
|
||||
progress: number;
|
||||
error?: boolean;
|
||||
toolName: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const getText = () => {
|
||||
if (error) {
|
||||
return localize('com_ui_error');
|
||||
}
|
||||
|
||||
if (toolName === 'image_edit_oai') {
|
||||
if (progress >= 1) {
|
||||
return localize('com_ui_image_edited');
|
||||
}
|
||||
if (progress >= 0.7) {
|
||||
return localize('com_ui_final_touch');
|
||||
}
|
||||
if (progress >= 0.5) {
|
||||
return localize('com_ui_adding_details');
|
||||
}
|
||||
if (progress >= 0.3) {
|
||||
return localize('com_ui_edit_editing_image');
|
||||
}
|
||||
return localize('com_ui_getting_started');
|
||||
}
|
||||
|
||||
if (progress >= 1) {
|
||||
return localize('com_ui_image_created');
|
||||
}
|
||||
if (progress >= 0.7) {
|
||||
return localize('com_ui_final_touch');
|
||||
}
|
||||
if (progress >= 0.5) {
|
||||
return localize('com_ui_adding_details');
|
||||
}
|
||||
if (progress >= 0.3) {
|
||||
return localize('com_ui_creating_image');
|
||||
}
|
||||
return localize('com_ui_getting_started');
|
||||
};
|
||||
|
||||
const text = getText();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'progress-text-content pointer-events-none absolute left-0 top-0 inline-flex w-full items-center gap-2 overflow-visible whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
<span className={`font-medium ${progress < 1 ? 'shimmer' : ''}`}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as OpenAIImageGen } from './OpenAIImageGen';
|
||||
|
|
@ -17,7 +17,7 @@ const Stdout: React.FC<StdoutProps> = ({ output = '' }) => {
|
|||
return (
|
||||
processedContent && (
|
||||
<pre className="shrink-0">
|
||||
<div>{processedContent}</div>
|
||||
<div className="text-text-primary">{processedContent}</div>
|
||||
</pre>
|
||||
)
|
||||
);
|
||||
|
|
|
|||
10
client/src/components/Chat/Messages/Content/Parts/index.ts
Normal file
10
client/src/components/Chat/Messages/Content/Parts/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export * from './Attachment';
|
||||
export * from './OpenAIImageGen';
|
||||
|
||||
export { default as Text } from './Text';
|
||||
export { default as Reasoning } from './Reasoning';
|
||||
export { default as EmptyText } from './EmptyText';
|
||||
export { default as LogContent } from './LogContent';
|
||||
export { default as ExecuteCode } from './ExecuteCode';
|
||||
export { default as AgentUpdate } from './AgentUpdate';
|
||||
export { default as EditTextPart } from './EditTextPart';
|
||||
Loading…
Add table
Add a link
Reference in a new issue