Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ yarn-error.log*
.contentlayer
.env

docs/
docs/
data/
.playwright-mcp/

# Representative images (generated from data/)
public/representatives/
129 changes: 98 additions & 31 deletions app/api/chat/route.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { openai } from "@ai-sdk/openai"
import { streamText } from "ai"

import { findRelevantContent } from "@/lib/ai/embedding"
import { findRelevantContent, findRelevantRepresentatives, detectQueryTypes } from "@/lib/ai/embedding"

export const maxDuration = 30

Expand All @@ -10,69 +10,136 @@ export async function POST(req: Request) {

// Get relevant content for the last message
const lastMessage = messages[messages.length - 1]
const relevantContent = await findRelevantContent(lastMessage.content)

// Format the content for the AI to use
const contextString = relevantContent
.map((content) => {
return `Document: ${content.documentTitle}
Type: ${content.documentType}
Content: ${content.content}
---`
})
.join("\n\n")

// Use AI to detect query types (can be multiple)
const queryTypes = await detectQueryTypes(lastMessage.content)

const contextParts: string[] = []

// Search all relevant content types in parallel
const searchPromises = []

if (queryTypes.includes('representative')) {
searchPromises.push(
findRelevantRepresentatives(lastMessage.content).then((reps) => ({
type: 'representative' as const,
data: reps,
}))
)
}

if (queryTypes.includes('bill') || queryTypes.includes('document')) {
searchPromises.push(
findRelevantContent(lastMessage.content).then((docs) => ({
type: 'document' as const,
data: docs,
}))
)
}

// Execute searches in parallel
const results = await Promise.all(searchPromises)

// Format results
for (const result of results) {
if (result.type === 'representative' && result.data.length > 0) {
const repContext = result.data
.map((rep) => {
return `Representative: ${rep.name}
Constituency: ${rep.constituencyCode} - ${rep.constituencyName || rep.constituency}
District: ${rep.district || 'N/A'}
Province: ${rep.province}
Party: ${rep.party}
Phone: ${rep.phone || 'Not available'}
Permanent Address: ${rep.permanentAddress || 'Not available'}
Islamabad Address: ${rep.islamabadAddress || 'Not available'}
---`
})
.join("\n\n")
contextParts.push(repContext)
}

if (result.type === 'document' && result.data.length > 0) {
const docContext = result.data
.map((content) => {
return `Document: ${content.documentTitle}
Type: ${content.documentType}
Content: ${content.content}
---`
})
.join("\n\n")
contextParts.push(docContext)
}
}

const contextString = contextParts.join("\n\n=== DIFFERENT SOURCE TYPE ===\n\n")

const result = streamText({
model: openai("gpt-4o-mini"),
messages,
system: `You are Numainda, an AI assistant designed to share insights and facts derived exclusively from Pakistan's Constitution, Elections Act 2017, parliamentary proceedings, and National Assembly bills. Your purpose is to make Pakistan's legislative framework accessible and engaging.
system: `You are Numainda, an AI assistant designed to help Pakistani citizens connect with their National Assembly representatives and understand Pakistan's Constitution, Elections Act 2017, parliamentary proceedings, and bills. Your purpose is to make Pakistan's legislative framework and representatives accessible to all.

Here is the relevant information to help answer the question:

Here is the relevant information from official documents to help answer the question:

${contextString}

Core Instructions:
1. Base your responses EXCLUSIVELY on the provided information above. Never venture into speculative or inferred information not directly available from the sources.

2. Response Structure:
- Begin by citing your source document(s)
- Begin by citing your source (document or representative name)
- For representatives: Include name, constituency, party, and contact information
- For bills: Include bill status, passage date (if passed), and key provisions
- Use clear, simple language that's accessible to all
- Incorporate relevant emojis to enhance readability
- Add appropriate hashtags (e.g., #PakistanLaws, #NABill, #PakParliament)

3. When discussing bills:
- Add appropriate hashtags (e.g., #PakistanLaws, #NABill, #PakParliament, #YourRepresentative)

3. When discussing representatives:
- Clearly present the representative's name and constituency
- Include their political party affiliation
- Provide contact information (phone, addresses) if available
- Use format: "👤 [Name] represents [Constituency] ([Party])"
- Always encourage citizens to reach out to their representatives

4. For cross-entity queries (e.g., "Did representative X present a bill?"):
- You may receive BOTH representative AND document information
- Synthesize information from multiple source types
- Connect representatives to bills/legislation when relevant
- Be transparent if connection cannot be established from provided data
- Example: "Based on the information provided, I can tell you about [Rep Name] and these bills, but I cannot confirm direct authorship/sponsorship"

5. When discussing bills:
- Clearly state the bill's current status (pending/passed/rejected)
- Highlight main objectives and key provisions
- If passed, mention the passage date and implementation timeline
- Explain potential impacts on citizens or institutions
- Use format: "Bill Title (Status): Key Points"
4. For questions without relevant information:

6. For questions without relevant information:
- Respond: "I don't have sufficient information in the provided documents to answer this question."
- Suggest related bills or legislation you do have information about
- Suggest related bills, legislation, or representatives you do have information about
- Maintain transparency about knowledge limitations
5. When synthesizing multiple sources:

7. When synthesizing multiple sources:
- Present information chronologically or by relevance
- Show relationships between bills and existing laws
- Highlight any amendments or changes to existing legislation
- Use direct quotes sparingly and only for crucial details
6. Special Content Types:

8. Special Content Types:
If asked for a "tweet":
- Create engaging, fact-based content within 280 characters
- Include source attribution and bill status for legislation
- Use emojis and hashtags appropriately
- Example: "📜 New Bill Alert! The [Bill Name] aims to [main objective]. Current status: [Status] 🏛️ #PakParliament"
7. Tone and Style:

9. Tone and Style:
- Maintain a balance between authoritative and engaging
- Use formal language for legislative matters
- Add appropriate emojis and hashtags to enhance engagement
- Keep responses clear, concise, and educational

8. Do not hallucinate or speculate:
10. Do not hallucinate or speculate:
- Stick strictly to information in the provided documents
- For bills: Only discuss provisions explicitly stated
- If asked about implementation details not in the text, acknowledge the limitation
Expand Down
47 changes: 47 additions & 0 deletions app/api/representatives/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { representatives } from '@/lib/db/schema/representatives';
import { eq } from 'drizzle-orm';

export const dynamic = 'force-dynamic';

export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;

// Fetch representative by ID
const rep = await db
.select()
.from(representatives)
.where(eq(representatives.id, id))
.limit(1);

if (rep.length === 0) {
return NextResponse.json(
{ error: 'Representative not found' },
{ status: 404 }
);
}

// Transform imageLocalPath to public URL
const repWithImage = {
...rep[0],
imageUrl: rep[0].imageLocalPath
? `/representatives/${rep[0].imageLocalPath.split('/').pop()}`
: rep[0].imageUrl,
};

return NextResponse.json({
data: repWithImage,
});
} catch (error) {
console.error('Failed to fetch representative:', error);
return NextResponse.json(
{ error: 'Failed to fetch representative' },
{ status: 500 }
);
}
}
93 changes: 93 additions & 0 deletions app/api/representatives/by-location/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { representatives } from '@/lib/db/schema/representatives';
import { sql, isNotNull } from 'drizzle-orm';

export const dynamic = 'force-dynamic';

/**
* Find representatives by location (lat/lng)
* Uses Haversine formula to calculate distance
*
* Query params:
* - lat: latitude (required)
* - lng: longitude (required)
* - radius: search radius in km (default: 50)
* - limit: max results (default: 10)
*/
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const lat = parseFloat(searchParams.get('lat') || '');
const lng = parseFloat(searchParams.get('lng') || '');
const radius = parseFloat(searchParams.get('radius') || '50'); // km
const limit = parseInt(searchParams.get('limit') || '10', 10);

if (isNaN(lat) || isNaN(lng)) {
return NextResponse.json(
{ error: 'Valid lat and lng parameters are required' },
{ status: 400 }
);
}

// Use subquery to calculate distance, then filter
const results = await db.execute(sql`
SELECT * FROM (
SELECT
id,
name,
name_clean as "nameClean",
constituency,
constituency_code as "constituencyCode",
constituency_name as "constituencyName",
district,
province,
party,
image_url as "imageUrl",
image_local_path as "imageLocalPath",
phone,
latitude,
longitude,
permanent_address as "permanentAddress",
islamabad_address as "islamabadAddress",
(
6371 * acos(
cos(radians(${lat}))
* cos(radians(latitude))
* cos(radians(longitude) - radians(${lng}))
+ sin(radians(${lat}))
* sin(radians(latitude))
)
) as distance
FROM representatives
WHERE latitude IS NOT NULL
) as with_distance
WHERE distance <= ${radius}
ORDER BY distance
LIMIT ${limit}
`);

// Transform imageLocalPath to public URL
const resultsWithImages = results.map((rep: any) => ({
...rep,
imageUrl: rep.imageLocalPath
? `/representatives/${rep.imageLocalPath.split('/').pop()}`
: rep.imageUrl,
}));

return NextResponse.json({
data: resultsWithImages,
query: {
lat,
lng,
radius,
},
});
} catch (error) {
console.error('Failed to fetch representatives by location:', error);
return NextResponse.json(
{ error: 'Failed to fetch representatives by location' },
{ status: 500 }
);
}
}
61 changes: 61 additions & 0 deletions app/api/representatives/filters/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { representatives } from '@/lib/db/schema/representatives';
import { sql } from 'drizzle-orm';

export const dynamic = 'force-dynamic';

/**
* Get available filter options for representatives
* Returns unique values for provinces, parties, and districts
*/
export async function GET() {
try {
// Get unique provinces
const provincesResult = await db
.selectDistinct({ province: representatives.province })
.from(representatives)
.where(sql`${representatives.province} IS NOT NULL`)
.orderBy(representatives.province);

// Get unique parties
const partiesResult = await db
.selectDistinct({ party: representatives.party })
.from(representatives)
.where(sql`${representatives.party} IS NOT NULL`)
.orderBy(representatives.party);

// Get unique districts
const districtsResult = await db
.selectDistinct({ district: representatives.district })
.from(representatives)
.where(sql`${representatives.district} IS NOT NULL`)
.orderBy(representatives.district);

const provinces = provincesResult
.map((r) => r.province)
.filter((p): p is string => p !== null);

const parties = partiesResult
.map((r) => r.party)
.filter((p): p is string => p !== null);

const districts = districtsResult
.map((r) => r.district)
.filter((d): d is string => d !== null);

return NextResponse.json({
data: {
provinces,
parties,
districts,
},
});
} catch (error) {
console.error('Failed to fetch filter options:', error);
return NextResponse.json(
{ error: 'Failed to fetch filter options' },
{ status: 500 }
);
}
}
Loading
Loading