From 40f66144fcbea043bc4f54d98edb52bab482f6e8 Mon Sep 17 00:00:00 2001 From: Claude Code Assistant Date: Tue, 22 Jul 2025 16:04:53 +0100 Subject: [PATCH] Implement AI assistant enhancements and invitation system improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## New Features - **Animated Morpho Butterfly**: Vector-based butterfly with realistic wing flapping animation - **Personalized AI Assistant**: Welcomes users by first name from invitation flow - **Post-inquiry Action Buttons**: Interactive buttons for network import, security setup, and platform features - **Enhanced Invitation System**: Complete invite form with relationship type selection - **Improved Invitation Messages**: First-person language and simplified security guidance ## UI/UX Improvements - **80% Dialog Size**: AI assistant now uses 80% of viewport for better usability - **Mobile-Responsive Layout**: Action buttons stack properly on mobile group pages - **Optimized Input Position**: AI input field moved to bottom for more conversation space - **Visual Consistency**: White backgrounds with blue borders for group avatars ## Technical Enhancements - **NAOG1 Group**: Added as first group with custom butterfly logo and governance description - **Parameter Passing**: Fixed invitation flow to properly pass user first names - **TypeScript Fixes**: Resolved all build errors across components - **Component Architecture**: New reusable components for AI rating, invite forms, and group tours ## Files Changed - Created: AnimatedMorphoButterfly, AIResponseRating, InviteForm, GroupTour components - Enhanced: GroupDetailPage with full AI assistant integration and mobile layout - Updated: SocialContractPage, OnboardingPage, InvitationPage with personalized messaging - Added: NAOG1 group data and custom butterfly logo SVG 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- butterfly-simple-test.html | 193 +++++++ butterfly-wing-test.html | 275 +++++++++ public/groups.json | 13 + public/naog1-butterfly-logo.svg | 127 ++++ src/components/account/MyCollectionPage.tsx | 1 + src/components/ai/AIResponseRating.tsx | 249 ++++++++ src/components/invite/InviteForm.tsx | 226 ++++++++ src/components/tour/GroupTour.tsx | 330 +++++++++++ src/components/ui/AnimatedMorphoButterfly.tsx | 297 ++++++++++ src/pages/GroupDetailPage.tsx | 542 ++++++++++++++++-- src/pages/GroupPage.tsx | 10 +- src/pages/InvitationPage.tsx | 169 +++--- src/pages/OnboardingPage.tsx | 59 +- src/pages/SocialContractPage.tsx | 34 +- 14 files changed, 2396 insertions(+), 129 deletions(-) create mode 100644 butterfly-simple-test.html create mode 100644 butterfly-wing-test.html create mode 100644 public/naog1-butterfly-logo.svg create mode 100644 src/components/ai/AIResponseRating.tsx create mode 100644 src/components/invite/InviteForm.tsx create mode 100644 src/components/tour/GroupTour.tsx create mode 100644 src/components/ui/AnimatedMorphoButterfly.tsx diff --git a/butterfly-simple-test.html b/butterfly-simple-test.html new file mode 100644 index 0000000..12f1060 --- /dev/null +++ b/butterfly-simple-test.html @@ -0,0 +1,193 @@ + + + + + + Simple Blue Butterfly - Matching Reference Frames + + + +
+

🦋 Simple Blue Butterfly

+

Replicating the reference frame animation

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

Wing Flapping Speed:

+ + + +
+ +

Simple up/down wing flapping motion

+
+ + + + \ No newline at end of file diff --git a/butterfly-wing-test.html b/butterfly-wing-test.html new file mode 100644 index 0000000..d6f06ae --- /dev/null +++ b/butterfly-wing-test.html @@ -0,0 +1,275 @@ + + + + + + Butterfly Wing Flapping Test + + + +
+

🦋 Butterfly Wing Flapping Test

+

Testing different wing flapping animations with 3D rotation

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+

Wing Flapping Speed:

+ + + +
+ +

Watch the wings! They should flap up and down with 3D rotation.

