Merge pull request #5 from TinyCloudLabs/feature/ai-assistant-improvements

Implement AI assistant enhancements and invitation system improvements
main
olisb 2 months ago committed by GitHub
commit 0e8625c53f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 193
      butterfly-simple-test.html
  2. 275
      butterfly-wing-test.html
  3. 13
      public/groups.json
  4. 127
      public/naog1-butterfly-logo.svg
  5. 1
      src/components/account/MyCollectionPage.tsx
  6. 249
      src/components/ai/AIResponseRating.tsx
  7. 226
      src/components/invite/InviteForm.tsx
  8. 330
      src/components/tour/GroupTour.tsx
  9. 297
      src/components/ui/AnimatedMorphoButterfly.tsx
  10. 542
      src/pages/GroupDetailPage.tsx
  11. 10
      src/pages/GroupPage.tsx
  12. 169
      src/pages/InvitationPage.tsx
  13. 59
      src/pages/OnboardingPage.tsx
  14. 34
      src/pages/SocialContractPage.tsx

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

@ -1,4 +1,17 @@
[ [
{
"id": "7",
"name": "NAOG1",
"description": "Designing and deploying the culture, governance, legal framework and App design for the NAO Genesis 1. This foundational group focuses on establishing the core principles, organizational structures, and technical architecture that will guide the Network Autonomous Organization's inaugural implementation.",
"memberCount": 25,
"memberIds": ["1", "2", "3", "4", "5", "6"],
"createdBy": "1",
"createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2024-12-15T18:00:00Z",
"isPrivate": false,
"tags": ["nao", "genesis", "governance", "culture", "legal", "architecture"],
"image": "/naog1-butterfly-logo.svg"
},
{ {
"id": "1", "id": "1",
"name": "React Developers", "name": "React Developers",

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="naogBlue" 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="naogBlueSecondary" 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="naogBodyGradient" 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="naogGlow">
<feGaussianBlur stdDeviation="1" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background circle -->
<circle cx="50" cy="50" r="45" fill="url(#naogBlue)" opacity="0.1" stroke="url(#naogBlue)" stroke-width="2"/>
<!-- Left wings -->
<g transform="translate(50,50)">
<!-- Left upper wing -->
<path
d="M-15 -10 Q-35 -25 -42 -35 Q-45 -40 -42 -42 Q-35 -45 -25 -38 Q-18 -30 -15 -20 Z"
fill="url(#naogBlue)"
stroke="#001122"
stroke-width="0.5"
filter="url(#naogGlow)"
/>
<!-- Left lower wing -->
<path
d="M-15 -10 Q-30 0 -38 10 Q-42 15 -40 18 Q-35 22 -25 15 Q-18 5 -15 -5 Z"
fill="url(#naogBlueSecondary)"
stroke="#001122"
stroke-width="0.5"
filter="url(#naogGlow)"
/>
<!-- Right upper wing -->
<path
d="M15 -10 Q35 -25 42 -35 Q45 -40 42 -42 Q35 -45 25 -38 Q18 -30 15 -20 Z"
fill="url(#naogBlue)"
stroke="#001122"
stroke-width="0.5"
filter="url(#naogGlow)"
/>
<!-- Right lower wing -->
<path
d="M15 -10 Q30 0 38 10 Q42 15 40 18 Q35 22 25 15 Q18 5 15 -5 Z"
fill="url(#naogBlueSecondary)"
stroke="#001122"
stroke-width="0.5"
filter="url(#naogGlow)"
/>
<!-- Wing spots/patterns -->
<circle cx="-28" cy="-30" r="3" fill="#00AAFF" opacity="0.8" />
<circle cx="-32" cy="-22" r="2" fill="#33BBFF" opacity="0.7" />
<circle cx="-25" cy="5" r="2.5" fill="#00AAFF" opacity="0.8" />
<circle cx="28" cy="-30" r="3" fill="#00AAFF" opacity="0.8" />
<circle cx="32" cy="-22" r="2" fill="#33BBFF" opacity="0.7" />
<circle cx="25" cy="5" r="2.5" fill="#00AAFF" opacity="0.8" />
<!-- Butterfly body -->
<ellipse
cx="0"
cy="0"
rx="2"
ry="20"
fill="url(#naogBodyGradient)"
stroke="#0D0505"
stroke-width="0.3"
/>
<!-- Head -->
<circle
cx="0"
cy="-15"
r="3"
fill="#2D1B1B"
stroke="#0D0505"
stroke-width="0.3"
/>
<!-- Antennae -->
<path
d="M-2 -17 Q-5 -23 -8 -28"
stroke="#2D1B1B"
stroke-width="0.8"
fill="none"
stroke-linecap="round"
/>
<path
d="M2 -17 Q5 -23 8 -28"
stroke="#2D1B1B"
stroke-width="0.8"
fill="none"
stroke-linecap="round"
/>
<!-- Antennae tips -->
<circle cx="-8" cy="-28" r="0.8" fill="#2D1B1B" />
<circle cx="8" cy="-28" r="0.8" fill="#2D1B1B" />
<!-- NAOG1 text -->
<text x="0" y="35" font-family="Arial, sans-serif" font-size="8" font-weight="bold" text-anchor="middle" fill="#003366">NAOG1</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@ -621,6 +621,7 @@ const MyCollectionPage = ({}: MyCollectionPageProps) => {
)} )}
</Box> </Box>
{/* Query Dialog */} {/* Query Dialog */}
<Dialog open={showQueryDialog} onClose={() => setShowQueryDialog(false)} maxWidth="md" fullWidth> <Dialog open={showQueryDialog} onClose={() => setShowQueryDialog(false)} maxWidth="md" fullWidth>
<DialogTitle> <DialogTitle>

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

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { import {
Typography, Typography,
Box, Box,
@ -13,7 +13,11 @@ import {
Chip, Chip,
Badge, Badge,
alpha, alpha,
useTheme useTheme,
Dialog,
DialogTitle,
DialogContent,
TextField
} from '@mui/material'; } from '@mui/material';
import { import {
ArrowBack, ArrowBack,
@ -28,15 +32,20 @@ import {
MoreVert, MoreVert,
Description, Description,
TableChart, TableChart,
PersonAdd PersonAdd,
AutoAwesome
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Group, GroupPost, GroupLink } from '../types/group'; import type { Group, GroupPost, GroupLink } from '../types/group';
import PostCreateButton from '../components/PostCreateButton'; import PostCreateButton from '../components/PostCreateButton';
import GroupTour from '../components/tour/GroupTour';
import AIResponseRatingComponent, { type AIResponseRating } from '../components/ai/AIResponseRating';
import InviteForm, { type InviteFormData } from '../components/invite/InviteForm';
const GroupDetailPage = () => { const GroupDetailPage = () => {
const { groupId } = useParams<{ groupId: string }>(); const { groupId } = useParams<{ groupId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const theme = useTheme(); const theme = useTheme();
const [group, setGroup] = useState<Group | null>(null); const [group, setGroup] = useState<Group | null>(null);
@ -44,6 +53,14 @@ const GroupDetailPage = () => {
const [links, setLinks] = useState<GroupLink[]>([]); const [links, setLinks] = useState<GroupLink[]>([]);
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showAIAssistant, setShowAIAssistant] = useState(false);
const [showGroupTour, setShowGroupTour] = useState(false);
const [initialPrompt, setInitialPrompt] = useState<string>();
const [aiMessages, setAiMessages] = useState<Array<{id: string, prompt: string, response: string, timestamp: Date, isTyping?: boolean}>>([]);
const [currentInput, setCurrentInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [showInviteForm, setShowInviteForm] = useState(false);
const [userFirstName, setUserFirstName] = useState<string>();
useEffect(() => { useEffect(() => {
const loadGroupData = async () => { const loadGroupData = async () => {
@ -54,6 +71,34 @@ const GroupDetailPage = () => {
const groupData = await dataService.getGroup(groupId); const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null); setGroup(groupData || null);
// Check if this is user's first visit to this group or came from invitation
const hasVisitedKey = `hasVisited_group_${groupId}`;
const hasVisited = localStorage.getItem(hasVisitedKey);
const fromInvite = searchParams.get('fromInvite') === 'true';
const newMember = searchParams.get('newMember') === 'true';
// Extract user's firstname if available from invitation flow
const firstName = searchParams.get('firstName') || searchParams.get('inviteeName');
if (firstName) {
setUserFirstName(firstName);
}
if ((!hasVisited || fromInvite || newMember) && groupData) {
// Mark as visited and open AI assistant directly
localStorage.setItem(hasVisitedKey, 'true');
setTimeout(() => setShowAIAssistant(true), 1000); // Small delay for better UX
// Clean up URL parameters after showing the welcome
if (fromInvite || newMember) {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete('fromInvite');
newSearchParams.delete('newMember');
newSearchParams.delete('firstName');
newSearchParams.delete('inviteeName');
setSearchParams(newSearchParams);
}
}
// Mock data for posts and links until we have real data // Mock data for posts and links until we have real data
const mockPosts: GroupPost[] = [ const mockPosts: GroupPost[] = [
{ {
@ -108,6 +153,13 @@ const GroupDetailPage = () => {
loadGroupData(); loadGroupData();
}, [groupId]); }, [groupId]);
// Set initial prompt when provided
useEffect(() => {
if (initialPrompt && showAIAssistant) {
setCurrentInput(initialPrompt);
}
}, [initialPrompt, showAIAssistant]);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue); setTabValue(newValue);
}; };
@ -117,10 +169,88 @@ const GroupDetailPage = () => {
}; };
const handleInviteToGroup = () => { const handleInviteToGroup = () => {
// TODO: Implement invite to group functionality setShowInviteForm(true);
console.log('Inviting to group:', group?.name); };
// This could navigate to an invite page or open a modal
navigate(`/invite?groupId=${groupId}`); const handleInviteSubmit = (inviteData: InviteFormData) => {
console.log('Sending invite:', inviteData);
// TODO: Generate personalized invitation link and send email
// For now, navigate to invite page with the data
const inviteParams = new URLSearchParams({
groupId: groupId!,
inviteeName: inviteData.inviteeName,
inviterName: inviteData.inviterName,
relationshipType: inviteData.relationshipType,
});
setShowInviteForm(false);
navigate(`/invite?${inviteParams.toString()}`);
};
const handleStartAIAssistant = (prompt?: string) => {
setInitialPrompt(prompt);
setShowAIAssistant(true);
};
const handleAIRatingSubmit = (rating: AIResponseRating) => {
console.log('AI Rating submitted:', rating);
// TODO: Send rating to backend
};
const getMockAIResponse = (prompt: string): string => {
const lowerPrompt = prompt.toLowerCase();
if (lowerPrompt.includes('engagement') || lowerPrompt.includes('activity')) {
return `Based on recent activity in ${group?.name || 'this group'}, here's what I found:\n\nMost Active Contributors & Their Focus Areas:\n\n👨💼 John Doe - Governance Framework Lead\n• Posted 8 governance proposals\n• Leading legal structure discussions\n• Members particularly value his constitutional law expertise\n\n👩💻 Sarah Chen - Technical Architecture\n• Shared 12 technical specifications\n• Posted blockchain integration proposals\n• Her smart contract designs received highest engagement\n\n👨🎨 Mike Torres - App Design & UX\n• Posted 15 wireframes and prototypes\n• Leading user experience research\n• His onboarding flow designs are most discussed\n\n👩💼 Jane Smith - Community & Process\n• Posted 6 process improvement proposals\n• Facilitating member onboarding discussions\n• Her social contract revisions received widespread support\n\nEngagement Quality Insights:\n• Governance posts get 3x more detailed comments than other topics\n• Technical architecture discussions have longest threads (avg 18 replies)\n• UX/Design posts receive most "helpful" reactions from members\n• Process improvement suggestions get fastest implementation (avg 2.3 days)\n• Cross-functional collaboration posts (touching multiple areas) get highest ratings\n• Members engage 4x more with posts that include specific action items\n• Questions about "Genesis 1 launch timeline" consistently generate most participation`;
}
if (lowerPrompt.includes('content') || lowerPrompt.includes('resources')) {
return `Here are the most valuable content and resources in ${group?.name || 'this group'}:\n\nTop Shared Resources:\n📄 "NAO Governance Framework v2.1" (shared by John, 12 likes)\n🔗 Legal Structure Template (shared by Jane, 8 saves)\n📊 App Architecture Proposal (shared by Mike, 15 comments)\n\nRecent Valuable Discussions:\n• "Voting mechanisms for Genesis 1" - 23 participants\n• "Technical roadmap priorities" - 18 participants\n• "Community onboarding process" - 14 participants\n\nRecommended Actions:\n✅ Pin the governance framework document\n✅ Create a resources collection for legal templates\n✅ Consider weekly architecture review meetings`;
}
if (lowerPrompt.includes('connect') || lowerPrompt.includes('members') || lowerPrompt.includes('networking')) {
return `Here are some great connections to make in ${group?.name || 'this group'}:\n\nSubject Matter Experts:\n👨💼 John Doe - Governance & Legal (15+ years experience)\n👩💻 Sarah Chen - Technical Architecture (blockchain specialist)\n👨🎨 Mike Torres - UX/UI Design (product design background)\n\nActive Contributors:\n• Jane Smith - Great at facilitating discussions\n• Alex Rodriguez - Excellent technical documentation\n• Lisa Park - Community building expertise\n\nNetworking Opportunities:\n🤝 Join the weekly "Genesis 1 Planning" calls\n💡 Participate in Friday "Innovation Hour" sessions\n📝 Contribute to the shared documentation effort\n\nPro Tip: Members are most responsive to direct messages on Tuesday-Thursday afternoons!`;
}
if (lowerPrompt.includes('task') || lowerPrompt.includes('help') || lowerPrompt.includes('project')) {
return `Current project status and who needs help in ${group?.name || 'this group'}:\n\nActive Tasks & Owners:\n🔄 Governance Framework Review - John Doe (needs legal input)\n🔄 Technical Architecture - Sarah Chen (looking for developers)\n🔄 App Wireframes - Mike Torres (needs user feedback)\n🔄 Community Guidelines - Jane Smith (needs review/approval)\n\nMembers Seeking Help:\n❓ John: "Need feedback on voting mechanism design"\n❓ Sarah: "Looking for React developers for frontend"\n❓ Lisa: "Need help with onboarding flow testing"\n\nAvailable to Help:\n✅ Alex Rodriguez - Technical documentation\n✅ Mike Torres - Design review and feedback\n✅ Jane Smith - Process facilitation\n\nSuggested Actions:\n• Reach out to John about governance feedback\n• Connect with Sarah if you have React experience\n• Join Lisa's onboarding testing group`;
}
if (lowerPrompt.includes('network') || lowerPrompt.includes('import') || lowerPrompt.includes('trust graph')) {
return `Great question about building your trust network! Here's how to get started:\n\nBuilding Your Trust Graph:\n📊 Import contacts from Gmail, LinkedIn, or phone to identify existing connections\n🔗 NAO uses your network to verify identity and build reputation\n⭐ Connected users can vouch for your skills and endorsements\n\nNetwork Import Benefits:\n• Faster verification when joining new groups\n• Enhanced credibility through mutual connections\n• Easier discovery of relevant opportunities\n• Stronger security through social recovery\n\nGetting Started:\n1. Go to Account Settings > Network Import\n2. Connect your preferred platforms (Gmail, LinkedIn, Phone)\n3. Review and invite contacts already on NAO\n4. Set privacy preferences for network visibility\n\n🔒 Security Tip: Your network data is encrypted and you control who sees your connections!`;
}
if (lowerPrompt.includes('security') || lowerPrompt.includes('secure') || lowerPrompt.includes('account security')) {
return `Great idea to secure your NAO account! Here's how to get started:\n\nRecommended Security Setup:\n🔐 Enable multi-factor authentication (MFA) with your phone\n🔑 Set up social recovery with 3-5 trusted contacts\n📱 Configure device authentication for biometric access\n🛡 Enable privacy shields for sensitive data\n\nSocial Recovery (Unique to NAO):\n• Choose trusted family/friends as recovery contacts\n• They can help restore access if you lose devices\n• Requires majority approval (3 of 5 contacts)\n• More secure than traditional email recovery\n\nPrivacy Controls:\n• Set different privacy levels per relationship type (rCards)\n• Control what each connection type can see\n• Manage location sharing preferences\n• Configure notification and activity visibility\n\nNext Steps:\n1. Go to Account > Security Settings\n2. Enable MFA and social recovery\n3. Review privacy settings for each rCard type\n4. Set up trusted recovery contacts\n\n💡 Pro tip: Social recovery makes your account more secure AND helps build trust in the NAO network!`;
}
// Default response
return `I'd be happy to help you with ${group?.name || 'this group'}! I can assist with:\n\n• Member Analysis - Who's most active, expertise areas, connection opportunities\n• Content Insights - Popular posts, valuable resources, trending topics\n• Project Status - Current tasks, who needs help, collaboration opportunities\n• Engagement Tips - Best times to post, most engaging content types\n\nTry asking me something specific like:\n• "Who are the technical experts in this group?"\n• "What resources should I check out first?"\n• "Who's working on the governance framework?"\n• "When is the group most active?"`;
};
const handleSendMessage = async () => {
if (!currentInput.trim()) return;
const userPrompt = currentInput.trim();
setCurrentInput('');
setIsTyping(true);
// Add user message immediately
const messageId = `msg_${Date.now()}`;
// Simulate AI thinking time
setTimeout(() => {
const response = getMockAIResponse(userPrompt);
setAiMessages(prev => [...prev, {
id: messageId,
prompt: userPrompt,
response: response,
timestamp: new Date()
}]);
setIsTyping(false);
}, 1500);
}; };
const handleCreatePost = (type: 'post' | 'offer' | 'want', groupId?: string) => { const handleCreatePost = (type: 'post' | 'offer' | 'want', groupId?: string) => {
@ -369,44 +499,91 @@ const GroupDetailPage = () => {
return ( return (
<Box sx={{ height: '100%' }}> <Box sx={{ height: '100%' }}>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}> <Box sx={{ mb: 3 }}>
<IconButton onClick={handleBack} size="large"> {/* Top row with back button, avatar, and title/description */}
<ArrowBack /> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: { xs: 2, md: 0 } }}>
</IconButton> <IconButton onClick={handleBack} size="large">
<Avatar <ArrowBack />
src={group.image} </IconButton>
alt={group.name} <Avatar
sx={{ width: 64, height: 64, bgcolor: 'primary.main' }} src={group.image}
> alt={group.name}
{group.name.charAt(0)} sx={{
</Avatar> width: 64,
<Box sx={{ flexGrow: 1 }}> height: 64,
<Typography variant="h4" component="h1" sx={{ fontWeight: 700 }}> bgcolor: 'white',
{group.name} border: 1,
</Typography> borderColor: 'primary.main',
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}> color: 'primary.main'
<Badge badgeContent={group.memberCount} color="primary"> }}
<People sx={{ fontSize: 20, color: 'text.secondary' }} /> >
</Badge> {group.name.charAt(0)}
<Typography variant="body2" color="text.secondary"> </Avatar>
{group.memberCount} members <Box sx={{ flex: 1, minWidth: 0, pr: { xs: 0, md: 2 } }}>
<Typography variant="h4" component="h1" sx={{ fontWeight: 700 }}>
{group.name}
</Typography> </Typography>
{group.isPrivate && ( <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
<Chip label="Private" size="small" variant="outlined" /> <Badge badgeContent={group.memberCount} color="primary">
<People sx={{ fontSize: 20, color: 'text.secondary' }} />
</Badge>
<Typography variant="body2" color="text.secondary">
{group.memberCount} members
</Typography>
{group.isPrivate && (
<Chip label="Private" size="small" variant="outlined" />
)}
</Box>
{group.description && (
<Typography variant="body1" color="text.secondary" sx={{ mt: 1 }}>
{group.description}
</Typography>
)} )}
</Box> </Box>
{group.description && ( {/* Desktop buttons */}
<Typography variant="body1" color="text.secondary" sx={{ mt: 1 }}> <Box sx={{
{group.description} display: { xs: 'none', md: 'flex' },
</Typography> gap: 1,
)} alignItems: 'flex-start'
}}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => handleStartAIAssistant()}
sx={{ borderRadius: 2 }}
>
AI Assistant
</Button>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleInviteToGroup}
sx={{ borderRadius: 2 }}
>
Invite to Group
</Button>
</Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> {/* Mobile buttons row */}
<Box sx={{
display: { xs: 'flex', md: 'none' },
gap: 1,
justifyContent: 'center',
mt: 2
}}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => handleStartAIAssistant()}
sx={{ borderRadius: 2, flex: 1 }}
>
AI Assistant
</Button>
<Button <Button
variant="contained" variant="contained"
startIcon={<PersonAdd />} startIcon={<PersonAdd />}
onClick={handleInviteToGroup} onClick={handleInviteToGroup}
sx={{ borderRadius: 2 }} sx={{ borderRadius: 2, flex: 1 }}
> >
Invite to Group Invite to Group
</Button> </Button>
@ -453,6 +630,297 @@ const GroupDetailPage = () => {
onCreatePost={handleCreatePost} onCreatePost={handleCreatePost}
/> />
)} )}
{/* Group Tour */}
<GroupTour
open={showGroupTour}
onClose={() => setShowGroupTour(false)}
group={group!}
onStartAIAssistant={handleStartAIAssistant}
/>
{/* AI Group Assistant Dialog */}
<Dialog
open={showAIAssistant}
onClose={() => setShowAIAssistant(false)}
maxWidth={false}
fullWidth
PaperProps={{
sx: {
width: '80vw',
maxWidth: '80vw',
height: '80vh',
maxHeight: '80vh'
}
}}
>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AutoAwesome />
AI Group Assistant - {group?.name}
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0, height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Chat-like interface */}
<Box sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
bgcolor: 'grey.50',
border: '1px solid',
borderColor: 'divider',
minHeight: 0
}}>
<Box sx={{
flex: 1,
p: 3,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 2
}}>
{/* AI intro message */}
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main', width: 32, height: 32 }}>
<AutoAwesome fontSize="small" />
</Avatar>
<Box sx={{
bgcolor: 'background.paper',
p: 2,
borderRadius: 2,
maxWidth: '80%',
boxShadow: 1
}}>
<Typography variant="body2">
{userFirstName ? `Hi ${userFirstName}! ` : 'Hi! '}I'm the AI assistant for <strong>{group?.name}</strong>. I can help you:
<br /> Analyze group engagement and member activity
<br /> Suggest relevant content and resources
<br /> Connect with members
<br /><br />
What would you like to explore for this group?
{initialPrompt && (
<>
<br /><br />
<strong>Suggested prompt:</strong> "{initialPrompt}"
</>
)}
</Typography>
</Box>
</Box>
{/* AI Messages */}
{aiMessages.map((message) => (
<Box key={message.id}>
{/* User message */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1 }}>
<Box sx={{
bgcolor: 'primary.main',
color: 'white',
p: 2,
borderRadius: 2,
maxWidth: '80%'
}}>
<Typography variant="body2">{message.prompt}</Typography>
</Box>
</Box>
{/* AI response */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main', width: 32, height: 32 }}>
<AutoAwesome fontSize="small" />
</Avatar>
<Box sx={{
bgcolor: 'background.paper',
p: 2,
borderRadius: 2,
maxWidth: '80%',
boxShadow: 1
}}>
<Typography variant="body2" sx={{ whiteSpace: 'pre-line' }}>
{message.response}
</Typography>
</Box>
</Box>
{/* Rating component */}
<AIResponseRatingComponent
responseId={message.id}
onRatingSubmit={handleAIRatingSubmit}
/>
</Box>
))}
{/* Helpful action buttons after first response */}
{aiMessages.length > 0 && !isTyping && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center', py: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic', textAlign: 'center' }}>
💡 Quick actions to get the most out of NAO:
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', justifyContent: 'center' }}>
<Button
size="small"
variant="outlined"
onClick={() => {
const prompt = 'How do I import my existing network to build my trust graph?';
setCurrentInput('');
if (!isTyping) {
setIsTyping(true);
const messageId = `msg_${Date.now()}`;
setTimeout(() => {
const response = getMockAIResponse(prompt);
setAiMessages(prev => [...prev, {
id: messageId,
prompt: prompt,
response: response,
timestamp: new Date()
}]);
setIsTyping(false);
}, 1500);
}
}}
sx={{ borderRadius: 2, textTransform: 'none' }}
>
Import Network
</Button>
<Button
size="small"
variant="outlined"
onClick={() => {
const prompt = 'How do I set up additional account security?';
setCurrentInput('');
if (!isTyping) {
setIsTyping(true);
const messageId = `msg_${Date.now()}`;
setTimeout(() => {
const response = getMockAIResponse(prompt);
setAiMessages(prev => [...prev, {
id: messageId,
prompt: prompt,
response: response,
timestamp: new Date()
}]);
setIsTyping(false);
}, 1500);
}
}}
sx={{ borderRadius: 2, textTransform: 'none' }}
>
Account Security
</Button>
<Button
size="small"
variant="outlined"
onClick={() => {
const prompt = 'What are the key features of the NAO platform?';
setCurrentInput('');
if (!isTyping) {
setIsTyping(true);
const messageId = `msg_${Date.now()}`;
setTimeout(() => {
const response = getMockAIResponse(prompt);
setAiMessages(prev => [...prev, {
id: messageId,
prompt: prompt,
response: response,
timestamp: new Date()
}]);
setIsTyping(false);
}, 1500);
}
}}
sx={{ borderRadius: 2, textTransform: 'none' }}
>
Platform Features
</Button>
</Box>
</Box>
)}
{/* Typing indicator */}
{isTyping && (
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main', width: 32, height: 32 }}>
<AutoAwesome fontSize="small" />
</Avatar>
<Box sx={{
bgcolor: 'background.paper',
p: 2,
borderRadius: 2,
boxShadow: 1
}}>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Analyzing group data...
</Typography>
</Box>
</Box>
)}
</Box>
{/* Input area */}
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider', bgcolor: 'background.paper' }}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<TextField
fullWidth
multiline
maxRows={4}
placeholder={`Ask about ${group?.name || 'this group'}, get suggestions, or plan activities...`}
variant="outlined"
size="small"
value={currentInput}
onChange={(e) => setCurrentInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
}
}}
/>
<Button
variant="contained"
onClick={handleSendMessage}
disabled={!currentInput.trim() || isTyping}
sx={{
minWidth: 'auto',
px: 3,
py: 1.5,
borderRadius: 3,
background: 'linear-gradient(45deg, #2196F3 30%, #21CBF3 90%)',
boxShadow: '0 3px 5px 2px rgba(33, 203, 243, .3)',
'&:hover': {
background: 'linear-gradient(45deg, #1976D2 30%, #1CB5E0 90%)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 8px 2px rgba(33, 203, 243, .4)',
},
'&:disabled': {
background: '#ccc',
boxShadow: 'none',
},
transition: 'all 0.2s ease'
}}
>
{isTyping ? 'Thinking...' : 'Send'}
</Button>
</Box>
</Box>
</Box>
</DialogContent>
</Dialog>
{/* Invite Form */}
{group && (
<InviteForm
open={showInviteForm}
onClose={() => setShowInviteForm(false)}
onSubmit={handleInviteSubmit}
group={group}
/>
)}
</Box> </Box>
); );
}; };

