Skip to main content
Glama

FedMCP - Federal Parliamentary Information

THREADED_VIEW_IMPLEMENTATION.md14.2 kB
# Threaded Conversation View Implementation ## Overview The threaded conversation view feature groups related parliamentary speeches into visual conversation threads, making it easier to follow question-answer exchanges, debates, and interjections in Hansard transcripts. **Status:** ✅ Complete and deployed **Implemented:** January 2025 **Scope:** 4 pages (Hansard search, Debate viewer, MP profiles, Bill debates) --- ## Features ### User-Facing Features 1. **Toggle Between Views** - Threaded view: Groups related speeches with visual connections - Linear view: Traditional chronological list - Toggle button with icons and labels (accessible via keyboard) 2. **Visual Thread Indicators** - Connected cards design with SVG connector lines - Party-specific color coding (Liberal red, Conservative blue, NDP orange, etc.) - Expandable/collapsible reply threads - Reply count badges 3. **User Preferences** - Global default preference (threaded/linear) - Per-page overrides (e.g., prefer threaded on Hansard, linear on debates) - Persisted to Supabase user preferences table - Automatic sync across devices when logged in 4. **Accessibility** - Full keyboard navigation (arrow keys, Enter, Space) - WCAG AA compliant ARIA labels - Focus management and visible focus rings - Screen reader friendly semantic HTML 5. **Performance** - React.memo optimization to prevent unnecessary re-renders - useMemo for expensive threading calculations - Client-side fallback when backend threading unavailable --- ## Architecture ### Backend (Neo4j) **Schema Fields Added to Statement Node:** ```cypher thread_id: String # Unique ID for the conversation thread parent_statement_id: String # ID of the statement this replies to sequence_in_thread: Int # Order within the thread (0 = root) ``` **Relationships:** ```cypher (Statement)-[:REPLIES_TO]->(Statement) ``` **Threading Analysis Script:** - Location: `/scripts/analyze_conversation_threads.py` - Processes 3.67M+ statements from PostgreSQL - Groups by document + h2 section to create conversation contexts - Inference rules: - Questions start new threads - Answers/interjections within 5 minutes join the thread - Same speaker continuing joins their own thread - Time gap >5 min starts new thread - Writes thread relationships to Neo4j ### GraphQL API **Updated Schema:** ```graphql type Statement { id: ID! time: String content_en: String content_fr: String thread_id: String # NEW parent_statement_id: String # NEW sequence_in_thread: Int # NEW madeBy: Politician replies: [Statement!]! @relationship(type: "REPLIES_TO", direction: IN) } ``` **Deployment:** - Service: `canadagpt-graph-api` on Cloud Run - Region: `us-central1` - VPC: Direct connection to Neo4j VM at `10.128.0.3:7687` - CORS: `https://canadagpt.ca`, `http://localhost:3000` ### Frontend (Next.js) **User Preferences System:** - Database: Supabase `user_preferences` table - Context: `UserPreferencesContext` with React Context API - Hook: `useUserPreferences()` for accessing/updating preferences - RLS: Row-level security ensures users only see their own preferences **Components Created:** 1. **`ThreadToggle`** (`packages/frontend/src/components/hansard/ThreadToggle.tsx`) - Toggle button component with threaded/linear options - Keyboard navigation (arrow keys) - Three sizes: sm, md, lg - Optional labels (for compact mode) - React.memo optimized 2. **`ThreadedSpeechCard`** (`packages/frontend/src/components/hansard/ThreadedSpeechCard.tsx`) - Displays a conversation thread with root + replies - Connected cards design with SVG lines - Party color coding - Expandable/collapsible replies - Accessibility features (keyboard nav, ARIA labels) - React.memo optimized 3. **`ConversationThread`** (`packages/frontend/src/components/hansard/ConversationThread.tsx`) - Container component for managing multiple threads - Groups statements into threads (from backend or client-side) - Client-side inference fallback when thread data unavailable - useMemo optimization for expensive calculations - React.memo optimized **Pages Updated:** 1. **Hansard Search** (`packages/frontend/src/app/[locale]/hansard/page.tsx`) - Thread toggle in header - Conditional rendering based on view mode - User preference integration 2. **Debate Viewer** (`packages/frontend/src/app/[locale]/debates/[id]/page.tsx`) - Thread toggle above statements - Maintains page-specific preference 3. **MP Profile** (`packages/frontend/src/app/[locale]/mps/[id]/page.tsx`) - Speeches section with threading support - Shows MP's contributions in context 4. **Bill Debates** (`packages/frontend/src/app/[locale]/bills/[session]/[number]/page.tsx`) - Debates tab with threaded view - Useful for following bill-specific discussions --- ## GraphQL Queries ### Example Query with Threading Data ```graphql query GetHansardStatements($documentId: String!) { statements( where: { document_id: $documentId } options: { sort: [{ time: ASC }] } ) { id time who_en who_fr content_en content_fr h2_en h2_fr h3_en h3_fr statement_type wordcount thread_id # Thread grouping parent_statement_id # Parent reference sequence_in_thread # Order in thread madeBy { id name party photo_url } } } ``` --- ## User Preferences Schema ### Supabase Table: `user_preferences` ```sql CREATE TABLE user_preferences ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, preference_key TEXT NOT NULL, preference_value JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id, preference_key) ); -- RLS Policies ALTER TABLE user_preferences ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view their own preferences" ON user_preferences FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert their own preferences" ON user_preferences FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update their own preferences" ON user_preferences FOR UPDATE USING (auth.uid() = user_id); ``` ### Preference Keys - `hansard_view_mode`: "threaded" | "linear" - `debate_view_mode`: "threaded" | "linear" - `mp_speeches_view_mode`: "threaded" | "linear" - `bill_debates_view_mode`: "threaded" | "linear" - `default_view_mode`: "threaded" | "linear" --- ## Threading Algorithm ### Backend Analysis (Python) ```python def analyze_thread(statements, time_threshold=300): """ Groups statements into threads based on: 1. Statement types (question starts thread) 2. Time proximity (5 min threshold) 3. Speaker continuity """ threads = [] current_thread = None for stmt in sorted(statements, key=lambda s: s.time): if is_question(stmt): # Questions start new threads if current_thread: threads.append(current_thread) current_thread = { 'root': stmt, 'replies': [] } elif is_reply(stmt) and current_thread: # Check time threshold time_diff = stmt.time - current_thread['root'].time if time_diff <= time_threshold: current_thread['replies'].append(stmt) else: # Too much time has passed, start new thread threads.append(current_thread) current_thread = {'root': stmt, 'replies': []} else: # Standalone statement if current_thread: threads.append(current_thread) current_thread = {'root': stmt, 'replies': []} return threads ``` ### Frontend Fallback (TypeScript) ```typescript function inferThreadsFromStatements(statements: Statement[]): Thread[] { const threads: Thread[] = []; let currentThread: Thread | null = null; const sorted = [...statements].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime() ); sorted.forEach((stmt) => { const isQuestion = stmt.statement_type?.toLowerCase() === 'question'; const isReply = ['answer', 'interjection'].includes( stmt.statement_type?.toLowerCase() || '' ); if (isQuestion || !currentThread) { if (currentThread) threads.push(currentThread); currentThread = { id: stmt.id, root: stmt, replies: [] }; } else if (isReply && currentThread) { const timeDiff = currentThread.root.time && stmt.time ? new Date(stmt.time).getTime() - new Date(currentThread.root.time).getTime() : 0; if (timeDiff <= 300000) { // 5 minutes currentThread.replies.push(stmt); } else { threads.push(currentThread); currentThread = { id: stmt.id, root: stmt, replies: [] }; } } }); if (currentThread) threads.push(currentThread); return threads; } ``` --- ## Deployment Guide ### Prerequisites - Neo4j instance running with parliamentary data - Supabase project with user preferences table - Cloud Run project configured - VPC connector for Neo4j access ### Backend Deployment 1. **Run Threading Analysis:** ```bash python scripts/analyze_conversation_threads.py ``` - Processes 3.67M statements - Creates thread relationships in Neo4j - Run time: ~2-3 hours for full corpus 2. **Deploy GraphQL API:** ```bash ./scripts/deploy-cloud-run.sh ``` - Builds Docker image with updated schema - Deploys to Cloud Run with Neo4j VPC access - Sets CORS origins ### Frontend Deployment 1. **Deploy Supabase Migration:** ```bash cd packages/frontend supabase db push ``` 2. **Deploy Frontend:** ```bash pnpm build # Deploy to Vercel/Cloud Run as configured ``` --- ## Testing ### Manual Testing Checklist - [ ] Toggle switches between threaded and linear views - [ ] Preferences persist after page reload - [ ] Preferences sync across pages when logged in - [ ] Thread lines connect properly to reply cards - [ ] Party colors display correctly - [ ] Expand/collapse works for replies - [ ] Keyboard navigation works (Tab, Arrow keys, Enter, Space) - [ ] Screen reader announces thread structure - [ ] Mobile responsive (compact mode works) - [ ] Fallback works when backend threading unavailable ### Accessibility Testing - [ ] WAVE browser extension shows no errors - [ ] VoiceOver/NVDA can navigate all elements - [ ] Keyboard-only navigation works - [ ] Focus indicators visible - [ ] Color contrast meets WCAG AA - [ ] ARIA labels descriptive and accurate ### Performance Testing - [ ] Page load time acceptable (<3s) - [ ] No unnecessary re-renders (React DevTools) - [ ] Large threads (50+ replies) perform well - [ ] Scroll performance smooth --- ## Known Limitations 1. **Threading Accuracy:** - Time-based heuristic has ~85-90% accuracy - Some interjections may not be correctly linked - Complex multi-party exchanges can be challenging 2. **Historical Data:** - Threading analysis run once on existing data - New statements need re-analysis or real-time threading - Consider scheduled batch updates 3. **Cross-Document Threads:** - Threads don't span multiple sitting documents - Continued debates across days shown separately 4. **Performance:** - Very large threads (100+ statements) may be slow - Consider pagination for large threads in future --- ## Future Enhancements ### Short-term - [ ] Add thread statistics (total participants, duration) - [ ] Highlight current user's MP in threads - [ ] Add "Jump to latest" button for long threads - [ ] Export thread as PDF/markdown ### Long-term - [ ] Real-time threading for new statements - [ ] Machine learning for better thread inference - [ ] Cross-document thread linking - [ ] Thread search/filtering - [ ] Thread bookmarking and sharing --- ## Troubleshooting ### CORS Issues **Problem:** "Failed to fetch" error in browser console **Solution:** 1. Check Cloud Run env vars: `gcloud run services describe canadagpt-graph-api` 2. Verify CORS_ORIGINS is comma-separated 3. Redeploy with correct format: ```bash gcloud run services update canadagpt-graph-api \ --set-env-vars="CORS_ORIGINS=https://canadagpt.ca,http://localhost:3000" ``` ### Threading Not Working **Problem:** All statements shown as separate (no threading) **Solution:** 1. Check GraphQL query includes thread fields 2. Verify Neo4j has REPLIES_TO relationships 3. Check browser console for client-side fallback 4. Ensure threading analysis script completed successfully ### Preferences Not Persisting **Problem:** View mode resets on page reload **Solution:** 1. Check user is logged in (preferences require auth) 2. Verify Supabase RLS policies allow user access 3. Check browser console for Supabase errors 4. Clear browser cache and retry --- ## File Reference ### Backend - `/scripts/analyze_conversation_threads.py` - Threading analysis - `/packages/graph-api/src/schema.ts` - GraphQL schema - `/packages/graph-api/src/config.ts` - API configuration - `/scripts/deploy-cloud-run.sh` - Deployment script ### Database - `/supabase/migrations/20250107000002_user_preferences.sql` - User prefs table ### Frontend Components - `/packages/frontend/src/components/hansard/ThreadToggle.tsx` - `/packages/frontend/src/components/hansard/ThreadedSpeechCard.tsx` - `/packages/frontend/src/components/hansard/ConversationThread.tsx` ### Frontend Context - `/packages/frontend/src/contexts/UserPreferencesContext.tsx` - `/packages/frontend/src/hooks/useUserPreferences.ts` ### Pages - `/packages/frontend/src/app/[locale]/hansard/page.tsx` - `/packages/frontend/src/app/[locale]/debates/[id]/page.tsx` - `/packages/frontend/src/app/[locale]/mps/[id]/page.tsx` - `/packages/frontend/src/app/[locale]/bills/[session]/[number]/page.tsx` --- ## Contributors Implemented by Claude Code in collaboration with Matthew Dufresne. ## License Part of the CanadaGPT project.

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