+
+ + + + \ No newline at end of file diff --git a/public/groups.json b/public/groups.json index 51b69a9..3688723 100644 --- a/public/groups.json +++ b/public/groups.json @@ -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", "name": "React Developers", diff --git a/public/naog1-butterfly-logo.svg b/public/naog1-butterfly-logo.svg new file mode 100644 index 0000000..9c21529 --- /dev/null +++ b/public/naog1-butterfly-logo.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NAOG1 + + \ No newline at end of file diff --git a/src/components/account/MyCollectionPage.tsx b/src/components/account/MyCollectionPage.tsx index e811815..e31b207 100644 --- a/src/components/account/MyCollectionPage.tsx +++ b/src/components/account/MyCollectionPage.tsx @@ -621,6 +621,7 @@ const MyCollectionPage = ({}: MyCollectionPageProps) => { )} + {/* Query Dialog */} setShowQueryDialog(false)} maxWidth="md" fullWidth> diff --git a/src/components/ai/AIResponseRating.tsx b/src/components/ai/AIResponseRating.tsx new file mode 100644 index 0000000..c09760e --- /dev/null +++ b/src/components/ai/AIResponseRating.tsx @@ -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 = ({ + responseId, + onRatingSubmit, + existingRating, +}) => { + const [rating, setRating] = useState(existingRating?.rating || 0); + const [feedback, setFeedback] = useState(existingRating?.feedback || ''); + const [helpfulVote, setHelpfulVote] = useState<'helpful' | 'not-helpful' | undefined>( + existingRating?.helpfulVote + ); + const [selectedCategories, setSelectedCategories] = useState( + 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 ( + + + + + + Thank you for rating this response! + + + + + + ); + } + + return ( + + {/* Quick Rating Buttons */} + {!showDetailedRating && !hasSubmitted && ( + + + Was this response helpful? + + + + + + + + + + )} + + {/* Detailed Rating Panel */} + + + + Rate This Response + setShowDetailedRating(false)} + > + + + + + {/* Star Rating */} + + + Overall Rating + + setRating(newValue || 0)} + size="large" + /> + + + {/* Categories */} + + + What made this response good? (optional) + + + {ratingCategories.map((category) => ( + handleCategoryToggle(category.id)} + size="small" + /> + ))} + + + + {/* Feedback */} + + setFeedback(e.target.value)} + variant="outlined" + size="small" + /> + + + {/* Submit Button */} + + + + + + + + ); +}; + +export default AIResponseRatingComponent; \ No newline at end of file diff --git a/src/components/invite/InviteForm.tsx b/src/components/invite/InviteForm.tsx new file mode 100644 index 0000000..b3314d5 --- /dev/null +++ b/src/components/invite/InviteForm.tsx @@ -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 = ({ + open, + onClose, + onSubmit, + group +}) => { + const theme = useTheme(); + const [formData, setFormData] = useState({ + inviteeName: '', + inviteeEmail: '', + relationshipType: '', + inviterName: 'Oli S-B', // Current user + }); + const [selectedRelationship, setSelectedRelationship] = useState(''); + + const getRCardIcon = (iconName: string) => { + const iconMap: Record = { + Business: , + PersonOutline: , + Groups: , + FamilyRestroom: , + Favorite: , + Home: , + }; + return iconMap[iconName] || ; + }; + + 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 ( + + + + + + Invite Someone to {group.name} + + + + + + {/* Basic Info */} + + + Who are you inviting? + + + + setFormData(prev => ({ + ...prev, + inviteeName: e.target.value + }))} + required + /> + setFormData(prev => ({ + ...prev, + inviteeEmail: e.target.value + }))} + required + /> + + + + {/* Relationship Selection */} + + + What's your relationship with them? + + + + {DEFAULT_RCARDS.map((rCard) => ( + + ))} + + + + + + + + + + + ); +}; + +export default InviteForm; \ No newline at end of file diff --git a/src/components/tour/GroupTour.tsx b/src/components/tour/GroupTour.tsx new file mode 100644 index 0000000..a741914 --- /dev/null +++ b/src/components/tour/GroupTour.tsx @@ -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 = ({ + 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: , + }, + { + 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: , + 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: , + target: 'members-tab', + }, + { + title: 'Group Chat', + description: 'Real-time messaging with the entire group. Perfect for quick discussions and staying connected.', + icon: , + target: 'chat-tab', + }, + { + title: 'Collaborative Files', + description: 'Share documents, spreadsheets, and other files with the group. Work together on projects in real-time.', + icon: , + target: 'files-tab', + }, + { + title: 'Useful Links', + description: 'Important resources, websites, and references shared by group members. Bookmark and discover valuable content.', + icon: , + 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: , + }, + ]; + + // 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 = () => ( + + + {tourSteps[currentStep].icon} + + + {tourSteps[currentStep].title} + + + {tourSteps[currentStep].description} + + + {/* Progress indicator */} + + {tourSteps.map((_, index) => ( + + + + ))} + + + ); + + const renderPopularPrompts = () => ( + + + + + Try the AI Assistant! + + + Here are some popular questions other members have asked the AI assistant. + These prompts received high ratings from the community: + + + + + {popularPrompts.map((prompt) => ( + + handleUsePrompt(prompt.prompt)} + > + + + + + + ({prompt.responseCount}) + + + + + "{prompt.prompt}" + + + + + {prompt.averageRating.toFixed(1)}/5.0 average rating • Click to try this prompt + + + + + ))} + + + + + + You can also ask your own questions! The AI assistant knows about group members, + recent activity, shared files, and can help you get oriented. + + + + ); + + return ( + + + + + + + + {showPopularPrompts ? 'AI Assistant Examples' : 'Group Tour'} + + + + + + {showPopularPrompts ? renderPopularPrompts() : renderTourStep()} + + + + {!showPopularPrompts ? ( + <> + + + + + + + ) : ( + <> + + + + )} + + + ); +}; + +export default GroupTour; \ No newline at end of file diff --git a/src/components/ui/AnimatedMorphoButterfly.tsx b/src/components/ui/AnimatedMorphoButterfly.tsx new file mode 100644 index 0000000..5e83bd4 --- /dev/null +++ b/src/components/ui/AnimatedMorphoButterfly.tsx @@ -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 = ({ + size = 48, + className, + variant = 'static' +}) => { + return ( + + + {/* Wing gradients */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Left wings */} + + {/* Left upper wing */} + + + {/* Left lower wing */} + + + {/* Wing spots/patterns */} + + + + + + {/* Right wings */} + + {/* Right upper wing */} + + + {/* Right lower wing */} + + + {/* Wing spots/patterns */} + + + + + + {/* Butterfly body */} + + + {/* Head */} + + + {/* Antennae */} + + + + {/* Antennae tips */} + + + + {/* Eyes */} + + + + + + + ); +}; + +export default AnimatedMorphoButterfly; \ No newline at end of file diff --git a/src/pages/GroupDetailPage.tsx b/src/pages/GroupDetailPage.tsx index fa2f2b1..b43c670 100644 --- a/src/pages/GroupDetailPage.tsx +++ b/src/pages/GroupDetailPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { Typography, Box, @@ -13,7 +13,11 @@ import { Chip, Badge, alpha, - useTheme + useTheme, + Dialog, + DialogTitle, + DialogContent, + TextField } from '@mui/material'; import { ArrowBack, @@ -28,15 +32,20 @@ import { MoreVert, Description, TableChart, - PersonAdd + PersonAdd, + AutoAwesome } from '@mui/icons-material'; import { dataService } from '../services/dataService'; import type { Group, GroupPost, GroupLink } from '../types/group'; 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 { groupId } = useParams<{ groupId: string }>(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const theme = useTheme(); const [group, setGroup] = useState(null); @@ -44,6 +53,14 @@ const GroupDetailPage = () => { const [links, setLinks] = useState([]); const [tabValue, setTabValue] = useState(0); const [isLoading, setIsLoading] = useState(true); + const [showAIAssistant, setShowAIAssistant] = useState(false); + const [showGroupTour, setShowGroupTour] = useState(false); + const [initialPrompt, setInitialPrompt] = useState(); + const [aiMessages, setAiMessages] = useState>([]); + const [currentInput, setCurrentInput] = useState(''); + const [isTyping, setIsTyping] = useState(false); + const [showInviteForm, setShowInviteForm] = useState(false); + const [userFirstName, setUserFirstName] = useState(); useEffect(() => { const loadGroupData = async () => { @@ -54,6 +71,34 @@ const GroupDetailPage = () => { const groupData = await dataService.getGroup(groupId); 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 const mockPosts: GroupPost[] = [ { @@ -108,6 +153,13 @@ const GroupDetailPage = () => { loadGroupData(); }, [groupId]); + // Set initial prompt when provided + useEffect(() => { + if (initialPrompt && showAIAssistant) { + setCurrentInput(initialPrompt); + } + }, [initialPrompt, showAIAssistant]); + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setTabValue(newValue); }; @@ -117,10 +169,88 @@ const GroupDetailPage = () => { }; const handleInviteToGroup = () => { - // TODO: Implement invite to group functionality - console.log('Inviting to group:', group?.name); - // This could navigate to an invite page or open a modal - navigate(`/invite?groupId=${groupId}`); + setShowInviteForm(true); + }; + + 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) => { @@ -369,44 +499,91 @@ const GroupDetailPage = () => { return ( {/* Header */} - - - - - - {group.name.charAt(0)} - - - - {group.name} - - - - - - - {group.memberCount} members + + {/* Top row with back button, avatar, and title/description */} + + + + + + {group.name.charAt(0)} + + + + {group.name} - {group.isPrivate && ( - + + + + + + {group.memberCount} members + + {group.isPrivate && ( + + )} + + {group.description && ( + + {group.description} + )} - {group.description && ( - - {group.description} - - )} + {/* Desktop buttons */} + + + + - + {/* Mobile buttons row */} + + @@ -453,6 +630,297 @@ const GroupDetailPage = () => { onCreatePost={handleCreatePost} /> )} + + + {/* Group Tour */} + setShowGroupTour(false)} + group={group!} + onStartAIAssistant={handleStartAIAssistant} + /> + + {/* AI Group Assistant Dialog */} + setShowAIAssistant(false)} + maxWidth={false} + fullWidth + PaperProps={{ + sx: { + width: '80vw', + maxWidth: '80vw', + height: '80vh', + maxHeight: '80vh' + } + }} + > + + + + AI Group Assistant - {group?.name} + + + + {/* Chat-like interface */} + + + {/* AI intro message */} + + + + + + + {userFirstName ? `Hi ${userFirstName}! ` : 'Hi! '}I'm the AI assistant for {group?.name}. I can help you: +
• Analyze group engagement and member activity +
• Suggest relevant content and resources +
• Connect with members +

