🖼️ 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:
Marco Beretta 2025-05-16 17:50:18 +02:00 committed by Danny Avila
parent 739b0d3012
commit c79ee32006
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
44 changed files with 1452 additions and 527 deletions

View file

@ -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',

View file

@ -32,7 +32,7 @@ const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
export const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (

View file

@ -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<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pixelsRef = useRef<Pixel[]>([]);
const animationRef = useRef<number>();
const timePrevRef = useRef(performance.now());
const progressRef = useRef<number | undefined>(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<HTMLDivElement> = (e) => {
if (
!disableFocus &&
!e.currentTarget.contains(e.relatedTarget) &&
progressRef.current === undefined
) {
startAnim('appear');
}
};
const focusOut: React.FocusEventHandler<HTMLDivElement> = (e) => {
if (
!disableFocus &&
!e.currentTarget.contains(e.relatedTarget) &&
progressRef.current === undefined
) {
startAnim('disappear');
}
};
return (
<div
ref={containerRef}
style={{
width: width || '100%',
height: height || '100%',
}}
>
<div
className={cn(
'ease-[cubic-bezier(0.5,1,0.89,1)] relative isolate grid select-none place-items-center overflow-hidden rounded-lg border border-border-light shadow-md transition-colors duration-200',
className,
)}
style={{
width: '100%',
height: '100%',
}}
onMouseEnter={hoverIn}
onMouseLeave={hoverOut}
onFocus={disableFocus ? undefined : focusIn}
onBlur={disableFocus ? undefined : focusOut}
tabIndex={disableFocus ? -1 : 0}
>
<canvas
ref={canvasRef}
className="pointer-events-none absolute inset-0 block"
width={width && width !== 'auto' ? parseInt(String(width)) : undefined}
height={height && height !== 'auto' ? parseInt(String(height)) : undefined}
/>
</div>
</div>
);
}

View file

@ -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';