import { useState, useCallback } from 'react'; import { useAI, useAuth, useBroadcast, useCollection, useNotifications, usePresence, sendEmail } from '@deplixo/sdk'; import { ProfileSetup } from './components/ProfileSetup.jsx'; import { ExchangeBoard } from './components/ExchangeBoard.jsx'; import { Matches } from './components/Matches.jsx'; import { MyProfile } from './components/MyProfile.jsx'; function App() { const { user, loading, login, logout } = useAuth(); const { items: profiles, loading: profilesLoading, add, update } = useCollection('language_profiles', { personal: true }); const { items: loungeItems, loading: loungeLoading, add: addLounge, update: updateLounge } = useCollection('chat_lounges', { personal: true }); const { items: loungeMessageItems, loading: loungeMessagesLoading, add: addLoungeMessage } = useCollection('chat_lounge_messages', { personal: true }); const { items: introItems, loading: introLoading, add: addIntro, update: updateIntro } = useCollection('match_intros', { personal: true }); const { notifications, unreadCount, send: sendNotification, markRead, loading: notificationsLoading } = useNotifications(); const { generate, loading: aiLoading, error: aiError } = useAI(); const [activeTab, setActiveTab] = useState('board'); const [editMode, setEditMode] = useState(false); const [loungeName, setLoungeName] = useState(''); const [joinedLoungeId, setJoinedLoungeId] = useState(''); const [messageText, setMessageText] = useState(''); const [matchSuggestions, setMatchSuggestions] = useState([]); const [matchLoading, setMatchLoading] = useState(false); const [matchError, setMatchError] = useState(''); const [notifiedMatchIds, setNotifiedMatchIds] = useState([]); const [introModalMatch, setIntroModalMatch] = useState(null); const [introDraft, setIntroDraft] = useState(''); const [introGenerating, setIntroGenerating] = useState(false); const [introSending, setIntroSending] = useState(false); const [introStatusMessage, setIntroStatusMessage] = useState(''); const presence = usePresence( user ? { name: user.name, avatar: user.avatar, status: 'online', } : { name: 'Guest', avatar: '', status: 'offline' } ); const onlineUsers = presence?.users || []; const activeLounge = loungeItems?.find(lounge => lounge.id === joinedLoungeId) || null; const loungeMessages = (loungeMessageItems || []).filter(message => message.loungeId === joinedLoungeId); const sentIntros = introItems || []; const handleLoungeBroadcast = useCallback((data, authorId) => { if (!data?.loungeId || data.loungeId !== joinedLoungeId) return; if (data.type === 'message' || data.type === 'join' || data.type === 'create') { addLoungeMessage({ loungeId: data.loungeId, authorId: data.authorId || authorId || 'system', authorName: data.authorName || 'Anonymous', authorAvatar: data.authorAvatar || '', type: data.type, text: data.text || '', createdAt: data.createdAt || new Date().toISOString(), }); } }, [joinedLoungeId, addLoungeMessage]); const { send: sendLoungeBroadcast } = useBroadcast('lounge-message', handleLoungeBroadcast); const userProfile = profiles?.find(profile => profile.userId === user?.id) || null; const buildIntroPrompt = useCallback((match, profile) => { const myName = user?.name || 'User'; const partnerName = match?.userName || 'your match'; const myNative = profile?.nativeLanguage || 'unspecified'; const myLearning = profile?.learningLanguage || 'unspecified'; const partnerNative = match?.nativeLanguage || 'unspecified'; const partnerLearning = match?.learningLanguage || 'unspecified'; return [ `Write a warm, concise email introduction for two matched language partners.`, `Sender name: ${myName}`, `Recipient name: ${partnerName}`, `Sender native language: ${myNative}`, `Sender learning language: ${myLearning}`, `Recipient native language: ${partnerNative}`, `Recipient learning language: ${partnerLearning}`, `Tone: friendly, encouraging, and practical.`, `Include a short greeting, why they were matched, and a simple invitation to reply and start practicing.`, `Keep it under 180 words.`, ].join('\n'); }, [user?.name]); const handleOpenIntro = useCallback(async match => { setIntroModalMatch(match || null); setIntroStatusMessage(''); setIntroDraft(''); if (!match) return; const existingIntro = sentIntros.find(item => item.matchUserId === match.userId && item.senderId === user?.id); if (existingIntro?.introHtml) { setIntroDraft(existingIntro.introHtml); return; } setIntroGenerating(true); try { const prompt = buildIntroPrompt(match, userProfile); const result = await generate({ system: 'You create short, friendly email introductions for language exchange partners.', user: prompt, json: true, }); const html = typeof result === 'string' ? result : (result?.html || result?.emailHtml || result?.body || ''); setIntroDraft(html || `

Hi ${match?.userName || 'there'}

I’d love to introduce you to a new language exchange partner.

`); } catch (error) { setIntroDraft(`

Hi ${match?.userName || 'there'}

You’ve been matched with a language exchange partner. Reply to start practicing together.

`); } finally { setIntroGenerating(false); } }, [buildIntroPrompt, generate, sentIntros, user?.id, userProfile]); const refreshMatchSuggestions = useCallback(async () => { if (!user || !profiles) return; setMatchLoading(true); setMatchError(''); try { const preferencesSummary = userProfile ? [ `Native language: ${userProfile.nativeLanguage || 'unspecified'}`, `Learning language: ${userProfile.learningLanguage || 'unspecified'}`, `Timezone: ${userProfile.timezone || 'unspecified'}`, `Preferred practice style: ${userProfile.practiceStyle || 'unspecified'}`, `Availability: ${userProfile.availability || 'unspecified'}`, ].join('\n') : 'No profile found yet. Use whatever is available in the user profile data.'; const candidateProfiles = (profiles || []) .filter(profile => profile.userId !== user.id) .map(profile => ({ userId: profile.userId || '', userName: profile.userName || 'Anonymous', nativeLanguage: profile.nativeLanguage || '', learningLanguage: profile.learningLanguage || '', timezone: profile.timezone || '', practiceStyle: profile.practiceStyle || '', availability: profile.availability || '', goals: profile.goals || '', bio: profile.bio || '', avatar: profile.userAvatar || profile.avatar || '', })); const response = await generateMatchSuggestions({ userName: user.name, userPreferences: preferencesSummary, candidates: candidateProfiles, }); const parsed = Array.isArray(response) ? response : (response?.matches || response?.suggestions || []); setMatchSuggestions(parsed.slice(0, 5)); } catch (error) { setMatchError('Unable to generate AI matchmaking suggestions right now.'); } finally { setMatchLoading(false); } }, [profiles, user, userProfile]); const { send: sendMatchRefreshBroadcast } = useBroadcast('match-refresh', async () => {}); useEffect(() => { if (user && activeTab === 'matches' && profiles && !profilesLoading) { refreshMatchSuggestions(); } }, [activeTab, profiles, profilesLoading, refreshMatchSuggestions, user]); useEffect(() => { if (!user || !matchSuggestions.length || !profiles?.length) return; const matchedIds = new Set([ ...(matchSuggestions || []).map(match => match?.userId).filter(Boolean), ...(notifiedMatchIds || []), ]); const targetProfiles = (profiles || []).filter(profile => matchedIds.has(profile.userId)); if (!targetProfiles.length) return; const onlineIds = new Set((onlineUsers || []).map(person => person.id).filter(Boolean)); const availableMatches = targetProfiles.filter(profile => onlineIds.has(profile.userId)); const notifyAvailableMatches = async () => { for (const profile of availableMatches) { if (!profile?.userId || (notifiedMatchIds || []).includes(profile.userId)) continue; await sendNotification({ to: user.id, title: 'Good match is online', body: `${profile.userName || 'A recommended partner'} is now available to chat.`, }); setNotifiedMatchIds(prev => (prev.includes(profile.userId) ? prev : [...prev, profile.userId])); } }; notifyAvailableMatches(); }, [matchSuggestions, notifiedMatchIds, onlineUsers, profiles, sendNotification, user]); const handleMatchBroadcast = useCallback((data) => { if (!data?.type || data.type !== 'match-refresh' || data.userId !== user?.id) return; if (Array.isArray(data.suggestions)) { setMatchSuggestions(data.suggestions); } }, [user?.id]); useBroadcast('match-refresh', handleMatchBroadcast); const handleSendIntroEmail = useCallback(async () => { if (!user || !introModalMatch?.userId) return; setIntroSending(true); setIntroStatusMessage(''); try { const recipientProfile = profiles?.find(profile => profile.userId === introModalMatch.userId) || null; const recipientEmail = recipientProfile?.userEmail || recipientProfile?.email || ''; const senderName = user.name || 'A language exchange member'; const recipientName = introModalMatch.userName || recipientProfile?.userName || 'there'; const subject = `Intro from ${senderName} via LangBridge`; const html = introDraft || `

Hi ${recipientName}

${senderName} would love to connect and start practicing together.

`; if (!recipientEmail) { throw new Error('Recipient email unavailable'); } if (typeof sendEmail === 'function') { await sendEmail({ to: recipientEmail, subject, html, }); } const payload = { senderId: user.id, senderName: senderName, senderEmail: user.email || '', matchUserId: introModalMatch.userId, matchUserName: recipientName, recipientEmail, subject, introHtml: html, sentAt: new Date().toISOString(), status: 'sent', }; const created = await addIntro(payload); if (created?.id) { await updateIntro(created.id, { ...payload, id: created.id }); } await sendNotification({ to: user.id, title: 'Intro email sent', body: `Your introduction email to ${recipientName} has been sent.`, }); setIntroStatusMessage('Intro email sent successfully.'); setIntroModalMatch(null); setIntroDraft(''); } catch (error) { setIntroStatusMessage('Unable to send intro email right now.'); } finally { setIntroSending(false); } }, [addIntro, introDraft, introModalMatch, profiles, sendNotification, user, updateIntro]); if (loading) { return (
🌍

Connecting you to the world...

); } if (!user) { return (
🗣️

LangBridge

Language Exchange Board
🌍

Sign in with Google

Create your language exchange profile, browse partners, and find matching learners.

); } const tabs = [ { id: 'board', label: 'Exchange Board', icon: '🌐' }, { id: 'matches', label: 'My Matches', icon: '🤝' }, { id: 'lounge', label: 'Chat Lounge', icon: '💬' }, { id: 'profile', label: 'My Profile', icon: '👤' }, ]; const handleSaveProfile = async profileData => { const payload = { ...profileData, userId: user.id, userName: user.name, userEmail: user.email, userAvatar: user.avatar, updatedAt: new Date().toISOString(), }; if (userProfile?.id) { await update(userProfile.id, payload); } else { await add({ ...payload, createdAt: new Date().toISOString(), }); } setEditMode(false); setActiveTab('profile'); }; const handleEditProfile = () => { setActiveTab('profile'); setEditMode(true); }; const handleCreateLounge = async e => { e.preventDefault(); const name = loungeName.trim(); if (!name) return; const newLounge = { name, createdBy: user.id, createdByName: user.name, createdByAvatar: user.avatar, createdAt: new Date().toISOString(), memberIds: [user.id], }; const created = await addLounge(newLounge); const createdId = created?.id || created?.data?.id || ''; if (createdId) { setJoinedLoungeId(createdId); sendLoungeBroadcast({ type: 'create', loungeId: createdId, authorId: user.id, authorName: user.name, authorAvatar: user.avatar, text: `${user.name} created lounge "${name}"`, createdAt: new Date().toISOString(), }); } setLoungeName(''); setActiveTab('lounge'); }; const handleJoinLounge = async lounge => { setJoinedLoungeId(lounge.id); const nextMembers = Array.from(new Set([...(lounge.memberIds || []), user.id])); if (lounge.id) { await updateLounge(lounge.id, { ...lounge, memberIds: nextMembers, updatedAt: new Date().toISOString() }); } sendLoungeBroadcast({ type: 'join', loungeId: lounge.id, authorId: user.id, authorName: user.name, authorAvatar: user.avatar, text: `${user.name} joined ${lounge.name}`, createdAt: new Date().toISOString(), }); setActiveTab('lounge'); }; const handleSendMessage = async e => { e.preventDefault(); const text = messageText.trim(); if (!text || !joinedLoungeId) return; const payload = { loungeId: joinedLoungeId, authorId: user.id, authorName: user.name, authorAvatar: user.avatar, type: 'message', text, createdAt: new Date().toISOString(), }; await addLoungeMessage(payload); sendLoungeBroadcast(payload); setMessageText(''); }; if (profilesLoading || loungeLoading || loungeMessagesLoading || notificationsLoading || introLoading) { return (
🌍

Loading your profile...

); } return (
🗣️

LangBridge

Language Exchange Board
{onlineUsers.length} online now
🔔 {unreadCount} unread
{user.avatar ? {user.name} : 👤} {user.name}
{editMode && ( setEditMode(false)} /> )} {!editMode && activeTab === 'board' && setEditMode(true)} />} {!editMode && activeTab === 'matches' && (

AI Matchmaking Suggestions

Recommendations are based on language pair compatibility, timezone overlap, and your profile preferences.

{matchError ?

{matchError}

: null} {aiError ?

{aiError}

: null} {introStatusMessage ?

{introStatusMessage}

: null} {!userProfile ? (

Complete your profile to get personalized match recommendations.

) : (
{matchSuggestions.length > 0 ? ( matchSuggestions.map((match, index) => ( handleOpenIntro(match)} /> )) ) : (

No suggestions yet. Generate a fresh set of AI recommendations.

)}
)}
)} {!editMode && activeTab === 'lounge' && (

Group Chat Lounge

Create a lounge, join others, and chat live with broadcast updates.

setLoungeName(e.target.value)} placeholder="Create a new lounge name" className="lounge-input" />

Available Lounges

{(loungeItems || []).map(lounge => ( ))}

{activeLounge ? activeLounge.name : 'Select a lounge to start chatting'}

{loungeMessages.map(message => (
{message.authorAvatar ? {message.authorName} : 👤}
{message.authorName}

{message.text}

))}
setMessageText(e.target.value)} placeholder="Type a message..." className="lounge-input" disabled={!joinedLoungeId} />
)} {!editMode && activeTab === 'profile' && } setIntroModalMatch(null)} onChangeDraft={setIntroDraft} onGenerate={() => introModalMatch ? handleOpenIntro(introModalMatch) : null} onSend={handleSendIntroEmail} />
); } export { App }; function PresenceRoster({ users = [], title = 'Online now' }) { return (

{title}

{users.length}
{users.map(user => (
{user.avatar ? {user.name} : null} {user.name}
))}
); } function MatchSuggestionCard({ match, onIntro }) { const score = typeof match?.score === 'number' ? match.score : null; const reasons = Array.isArray(match?.reasons) ? match.reasons : []; const overlap = match?.timezoneOverlap || match?.overlap || 'Good overlap'; return (
{match?.avatar ? {match.userName : 👤}

{match?.userName || 'Suggested Partner'}

{score !== null ? {score}% match : null}

{match?.nativeLanguage ? `${match.nativeLanguage} native` : 'Language data unavailable'} {match?.learningLanguage ? ` • learning ${match.learningLanguage}` : ''}

Timezone overlap: {overlap}

{match?.summary ?

{match.summary}

: null} {reasons.length > 0 ? ( ) : null}
{match?.userId ? : null}
); } function NotificationCenter({ notifications = [], unreadCount = 0, markRead }) { return (

Notifications

Updates about recommended partners, matches, and activity.

{unreadCount} unread
{notifications.length > 0 ? notifications.map(notification => ( )) : (

No notifications yet.

)}
); } function MatchIntroModal({ isOpen, match, draftHtml = '', generating = false, sending = false, onClose, onChangeDraft, onGenerate, onSend }) { if (!isOpen || !match) return null; return (

Send intro email

Review the AI-drafted introduction before sending it to your match.

Recipient: {match.userName || 'Suggested Partner'}