+ What would you like to explore for this group? + {initialPrompt && ( + <> +

+ Suggested prompt: "{initialPrompt}" + + )} +
+
+
+ + {/* AI Messages */} + {aiMessages.map((message) => ( + + {/* User message */} + + + {message.prompt} + + + + {/* AI response */} + + + + + + + {message.response} + + + + + {/* Rating component */} + + + ))} + + {/* Helpful action buttons after first response */} + {aiMessages.length > 0 && !isTyping && ( + + + 💡 Quick actions to get the most out of NAO: + + + + + + + + )} + + {/* Typing indicator */} + {isTyping && ( + + + + + + + Analyzing group data... + + + + )} +
+ + {/* Input area */} + + + setCurrentInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: 3, + } + }} + /> + + + +
+
+
+ + {/* Invite Form */} + {group && ( + setShowInviteForm(false)} + onSubmit={handleInviteSubmit} + group={group} + /> + )}
); }; diff --git a/src/pages/GroupPage.tsx b/src/pages/GroupPage.tsx index 78b24ec..f35a72d 100644 --- a/src/pages/GroupPage.tsx +++ b/src/pages/GroupPage.tsx @@ -163,7 +163,14 @@ const GroupPage = () => { @@ -233,6 +240,7 @@ const GroupPage = () => { )}
+
); }; diff --git a/src/pages/InvitationPage.tsx b/src/pages/InvitationPage.tsx index 4743568..2021c22 100644 --- a/src/pages/InvitationPage.tsx +++ b/src/pages/InvitationPage.tsx @@ -6,8 +6,6 @@ import { Box, Paper, Button, - Card, - CardContent, Grid, Divider, IconButton, @@ -35,6 +33,11 @@ import type { Group } from '../types/group'; const InvitationPage = () => { const [invitationUrl, setInvitationUrl] = useState(''); + const [personalizedInvite, setPersonalizedInvite] = useState<{ + inviteeName?: string; + inviterName?: string; + relationshipType?: string; + }>({}); const [copySuccess, setCopySuccess] = useState(false); const [invitationId, setInvitationId] = useState(''); const [group, setGroup] = useState(null); @@ -46,6 +49,17 @@ const InvitationPage = () => { const loadGroupAndGenerateInvitation = async () => { 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) { setIsGroupInvite(true); try { @@ -58,9 +72,17 @@ const InvitationPage = () => { const id = Math.random().toString(36).substring(2, 15); setInvitationId(id); - const url = groupId - ? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}` - : `${window.location.origin}/onboarding?invite=${id}`; + + // Build URL with personalized parameters + const urlParams = new URLSearchParams({ + invite: id, + ...(groupId && { groupId }), + ...(inviteeName && { inviteeName }), + ...(inviterName && { inviterName }), + ...(relationshipType && { relationshipType }), + }); + + const url = `${window.location.origin}/onboarding?${urlParams.toString()}`; setInvitationUrl(url); }; @@ -79,10 +101,13 @@ const InvitationPage = () => { const handleShare = async () => { if (navigator.share) { 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 - ? `Join our ${group?.name} group to collaborate and stay connected!` - : 'Join my personal network to stay connected!'; + ? (personalizedInvite.inviteeName + ? `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({ title, @@ -98,26 +123,39 @@ const InvitationPage = () => { }; const handleEmailShare = () => { + const inviterName = personalizedInvite.inviterName || 'Oli S-B'; + const inviteeName = personalizedInvite.inviteeName; + const subject = isGroupInvite - ? encodeURIComponent(`Join ${group?.name}`) - : encodeURIComponent('Join My Network'); + ? encodeURIComponent(`Join me in the ${group?.name} Group`) + : encodeURIComponent(`Join my network on NAO`); + + const greeting = inviteeName ? `Hi ${inviteeName},\n\n` : 'Hi!\n\n'; const body = isGroupInvite - ? encodeURIComponent(`I'd like to invite you to join our ${group?.name} group. Please use this link to join: ${invitationUrl}`) - : encodeURIComponent(`I'd like to add you to my personal network. 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(`${greeting}I'd like to add you to my personal network.\n\nClick here to join: ${invitationUrl}`); window.open(`mailto:?subject=${subject}&body=${body}`); }; const handleWhatsAppShare = () => { + const inviterName = personalizedInvite.inviterName || 'Oli S-B'; + const inviteeName = personalizedInvite.inviteeName; + + const greeting = inviteeName ? `Hi ${inviteeName}! ` : 'Hi! '; const text = isGroupInvite - ? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`) - : encodeURIComponent(`Join my personal network: ${invitationUrl}`); + ? encodeURIComponent(`${greeting}I'd like to invite you to join the ${group?.name} Group on the NAO network. Join here: ${invitationUrl}`) + : encodeURIComponent(`${greeting}I'd like to invite you to join my network: ${invitationUrl}`); window.open(`https://wa.me/?text=${text}`); }; const handleSMSShare = () => { + const inviterName = personalizedInvite.inviterName || 'Oli S-B'; + const inviteeName = personalizedInvite.inviteeName; + + const greeting = inviteeName ? `Hi ${inviteeName}! ` : 'Hi! '; const text = isGroupInvite - ? encodeURIComponent(`Join our ${group?.name} group: ${invitationUrl}`) - : encodeURIComponent(`Join my personal network: ${invitationUrl}`); + ? encodeURIComponent(`${greeting}I'd like to invite you to join the ${group?.name} Group on the NAO network. Join: ${invitationUrl}`) + : encodeURIComponent(`${greeting}I'd like to invite you to join my network: ${invitationUrl}`); window.open(`sms:?body=${text}`); }; @@ -147,21 +185,30 @@ const InvitationPage = () => { const handleNewInvitation = () => { 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); setInvitationId(id); - const url = groupId - ? `${window.location.origin}/onboarding?invite=${id}&groupId=${groupId}` - : `${window.location.origin}/onboarding?invite=${id}`; + + // Build URL with personalized parameters + const urlParams = new URLSearchParams({ + invite: id, + ...(groupId && { groupId }), + ...(inviteeName && { inviteeName }), + ...(inviterName && { inviterName }), + ...(relationshipType && { relationshipType }), + }); + + const url = `${window.location.origin}/onboarding?${urlParams.toString()}`; setInvitationUrl(url); }; - const handleGoToContacts = () => { - navigate('/contacts'); - }; const handleBack = () => { if (isGroupInvite && group) { - navigate(`/groups/${group.id}`); + navigate(`/groups/${group.id}?newMember=true&fromInvite=true`); } else { navigate('/contacts'); } @@ -186,7 +233,14 @@ const InvitationPage = () => { @@ -203,21 +257,27 @@ const InvitationPage = () => { {!isGroupInvite && ( - Invite to Your Network + {personalizedInvite.inviteeName + ? `Invite ${personalizedInvite.inviteeName} to Your Network` + : 'Invite to Your Network' + } + + )} + + {isGroupInvite && ( + + {personalizedInvite.inviteeName + ? `Invite ${personalizedInvite.inviteeName} to ${group?.name}` + : `Invite to ${group?.name}` + } )} - - {isGroupInvite - ? `Share your ${group?.name} group invitation` - : 'Share your personal network invitation' - } -
- + QR Code @@ -260,7 +320,7 @@ const InvitationPage = () => { - + Share Link @@ -332,49 +392,6 @@ const InvitationPage = () => { - - - - How it works - - - {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:" - } - - -
  • - - They'll enter their basic information (name, email, etc.) - -
  • -
  • - - They can connect their social accounts for easier contact sharing - -
  • -
  • - - {isGroupInvite - ? `They'll be added to ${group?.name} and your network automatically` - : "They'll be added to your network automatically" - } - -
  • -
    -
    -
    - - - - { const { state, nextStep, prevStep, completeOnboarding } = useOnboarding(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const inviteId = searchParams.get('invite'); + const groupId = searchParams.get('groupId'); + const [group, setGroup] = useState(null); + const [inviteData, setInviteData] = useState<{ + inviteeName?: string; + inviterName?: string; + relationshipType?: string; + }>({}); useEffect(() => { - if (inviteId) { - console.log('Onboarding with invitation:', inviteId); - } - }, [inviteId]); + const loadInviteData = async () => { + if (inviteId) { + console.log('Onboarding with invitation:', 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 = [ { @@ -67,17 +100,19 @@ const OnboardingPage = () => { return ( - - 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' + } Let's get you set up in just a few steps - {inviteId && ( - - Joining via invitation: {inviteId} - - )} diff --git a/src/pages/SocialContractPage.tsx b/src/pages/SocialContractPage.tsx index 0060b96..1b4226b 100644 --- a/src/pages/SocialContractPage.tsx +++ b/src/pages/SocialContractPage.tsx @@ -35,6 +35,11 @@ const SocialContractPage = () => { const [group, setGroup] = useState(null); const [isGroupInvite, setIsGroupInvite] = useState(false); const [showMoreInfo, setShowMoreInfo] = useState(false); + const [inviteData, setInviteData] = useState<{ + inviteeName?: string; + inviterName?: string; + relationshipType?: string; + }>({}); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const theme = useTheme(); @@ -44,6 +49,17 @@ const SocialContractPage = () => { const groupId = searchParams.get('groupId'); 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) { setIsGroupInvite(true); try { @@ -72,7 +88,12 @@ const SocialContractPage = () => { // Navigate directly to the appropriate page 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 { navigate('/contacts'); } @@ -129,8 +150,15 @@ const SocialContractPage = () => { > {/* Header */} - - 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' + }