Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

MOBILE_IMPLEMENTATION_GUIDE.md14.2 kB
# CanadaGPT Mobile Implementation Guide Complete guide for implementing the mobile-optimized UI with voice features. ## Table of Contents 1. [Quick Start](#quick-start) 2. [Voice System](#voice-system) 3. [Mobile Components](#mobile-components) 4. [Integration Examples](#integration-examples) 5. [PWA Setup](#pwa-setup) 6. [Testing](#testing) 7. [Troubleshooting](#troubleshooting) --- ## Quick Start ### 1. Dependencies All required dependencies are already installed: ```bash # Installed dependencies: # - lucide-react (icons) # - @use-gesture/react (swipe gestures) # - react-window (virtual scrolling) ``` ### 2. Import CSS Add to your global CSS or layout file: ```tsx // In app/layout.tsx or similar import '@/components/voice/voice-system.css'; import '@/components/mobile/mobile-cards.css'; import '@/components/mobile/mobile-navigation.css'; import '@/components/mobile/mobile-debate-viewer.css'; ``` ### 3. Basic Usage ```tsx import { VoiceSearch } from '@/components/voice'; import { MobileBottomNav } from '@/components/mobile'; export default function Layout({ children }) { return ( <> {children} <MobileBottomNav locale="en" /> </> ); } ``` --- ## Voice System ### VoiceSearch Voice-enabled search with live transcription. **Props:** - `onSearch: (query: string) => void` - Search callback - `placeholder?: string` - Input placeholder - `className?: string` - Additional CSS classes **Example:** ```tsx import { VoiceSearch } from '@/components/voice'; function SearchPage() { const handleSearch = (query: string) => { router.push(`/search?q=${encodeURIComponent(query)}`); }; return <VoiceSearch onSearch={handleSearch} placeholder="Search MPs, bills..." />; } ``` **Browser Support:** - iOS Safari 14.5+ - Chrome Android 33+ - Requires HTTPS (except localhost) --- ### VoiceChat Conversational AI interface with BYOK (Bring Your Own Key). **Props:** - `apiEndpoint: string` - Your API endpoint - `apiKey: string` - User's Claude/OpenAI key - `provider: 'claude' | 'openai'` - AI provider - `context?: { page, mpId, billId, debateId }` - Page context - `className?: string` **Example:** ```tsx import { VoiceChat } from '@/components/voice'; function MPPage({ mp }) { return ( <VoiceChat apiEndpoint="/api/chat" apiKey={user.apiKey} provider="claude" context={{ page: 'mp', mpId: mp.id, mpName: mp.name, }} /> ); } ``` **API Endpoint Example:** ```ts // app/api/chat/route.ts export async function POST(req: Request) { const { provider, messages, context } = await req.json(); // Call Claude or OpenAI with context-aware system prompt const response = await callAI(provider, messages, context); return Response.json({ response }); } ``` --- ### VoiceNotes Context-aware voice note-taking. **Props:** - `context: { type, id, title, metadata }` - What the note is about - `onSave: (note) => Promise<void>` - Save callback - `className?: string` **Example:** ```tsx import { VoiceNotes } from '@/components/voice'; function StatementPage({ statement }) { const saveNote = async (note) => { await fetch('/api/notes', { method: 'POST', body: JSON.stringify(note), }); }; return ( <VoiceNotes context={{ type: 'statement', id: statement.id, title: `${statement.madeBy.name} - ${statement.h2_en}`, metadata: { date: statement.time, speaker: statement.madeBy.name }, }} onSave={saveNote} /> ); } ``` --- ### Voice Query Parser Convert natural language to Neo4j Cypher queries. **Example:** ```ts import { parseVoiceQuery } from '@/components/voice'; // General query const result = parseVoiceQuery("Show me Pierre Poilievre's votes on Bill C-21"); console.log(result.cypher); // MATCH (mp:MP)-[v:VOTED]->(vote:Vote)-[:CONCERNS]->(bill:Bill) // WHERE toLower(mp.name) CONTAINS "poilievre" AND bill.number = "BILL C-21" // RETURN mp.name, vote.description, v.position, vote.date // Context-aware query (on MP page) const mpResult = parseVoiceQuery("What has he said about carbon tax?", { type: 'mp', mpId: 'pierre-poilievre', mpName: 'Pierre Poilievre' }); console.log(mpResult.cypher); // MATCH (mp:MP {id: "pierre-poilievre"})-[:MADE_BY]-(s:Statement) // WHERE toLower(s.content_en) CONTAINS "carbon tax" // RETURN s.content_en, s.time, s.h2_en ``` --- ## Mobile Components ### MobileStatementCard Twitter/Instagram-style speech card with swipe gestures. ```tsx import { MobileStatementCard } from '@/components/mobile'; <MobileStatementCard statement={statement} onSwipeLeft={() => nextStatement()} onSwipeRight={() => prevStatement()} showFullContent={false} locale="en" /> ``` **Features:** - Party-color accent border - Read more/less toggle - Share, like, bookmark actions - Swipeable for navigation --- ### MobileMPCard Compact MP profile card for lists. ```tsx import { MobileMPCard } from '@/components/mobile'; <MobileMPCard mp={mp} stats={{ bills: 12, votes: 345, speeches: 89, }} locale="en" /> ``` --- ### MobileDebateCard Debate preview card. ```tsx import { MobileDebateCard } from '@/components/mobile'; <MobileDebateCard debate={{ id: "123", date: "2025-11-16", statement_count: 150, speaker_count: 45, }} title="Question Period" preview="Today's debate featured discussion on housing..." /> ``` --- ### MobileBillCard Bill card with progress indicator. ```tsx import { MobileBillCard } from '@/components/mobile'; <MobileBillCard bill={{ number: "C-21", session: "45-1", title: "An Act to amend...", status: "Second Reading", progress: 65, }} /> ``` --- ### MobileBottomNav Sticky bottom navigation bar. ```tsx import { MobileBottomNav } from '@/components/mobile'; // In layout <MobileBottomNav locale="en" /> ``` **Navigation Items:** - Home - Search - Bookmarks - Profile --- ### MobileHeader Mobile-optimized header with voice search. ```tsx import { MobileHeader } from '@/components/mobile'; <MobileHeader title="Pierre Poilievre" showSearch={true} onMenuClick={() => setMenuOpen(true)} locale="en" /> ``` --- ### SwipeableDrawer Bottom sheet drawer with swipe-to-close. ```tsx import { SwipeableDrawer } from '@/components/mobile'; <SwipeableDrawer isOpen={isOpen} onClose={() => setIsOpen(false)} title="Filters" height="half" > {/* Filter content */} </SwipeableDrawer> ``` **Heights:** `'half'`, `'full'`, `'auto'` --- ### MobileDebateViewer Twitter/Instagram-style debate reader. ```tsx import { MobileDebateViewer } from '@/components/mobile'; <MobileDebateViewer statements={debateStatements} locale="en" /> ``` **Features:** - Scroll mode: Infinite vertical scroll - Focus mode: One statement at a time with swipe navigation - Party filter: Show only specific party speeches - Timeline scrubber: Color-coded dots for quick navigation --- ## Custom Hooks ### useMobileDetect Detect device type and screen properties. ```tsx import { useMobileDetect } from '@/hooks/useMobileDetect'; function MyComponent() { const { isMobile, isTablet, isIOS, screenWidth, orientation } = useMobileDetect(); if (isMobile) { return <MobileView />; } return <DesktopView />; } ``` **Returns:** - `isMobile: boolean` - `isTablet: boolean` - `isDesktop: boolean` - `isTouchDevice: boolean` - `isIOS: boolean` - `isAndroid: boolean` - `screenWidth: number` - `screenHeight: number` - `orientation: 'portrait' | 'landscape'` --- ### useSwipeGesture Swipe gesture detection. ```tsx import { useSwipeGesture } from '@/hooks/useSwipeGesture'; function SwipeableCard() { const bind = useSwipeGesture({ onSwipeLeft: () => console.log('Swiped left'), onSwipeRight: () => console.log('Swiped right'), threshold: 50, velocityThreshold: 0.3, }); return <div {...bind()}>Swipe me!</div>; } ``` **Alternative (no dependencies):** ```tsx import { useSimpleSwipe } from '@/hooks/useSwipeGesture'; const swipeHandlers = useSimpleSwipe({ onSwipeLeft, onSwipeRight, }); return <div {...swipeHandlers}>Swipe me!</div>; ``` --- ## Integration Examples ### Example 1: Mobile MP Profile Page ```tsx // app/[locale]/mps/[id]/mobile-page.tsx 'use client'; import { useMobileDetect } from '@/hooks/useMobileDetect'; import { MobileHeader, MobileBottomNav } from '@/components/mobile'; import { VoiceChat, VoiceNotes } from '@/components/voice'; import { useState } from 'react'; export default function MobileMPPage({ mp }) { const { isMobile } = useMobileDetect(); const [isChatOpen, setIsChatOpen] = useState(false); if (!isMobile) return <DesktopMPPage mp={mp} />; return ( <> <MobileHeader title={mp.name} showSearch /> {/* Content */} <div className="p-4"> {/* Quick stats */} {/* Swipeable tabs */} {/* Voice notes */} </div> {/* Voice chat FAB */} <button onClick={() => setIsChatOpen(true)} className="fixed bottom-20 right-4 w-14 h-14 bg-red-600 rounded-full" > <Mic /> </button> <SwipeableDrawer isOpen={isChatOpen} onClose={() => setIsChatOpen(false)} title="Ask about this MP" height="full" > <VoiceChat apiEndpoint="/api/chat" apiKey={user.apiKey} provider="claude" context={{ page: 'mp', mpId: mp.id }} /> </SwipeableDrawer> <MobileBottomNav /> </> ); } ``` --- ### Example 2: Mobile Debate Page ```tsx // app/[locale]/debates/[id]/mobile-page.tsx 'use client'; import { MobileDebateViewer } from '@/components/mobile'; export default function MobileDebatePage({ debate, statements }) { return ( <MobileDebateViewer statements={statements} locale="en" /> ); } ``` --- ## PWA Setup ### 1. Create Manifest File: `public/manifest.json` ```json { "name": "CanadaGPT", "short_name": "CanadaGPT", "description": "Canadian Parliamentary Data Platform", "start_url": "/", "display": "standalone", "background_color": "#0a0a0a", "theme_color": "#dc2626", "orientation": "portrait-primary", "icons": [ { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } ], "permissions": ["microphone"], "features": ["voice-recognition", "web-share"] } ``` ### 2. Add to Layout ```tsx // app/layout.tsx export const metadata = { manifest: '/manifest.json', }; ``` ### 3. Service Worker (Optional) File: `public/sw.js` ```js self.addEventListener('install', (event) => { event.waitUntil( caches.open('canadagpt-v1').then((cache) => { return cache.addAll([ '/', '/manifest.json', // Add critical resources ]); }) ); }); ``` --- ## Testing ### Mobile Testing Checklist **iOS Safari:** - [ ] Voice recognition works (requires HTTPS) - [ ] Touch targets are at least 44x44px - [ ] Swipe gestures work smoothly - [ ] Bottom nav respects safe area - [ ] No zoom on input focus (font-size >= 16px) **Chrome Android:** - [ ] Voice recognition works - [ ] Haptic feedback works - [ ] Pull-to-refresh disabled where needed - [ ] Share API works **General:** - [ ] Responsive breakpoints work (320px, 375px, 414px, 768px) - [ ] Smooth scrolling with momentum - [ ] No layout shift on keyboard open - [ ] Offline voice notes queue works ### Testing Tools ```bash # Chrome DevTools Mobile Emulation # 1. Open DevTools (F12) # 2. Click Device Toolbar (Ctrl+Shift+M) # 3. Select device: iPhone 14 Pro, Pixel 7, etc. # Test on real device via network npm run dev -- --host # Visit http://YOUR_IP:3000 on mobile device ``` --- ## Troubleshooting ### Voice Recognition Not Working **Problem:** "Speech recognition not supported" **Solutions:** 1. Ensure HTTPS (required for iOS Safari) 2. Check browser support (iOS 14.5+, Chrome Android 33+) 3. Grant microphone permission in browser settings 4. Test in supported browser (not Firefox) --- ### Swipe Gestures Not Working **Problem:** Swipes not detected or interfering with scroll **Solutions:** 1. Check if `@use-gesture/react` is installed 2. Adjust `threshold` and `velocityThreshold` values 3. Use `useSimpleSwipe` for basic swipe detection 4. Prevent default touch behavior where needed --- ### Bottom Nav Hidden Behind Content **Problem:** Content overlaps bottom navigation **Solution:** ```css /* Add padding to page container */ .page-container { padding-bottom: 80px; /* Height of bottom nav + safe area */ } ``` --- ### Voice Input Zoom on iOS **Problem:** Input field zooms when focused **Solution:** ```css /* Ensure font-size is at least 16px */ .voice-search-input { font-size: 16px; /* Prevents iOS zoom */ } ``` --- ### Performance Issues on Long Lists **Problem:** Slow scrolling with many statements **Solution:** ```tsx // Use react-window for virtual scrolling import { FixedSizeList as List } from 'react-window'; <List height={600} itemCount={statements.length} itemSize={200} width="100%" > {({ index, style }) => ( <div style={style}> <MobileStatementCard statement={statements[index]} /> </div> )} </List> ``` --- ## Production Deployment ### HTTPS Requirements Voice API **requires HTTPS** in production: 1. **Vercel/Netlify:** HTTPS automatic 2. **Custom server:** Use Let's Encrypt or Cloudflare 3. **Development:** Use `localhost` (HTTPS not required) ### Environment Variables ```env # .env.local NEXT_PUBLIC_API_ENDPOINT=https://api.canadagpt.ca NEO4J_URI=bolt://your-neo4j-instance ``` ### Performance Tips 1. **Code Splitting:** Mobile components lazy-loaded on mobile devices only 2. **Image Optimization:** Use Next.js `<Image>` component 3. **Font Loading:** Preload critical fonts 4. **Analytics:** Track mobile vs desktop usage --- ## Support **Issues:** https://github.com/your-repo/issues **Documentation:** This file + component JSDoc comments **Voice API Docs:** https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server