Merge pull request #5 from TinyCloudLabs/feature/ai-assistant-improvements
Implement AI assistant enhancements and invitation system improvementsmain
commit
0e8625c53f
@ -0,0 +1,193 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Simple Blue Butterfly - Matching Reference Frames</title> |
||||
<style> |
||||
body { |
||||
margin: 0; |
||||
padding: 50px; |
||||
background: linear-gradient(135deg, #e8f4fd 0%, #f0f8ff 100%); |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
min-height: 100vh; |
||||
font-family: Arial, sans-serif; |
||||
} |
||||
|
||||
.container { |
||||
text-align: center; |
||||
} |
||||
|
||||
.butterfly { |
||||
width: 120px; |
||||
height: 80px; |
||||
position: relative; |
||||
margin: 50px auto; |
||||
transform: scale(2); |
||||
} |
||||
|
||||
/* Wing animations - simple up/down like reference images */ |
||||
@keyframes wingFlap { |
||||
0% { |
||||
transform: rotateZ(0deg); |
||||
} |
||||
25% { |
||||
transform: rotateZ(-30deg); |
||||
} |
||||
50% { |
||||
transform: rotateZ(-50deg); |
||||
} |
||||
75% { |
||||
transform: rotateZ(-30deg); |
||||
} |
||||
100% { |
||||
transform: rotateZ(0deg); |
||||
} |
||||
} |
||||
|
||||
.wing-left { |
||||
transform-origin: 48px 40px; |
||||
animation: wingFlap 0.8s ease-in-out infinite; |
||||
} |
||||
|
||||
.wing-right { |
||||
transform-origin: 72px 40px; |
||||
animation: wingFlap 0.8s ease-in-out infinite; |
||||
} |
||||
|
||||
/* Controls */ |
||||
.controls { |
||||
margin: 20px 0; |
||||
} |
||||
|
||||
button { |
||||
margin: 0 10px; |
||||
padding: 10px 20px; |
||||
border: 2px solid #1976d2; |
||||
background: white; |
||||
color: #1976d2; |
||||
border-radius: 5px; |
||||
cursor: pointer; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
button.active { |
||||
background: #1976d2; |
||||
color: white; |
||||
} |
||||
|
||||
.butterfly.slow .wing-left, |
||||
.butterfly.slow .wing-right { |
||||
animation-duration: 1.5s; |
||||
} |
||||
|
||||
.butterfly.fast .wing-left, |
||||
.butterfly.fast .wing-right { |
||||
animation-duration: 0.4s; |
||||
} |
||||
|
||||
h1 { |
||||
color: #333; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
p { |
||||
color: #666; |
||||
margin: 5px 0; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<h1>🦋 Simple Blue Butterfly</h1> |
||||
<p>Replicating the reference frame animation</p> |
||||
|
||||
<div class="butterfly" id="butterfly"> |
||||
<svg viewBox="0 0 120 80" width="120" height="80"> |
||||
<!-- Simple gradients --> |
||||
<defs> |
||||
<linearGradient id="blueWing" x1="0%" y1="0%" x2="100%" y2="100%"> |
||||
<stop offset="0%" style="stop-color: #4FC3F7; stop-opacity: 1" /> |
||||
<stop offset="50%" style="stop-color: #2196F3; stop-opacity: 1" /> |
||||
<stop offset="100%" style="stop-color: #1565C0; stop-opacity: 1" /> |
||||
</linearGradient> |
||||
|
||||
<linearGradient id="darkBlue" x1="0%" y1="0%" x2="100%" y2="100%"> |
||||
<stop offset="0%" style="stop-color: #1976D2; stop-opacity: 1" /> |
||||
<stop offset="100%" style="stop-color: #0D47A1; stop-opacity: 1" /> |
||||
</linearGradient> |
||||
</defs> |
||||
|
||||
<!-- Left Wing --> |
||||
<g class="wing-left"> |
||||
<!-- Upper left wing --> |
||||
<ellipse cx="30" cy="25" rx="18" ry="12" fill="url(#blueWing)" stroke="#0D47A1" stroke-width="0.5"/> |
||||
<!-- Lower left wing --> |
||||
<ellipse cx="35" cy="50" rx="13" ry="10" fill="url(#darkBlue)" stroke="#0D47A1" stroke-width="0.5"/> |
||||
|
||||
<!-- White spots --> |
||||
<circle cx="25" cy="22" r="2" fill="white" opacity="0.8" /> |
||||
<circle cx="35" cy="28" r="1.5" fill="white" opacity="0.7" /> |
||||
<circle cx="30" cy="47" r="1.8" fill="white" opacity="0.8" /> |
||||
</g> |
||||
|
||||
<!-- Right Wing --> |
||||
<g class="wing-right"> |
||||
<!-- Upper right wing --> |
||||
<ellipse cx="90" cy="25" rx="18" ry="12" fill="url(#blueWing)" stroke="#0D47A1" stroke-width="0.5"/> |
||||
<!-- Lower right wing --> |
||||
<ellipse cx="85" cy="50" rx="13" ry="10" fill="url(#darkBlue)" stroke="#0D47A1" stroke-width="0.5"/> |
||||
|
||||
<!-- White spots --> |
||||
<circle cx="95" cy="22" r="2" fill="white" opacity="0.8" /> |
||||
<circle cx="85" cy="28" r="1.5" fill="white" opacity="0.7" /> |
||||
<circle cx="90" cy="47" r="1.8" fill="white" opacity="0.8" /> |
||||
</g> |
||||
|
||||
<!-- Body --> |
||||
<ellipse cx="60" cy="40" rx="3" ry="25" fill="#212121" stroke="#000" stroke-width="0.5"/> |
||||
|
||||
<!-- Head --> |
||||
<circle cx="60" cy="20" r="4" fill="#424242" stroke="#000" stroke-width="0.5"/> |
||||
|
||||
<!-- Simple antennae --> |
||||
<line x1="58" y1="18" x2="55" y2="12" stroke="#212121" stroke-width="1.5" stroke-linecap="round"/> |
||||
<line x1="62" y1="18" x2="65" y2="12" stroke="#212121" stroke-width="1.5" stroke-linecap="round"/> |
||||
<circle cx="55" cy="12" r="1" fill="#212121"/> |
||||
<circle cx="65" cy="12" r="1" fill="#212121"/> |
||||
|
||||
<!-- Simple eyes --> |
||||
<circle cx="57" cy="18" r="1" fill="white" opacity="0.3"/> |
||||
<circle cx="63" cy="18" r="1" fill="white" opacity="0.3"/> |
||||
</svg> |
||||
</div> |
||||
|
||||
<div class="controls"> |
||||
<p>Wing Flapping Speed:</p> |
||||
<button class="active" onclick="setSpeed('normal')">Normal (0.8s)</button> |
||||
<button onclick="setSpeed('slow')">Slow (1.5s)</button> |
||||
<button onclick="setSpeed('fast')">Fast (0.4s)</button> |
||||
</div> |
||||
|
||||
<p>Simple up/down wing flapping motion</p> |
||||
</div> |
||||
|
||||
<script> |
||||
function setSpeed(speed) { |
||||
const butterfly = document.getElementById('butterfly'); |
||||
const buttons = document.querySelectorAll('button'); |
||||
|
||||
butterfly.classList.remove('slow', 'fast'); |
||||
buttons.forEach(btn => btn.classList.remove('active')); |
||||
|
||||
if (speed !== 'normal') { |
||||
butterfly.classList.add(speed); |
||||
} |
||||
|
||||
event.target.classList.add('active'); |
||||
} |
||||
</script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,275 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Butterfly Wing Flapping Test</title> |
||||
<style> |
||||
body { |
||||
margin: 0; |
||||
padding: 50px; |
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
min-height: 100vh; |
||||
font-family: Arial, sans-serif; |
||||
} |
||||
|
||||
.container { |
||||
text-align: center; |
||||
} |
||||
|
||||
.butterfly { |
||||
width: 200px; |
||||
height: 160px; |
||||
position: relative; |
||||
margin: 50px auto; |
||||
} |
||||
|
||||
/* Simple synchronized wing flapping - like reference images */ |
||||
@keyframes wingFlapLeft { |
||||
0% { |
||||
transform: rotateZ(-20deg); |
||||
} |
||||
50% { |
||||
transform: rotateZ(-70deg); |
||||
} |
||||
100% { |
||||
transform: rotateZ(-20deg); |
||||
} |
||||
} |
||||
|
||||
@keyframes wingFlapRight { |
||||
0% { |
||||
transform: rotateZ(20deg); |
||||
} |
||||
50% { |
||||
transform: rotateZ(70deg); |
||||
} |
||||
100% { |
||||
transform: rotateZ(20deg); |
||||
} |
||||
} |
||||
|
||||
.wing-left { |
||||
transform-origin: 70px 80px; /* Where wing attaches to body */ |
||||
animation: wingFlapLeft 0.6s ease-in-out infinite; |
||||
} |
||||
|
||||
.wing-right { |
||||
transform-origin: 130px 80px; /* Where wing attaches to body */ |
||||
animation: wingFlapRight 0.6s ease-in-out infinite; |
||||
} |
||||
|
||||
/* Test different speeds */ |
||||
.butterfly.slow .wing-left, |
||||
.butterfly.slow .wing-right { |
||||
animation-duration: 1.2s; |
||||
} |
||||
|
||||
.butterfly.fast .wing-left, |
||||
.butterfly.fast .wing-right { |
||||
animation-duration: 0.3s; |
||||
} |
||||
|
||||
.controls { |
||||
margin: 20px 0; |
||||
} |
||||
|
||||
button { |
||||
margin: 0 10px; |
||||
padding: 10px 20px; |
||||
border: 2px solid #1976d2; |
||||
background: white; |
||||
color: #1976d2; |
||||
border-radius: 5px; |
||||
cursor: pointer; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
button.active { |
||||
background: #1976d2; |
||||
color: white; |
||||
} |
||||
|
||||
h1 { |
||||
color: #333; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
p { |
||||
color: #666; |
||||
margin: 5px 0; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<h1>🦋 Butterfly Wing Flapping Test</h1> |
||||
<p>Testing different wing flapping animations with 3D rotation</p> |
||||
|
||||
<div class="butterfly" id="butterfly"> |
||||
<svg viewBox="0 0 200 160" width="200" height="160"> |
||||
<!-- Wing gradients --> |
||||
<defs> |
||||
<radialGradient id="morphoBlue" cx="50%" cy="30%" r="70%"> |
||||
<stop offset="0%" style="stop-color: #00D4FF; stop-opacity: 1" /> |
||||
<stop offset="40%" style="stop-color: #0099CC; stop-opacity: 1" /> |
||||
<stop offset="80%" style="stop-color: #003366; stop-opacity: 1" /> |
||||
<stop offset="100%" style="stop-color: #001122; stop-opacity: 1" /> |
||||
</radialGradient> |
||||
|
||||
<radialGradient id="morphoBlueSecondary" cx="50%" cy="30%" r="70%"> |
||||
<stop offset="0%" style="stop-color: #33AAFF; stop-opacity: 1" /> |
||||
<stop offset="40%" style="stop-color: #0077AA; stop-opacity: 1" /> |
||||
<stop offset="80%" style="stop-color: #002244; stop-opacity: 1" /> |
||||
<stop offset="100%" style="stop-color: #000D1A; stop-opacity: 1" /> |
||||
</radialGradient> |
||||
|
||||
<linearGradient id="bodyGradient" x1="0%" y1="0%" x2="0%" y2="100%"> |
||||
<stop offset="0%" style="stop-color: #2D1B1B; stop-opacity: 1" /> |
||||
<stop offset="50%" style="stop-color: #1A0F0F; stop-opacity: 1" /> |
||||
<stop offset="100%" style="stop-color: #0D0505; stop-opacity: 1" /> |
||||
</linearGradient> |
||||
|
||||
<filter id="glow"> |
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/> |
||||
<feMerge> |
||||
<feMergeNode in="coloredBlur"/> |
||||
<feMergeNode in="SourceGraphic"/> |
||||
</feMerge> |
||||
</filter> |
||||
</defs> |
||||
|
||||
<!-- Left wings --> |
||||
<g class="wing-left"> |
||||
<!-- Left upper wing --> |
||||
<path |
||||
d="M70 80 Q30 50 15 30 Q10 20 15 15 Q30 10 50 25 Q64 40 70 60 Z" |
||||
fill="url(#morphoBlue)" |
||||
stroke="#001122" |
||||
stroke-width="1" |
||||
filter="url(#glow)" |
||||
/> |
||||
|
||||
<!-- Left lower wing --> |
||||
<path |
||||
d="M70 80 Q40 100 25 120 Q15 130 20 135 Q30 145 50 125 Q64 105 70 90 Z" |
||||
fill="url(#morphoBlueSecondary)" |
||||
stroke="#001122" |
||||
stroke-width="1" |
||||
filter="url(#glow)" |
||||
/> |
||||
|
||||
<!-- Wing spots --> |
||||
<circle cx="45" cy="40" r="4" fill="#00AAFF" opacity="0.7" /> |
||||
<circle cx="35" cy="55" r="3" fill="#33BBFF" opacity="0.6" /> |
||||
<circle cx="50" cy="110" r="3.5" fill="#00AAFF" opacity="0.7" /> |
||||
</g> |
||||
|
||||
<!-- Right wings --> |
||||
<g class="wing-right"> |
||||
<!-- Right upper wing --> |
||||
<path |
||||
d="M130 80 Q170 50 185 30 Q190 20 185 15 Q170 10 150 25 Q136 40 130 60 Z" |
||||
fill="url(#morphoBlue)" |
||||
stroke="#001122" |
||||
stroke-width="1" |
||||
filter="url(#glow)" |
||||
/> |
||||
|
||||
<!-- Right lower wing --> |
||||
<path |
||||
d="M130 80 Q160 100 175 120 Q185 130 180 135 Q170 145 150 125 Q136 105 130 90 Z" |
||||
fill="url(#morphoBlueSecondary)" |
||||
stroke="#001122" |
||||
stroke-width="1" |
||||
filter="url(#glow)" |
||||
/> |
||||
|
||||
<!-- Wing spots --> |
||||
<circle cx="155" cy="40" r="4" fill="#00AAFF" opacity="0.7" /> |
||||
<circle cx="165" cy="55" r="3" fill="#33BBFF" opacity="0.6" /> |
||||
<circle cx="150" cy="110" r="3.5" fill="#00AAFF" opacity="0.7" /> |
||||
</g> |
||||
|
||||
<!-- Butterfly body --> |
||||
<ellipse |
||||
cx="100" |
||||
cy="80" |
||||
rx="6" |
||||
ry="50" |
||||
fill="url(#bodyGradient)" |
||||
stroke="#0D0505" |
||||
stroke-width="1" |
||||
/> |
||||
|
||||
<!-- Head --> |
||||
<circle |
||||
cx="100" |
||||
cy="40" |
||||
r="8" |
||||
fill="#2D1B1B" |
||||
stroke="#0D0505" |
||||
stroke-width="1" |
||||
/> |
||||
|
||||
<!-- Antennae --> |
||||
<path |
||||
d="M95 35 Q90 25 85 15" |
||||
stroke="#2D1B1B" |
||||
stroke-width="2" |
||||
fill="none" |
||||
stroke-linecap="round" |
||||
/> |
||||
<path |
||||
d="M105 35 Q110 25 115 15" |
||||
stroke="#2D1B1B" |
||||
stroke-width="2" |
||||
fill="none" |
||||
stroke-linecap="round" |
||||
/> |
||||
|
||||
<!-- Antennae tips --> |
||||
<circle cx="85" cy="15" r="2" fill="#2D1B1B" /> |
||||
<circle cx="115" cy="15" r="2" fill="#2D1B1B" /> |
||||
|
||||
<!-- Eyes --> |
||||
<circle cx="93" cy="35" r="2" fill="#000" /> |
||||
<circle cx="107" cy="35" r="2" fill="#000" /> |
||||
<circle cx="93" cy="35" r="1" fill="#FFF" opacity="0.6" /> |
||||
<circle cx="107" cy="35" r="1" fill="#FFF" opacity="0.6" /> |
||||
</svg> |
||||
</div> |
||||
|
||||
<div class="controls"> |
||||
<p>Wing Flapping Speed:</p> |
||||
<button class="active" onclick="setSpeed('normal')">Normal (0.6s)</button> |
||||
<button onclick="setSpeed('slow')">Slow (1.2s)</button> |
||||
<button onclick="setSpeed('fast')">Fast (0.3s)</button> |
||||
</div> |
||||
|
||||
<p>Watch the wings! They should flap up and down with 3D rotation.</p> |
||||
</div> |
||||
|
||||
<script> |
||||
function setSpeed(speed) { |
||||
const butterfly = document.getElementById('butterfly'); |
||||
const buttons = document.querySelectorAll('button'); |
||||
|
||||
// Remove all speed classes |
||||
butterfly.classList.remove('slow', 'fast'); |
||||
buttons.forEach(btn => btn.classList.remove('active')); |
||||
|
||||
// Add new speed class |
||||
if (speed !== 'normal') { |
||||
butterfly.classList.add(speed); |
||||
} |
||||
|
||||
// Mark active button |
||||
event.target.classList.add('active'); |
||||
} |
||||
</script> |
||||
</body> |
||||
</html> |
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,249 @@ |
||||
import { useState } from 'react'; |
||||
import { |
||||
Box, |
||||
Typography, |
||||
Rating, |
||||
Button, |
||||
TextField, |
||||
Collapse, |
||||
IconButton, |
||||
Chip, |
||||
Paper, |
||||
} from '@mui/material'; |
||||
import { |
||||
ThumbUp, |
||||
ThumbDown, |
||||
Feedback, |
||||
Send, |
||||
ExpandLess, |
||||
} from '@mui/icons-material'; |
||||
|
||||
interface AIResponseRatingProps { |
||||
responseId: string; |
||||
onRatingSubmit: (rating: AIResponseRating) => void; |
||||
existingRating?: AIResponseRating; |
||||
} |
||||
|
||||
export interface AIResponseRating { |
||||
responseId: string; |
||||
rating: number; // 1-5 stars
|
||||
feedback?: string; |
||||
helpfulVote?: 'helpful' | 'not-helpful'; |
||||
categories?: string[]; // e.g., ['accurate', 'comprehensive', 'actionable']
|
||||
userId: string; |
||||
timestamp: Date; |
||||
} |
||||
|
||||
const AIResponseRatingComponent: React.FC<AIResponseRatingProps> = ({ |
||||
responseId, |
||||
onRatingSubmit, |
||||
existingRating, |
||||
}) => { |
||||
const [rating, setRating] = useState<number>(existingRating?.rating || 0); |
||||
const [feedback, setFeedback] = useState<string>(existingRating?.feedback || ''); |
||||
const [helpfulVote, setHelpfulVote] = useState<'helpful' | 'not-helpful' | undefined>( |
||||
existingRating?.helpfulVote |
||||
); |
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>( |
||||
existingRating?.categories || [] |
||||
); |
||||
const [showDetailedRating, setShowDetailedRating] = useState(false); |
||||
const [hasSubmitted, setHasSubmitted] = useState(!!existingRating); |
||||
|
||||
const ratingCategories = [ |
||||
{ id: 'accurate', label: 'Accurate', color: 'success' as const }, |
||||
{ id: 'comprehensive', label: 'Comprehensive', color: 'info' as const }, |
||||
{ id: 'actionable', label: 'Actionable', color: 'primary' as const }, |
||||
{ id: 'relevant', label: 'Relevant', color: 'secondary' as const }, |
||||
{ id: 'clear', label: 'Clear', color: 'default' as const }, |
||||
{ id: 'timely', label: 'Timely', color: 'warning' as const }, |
||||
]; |
||||
|
||||
const handleCategoryToggle = (categoryId: string) => { |
||||
setSelectedCategories(prev =>
|
||||
prev.includes(categoryId) |
||||
? prev.filter(id => id !== categoryId) |
||||
: [...prev, categoryId] |
||||
); |
||||
}; |
||||
|
||||
const handleQuickVote = (vote: 'helpful' | 'not-helpful') => { |
||||
setHelpfulVote(vote); |
||||
|
||||
// For quick votes, submit immediately with minimal data
|
||||
const quickRating: AIResponseRating = { |
||||
responseId, |
||||
rating: vote === 'helpful' ? 4 : 2, // Default ratings for quick votes
|
||||
helpfulVote: vote, |
||||
categories: vote === 'helpful' ? ['relevant'] : [], |
||||
userId: 'current-user', // Would be actual user ID
|
||||
timestamp: new Date(), |
||||
}; |
||||
|
||||
onRatingSubmit(quickRating); |
||||
setHasSubmitted(true); |
||||
}; |
||||
|
||||
const handleDetailedSubmit = () => { |
||||
if (rating === 0) return; |
||||
|
||||
const detailedRating: AIResponseRating = { |
||||
responseId, |
||||
rating, |
||||
feedback: feedback.trim() || undefined, |
||||
helpfulVote, |
||||
categories: selectedCategories, |
||||
userId: 'current-user', // Would be actual user ID
|
||||
timestamp: new Date(), |
||||
}; |
||||
|
||||
onRatingSubmit(detailedRating); |
||||
setHasSubmitted(true); |
||||
setShowDetailedRating(false); |
||||
}; |
||||
|
||||
if (hasSubmitted && !showDetailedRating) { |
||||
return ( |
||||
<Paper sx={{ p: 2, mt: 2, bgcolor: 'success.50', border: 1, borderColor: 'success.200' }}> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<ThumbUp sx={{ color: 'success.main', fontSize: 20 }} /> |
||||
<Typography variant="body2" color="success.dark"> |
||||
Thank you for rating this response! |
||||
</Typography> |
||||
</Box> |
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setShowDetailedRating(true)} |
||||
sx={{ color: 'success.dark' }} |
||||
> |
||||
Edit Rating |
||||
</Button> |
||||
</Box> |
||||
</Paper> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<Box sx={{ mt: 2 }}> |
||||
{/* Quick Rating Buttons */} |
||||
{!showDetailedRating && !hasSubmitted && ( |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> |
||||
<Typography variant="body2" color="text.secondary"> |
||||
Was this response helpful? |
||||
</Typography> |
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}> |
||||
<Button |
||||
size="small" |
||||
variant={helpfulVote === 'helpful' ? 'contained' : 'outlined'} |
||||
startIcon={<ThumbUp />} |
||||
onClick={() => handleQuickVote('helpful')} |
||||
color="success" |
||||
> |
||||
Yes |
||||
</Button> |
||||
<Button |
||||
size="small" |
||||
variant={helpfulVote === 'not-helpful' ? 'contained' : 'outlined'} |
||||
startIcon={<ThumbDown />} |
||||
onClick={() => handleQuickVote('not-helpful')} |
||||
color="error" |
||||
> |
||||
No |
||||
</Button> |
||||
</Box> |
||||
|
||||
<Button |
||||
size="small" |
||||
startIcon={<Feedback />} |
||||
onClick={() => setShowDetailedRating(true)} |
||||
sx={{ ml: 'auto' }} |
||||
> |
||||
Detailed Rating |
||||
</Button> |
||||
</Box> |
||||
)} |
||||
|
||||
{/* Detailed Rating Panel */} |
||||
<Collapse in={showDetailedRating}> |
||||
<Paper sx={{ p: 3, border: 1, borderColor: 'divider' }}> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}> |
||||
<Typography variant="h6">Rate This Response</Typography> |
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowDetailedRating(false)} |
||||
> |
||||
<ExpandLess /> |
||||
</IconButton> |
||||
</Box> |
||||
|
||||
{/* Star Rating */} |
||||
<Box sx={{ mb: 3 }}> |
||||
<Typography variant="body2" gutterBottom> |
||||
Overall Rating |
||||
</Typography> |
||||
<Rating |
||||
value={rating} |
||||
onChange={(_, newValue) => setRating(newValue || 0)} |
||||
size="large" |
||||
/> |
||||
</Box> |
||||
|
||||
{/* Categories */} |
||||
<Box sx={{ mb: 3 }}> |
||||
<Typography variant="body2" gutterBottom> |
||||
What made this response good? (optional) |
||||
</Typography> |
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}> |
||||
{ratingCategories.map((category) => ( |
||||
<Chip |
||||
key={category.id} |
||||
label={category.label} |
||||
variant={selectedCategories.includes(category.id) ? 'filled' : 'outlined'} |
||||
color={selectedCategories.includes(category.id) ? category.color : 'default'} |
||||
onClick={() => handleCategoryToggle(category.id)} |
||||
size="small" |
||||
/> |
||||
))} |
||||
</Box> |
||||
</Box> |
||||
|
||||
{/* Feedback */} |
||||
<Box sx={{ mb: 3 }}> |
||||
<TextField |
||||
fullWidth |
||||
multiline |
||||
rows={3} |
||||
placeholder="Any additional feedback to help improve AI responses? (optional)" |
||||
value={feedback} |
||||
onChange={(e) => setFeedback(e.target.value)} |
||||
variant="outlined" |
||||
size="small" |
||||
/> |
||||
</Box> |
||||
|
||||
{/* Submit Button */} |
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}> |
||||
<Button
|
||||
onClick={() => setShowDetailedRating(false)} |
||||
variant="outlined" |
||||
> |
||||
Cancel |
||||
</Button> |
||||
<Button |
||||
onClick={handleDetailedSubmit} |
||||
variant="contained" |
||||
startIcon={<Send />} |
||||
disabled={rating === 0} |
||||
> |
||||
Submit Rating |
||||
</Button> |
||||
</Box> |
||||
</Paper> |
||||
</Collapse> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default AIResponseRatingComponent; |
@ -0,0 +1,226 @@ |
||||
import { useState } from 'react'; |
||||
import { |
||||
Dialog, |
||||
DialogTitle, |
||||
DialogContent, |
||||
DialogActions, |
||||
Button, |
||||
TextField, |
||||
Box, |
||||
Typography, |
||||
Avatar, |
||||
alpha, |
||||
useTheme, |
||||
} from '@mui/material'; |
||||
import { |
||||
Business, |
||||
PersonOutline, |
||||
Groups, |
||||
FamilyRestroom, |
||||
Favorite, |
||||
Home, |
||||
PersonAdd, |
||||
} from '@mui/icons-material'; |
||||
import { DEFAULT_RCARDS } from '../../types/notification'; |
||||
import type { Group } from '../../types/group'; |
||||
|
||||
interface InviteFormProps { |
||||
open: boolean; |
||||
onClose: () => void; |
||||
onSubmit: (inviteData: InviteFormData) => void; |
||||
group: Group; |
||||
} |
||||
|
||||
export interface InviteFormData { |
||||
inviteeName: string; |
||||
inviteeEmail: string; |
||||
relationshipType: string; |
||||
relationshipData: { |
||||
name: string; |
||||
description: string; |
||||
color: string; |
||||
icon: string; |
||||
}; |
||||
inviterName: string; // Will be set from current user
|
||||
} |
||||
|
||||
interface InviteFormState { |
||||
inviteeName?: string; |
||||
inviteeEmail?: string; |
||||
relationshipType?: string; |
||||
inviterName?: string; |
||||
} |
||||
|
||||
const InviteForm: React.FC<InviteFormProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
group
|
||||
}) => { |
||||
const theme = useTheme(); |
||||
const [formData, setFormData] = useState<InviteFormState>({ |
||||
inviteeName: '', |
||||
inviteeEmail: '', |
||||
relationshipType: '', |
||||
inviterName: 'Oli S-B', // Current user
|
||||
}); |
||||
const [selectedRelationship, setSelectedRelationship] = useState<string>(''); |
||||
|
||||
const getRCardIcon = (iconName: string) => { |
||||
const iconMap: Record<string, React.ReactElement> = { |
||||
Business: <Business />, |
||||
PersonOutline: <PersonOutline />, |
||||
Groups: <Groups />, |
||||
FamilyRestroom: <FamilyRestroom />, |
||||
Favorite: <Favorite />, |
||||
Home: <Home />, |
||||
}; |
||||
return iconMap[iconName] || <PersonOutline />; |
||||
}; |
||||
|
||||
const handleSubmit = () => { |
||||
if (!formData.inviteeName || !formData.inviteeEmail || !selectedRelationship) { |
||||
return; // TODO: Add validation feedback
|
||||
} |
||||
|
||||
const selectedRCard = DEFAULT_RCARDS.find(card => card.name === selectedRelationship); |
||||
|
||||
if (selectedRCard && formData.inviteeName && formData.inviteeEmail) { |
||||
const inviteData: InviteFormData = { |
||||
inviteeName: formData.inviteeName, |
||||
inviteeEmail: formData.inviteeEmail, |
||||
relationshipType: selectedRelationship, |
||||
relationshipData: { |
||||
name: selectedRCard.name || 'Unknown', |
||||
description: selectedRCard.description || 'No description', |
||||
color: selectedRCard.color || '#2563eb', |
||||
icon: selectedRCard.icon || 'PersonOutline', |
||||
}, |
||||
inviterName: formData.inviterName || 'Current User', |
||||
}; |
||||
|
||||
onSubmit(inviteData); |
||||
} |
||||
}; |
||||
|
||||
const handleRelationshipSelect = (relationshipName: string) => { |
||||
setSelectedRelationship(relationshipName); |
||||
setFormData(prev => ({ |
||||
...prev, |
||||
relationshipType: relationshipName |
||||
})); |
||||
}; |
||||
|
||||
return ( |
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> |
||||
<DialogTitle> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<PersonAdd /> |
||||
<Typography variant="h6"> |
||||
Invite Someone to {group.name} |
||||
</Typography> |
||||
</Box> |
||||
</DialogTitle> |
||||
|
||||
<DialogContent sx={{ p: 3 }}> |
||||
{/* Basic Info */} |
||||
<Box sx={{ mb: 4 }}> |
||||
<Typography variant="h6" gutterBottom> |
||||
Who are you inviting? |
||||
</Typography> |
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexDirection: { xs: 'column', sm: 'row' } }}> |
||||
<TextField |
||||
sx={{ flex: 1 }} |
||||
label="First Name" |
||||
value={formData.inviteeName || ''} |
||||
onChange={(e) => setFormData(prev => ({ |
||||
...prev, |
||||
inviteeName: e.target.value |
||||
}))} |
||||
required |
||||
/> |
||||
<TextField |
||||
sx={{ flex: 1 }} |
||||
label="Email Address" |
||||
type="email" |
||||
value={formData.inviteeEmail || ''} |
||||
onChange={(e) => setFormData(prev => ({ |
||||
...prev, |
||||
inviteeEmail: e.target.value |
||||
}))} |
||||
required |
||||
/> |
||||
</Box> |
||||
</Box> |
||||
|
||||
{/* Relationship Selection */} |
||||
<Box sx={{ mb: 3 }}> |
||||
<Typography variant="h6" gutterBottom> |
||||
What's your relationship with them? |
||||
</Typography> |
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1, |
||||
'@media (max-width: 600px)': { |
||||
flexDirection: 'column' |
||||
} |
||||
}}> |
||||
{DEFAULT_RCARDS.map((rCard) => ( |
||||
<Button |
||||
key={rCard.name} |
||||
variant={selectedRelationship === rCard.name ? "contained" : "outlined"} |
||||
onClick={() => handleRelationshipSelect(rCard.name)} |
||||
startIcon={ |
||||
<Avatar |
||||
sx={{ |
||||
bgcolor: selectedRelationship === rCard.name ? 'white' : rCard.color, |
||||
color: selectedRelationship === rCard.name ? rCard.color : 'white', |
||||
width: 24, |
||||
height: 24, |
||||
'& .MuiSvgIcon-root': { fontSize: 16 } |
||||
}} |
||||
> |
||||
{getRCardIcon(rCard.icon || 'PersonOutline')} |
||||
</Avatar> |
||||
} |
||||
sx={{ |
||||
flex: { xs: 'none', sm: '1 1 auto' }, |
||||
minWidth: 'fit-content', |
||||
borderRadius: 2, |
||||
textTransform: 'none', |
||||
py: 1.5, |
||||
px: 2, |
||||
'&:hover': { |
||||
transform: 'translateY(-1px)', |
||||
boxShadow: theme.shadows[4], |
||||
}, |
||||
}} |
||||
> |
||||
{rCard.name} |
||||
</Button> |
||||
))} |
||||
</Box> |
||||
</Box> |
||||
|
||||
</DialogContent> |
||||
|
||||
<DialogActions sx={{ p: 3, justifyContent: 'space-between' }}> |
||||
<Button onClick={onClose} variant="outlined"> |
||||
Cancel |
||||
</Button> |
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained" |
||||
disabled={!formData.inviteeName || !formData.inviteeEmail || !selectedRelationship} |
||||
> |
||||
Create Invite |
||||
</Button> |
||||
</DialogActions> |
||||
</Dialog> |
||||
); |
||||
}; |
||||
|
||||
export default InviteForm; |
@ -0,0 +1,330 @@ |
||||
import { useState } from 'react'; |
||||
import { |
||||
Dialog, |
||||
DialogTitle, |
||||
DialogContent, |
||||
DialogActions, |
||||
Button, |
||||
Box, |
||||
Typography, |
||||
Stepper, |
||||
Step, |
||||
StepLabel, |
||||
Chip, |
||||
Avatar, |
||||
List, |
||||
ListItem, |
||||
Paper, |
||||
Rating, |
||||
} from '@mui/material'; |
||||
import { |
||||
AutoAwesome, |
||||
RssFeed, |
||||
People, |
||||
Chat, |
||||
Folder, |
||||
Link as LinkIcon, |
||||
TipsAndUpdates, |
||||
ThumbUp, |
||||
QuestionAnswer, |
||||
CheckCircle, |
||||
} from '@mui/icons-material'; |
||||
import type { Group } from '../../types/group'; |
||||
|
||||
interface GroupTourProps { |
||||
open: boolean; |
||||
onClose: () => void; |
||||
group: Group; |
||||
onStartAIAssistant: (prompt?: string) => void; |
||||
} |
||||
|
||||
interface TourStep { |
||||
title: string; |
||||
description: string; |
||||
icon: React.ReactNode; |
||||
target?: string; |
||||
} |
||||
|
||||
interface PopularPrompt { |
||||
id: string; |
||||
prompt: string; |
||||
averageRating: number; |
||||
responseCount: number; |
||||
category: string; |
||||
} |
||||
|
||||
const GroupTour: React.FC<GroupTourProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
group,
|
||||
onStartAIAssistant
|
||||
}) => { |
||||
const [currentStep, setCurrentStep] = useState(0); |
||||
const [showPopularPrompts, setShowPopularPrompts] = useState(false); |
||||
|
||||
const tourSteps: TourStep[] = [ |
||||
{ |
||||
title: `Welcome to ${group.name}!`, |
||||
description: `Great! You've joined ${group.name}. Let me give you a quick tour of what you can do here.`, |
||||
icon: <CheckCircle sx={{ fontSize: 40, color: 'success.main' }} />, |
||||
}, |
||||
{ |
||||
title: 'Group Feed', |
||||
description: 'This is where group members share updates, discussions, and collaborate. You can post, comment, and engage with other members here.', |
||||
icon: <RssFeed sx={{ fontSize: 40, color: 'primary.main' }} />, |
||||
target: 'feed-tab', |
||||
}, |
||||
{ |
||||
title: 'Members', |
||||
description: `See all ${group.memberCount} members of the group, their roles, and activity levels. Great for networking and finding collaborators.`, |
||||
icon: <People sx={{ fontSize: 40, color: 'info.main' }} />, |
||||
target: 'members-tab', |
||||
}, |
||||
{ |
||||
title: 'Group Chat', |
||||
description: 'Real-time messaging with the entire group. Perfect for quick discussions and staying connected.', |
||||
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />, |
||||
target: 'chat-tab', |
||||
}, |
||||
{ |
||||
title: 'Collaborative Files', |
||||
description: 'Share documents, spreadsheets, and other files with the group. Work together on projects in real-time.', |
||||
icon: <Folder sx={{ fontSize: 40, color: 'warning.main' }} />, |
||||
target: 'files-tab', |
||||
}, |
||||
{ |
||||
title: 'Useful Links', |
||||
description: 'Important resources, websites, and references shared by group members. Bookmark and discover valuable content.', |
||||
icon: <LinkIcon sx={{ fontSize: 40, color: 'success.main' }} />, |
||||
target: 'links-tab', |
||||
}, |
||||
{ |
||||
title: 'AI Assistant', |
||||
description: 'Your smart companion for this group! Ask questions about members, projects, or get insights about group activity.', |
||||
icon: <AutoAwesome sx={{ fontSize: 40, color: 'primary.main' }} />, |
||||
}, |
||||
]; |
||||
|
||||
// Mock data for popular prompts - in real app, this would come from API
|
||||
const popularPrompts: PopularPrompt[] = [ |
||||
{ |
||||
id: '1', |
||||
prompt: "Who's highly engaged in this project?", |
||||
averageRating: 4.8, |
||||
responseCount: 23, |
||||
category: 'Members & Engagement', |
||||
}, |
||||
{ |
||||
id: '2', |
||||
prompt: "Who's working on which tasks and needs help?", |
||||
averageRating: 4.6, |
||||
responseCount: 18, |
||||
category: 'Project Management', |
||||
}, |
||||
{ |
||||
id: '3', |
||||
prompt: "What are the most important discussions happening right now?", |
||||
averageRating: 4.5, |
||||
responseCount: 15, |
||||
category: 'Group Activity', |
||||
}, |
||||
{ |
||||
id: '4', |
||||
prompt: "Show me recent files and documents shared by the team", |
||||
averageRating: 4.4, |
||||
responseCount: 12, |
||||
category: 'Resources', |
||||
}, |
||||
{ |
||||
id: '5', |
||||
prompt: "Who are the subject matter experts I should connect with?", |
||||
averageRating: 4.7, |
||||
responseCount: 20, |
||||
category: 'Networking', |
||||
}, |
||||
]; |
||||
|
||||
const handleNext = () => { |
||||
if (currentStep < tourSteps.length - 1) { |
||||
setCurrentStep(currentStep + 1); |
||||
} else { |
||||
setShowPopularPrompts(true); |
||||
} |
||||
}; |
||||
|
||||
const handleBack = () => { |
||||
if (currentStep > 0) { |
||||
setCurrentStep(currentStep - 1); |
||||
} |
||||
}; |
||||
|
||||
const handleSkipTour = () => { |
||||
setShowPopularPrompts(true); |
||||
}; |
||||
|
||||
const handleFinishTour = () => { |
||||
onClose(); |
||||
}; |
||||
|
||||
const handleUsePrompt = (prompt: string) => { |
||||
onClose(); |
||||
onStartAIAssistant(prompt); |
||||
}; |
||||
|
||||
const renderTourStep = () => ( |
||||
<Box sx={{ textAlign: 'center', py: 3 }}> |
||||
<Box sx={{ mb: 3 }}> |
||||
{tourSteps[currentStep].icon} |
||||
</Box> |
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600 }}> |
||||
{tourSteps[currentStep].title} |
||||
</Typography> |
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3, maxWidth: 400, mx: 'auto' }}> |
||||
{tourSteps[currentStep].description} |
||||
</Typography> |
||||
|
||||
{/* Progress indicator */} |
||||
<Stepper activeStep={currentStep} sx={{ mt: 4, mb: 3 }}> |
||||
{tourSteps.map((_, index) => ( |
||||
<Step key={index}> |
||||
<StepLabel /> |
||||
</Step> |
||||
))} |
||||
</Stepper> |
||||
</Box> |
||||
); |
||||
|
||||
const renderPopularPrompts = () => ( |
||||
<Box> |
||||
<Box sx={{ textAlign: 'center', mb: 3 }}> |
||||
<AutoAwesome sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} /> |
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600 }}> |
||||
Try the AI Assistant! |
||||
</Typography> |
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}> |
||||
Here are some popular questions other members have asked the AI assistant.
|
||||
These prompts received high ratings from the community: |
||||
</Typography> |
||||
</Box> |
||||
|
||||
<List sx={{ maxHeight: 400, overflow: 'auto' }}> |
||||
{popularPrompts.map((prompt) => ( |
||||
<ListItem key={prompt.id} sx={{ mb: 1 }}> |
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
width: '100%',
|
||||
cursor: 'pointer', |
||||
transition: 'all 0.2s', |
||||
'&:hover': { |
||||
boxShadow: 2, |
||||
transform: 'translateY(-1px)', |
||||
} |
||||
}} |
||||
onClick={() => handleUsePrompt(prompt.prompt)} |
||||
> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}> |
||||
<Chip
|
||||
label={prompt.category}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary" |
||||
/> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<Rating value={prompt.averageRating} readOnly precision={0.1} size="small" /> |
||||
<Typography variant="caption" color="text.secondary"> |
||||
({prompt.responseCount}) |
||||
</Typography> |
||||
</Box> |
||||
</Box> |
||||
<Typography variant="body1" sx={{ fontWeight: 500, mb: 1 }}> |
||||
"{prompt.prompt}" |
||||
</Typography> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<ThumbUp sx={{ fontSize: 16, color: 'success.main' }} /> |
||||
<Typography variant="caption" color="text.secondary"> |
||||
{prompt.averageRating.toFixed(1)}/5.0 average rating • Click to try this prompt |
||||
</Typography> |
||||
</Box> |
||||
</Paper> |
||||
</ListItem> |
||||
))} |
||||
</List> |
||||
|
||||
<Box sx={{ textAlign: 'center', mt: 3, p: 2, bgcolor: 'grey.50', borderRadius: 2 }}> |
||||
<TipsAndUpdates sx={{ color: 'info.main', mb: 1 }} /> |
||||
<Typography variant="body2" color="text.secondary"> |
||||
You can also ask your own questions! The AI assistant knows about group members,
|
||||
recent activity, shared files, and can help you get oriented. |
||||
</Typography> |
||||
</Box> |
||||
</Box> |
||||
); |
||||
|
||||
return ( |
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth |
||||
PaperProps={{ |
||||
sx: { minHeight: 500 } |
||||
}} |
||||
> |
||||
<DialogTitle> |
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> |
||||
<Avatar sx={{ bgcolor: 'primary.main' }}> |
||||
<AutoAwesome /> |
||||
</Avatar> |
||||
<Typography variant="h6"> |
||||
{showPopularPrompts ? 'AI Assistant Examples' : 'Group Tour'} |
||||
</Typography> |
||||
</Box> |
||||
</DialogTitle> |
||||
|
||||
<DialogContent sx={{ p: 3 }}> |
||||
{showPopularPrompts ? renderPopularPrompts() : renderTourStep()} |
||||
</DialogContent> |
||||
|
||||
<DialogActions sx={{ p: 3, justifyContent: 'space-between' }}> |
||||
{!showPopularPrompts ? ( |
||||
<> |
||||
<Button onClick={handleSkipTour} color="inherit"> |
||||
Skip Tour |
||||
</Button> |
||||
<Box sx={{ display: 'flex', gap: 1 }}> |
||||
<Button
|
||||
onClick={handleBack}
|
||||
disabled={currentStep === 0} |
||||
variant="outlined" |
||||
> |
||||
Back |
||||
</Button> |
||||
<Button
|
||||
onClick={handleNext}
|
||||
variant="contained" |
||||
> |
||||
{currentStep === tourSteps.length - 1 ? 'Continue to AI Assistant' : 'Next'} |
||||
</Button> |
||||
</Box> |
||||
</> |
||||
) : ( |
||||
<> |
||||
<Button
|
||||
onClick={() => onStartAIAssistant()}
|
||||
variant="outlined" |
||||
startIcon={<QuestionAnswer />} |
||||
> |
||||
Ask My Own Question |
||||
</Button> |
||||
<Button onClick={handleFinishTour} variant="contained"> |
||||
Finish Tour |
||||
</Button> |
||||
</> |
||||
)} |
||||
</DialogActions> |
||||
</Dialog> |
||||
); |
||||
}; |
||||
|
||||
export default GroupTour; |
@ -0,0 +1,297 @@ |
||||
import React from 'react'; |
||||
import { Box, keyframes } from '@mui/material'; |
||||
|
||||
const wingFlapLeft = keyframes` |
||||
0%, 100% { |
||||
transform: rotateZ(-5deg); |
||||
} |
||||
25% { |
||||
transform: rotateZ(-45deg); |
||||
} |
||||
75% { |
||||
transform: rotateZ(-60deg); |
||||
} |
||||
`;
|
||||
|
||||
const wingFlapRight = keyframes` |
||||
0%, 100% { |
||||
transform: rotateZ(5deg); |
||||
} |
||||
25% { |
||||
transform: rotateZ(45deg); |
||||
} |
||||
75% { |
||||
transform: rotateZ(60deg); |
||||
} |
||||
`;
|
||||
|
||||
const butterflyFlightPath = keyframes` |
||||
0% { |
||||
top: 25vh; |
||||
left: 15vw; |
||||
} |
||||
8% { |
||||
top: 20vh; |
||||
left: 25vw; |
||||
} |
||||
16% { |
||||
top: 35vh; |
||||
left: 45vw; |
||||
} |
||||
24% { |
||||
top: 15vh; |
||||
left: 65vw; |
||||
} |
||||
32% { |
||||
top: 40vh; |
||||
left: 75vw; |
||||
} |
||||
40% { |
||||
top: 60vh; |
||||
left: 80vw; |
||||
} |
||||
48% { |
||||
top: 70vh; |
||||
left: 65vw; |
||||
} |
||||
56% { |
||||
top: 75vh; |
||||
left: 45vw; |
||||
} |
||||
64% { |
||||
top: 60vh; |
||||
left: 25vw; |
||||
} |
||||
72% { |
||||
top: 45vh; |
||||
left: 10vw; |
||||
} |
||||
80% { |
||||
top: 30vh; |
||||
left: 5vw; |
||||
} |
||||
88% { |
||||
top: 20vh; |
||||
left: 8vw; |
||||
} |
||||
96% { |
||||
top: 22vh; |
||||
left: 12vw; |
||||
} |
||||
100% { |
||||
top: 25vh; |
||||
left: 15vw; |
||||
} |
||||
`;
|
||||
|
||||
const bodyPulse = keyframes` |
||||
0%, 100% { |
||||
transform: scale(1); |
||||
} |
||||
50% { |
||||
transform: scale(1.05); |
||||
} |
||||
`;
|
||||
|
||||
const shimmer = keyframes` |
||||
0%, 100% { |
||||
opacity: 0.8; |
||||
} |
||||
50% { |
||||
opacity: 1; |
||||
} |
||||
`;
|
||||
|
||||
interface AnimatedMorphoButterflyProps { |
||||
size?: number; |
||||
className?: string; |
||||
variant?: 'static' | 'floating'; |
||||
} |
||||
|
||||
const AnimatedMorphoButterfly: React.FC<AnimatedMorphoButterflyProps> = ({
|
||||
size = 48,
|
||||
className, |
||||
variant = 'static' |
||||
}) => { |
||||
return ( |
||||
<Box |
||||
className={className} |
||||
sx={{ |
||||
display: variant === 'floating' ? 'block' : 'inline-block', |
||||
width: size, |
||||
height: size, |
||||
...(variant === 'floating' && { |
||||
position: 'fixed', |
||||
animation: `${butterflyFlightPath} 20s ease-in-out infinite`, |
||||
zIndex: 1000, |
||||
pointerEvents: 'none', |
||||
}) |
||||
}} |
||||
> |
||||
<svg |
||||
viewBox="0 0 100 80" |
||||
width={size} |
||||
height={size * 0.8} |
||||
style={{ overflow: 'visible' }} |
||||
> |
||||
{/* Wing gradients */} |
||||
<defs> |
||||
<radialGradient id="morphoBlue" cx="50%" cy="30%" r="70%"> |
||||
<stop offset="0%" style={{ stopColor: '#00D4FF', stopOpacity: 1 }} /> |
||||
<stop offset="40%" style={{ stopColor: '#0099CC', stopOpacity: 1 }} /> |
||||
<stop offset="80%" style={{ stopColor: '#003366', stopOpacity: 1 }} /> |
||||
<stop offset="100%" style={{ stopColor: '#001122', stopOpacity: 1 }} /> |
||||
</radialGradient> |
||||
|
||||
<radialGradient id="morphoBlueSecondary" cx="50%" cy="30%" r="70%"> |
||||
<stop offset="0%" style={{ stopColor: '#33AAFF', stopOpacity: 1 }} /> |
||||
<stop offset="40%" style={{ stopColor: '#0077AA', stopOpacity: 1 }} /> |
||||
<stop offset="80%" style={{ stopColor: '#002244', stopOpacity: 1 }} /> |
||||
<stop offset="100%" style={{ stopColor: '#000D1A', stopOpacity: 1 }} /> |
||||
</radialGradient> |
||||
|
||||
<linearGradient id="bodyGradient" x1="0%" y1="0%" x2="0%" y2="100%"> |
||||
<stop offset="0%" style={{ stopColor: '#2D1B1B', stopOpacity: 1 }} /> |
||||
<stop offset="50%" style={{ stopColor: '#1A0F0F', stopOpacity: 1 }} /> |
||||
<stop offset="100%" style={{ stopColor: '#0D0505', stopOpacity: 1 }} /> |
||||
</linearGradient> |
||||
|
||||
<filter id="glow"> |
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/> |
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/> |
||||
<feMergeNode in="SourceGraphic"/> |
||||
</feMerge> |
||||
</filter> |
||||
</defs> |
||||
|
||||
{/* Left wings */} |
||||
<g |
||||
style={{ |
||||
transformOrigin: '35px 40px', |
||||
animation: `${wingFlapLeft} 0.3s ease-in-out infinite`, |
||||
}} |
||||
> |
||||
{/* Left upper wing */} |
||||
<path |
||||
d="M35 40 Q15 25 8 15 Q5 10 8 8 Q15 5 25 12 Q32 20 35 30 Z" |
||||
fill="url(#morphoBlue)" |
||||
stroke="#001122" |
||||
strokeWidth="0.5" |
||||
filter="url(#glow)" |
||||
style={{ |
||||
animation: `${shimmer} 2s ease-in-out infinite`, |
||||
}} |
||||
/> |
||||
|
||||
{/* Left lower wing */} |
||||
<path |
||||
d="M35 40 Q20 50 12 60 Q8 65 10 68 Q15 72 25 65 Q32 55 35 45 Z" |
||||
fill="url(#morphoBlueSecondary)" |
||||
stroke="#001122" |
||||
strokeWidth="0.5" |
||||
filter="url(#glow)" |
||||
style={{ |
||||
animation: `${shimmer} 2s ease-in-out infinite 0.3s`, |
||||
}} |
||||
/> |
||||
|
||||
{/* Wing spots/patterns */} |
||||
<circle cx="22" cy="20" r="2" fill="#00AAFF" opacity="0.7" /> |
||||
<circle cx="18" cy="28" r="1.5" fill="#33BBFF" opacity="0.6" /> |
||||
<circle cx="25" cy="55" r="1.8" fill="#00AAFF" opacity="0.7" /> |
||||
</g> |
||||
|
||||
{/* Right wings */} |
||||
<g |
||||
style={{ |
||||
transformOrigin: '65px 40px', |
||||
animation: `${wingFlapRight} 0.3s ease-in-out infinite`, |
||||
}} |
||||
> |
||||
{/* Right upper wing */} |
||||
<path |
||||
d="M65 40 Q85 25 92 15 Q95 10 92 8 Q85 5 75 12 Q68 20 65 30 Z" |
||||
fill="url(#morphoBlue)" |
||||
stroke="#001122" |
||||
strokeWidth="0.5" |
||||
filter="url(#glow)" |
||||
style={{ |
||||
animation: `${shimmer} 2s ease-in-out infinite 0.1s`, |
||||
}} |
||||
/> |
||||
|
||||
{/* Right lower wing */} |
||||
<path |
||||
d="M65 40 Q80 50 88 60 Q92 65 90 68 Q85 72 75 65 Q68 55 65 45 Z" |
||||
fill="url(#morphoBlueSecondary)" |
||||
stroke="#001122" |
||||
strokeWidth="0.5" |
||||
filter="url(#glow)" |
||||
style={{ |
||||
animation: `${shimmer} 2s ease-in-out infinite 0.4s`, |
||||
}} |
||||
/> |
||||
|
||||
{/* Wing spots/patterns */} |
||||
<circle cx="78" cy="20" r="2" fill="#00AAFF" opacity="0.7" /> |
||||
<circle cx="82" cy="28" r="1.5" fill="#33BBFF" opacity="0.6" /> |
||||
<circle cx="75" cy="55" r="1.8" fill="#00AAFF" opacity="0.7" /> |
||||
</g> |
||||
|
||||
{/* Butterfly body */} |
||||
<ellipse |
||||
cx="50" |
||||
cy="40" |
||||
rx="3" |
||||
ry="25" |
||||
fill="url(#bodyGradient)" |
||||
stroke="#0D0505" |
||||
strokeWidth="0.5" |
||||
style={{ |
||||
transformOrigin: '50px 40px', |
||||
animation: `${bodyPulse} 2s ease-in-out infinite`, |
||||
}} |
||||
/> |
||||
|
||||
{/* Head */} |
||||
<circle |
||||
cx="50" |
||||
cy="20" |
||||
r="4" |
||||
fill="#2D1B1B" |
||||
stroke="#0D0505" |
||||
strokeWidth="0.5" |
||||
/> |
||||
|
||||
{/* Antennae */} |
||||
<path |
||||
d="M48 18 Q45 12 42 8" |
||||
stroke="#2D1B1B" |
||||
strokeWidth="1" |
||||
fill="none" |
||||
strokeLinecap="round" |
||||
/> |
||||
<path |
||||
d="M52 18 Q55 12 58 8" |
||||
stroke="#2D1B1B" |
||||
strokeWidth="1" |
||||
fill="none" |
||||
strokeLinecap="round" |
||||
/> |
||||
|
||||
{/* Antennae tips */} |
||||
<circle cx="42" cy="8" r="1" fill="#2D1B1B" /> |
||||
<circle cx="58" cy="8" r="1" fill="#2D1B1B" /> |
||||
|
||||
{/* Eyes */} |
||||
<circle cx="47" cy="18" r="1" fill="#000" /> |
||||
<circle cx="53" cy="18" r="1" fill="#000" /> |
||||
<circle cx="47" cy="18" r="0.5" fill="#FFF" opacity="0.6" /> |
||||
<circle cx="53" cy="18" r="0.5" fill="#FFF" opacity="0.6" /> |
||||
</svg> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default AnimatedMorphoButterfly; |
Loading…
Reference in new issue