@ -163,7 +163,14 @@ const GroupPage = () => {
<Avatar <Avatar
src={group.image} src={group.image}
alt={group.name} alt={group.name}
sx={{ width: 48, height: 48, bgcolor: 'primary.main' }} sx={{
width: 48,
height: 48,
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
color: 'primary.main'
}}
> >
<Group /> <Group />
</Avatar> </Avatar>
@ -233,6 +240,7 @@ const GroupPage = () => {
)} )}
</Box> </Box>
</Card> </Card>
</Box> </Box>
); );
}; };

@ -6,8 +6,6 @@ import {
Box, Box,
Paper, Paper,
Button, Button,
Card,
CardContent,
Grid, Grid,
Divider, Divider,
IconButton, IconButton,
@ -35,6 +33,11 @@ import type { Group } from '../types/group';
const InvitationPage = () => { const InvitationPage = () => {
const [invitationUrl, setInvitationUrl] = useState(''); const [invitationUrl, setInvitationUrl] = useState('');
const [personalizedInvite, setPersonalizedInvite] = useState<{
inviteeName?: string;
inviterName?: string;
relationshipType?: string;
}>({});
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const [invitationId, setInvitationId] = useState(''); const [invitationId, setInvitationId] = useState('');
const [group, setGroup] = useState<Group | null>(null); const [group, setGroup] = useState<Group | null>(null);
@ -46,6 +49,17 @@ const InvitationPage = () => {
const loadGroupAndGenerateInvitation = async () => { const loadGroupAndGenerateInvitation = async () => {
const groupId = searchParams.get('groupId'); const groupId = searchParams.get('groupId');
// Extract personalized invite data
const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName');
const relationshipType = searchParams.get('relationshipType');
setPersonalizedInvite({
inviteeName: inviteeName || undefined,
inviterName: inviterName || undefined,
relationshipType: relationshipType || undefined,
});
if (groupId) { if (groupId) {
setIsGroupInvite(true); setIsGroupInvite(true);
try { try {
@ -58,9 +72,17 @@ const InvitationPage = () => {
const id = Math.random().toString(36).substring(2, 15); const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id); setInvitationId(id);
const url = groupId
? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}` // Build URL with personalized parameters
: `${window.location.origin}/onboarding?invite=${id}`; const urlParams = new URLSearchParams({
invite: id,
...(groupId && { groupId }),
...(inviteeName && { inviteeName }),
...(inviterName && { inviterName }),
...(relationshipType && { relationshipType }),
});
const url = `${window.location.origin}/onboarding?${urlParams.toString()}`;
setInvitationUrl(url); setInvitationUrl(url);
}; };
@ -79,10 +101,13 @@ const InvitationPage = () => {
const handleShare = async () => { const handleShare = async () => {
if (navigator.share) { if (navigator.share) {
try { try {
const title = isGroupInvite ? `Join ${group?.name}` : 'Join My Network'; const inviterName = personalizedInvite.inviterName || 'Oli S-B';
const title = isGroupInvite ? `Join ${group?.name}` : `Join ${inviterName}'s Network`;
const text = isGroupInvite const text = isGroupInvite
? `Join our ${group?.name} group to collaborate and stay connected!` ? (personalizedInvite.inviteeName
: 'Join my personal network to stay connected!'; ? `Hi ${personalizedInvite.inviteeName}, I'd like to invite you to join the ${group?.name} Group on the NAO network!`
: `I'd like to invite you to join the ${group?.name} Group on the NAO network - collaborate and stay connected!`)
: `I'd like to invite you to join my personal network!`;
await navigator.share({ await navigator.share({
title, title,
@ -98,26 +123,39 @@ const InvitationPage = () => {
}; };
const handleEmailShare = () => { const handleEmailShare = () => {
const inviterName = personalizedInvite.inviterName || 'Oli S-B';
const inviteeName = personalizedInvite.inviteeName;
const subject = isGroupInvite const subject = isGroupInvite
? encodeURIComponent(`Join ${group?.name}`) ? encodeURIComponent(`Join me in the ${group?.name} Group`)
: encodeURIComponent('Join My Network'); : encodeURIComponent(`Join my network on NAO`);
const greeting = inviteeName ? `Hi ${inviteeName},\n\n` : 'Hi!\n\n';
const body = isGroupInvite const body = isGroupInvite
? encodeURIComponent(`I'd like to invite you to join our ${group?.name} group. Please use this link to join: ${invitationUrl}`) ? encodeURIComponent(`${greeting}I'd like to invite you to join the ${group?.name} Group on the NAO network.\n\nClick here to join: ${invitationUrl}\n\nLooking forward to connecting!`)
: encodeURIComponent(`I'd like to add you to my personal network. Please use this link to join: ${invitationUrl}`); : encodeURIComponent(`${greeting}I'd like to add you to my personal network.\n\nClick here to join: ${invitationUrl}`);
window.open(`mailto:?subject=${subject}&body=${body}`); window.open(`mailto:?subject=${subject}&body=${body}`);
}; };
const handleWhatsAppShare = () => { const handleWhatsAppShare = () => {
const inviterName = personalizedInvite.inviterName || 'Oli S-B';
const inviteeName = personalizedInvite.inviteeName;
const greeting = inviteeName ? `Hi ${inviteeName}! ` : 'Hi! ';
const text = isGroupInvite const text = isGroupInvite
? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`) ? encodeURIComponent(`${greeting}I'd like to invite you to join the ${group?.name} Group on the NAO network. Join here: ${invitationUrl}`)
: encodeURIComponent(`Join my personal network: ${invitationUrl}`); : encodeURIComponent(`${greeting}I'd like to invite you to join my network: ${invitationUrl}`);
window.open(`https://wa.me/?text=${text}`); window.open(`https://wa.me/?text=${text}`);
}; };
const handleSMSShare = () => { const handleSMSShare = () => {
const inviterName = personalizedInvite.inviterName || 'Oli S-B';
const inviteeName = personalizedInvite.inviteeName;
const greeting = inviteeName ? `Hi ${inviteeName}! ` : 'Hi! ';
const text = isGroupInvite const text = isGroupInvite
? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`) ? encodeURIComponent(`${greeting}I'd like to invite you to join the ${group?.name} Group on the NAO network. Join: ${invitationUrl}`)
: encodeURIComponent(`Join my personal network: ${invitationUrl}`); : encodeURIComponent(`${greeting}I'd like to invite you to join my network: ${invitationUrl}`);
window.open(`sms:?body=${text}`); window.open(`sms:?body=${text}`);
}; };
@ -147,21 +185,30 @@ const InvitationPage = () => {
const handleNewInvitation = () => { const handleNewInvitation = () => {
const groupId = searchParams.get('groupId'); const groupId = searchParams.get('groupId');
const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName');
const relationshipType = searchParams.get('relationshipType');
const id = Math.random().toString(36).substring(2, 15); const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id); setInvitationId(id);
const url = groupId
? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}` // Build URL with personalized parameters
: `${window.location.origin}/onboarding?invite=${id}`; const urlParams = new URLSearchParams({
invite: id,
...(groupId && { groupId }),
...(inviteeName && { inviteeName }),
...(inviterName && { inviterName }),
...(relationshipType && { relationshipType }),
});
const url = `${window.location.origin}/onboarding?${urlParams.toString()}`;
setInvitationUrl(url); setInvitationUrl(url);
}; };
const handleGoToContacts = () => {
navigate('/contacts');
};
const handleBack = () => { const handleBack = () => {
if (isGroupInvite && group) { if (isGroupInvite && group) {
navigate(`/groups/${group.id}`); navigate(`/groups/${group.id}?newMember=true&fromInvite=true`);
} else { } else {
navigate('/contacts'); navigate('/contacts');
} }
@ -186,7 +233,14 @@ const InvitationPage = () => {
<Avatar <Avatar
src={group.image} src={group.image}
alt={group.name} alt={group.name}
sx={{ width: 64, height: 64, bgcolor: 'primary.main' }} sx={{
width: 64,
height: 64,
bgcolor: 'white',
border: 1,
borderColor: 'primary.main',
color: 'primary.main'
}}
> >
<Groups /> <Groups />
</Avatar> </Avatar>
@ -203,21 +257,27 @@ const InvitationPage = () => {
{!isGroupInvite && ( {!isGroupInvite && (
<Typography variant="h3" component="h1" gutterBottom> <Typography variant="h3" component="h1" gutterBottom>
Invite to Your Network {personalizedInvite.inviteeName
? `Invite ${personalizedInvite.inviteeName} to Your Network`
: 'Invite to Your Network'
}
</Typography>
)}
{isGroupInvite && (
<Typography variant="h3" component="h1" gutterBottom>
{personalizedInvite.inviteeName
? `Invite ${personalizedInvite.inviteeName} to ${group?.name}`
: `Invite to ${group?.name}`
}
</Typography> </Typography>
)} )}
<Typography variant="h6" color="text.secondary">
{isGroupInvite
? `Share your ${group?.name} group invitation`
: 'Share your personal network invitation'
}
</Typography>
</Box> </Box>
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Paper sx={{ p: 3, textAlign: 'center' }}> <Paper sx={{ p: 3, textAlign: 'center', height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
QR Code QR Code
</Typography> </Typography>
@ -260,7 +320,7 @@ const InvitationPage = () => {
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Share Link Share Link
</Typography> </Typography>
@ -332,49 +392,6 @@ const InvitationPage = () => {
</Grid> </Grid>
</Grid> </Grid>
<Card sx={{ mt: 4 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
How it works
</Typography>
<Typography variant="body1" paragraph>
{isGroupInvite
? `When someone scans your QR code or clicks your invitation link, they'll be guided through a simple process to join ${group?.name}:`
: "When someone scans your QR code or clicks your invitation link, they'll be guided through a simple onboarding process:"
}
</Typography>
<Box component="ol" sx={{ pl: 2 }}>
<li>
<Typography variant="body2">
They'll enter their basic information (name, email, etc.)
</Typography>
</li>
<li>
<Typography variant="body2">
They can connect their social accounts for easier contact sharing
</Typography>
</li>
<li>
<Typography variant="body2">
{isGroupInvite
? `They'll be added to ${group?.name} and your network automatically`
: "They'll be added to your network automatically"
}
</Typography>
</li>
</Box>
</CardContent>
</Card>
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Button
variant="contained"
onClick={handleGoToContacts}
size="large"
>
View My Contacts
</Button>
</Box>
<Snackbar <Snackbar
open={copySuccess} open={copySuccess}

@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { import {
Typography, Typography,
@ -14,18 +14,51 @@ import { ArrowBack, ArrowForward } from '@mui/icons-material';
import { useOnboarding } from '../context/OnboardingContext'; import { useOnboarding } from '../context/OnboardingContext';
import BasicInfoStep from '../components/onboarding/BasicInfoStep'; import BasicInfoStep from '../components/onboarding/BasicInfoStep';
import ConnectAccountsStep from '../components/onboarding/ConnectAccountsStep'; import ConnectAccountsStep from '../components/onboarding/ConnectAccountsStep';
import { dataService } from '../services/dataService';
import type { Group } from '../types/group';
const OnboardingPage = () => { const OnboardingPage = () => {
const { state, nextStep, prevStep, completeOnboarding } = useOnboarding(); const { state, nextStep, prevStep, completeOnboarding } = useOnboarding();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const inviteId = searchParams.get('invite'); const inviteId = searchParams.get('invite');
const groupId = searchParams.get('groupId');
const [group, setGroup] = useState<Group | null>(null);
const [inviteData, setInviteData] = useState<{
inviteeName?: string;
inviterName?: string;
relationshipType?: string;
}>({});
useEffect(() => { useEffect(() => {
if (inviteId) { const loadInviteData = async () => {
console.log('Onboarding with invitation:', inviteId); if (inviteId) {
} console.log('Onboarding with invitation:', inviteId);
}, [inviteId]);
// Extract invite data from URL parameters that would be passed from the invitation link
const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName') || 'Oli S-B';
const relationshipType = searchParams.get('relationshipType');
setInviteData({
inviteeName: inviteeName || undefined,
inviterName,
relationshipType: relationshipType || undefined,
});
}
if (groupId) {
try {
const groupData = await dataService.getGroup(groupId);
setGroup(groupData || null);
} catch (error) {
console.error('Failed to load group data:', error);
}
}
};
loadInviteData();
}, [inviteId, groupId, searchParams]);
const steps = [ const steps = [
{ {
@ -67,17 +100,19 @@ const OnboardingPage = () => {
return ( return (
<Box sx={{ height: '100%', maxWidth: 'md', mx: 'auto' }}> <Box sx={{ height: '100%', maxWidth: 'md', mx: 'auto' }}>
<Box sx={{ textAlign: 'center', mb: 4 }}> <Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom> <Typography variant="h3" component="h1" gutterBottom sx={{ whiteSpace: 'pre-line' }}>
Welcome to Your Network {inviteData.inviteeName && inviteData.inviterName && group
? `Welcome ${inviteData.inviteeName},\n${inviteData.inviterName} is inviting you to the ${group.name} Group,\npart of the NAO Network`
: inviteData.inviterName && group
? `Welcome,\n${inviteData.inviterName} is inviting you to the ${group.name} Group,\npart of the NAO Network`
: group
? `Welcome to the ${group.name} Group`
: 'Welcome to Your Network'
}
</Typography> </Typography>
<Typography variant="h6" color="text.secondary"> <Typography variant="h6" color="text.secondary">
Let's get you set up in just a few steps Let's get you set up in just a few steps
</Typography> </Typography>
{inviteId && (
<Typography variant="body2" color="primary" sx={{ mt: 1 }}>
Joining via invitation: {inviteId}
</Typography>
)}
</Box> </Box>
<Paper sx={{ p: { xs: 2, md: 3 }, mb: 3 }}> <Paper sx={{ p: { xs: 2, md: 3 }, mb: 3 }}>

@ -35,6 +35,11 @@ const SocialContractPage = () => {
const [group, setGroup] = useState<Group | null>(null); const [group, setGroup] = useState<Group | null>(null);
const [isGroupInvite, setIsGroupInvite] = useState(false); const [isGroupInvite, setIsGroupInvite] = useState(false);
const [showMoreInfo, setShowMoreInfo] = useState(false); const [showMoreInfo, setShowMoreInfo] = useState(false);
const [inviteData, setInviteData] = useState<{
inviteeName?: string;
inviterName?: string;
relationshipType?: string;
}>({});
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const theme = useTheme(); const theme = useTheme();
@ -44,6 +49,17 @@ const SocialContractPage = () => {
const groupId = searchParams.get('groupId'); const groupId = searchParams.get('groupId');
const inviteId = searchParams.get('invite'); const inviteId = searchParams.get('invite');
// Extract invite personalization data
const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName') || (inviteId ? 'Oli S-B' : undefined);
const relationshipType = searchParams.get('relationshipType');
setInviteData({
inviteeName: inviteeName || undefined,
inviterName,
relationshipType: relationshipType || undefined,
});
if (groupId) { if (groupId) {
setIsGroupInvite(true); setIsGroupInvite(true);
try { try {
@ -72,7 +88,12 @@ const SocialContractPage = () => {
// Navigate directly to the appropriate page // Navigate directly to the appropriate page
if (isGroupInvite && group) { if (isGroupInvite && group) {
navigate(`/groups/${group.id}`); const params = new URLSearchParams({
newMember: 'true',
fromInvite: 'true',
...(inviteData.inviteeName && { firstName: inviteData.inviteeName })
});
navigate(`/groups/${group.id}?${params.toString()}`);
} else { } else {
navigate('/contacts'); navigate('/contacts');
} }
@ -129,8 +150,15 @@ const SocialContractPage = () => {
> >
{/* Header */} {/* Header */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom> <Typography variant="h3" component="h1" gutterBottom sx={{ whiteSpace: 'pre-line' }}>
Welcome to NAO {inviteData.inviteeName && inviteData.inviterName && group
? `Welcome ${inviteData.inviteeName},\n${inviteData.inviterName} is inviting you to the ${group.name} Group,\npart of the NAO Network`
: inviteData.inviterName && group
? `Welcome,\n${inviteData.inviterName} is inviting you to the ${group.name} Group,\npart of the NAO Network`
: group
? `Welcome to the ${group.name} Group`
: 'Welcome to NAO'
}
</Typography> </Typography>
<Typography variant="h5" color="primary" gutterBottom> <Typography variant="h5" color="primary" gutterBottom>

Loading…
Cancel
Save