-
-
{renderIcon()}
-
0 ? localize('com_ui_requires_auth') : undefined
- }
- finishedText={getFinishedText()}
- hasInput={hasInfo}
- popover={true}
- />
- {hasInfo && (
- 0 && !cancelled && progress < 1}
- />
- )}
-
- {auth != null && auth && progress < 1 && !cancelled && (
-
-
-
-
- {localize('com_assistants_allow_sites_you_trust')}
-
-
- )}
+ <>
+
+
setShowInfo((prev) => !prev)}
+ inProgressText={localize('com_assistants_running_action')}
+ authText={
+ !cancelled && authDomain.length > 0 ? localize('com_ui_requires_auth') : undefined
+ }
+ finishedText={getFinishedText()}
+ hasInput={hasInfo}
+ isExpanded={showInfo}
+ error={cancelled}
+ />
- {attachments?.map((attachment, index) =>
)}
-
+
+
+
+ {showInfo && hasInfo && (
+ 0 && !cancelled && progress < 1}
+ />
+ )}
+
+
+
+ {auth != null && auth && progress < 1 && !cancelled && (
+
+
+
+
+
+
+ {localize('com_assistants_allow_sites_you_trust')}
+
+
+ )}
+ {attachments && attachments.length > 0 &&
}
+ >
);
}
diff --git a/client/src/components/Chat/Messages/Content/ToolCallInfo.tsx b/client/src/components/Chat/Messages/Content/ToolCallInfo.tsx
new file mode 100644
index 0000000000..fd0fe8e1db
--- /dev/null
+++ b/client/src/components/Chat/Messages/Content/ToolCallInfo.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { useLocalize } from '~/hooks';
+
+function OptimizedCodeBlock({ text, maxHeight = 320 }: { text: string; maxHeight?: number }) {
+ return (
+
+ );
+}
+
+export default function ToolCallInfo({
+ input,
+ output,
+ domain,
+ function_name,
+ pendingAuth,
+}: {
+ input: string;
+ function_name: string;
+ output?: string | null;
+ domain?: string;
+ pendingAuth?: boolean;
+}) {
+ const localize = useLocalize();
+ const formatText = (text: string) => {
+ try {
+ return JSON.stringify(JSON.parse(text), null, 2);
+ } catch {
+ return text;
+ }
+ };
+
+ let title =
+ domain != null && domain
+ ? localize('com_assistants_domain_info', { 0: domain })
+ : localize('com_assistants_function_use', { 0: function_name });
+ if (pendingAuth === true) {
+ title =
+ domain != null && domain
+ ? localize('com_assistants_action_attempt', { 0: domain })
+ : localize('com_assistants_attempt_info');
+ }
+
+ return (
+
+
+
{title}
+
+
+
+ {output && (
+ <>
+
+ {localize('com_ui_result')}
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/client/src/components/Chat/Messages/Content/ToolPopover.tsx b/client/src/components/Chat/Messages/Content/ToolPopover.tsx
deleted file mode 100644
index 198f64b3e0..0000000000
--- a/client/src/components/Chat/Messages/Content/ToolPopover.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as Popover from '@radix-ui/react-popover';
-import useLocalize from '~/hooks/useLocalize';
-
-export default function ToolPopover({
- input,
- output,
- domain,
- function_name,
- pendingAuth,
-}: {
- input: string;
- function_name: string;
- output?: string | null;
- domain?: string;
- pendingAuth?: boolean;
-}) {
- const localize = useLocalize();
- const formatText = (text: string) => {
- try {
- return JSON.stringify(JSON.parse(text), null, 2);
- } catch {
- return text;
- }
- };
-
- let title =
- domain != null && domain
- ? localize('com_assistants_domain_info', { 0: domain })
- : localize('com_assistants_function_use', { 0: function_name });
- if (pendingAuth === true) {
- title =
- domain != null && domain
- ? localize('com_assistants_action_attempt', { 0: domain })
- : localize('com_assistants_attempt_info');
- }
-
- return (
-
-
-
-
-
{title}
-
-
- {formatText(input)}
-
-
- {output != null && output && (
- <>
-
- {localize('com_ui_result')}
-
-
-
- {formatText(output)}
-
-
- >
- )}
-
-
-
-
- );
-}
diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx
index a0bf03e49d..46f217a83c 100644
--- a/client/src/components/Conversations/Conversations.tsx
+++ b/client/src/components/Conversations/Conversations.tsx
@@ -23,7 +23,7 @@ const LoadingSpinner = memo(() => {
return (
-
+
{localize('com_ui_loading')}
);
diff --git a/client/src/components/Messages/Content/CodeBlock.tsx b/client/src/components/Messages/Content/CodeBlock.tsx
index df80c90ecb..d48c8042b7 100644
--- a/client/src/components/Messages/Content/CodeBlock.tsx
+++ b/client/src/components/Messages/Content/CodeBlock.tsx
@@ -3,9 +3,9 @@ import { InfoIcon } from 'lucide-react';
import { Tools } from 'librechat-data-provider';
import React, { useRef, useState, useMemo, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
-import LogContent from '~/components/Chat/Messages/Content/Parts/LogContent';
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 Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
diff --git a/client/src/components/Messages/Content/RunCode.tsx b/client/src/components/Messages/Content/RunCode.tsx
index e80c589bd1..10adb7df3f 100644
--- a/client/src/components/Messages/Content/RunCode.tsx
+++ b/client/src/components/Messages/Content/RunCode.tsx
@@ -1,6 +1,6 @@
import debounce from 'lodash/debounce';
import { Tools, AuthType } from 'librechat-data-provider';
-import { TerminalSquareIcon, Loader } from 'lucide-react';
+import { TerminalSquareIcon } from 'lucide-react';
import React, { useMemo, useCallback, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
import { useVerifyAgentToolAuth, useToolCallMutation } from '~/data-provider';
@@ -9,6 +9,7 @@ import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { useMessageContext } from '~/Providers';
import { cn, normalizeLanguage } from '~/utils';
import { useToastContext } from '~/Providers';
+import { Spinner } from '~/components';
const RunCode: React.FC
= React.memo(({ lang, codeRef, blockIndex }) => {
const localize = useLocalize();
@@ -91,7 +92,7 @@ const RunCode: React.FC = React.memo(({ lang, codeRef, blockIndex
disabled={execute.isLoading}
>
{execute.isLoading ? (
-
+
) : (
)}
diff --git a/client/src/components/SidePanel/Agents/AgentFooter.tsx b/client/src/components/SidePanel/Agents/AgentFooter.tsx
index 75f10a3851..ce99e1189f 100644
--- a/client/src/components/SidePanel/Agents/AgentFooter.tsx
+++ b/client/src/components/SidePanel/Agents/AgentFooter.tsx
@@ -53,7 +53,7 @@ export default function AgentFooter({
const showButtons = activePanel === Panel.builder;
return (
-
+
{showButtons &&
}
{user?.role === SystemRoles.ADMIN && showButtons &&
}
{/* Context Button */}
diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx
index 23ede096a1..1cddf15180 100644
--- a/client/src/components/SidePanel/Agents/AgentPanel.tsx
+++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx
@@ -220,7 +220,7 @@ export default function AgentPanel({
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
aria-label="Agent configuration form"
>
-
+
-
+
+
+
+
+
+
);
}
diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx
index 35b2d103b7..1b2284507f 100644
--- a/client/src/components/ui/Button.tsx
+++ b/client/src/components/ui/Button.tsx
@@ -14,7 +14,7 @@ const buttonVariants = cva(
outline:
'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
+ ghost: 'hover:bg-surface-hover hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// hardcoded text color because of WCAG contrast issues (text-white)
submit: 'bg-surface-submit text-white hover:bg-surface-submit-hover',
diff --git a/client/src/components/ui/OriginalDialog.tsx b/client/src/components/ui/OriginalDialog.tsx
index c7b4c1f4c1..83405b1a60 100644
--- a/client/src/components/ui/OriginalDialog.tsx
+++ b/client/src/components/ui/OriginalDialog.tsx
@@ -32,7 +32,7 @@ const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
-const DialogOverlay = React.forwardRef<
+export const DialogOverlay = React.forwardRef<
React.ElementRef
,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
diff --git a/client/src/components/ui/PixelCard.tsx b/client/src/components/ui/PixelCard.tsx
new file mode 100644
index 0000000000..aabed69fb4
--- /dev/null
+++ b/client/src/components/ui/PixelCard.tsx
@@ -0,0 +1,375 @@
+import { useEffect, useRef, useCallback } from 'react';
+import { cn } from '~/utils';
+
+class Pixel {
+ width: number;
+ height: number;
+ ctx: CanvasRenderingContext2D;
+ x: number;
+ y: number;
+ color: string;
+ speed: number;
+ size: number;
+ sizeStep: number;
+ minSize: number;
+ maxSizeInteger: number;
+ maxSize: number;
+ delay: number;
+ counter: number;
+ counterStep: number;
+ isIdle: boolean;
+ isReverse: boolean;
+ isShimmer: boolean;
+ activationThreshold: number;
+
+ constructor(
+ canvas: HTMLCanvasElement,
+ context: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ color: string,
+ speed: number,
+ delay: number,
+ activationThreshold: number,
+ ) {
+ this.width = canvas.width;
+ this.height = canvas.height;
+ this.ctx = context;
+ this.x = x;
+ this.y = y;
+ this.color = color;
+ this.speed = this.random(0.1, 0.9) * speed;
+ this.size = 0;
+ this.sizeStep = Math.random() * 0.4;
+ this.minSize = 0.5;
+ this.maxSizeInteger = 2;
+ this.maxSize = this.random(this.minSize, this.maxSizeInteger);
+ this.delay = delay;
+ this.counter = 0;
+ this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;
+ this.isIdle = false;
+ this.isReverse = false;
+ this.isShimmer = false;
+ this.activationThreshold = activationThreshold;
+ }
+
+ private random(min: number, max: number) {
+ return Math.random() * (max - min) + min;
+ }
+
+ private draw() {
+ const offset = this.maxSizeInteger * 0.5 - this.size * 0.5;
+ this.ctx.fillStyle = this.color;
+ this.ctx.fillRect(this.x + offset, this.y + offset, this.size, this.size);
+ }
+
+ appear() {
+ this.isIdle = false;
+ if (this.counter <= this.delay) {
+ this.counter += this.counterStep;
+ return;
+ }
+ if (this.size >= this.maxSize) {
+ this.isShimmer = true;
+ }
+ if (this.isShimmer) {
+ this.shimmer();
+ } else {
+ this.size += this.sizeStep;
+ }
+ this.draw();
+ }
+
+ appearWithProgress(progress: number) {
+ const diff = progress - this.activationThreshold;
+ if (diff <= 0) {
+ this.isIdle = true;
+ return;
+ }
+ if (this.counter <= this.delay) {
+ this.counter += this.counterStep;
+ this.isIdle = false;
+ return;
+ }
+ if (this.size >= this.maxSize) {
+ this.isShimmer = true;
+ }
+ if (this.isShimmer) {
+ this.shimmer();
+ } else {
+ this.size += this.sizeStep;
+ }
+ this.isIdle = false;
+ this.draw();
+ }
+
+ disappear() {
+ this.isShimmer = false;
+ this.counter = 0;
+ if (this.size <= 0) {
+ this.isIdle = true;
+ return;
+ }
+ this.size -= 0.1;
+ this.draw();
+ }
+
+ private shimmer() {
+ if (this.size >= this.maxSize) {
+ this.isReverse = true;
+ } else if (this.size <= this.minSize) {
+ this.isReverse = false;
+ }
+ this.size += this.isReverse ? -this.speed : this.speed;
+ }
+}
+
+const getEffectiveSpeed = (value: number, reducedMotion: boolean) => {
+ const parsed = parseInt(String(value), 10);
+ const throttle = 0.001;
+ if (parsed <= 0 || reducedMotion) {
+ return 0;
+ }
+ if (parsed >= 100) {
+ return 100 * throttle;
+ }
+ return parsed * throttle;
+};
+
+const clamp = (n: number, min = 0, max = 1) => Math.min(Math.max(n, min), max);
+
+const VARIANTS = {
+ default: { gap: 5, speed: 35, colors: '#f8fafc,#f1f5f9,#cbd5e1', noFocus: false },
+ blue: { gap: 10, speed: 25, colors: '#e0f2fe,#7dd3fc,#0ea5e9', noFocus: false },
+ yellow: { gap: 3, speed: 20, colors: '#fef08a,#fde047,#eab308', noFocus: false },
+ pink: { gap: 6, speed: 80, colors: '#fecdd3,#fda4af,#e11d48', noFocus: true },
+} as const;
+
+interface PixelCardProps {
+ variant?: keyof typeof VARIANTS;
+ gap?: number;
+ speed?: number;
+ colors?: string;
+ noFocus?: boolean;
+ className?: string;
+ progress?: number;
+ randomness?: number;
+ width?: string;
+ height?: string;
+}
+
+export default function PixelCard({
+ variant = 'default',
+ gap,
+ speed,
+ colors,
+ noFocus,
+ className = '',
+ progress,
+ randomness = 0.3,
+ width,
+ height,
+}: PixelCardProps) {
+ const containerRef = useRef(null);
+ const canvasRef = useRef(null);
+ const pixelsRef = useRef([]);
+ const animationRef = useRef();
+ const timePrevRef = useRef(performance.now());
+ const progressRef = useRef(progress);
+ const reducedMotion = useRef(
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches,
+ ).current;
+
+ const cfg = VARIANTS[variant];
+ const g = gap ?? cfg.gap;
+ const s = speed ?? cfg.speed;
+ const palette = colors ?? cfg.colors;
+ const disableFocus = noFocus ?? cfg.noFocus;
+
+ const updateCanvasOpacity = useCallback(() => {
+ if (!canvasRef.current) {
+ return;
+ }
+ if (progressRef.current === undefined) {
+ canvasRef.current.style.opacity = '1';
+ return;
+ }
+ const fadeStart = 0.9;
+ const alpha =
+ progressRef.current >= fadeStart ? 1 - (progressRef.current - fadeStart) / 0.1 : 1;
+ canvasRef.current.style.opacity = String(clamp(alpha));
+ }, []);
+
+ const animate = useCallback(
+ (method: keyof Pixel) => {
+ animationRef.current = requestAnimationFrame(() => animate(method));
+
+ const now = performance.now();
+ const elapsed = now - timePrevRef.current;
+ if (elapsed < 1000 / 60) {
+ return;
+ }
+ timePrevRef.current = now - (elapsed % (1000 / 60));
+
+ const ctx = canvasRef.current?.getContext('2d');
+ if (!ctx || !canvasRef.current) {
+ return;
+ }
+
+ ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
+
+ let idle = true;
+ for (const p of pixelsRef.current) {
+ if (method === 'appearWithProgress') {
+ progressRef.current !== undefined
+ ? p.appearWithProgress(progressRef.current)
+ : (p.isIdle = true);
+ } else {
+ // @ts-ignore dynamic dispatch
+ p[method]();
+ }
+ if (!p.isIdle) {
+ idle = false;
+ }
+ }
+
+ updateCanvasOpacity();
+ if (idle) {
+ cancelAnimationFrame(animationRef.current!);
+ }
+ },
+ [updateCanvasOpacity],
+ );
+
+ const startAnim = useCallback(
+ (m: keyof Pixel) => {
+ cancelAnimationFrame(animationRef.current!);
+ animationRef.current = requestAnimationFrame(() => animate(m));
+ },
+ [animate],
+ );
+
+ const initPixels = useCallback(() => {
+ if (!containerRef.current || !canvasRef.current) {
+ return;
+ }
+
+ const { width: cw, height: ch } = containerRef.current.getBoundingClientRect();
+ const ctx = canvasRef.current.getContext('2d');
+ canvasRef.current.width = Math.floor(cw);
+ canvasRef.current.height = Math.floor(ch);
+
+ const cols = palette.split(',');
+ const px: Pixel[] = [];
+
+ const cx = cw / 2;
+ const cy = ch / 2;
+ const maxDist = Math.hypot(cx, cy);
+
+ for (let x = 0; x < cw; x += g) {
+ for (let y = 0; y < ch; y += g) {
+ const color = cols[Math.floor(Math.random() * cols.length)];
+ const distNorm = Math.hypot(x - cx, y - cy) / maxDist;
+ const threshold = clamp(distNorm * (1 - randomness) + Math.random() * randomness);
+ const delay = reducedMotion ? 0 : distNorm * maxDist;
+ if (!ctx) {
+ continue;
+ }
+ px.push(
+ new Pixel(
+ canvasRef.current,
+ ctx,
+ x,
+ y,
+ color,
+ getEffectiveSpeed(s, reducedMotion),
+ delay,
+ threshold,
+ ),
+ );
+ }
+ }
+ pixelsRef.current = px;
+
+ if (progressRef.current !== undefined) {
+ startAnim('appearWithProgress');
+ }
+ }, [g, palette, s, randomness, reducedMotion, startAnim]);
+
+ useEffect(() => {
+ progressRef.current = progress;
+ if (progress !== undefined) {
+ startAnim('appearWithProgress');
+ }
+ }, [progress, startAnim]);
+
+ useEffect(() => {
+ if (progress === undefined) {
+ cancelAnimationFrame(animationRef.current!);
+ }
+ }, [progress]);
+
+ useEffect(() => {
+ initPixels();
+ const obs = new ResizeObserver(initPixels);
+ containerRef.current && obs.observe(containerRef.current);
+ return () => {
+ obs.disconnect();
+ cancelAnimationFrame(animationRef.current!);
+ };
+ }, [initPixels]);
+
+ const hoverIn = () => progressRef.current === undefined && startAnim('appear');
+ const hoverOut = () => progressRef.current === undefined && startAnim('disappear');
+ const focusIn: React.FocusEventHandler = (e) => {
+ if (
+ !disableFocus &&
+ !e.currentTarget.contains(e.relatedTarget) &&
+ progressRef.current === undefined
+ ) {
+ startAnim('appear');
+ }
+ };
+ const focusOut: React.FocusEventHandler = (e) => {
+ if (
+ !disableFocus &&
+ !e.currentTarget.contains(e.relatedTarget) &&
+ progressRef.current === undefined
+ ) {
+ startAnim('disappear');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts
index 8d9eed0c85..2708379b6e 100644
--- a/client/src/components/ui/index.ts
+++ b/client/src/components/ui/index.ts
@@ -31,8 +31,9 @@ export { default as MCPIcon } from './MCPIcon';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as SplitText } from './SplitText';
-export { default as FileUpload } from './FileUpload';
export { default as FormInput } from './FormInput';
+export { default as PixelCard } from './PixelCard';
+export { default as FileUpload } from './FileUpload';
export { default as DropdownPopup } from './DropdownPopup';
export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 1d15ed42e7..827c5a26ce 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -29,7 +29,7 @@
"com_assistants_actions_info": "Let your Assistant retrieve information or take actions via API's",
"com_assistants_add_actions": "Add Actions",
"com_assistants_add_tools": "Add Tools",
- "com_assistants_allow_sites_you_trust": "Only allow sites you trust.",
+ "com_assistants_allow_sites_you_trust": "Only allow sites you trust",
"com_assistants_append_date": "Append Current Date & Time",
"com_assistants_append_date_tooltip": "When enabled, the current client date and time will be appended to the assistant system instructions.",
"com_assistants_attempt_info": "Assistant wants to send the following:",
@@ -871,6 +871,13 @@
"com_ui_x_selected": "{{0}} selected",
"com_ui_yes": "Yes",
"com_ui_zoom": "Zoom",
+ "com_ui_getting_started": "Getting Started",
+ "com_ui_creating_image": "Creating image. May take a moment",
+ "com_ui_adding_details": "Adding details",
+ "com_ui_final_touch": "Final touch",
+ "com_ui_image_created": "Image created",
+ "com_ui_edit_editing_image": "Editing image",
+ "com_ui_image_edited": "Image edited",
"com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
-}
\ No newline at end of file
+}
diff --git a/client/src/style.css b/client/src/style.css
index 1c29072797..309a27f01b 100644
--- a/client/src/style.css
+++ b/client/src/style.css
@@ -40,6 +40,17 @@
--red-800: #991b1b;
--red-900: #7f1d1d;
--red-950: #450a0a;
+ --amber-50: #fffbeb;
+ --amber-100: #fef3c7;
+ --amber-200: #fde68a;
+ --amber-300: #fcd34d;
+ --amber-400: #fbbf24;
+ --amber-500: #f59e0b;
+ --amber-600: #d97706;
+ --amber-700: #b45309;
+ --amber-800: #92400e;
+ --amber-900: #78350f;
+ --amber-950: #451a03;
--gizmo-gray-500: #999;
--gizmo-gray-600: #666;
--gizmo-gray-950: #0f0f0f;
@@ -55,6 +66,7 @@ html {
--text-secondary: var(--gray-600);
--text-secondary-alt: var(--gray-500);
--text-tertiary: var(--gray-500);
+ --text-warning: var(--amber-500);
--ring-primary: var(--gray-500);
--header-primary: var(--white);
--header-hover: var(--gray-50);
@@ -114,6 +126,7 @@ html {
--text-secondary: var(--gray-300);
--text-secondary-alt: var(--gray-400);
--text-tertiary: var(--gray-500);
+ --text-warning: var(--amber-500);
--header-primary: var(--gray-700);
--header-hover: var(--gray-600);
--header-button-hover: var(--gray-700);
@@ -715,8 +728,8 @@ pre {
.premium-scroll-button:hover:not(:active) {
transform: translateY(-1.5px) scale(1.02);
- box-shadow:
- 0 5px 10px rgba(0, 0, 0, 0.07),
+ box-shadow:
+ 0 5px 10px rgba(0, 0, 0, 0.07),
0 7px 14px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
@@ -2578,7 +2591,9 @@ html {
.animate-popover {
transform-origin: top;
opacity: 0;
- transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ transition:
+ opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
+ transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(0.95) translateY(-0.5rem);
}
@@ -2590,7 +2605,9 @@ html {
.animate-popover-left {
transform-origin: left;
opacity: 0;
- transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ transition:
+ opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
+ transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(0.95) translateX(-0.5rem);
}
@@ -2678,3 +2695,46 @@ html {
.badge-icon {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.shimmer {
+ display: inline-block;
+ position: relative;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0.8) 25%,
+ rgba(179, 179, 179, 0.25) 50%,
+ rgba(255, 255, 255, 0.8) 75%
+ );
+ background-size: 200% 100%;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shimmer 4s linear infinite;
+}
+
+:global(.dark) .shimmer {
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255) 25%,
+ rgba(129, 130, 134, 0.18) 50%,
+ rgb(255, 255, 255) 75%
+ );
+ background-size: 200% 100%;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shimmer 4s linear infinite;
+}
+
+.custom-style-2 {
+ padding: 12px;
+}
diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts
index 7518aeeb11..5363ff689c 100644
--- a/client/src/utils/index.ts
+++ b/client/src/utils/index.ts
@@ -18,6 +18,7 @@ export * from './promptGroups';
export { default as cn } from './cn';
export { default as logger } from './logger';
export { default as buildTree } from './buildTree';
+export { default as scaleImage } from './scaleImage';
export { default as getLoginError } from './getLoginError';
export { default as cleanupPreset } from './cleanupPreset';
export { default as buildDefaultConvo } from './buildDefaultConvo';
diff --git a/client/src/utils/scaleImage.ts b/client/src/utils/scaleImage.ts
new file mode 100644
index 0000000000..11e051fbd9
--- /dev/null
+++ b/client/src/utils/scaleImage.ts
@@ -0,0 +1,21 @@
+export default function scaleImage({
+ originalWidth,
+ originalHeight,
+ containerRef,
+}: {
+ originalWidth?: number;
+ originalHeight?: number;
+ containerRef: React.RefObject;
+}) {
+ const containerWidth = containerRef.current?.offsetWidth ?? 0;
+
+ if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
+ return { width: 'auto', height: 'auto' };
+ }
+
+ const aspectRatio = originalWidth / originalHeight;
+ const scaledWidth = Math.min(containerWidth, originalWidth);
+ const scaledHeight = scaledWidth / aspectRatio;
+
+ return { width: `${scaledWidth}px`, height: `${scaledHeight}px` };
+}
diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs
index fba7df05c2..f114a87334 100644
--- a/client/tailwind.config.cjs
+++ b/client/tailwind.config.cjs
@@ -67,6 +67,7 @@ module.exports = {
'text-secondary': 'var(--text-secondary)',
'text-secondary-alt': 'var(--text-secondary-alt)',
'text-tertiary': 'var(--text-tertiary)',
+ 'text-warning': 'var(--text-warning)',
'ring-primary': 'var(--ring-primary)',
'header-primary': 'var(--header-primary)',
'header-hover': 'var(--header-hover)',
diff --git a/librechat.example.yaml b/librechat.example.yaml
index ae14b0faae..dfa8626ecc 100644
--- a/librechat.example.yaml
+++ b/librechat.example.yaml
@@ -294,5 +294,8 @@ endpoints:
# fileSizeLimit: 5
# serverFileSizeLimit: 100 # Global server file size limit in MB
# avatarSizeLimit: 2 # Limit for user avatar image size in MB
+# imageGeneration: # Image Gen settings, either percentage or px
+# percentage: 100
+# px: 1024
# # See the Custom Configuration Guide for more information on Assistants Config:
# # https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/assistants_endpoint
diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts
index 3798b48d4d..09476cbe10 100644
--- a/packages/data-provider/src/file-config.ts
+++ b/packages/data-provider/src/file-config.ts
@@ -222,6 +222,12 @@ export const fileConfigSchema = z.object({
endpoints: z.record(endpointFileConfigSchema).optional(),
serverFileSizeLimit: z.number().min(0).optional(),
avatarSizeLimit: z.number().min(0).optional(),
+ imageGeneration: z
+ .object({
+ percentage: z.number().min(0).max(100).optional(),
+ px: z.number().min(0).optional(),
+ })
+ .optional(),
});
/** Helper function to safely convert string patterns to RegExp objects */