mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
optimize spinner
This commit is contained in:
@@ -280,18 +280,21 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
|
||||
return currentPath === path;
|
||||
};
|
||||
|
||||
const handleNewChat = async () => {
|
||||
const handleNewChat = React.useCallback(async () => {
|
||||
await startNewSession('', setView);
|
||||
};
|
||||
}, [setView]);
|
||||
|
||||
const handleSessionClick = async (session: Session) => {
|
||||
markSessionActive(session.id);
|
||||
resumeSession(session, setView);
|
||||
};
|
||||
const handleSessionClick = React.useCallback(
|
||||
async (session: Session) => {
|
||||
markSessionActive(session.id);
|
||||
resumeSession(session, setView);
|
||||
},
|
||||
[markSessionActive, setView]
|
||||
);
|
||||
|
||||
const handleViewAllClick = () => {
|
||||
const handleViewAllClick = React.useCallback(() => {
|
||||
navigate('/sessions');
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
const renderMenuItem = (entry: NavigationEntry, index: number) => {
|
||||
if (entry.type === 'separator') {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Loader2, AlertCircle } from 'lucide-react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { OptimizedSpinner } from './ui/optimized-spinner';
|
||||
|
||||
interface SessionIndicatorsProps {
|
||||
isStreaming?: boolean;
|
||||
@@ -10,33 +12,38 @@ interface SessionIndicatorsProps {
|
||||
/**
|
||||
* Visual indicators for session status
|
||||
* - Error: red alert icon (highest priority)
|
||||
* - Streaming: animated spinner
|
||||
* - Streaming: animated spinner with GPU acceleration
|
||||
* - Has activity: green dot (shown after streaming completes until viewed)
|
||||
*/
|
||||
export function SessionIndicators({
|
||||
isStreaming,
|
||||
hasUnread,
|
||||
hasError,
|
||||
className = '',
|
||||
}: SessionIndicatorsProps) {
|
||||
if (!isStreaming && !hasUnread && !hasError) {
|
||||
return null;
|
||||
}
|
||||
export const SessionIndicators = React.memo<SessionIndicatorsProps>(
|
||||
({ isStreaming, hasUnread, hasError, className = '' }) => {
|
||||
if (!isStreaming && !hasUnread && !hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
{hasError && (
|
||||
<AlertCircle
|
||||
className="w-3.5 h-3.5 text-red-500"
|
||||
aria-label="Session encountered an error"
|
||||
/>
|
||||
)}
|
||||
{isStreaming && !hasError && (
|
||||
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" aria-label="Streaming" />
|
||||
)}
|
||||
{hasUnread && !isStreaming && !hasError && (
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full" aria-label="Has new activity" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
{hasError && (
|
||||
<AlertCircle
|
||||
className="w-3.5 h-3.5 text-red-500"
|
||||
aria-label="Session encountered an error"
|
||||
/>
|
||||
)}
|
||||
{isStreaming && !hasError && (
|
||||
<OptimizedSpinner size="xs" className="text-blue-500" aria-label="Streaming" />
|
||||
)}
|
||||
{hasUnread && !isStreaming && !hasError && (
|
||||
<div
|
||||
className="w-2 h-2 bg-green-500 rounded-full"
|
||||
aria-label="Has new activity"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SessionIndicators.displayName = 'SessionIndicators';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
interface OptimizedSpinnerProps {
|
||||
className?: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
color?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
// Size mappings
|
||||
const sizeClasses = {
|
||||
xs: 'w-3 h-3',
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-6 h-6',
|
||||
lg: 'w-8 h-8',
|
||||
xl: 'w-10 h-10',
|
||||
};
|
||||
|
||||
/**
|
||||
* GPU-accelerated spinner component for smooth performance
|
||||
* Uses CSS transforms and containment for optimal rendering
|
||||
*/
|
||||
export const OptimizedSpinner = React.memo<OptimizedSpinnerProps>(
|
||||
({ className = '', size = 'sm', color, 'aria-label': ariaLabel = 'Loading' }) => {
|
||||
// Inject optimized keyframes on first mount
|
||||
React.useEffect(() => {
|
||||
const styleId = 'optimized-spin-keyframes';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
@keyframes optimized-spin {
|
||||
from {
|
||||
transform: rotate(0deg) translateZ(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg) translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
contain: layout style paint;
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('spinner-container', sizeClasses[size], className)}
|
||||
aria-label={ariaLabel}
|
||||
style={{
|
||||
animation: 'optimized-spin 1s linear infinite',
|
||||
color: color,
|
||||
}}
|
||||
>
|
||||
<Loader2 className="w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
OptimizedSpinner.displayName = 'OptimizedSpinner';
|
||||
Reference in New Issue
Block a user