u
This commit is contained in:
220
src/components/chat/ChatFab.jsx
Normal file
220
src/components/chat/ChatFab.jsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import ChatRoundedIcon from '@mui/icons-material/ChatRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded';
|
||||
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
||||
|
||||
import chatAutomat, { selectPendingAskUser, selectHasPendingAsk } from '../../automats/chatAutomat';
|
||||
|
||||
export default class ChatFab extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const s = chatAutomat.state;
|
||||
this.state = {
|
||||
isOpen: s.isOpen,
|
||||
conversationState: s.conversationState,
|
||||
streamPhase: s.streamPhase,
|
||||
isSending: s.isSending,
|
||||
hasPendingAsk: selectHasPendingAsk(s),
|
||||
pendingAsk: selectPendingAskUser(s),
|
||||
sseStatus: s.sseStatus,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = chatAutomat.subscribe(this, (state) => ({
|
||||
isOpen: state.isOpen,
|
||||
conversationState: state.conversationState,
|
||||
streamPhase: state.streamPhase,
|
||||
isSending: state.isSending,
|
||||
hasPendingAsk: selectHasPendingAsk(state),
|
||||
pendingAsk: selectPendingAskUser(state),
|
||||
sseStatus: state.sseStatus,
|
||||
}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
chatAutomat.actions.toggleChat();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen, conversationState, streamPhase, isSending, hasPendingAsk, pendingAsk } = this.state;
|
||||
const isBusy = conversationState === 'busy' || streamPhase === 'thinking' || streamPhase === 'working' || isSending;
|
||||
|
||||
let tooltipTitle = 'KI-Chat öffnen';
|
||||
let ariaLabel = 'KI-Chat';
|
||||
if (hasPendingAsk) {
|
||||
tooltipTitle = pendingAsk?.question
|
||||
? `Antwort erforderlich: „${pendingAsk.question}“`
|
||||
: 'Antwort erforderlich: Das Modell wartet auf Ihre Eingabe';
|
||||
ariaLabel = 'KI-Chat: Antwort erforderlich';
|
||||
} else if (isBusy) {
|
||||
tooltipTitle = 'Modell antwortet gerade…';
|
||||
ariaLabel = 'KI-Chat: Modell antwortet';
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
position: 'fixed',
|
||||
bottom: { xs: 20, sm: 28 },
|
||||
right: { xs: 20, sm: 28 },
|
||||
zIndex: theme.zIndex.speedDial || 1050,
|
||||
opacity: isOpen ? 0 : 1,
|
||||
transform: isOpen ? 'scale(0)' : 'scale(1)',
|
||||
pointerEvents: isOpen ? 'none' : 'auto',
|
||||
transition: 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s linear',
|
||||
})}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{/* Animated pulsing halo when awaiting user answer */}
|
||||
{hasPendingAsk && !isOpen && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
left: -8,
|
||||
right: -8,
|
||||
bottom: -8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#f59e0b',
|
||||
opacity: 0.5,
|
||||
animation: 'fabAskPulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'@keyframes fabAskPulse': {
|
||||
'0%': { transform: 'scale(0.95)', opacity: 0.8 },
|
||||
'50%': { transform: 'scale(1.35)', opacity: 0.12 },
|
||||
'100%': { transform: 'scale(0.95)', opacity: 0.8 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Animated pulsating halo when model is busy thinking/working */}
|
||||
{!hasPendingAsk && isBusy && !isOpen && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
right: -6,
|
||||
bottom: -6,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'warning.main',
|
||||
opacity: 0.4,
|
||||
animation: 'fabPulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'@keyframes fabPulse': {
|
||||
'0%': { transform: 'scale(0.95)', opacity: 0.7 },
|
||||
'50%': { transform: 'scale(1.25)', opacity: 0.15 },
|
||||
'100%': { transform: 'scale(0.95)', opacity: 0.7 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Circular spinner ring when model is busy */}
|
||||
{!hasPendingAsk && isBusy && !isOpen && (
|
||||
<CircularProgress
|
||||
size={66}
|
||||
thickness={2.5}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
left: -6,
|
||||
color: '#f59e0b',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title={tooltipTitle} placement="left" arrow disableHoverListener={isOpen}>
|
||||
<Badge
|
||||
badgeContent={hasPendingAsk ? '?' : undefined}
|
||||
color={hasPendingAsk ? 'error' : isBusy ? 'warning' : 'default'}
|
||||
variant={hasPendingAsk ? 'standard' : 'dot'}
|
||||
invisible={!hasPendingAsk && !isBusy}
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{
|
||||
'& .MuiBadge-badge': hasPendingAsk
|
||||
? {
|
||||
bgcolor: '#ef4444',
|
||||
color: '#ffffff',
|
||||
fontWeight: 900,
|
||||
fontSize: '0.85rem',
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 2px 10px rgba(239, 68, 68, 0.6)',
|
||||
animation: 'badgePop 1.5s ease infinite',
|
||||
'@keyframes badgePop': {
|
||||
'0%, 100%': { transform: 'scale(1)' },
|
||||
'50%': { transform: 'scale(1.2)' },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label={ariaLabel}
|
||||
onClick={this.handleClick}
|
||||
sx={(theme) => ({
|
||||
width: 54,
|
||||
height: 54,
|
||||
boxShadow: hasPendingAsk
|
||||
? '0 8px 24px rgba(245, 158, 11, 0.45)'
|
||||
: isBusy
|
||||
? '0 8px 24px rgba(245, 158, 11, 0.35)'
|
||||
: '0 8px 24px rgba(59, 91, 219, 0.35)',
|
||||
transition: 'transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.06)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.96)',
|
||||
},
|
||||
background: hasPendingAsk
|
||||
? 'linear-gradient(135deg, #f59e0b 0%, #ea580c 100%)'
|
||||
: isBusy
|
||||
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
|
||||
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
|
||||
})}
|
||||
>
|
||||
{isOpen ? (
|
||||
<CloseRoundedIcon sx={{ fontSize: 24 }} />
|
||||
) : hasPendingAsk ? (
|
||||
<HelpOutlineRoundedIcon
|
||||
sx={{
|
||||
fontSize: 26,
|
||||
animation: 'fabWiggle 2s ease infinite',
|
||||
'@keyframes fabWiggle': {
|
||||
'0%, 100%': { transform: 'rotate(0deg)' },
|
||||
'15%': { transform: 'rotate(-12deg)' },
|
||||
'30%': { transform: 'rotate(12deg)' },
|
||||
'45%': { transform: 'rotate(-8deg)' },
|
||||
'60%': { transform: 'rotate(0deg)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : isBusy ? (
|
||||
<AutoAwesomeRoundedIcon sx={{ fontSize: 24 }} />
|
||||
) : (
|
||||
<ChatRoundedIcon sx={{ fontSize: 24 }} />
|
||||
)}
|
||||
</Fab>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user