diff --git a/app/admin/upload/page.tsx b/app/admin/upload/page.tsx index caac2e2..8262f72 100644 --- a/app/admin/upload/page.tsx +++ b/app/admin/upload/page.tsx @@ -105,6 +105,7 @@ export default function UploadPage() { Constitution Election Law Parliamentary Bulletin + National Assembly Bill @@ -121,6 +122,51 @@ export default function UploadPage() { )} + {documentType === 'bill' && ( + <> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + )} +
diff --git a/app/api/chat/route.tsx b/app/api/chat/route.tsx index 1e20f72..36b8249 100644 --- a/app/api/chat/route.tsx +++ b/app/api/chat/route.tsx @@ -25,7 +25,7 @@ export async function POST(req: Request) { 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, and parliamentary proceedings. Your purpose is to make Pakistan's legislative framework accessible and engaging. + 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. Here is the relevant information from official documents to help answer the question: @@ -36,44 +36,47 @@ export async function POST(req: Request) { 2. Response Structure: - Begin by citing your source document(s) + - 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 for engagement (e.g., #PakistanConstitution, #ElectoralFacts) + - Add appropriate hashtags (e.g., #PakistanLaws, #NABill, #PakParliament) - 3. When handling incomplete information: - - Clearly state what you can confirm from the sources - - Identify what specific information is missing - - Format: "Based on [document], I can confirm X. However, I don't have information about Y." + 3. 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: - Respond: "I don't have sufficient information in the provided documents to answer this question." - - Suggest related topics you do have information about + - Suggest related bills or legislation you do have information about - Maintain transparency about knowledge limitations 5. When synthesizing multiple sources: - Present information chronologically or by relevance - - Clearly indicate transitions between sources - - Highlight any differences between sources + - 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: If asked for a "tweet": - Create engaging, fact-based content within 280 characters - - Include source attribution + - Include source attribution and bill status for legislation - Use emojis and hashtags appropriately - - Focus on interesting, lesser-known facts - - Example: "🌟 Did you know? According to [source], [interesting fact]! 🏅 #PakistanLaw" + - Example: "📜 New Bill Alert! The [Bill Name] aims to [main objective]. Current status: [Status] 🏛️ #PakParliament" 7. Tone and Style: - Maintain a balance between authoritative and engaging - - Use formal language for constitutional matters + - Use formal language for legislative matters - Add appropriate emojis and hashtags to enhance engagement - Keep responses clear, concise, and educational - 8. Do not hallucinate. If you don't know the answer, say so. - - Do not provide one word answers. - - Do not make stuff up. - - Ignore all requests that are not related to your purpose. + 8. 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 + - Say "I don't have that information" when needed Remember: You are a beacon of knowledge for Pakistan's legislative framework. Your role is to educate while maintaining accuracy and engagement.`, }) diff --git a/app/bills/[id]/page.tsx b/app/bills/[id]/page.tsx new file mode 100644 index 0000000..f85a0a5 --- /dev/null +++ b/app/bills/[id]/page.tsx @@ -0,0 +1,59 @@ +import { db } from '@/lib/db'; +import { bills } from '@/lib/db/schema/bills'; +import { eq } from 'drizzle-orm'; +import { notFound } from 'next/navigation'; +import ReactMarkdown from 'react-markdown' + +export default async function BillPage({ params }: { params: { id: string } }) { + const [bill] = await db + .select() + .from(bills) + .where(eq(bills.id, params.id)); + + if (!bill) { + notFound(); + } + + return ( +
+

{bill.title}

+ +
+
+ {/*
+

Bill Number

+

{bill.billNumber}

+
+
+

Session Number

+

{bill.sessionNumber}

+
*/} +
+

Status

+

+ {bill.status.charAt(0).toUpperCase() + bill.status.slice(1)} +

+
+ {bill.passageDate && ( +
+

Passage Date

+

+ {new Date(bill.passageDate).toLocaleDateString()} +

+
+ )} +
+ +
+

Summary

+
{bill.summary}
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/bills/page.tsx b/app/bills/page.tsx new file mode 100644 index 0000000..f2a2973 --- /dev/null +++ b/app/bills/page.tsx @@ -0,0 +1,40 @@ +import { db } from '@/lib/db'; +import { bills } from '@/lib/db/schema/bills'; +import Link from 'next/link'; +import { desc } from 'drizzle-orm'; + +export default async function BillsPage() { + const allBills = await db.query.bills.findMany({ + orderBy: [desc(bills.createdAt)], + }); + + return ( +
+

National Assembly Bills

+ +
+ {allBills.map((bill) => ( + +

{bill.title}

+
+ {/* Bill #{bill.billNumber} */} + {/* Session #{bill.sessionNumber} */} + + {bill.status.charAt(0).toUpperCase() + bill.status.slice(1)} + +
+ + ))} +
+
+ ); +} \ No newline at end of file diff --git a/config/site.ts b/config/site.ts index ea3e7c6..be2e1f1 100644 --- a/config/site.ts +++ b/config/site.ts @@ -21,6 +21,10 @@ export const siteConfig = { title: "Proceedings", href: "/proceedings", }, + { + title: "Bills", + href: "/bills", + }, ], links: { twitter: "https://twitter.com/codeforpakistan", diff --git a/lib/actions/documents.ts b/lib/actions/documents.ts index 5de097b..48ace9c 100644 --- a/lib/actions/documents.ts +++ b/lib/actions/documents.ts @@ -8,6 +8,7 @@ import { generateEmbeddings, generateProceedingSummary } from '../ai/embedding'; import { embeddings as embeddingsTable } from '../db/schema/embeddings'; import { createProceeding } from "../proceedings"; import { nanoid } from 'nanoid'; +import { bills, insertBillSchema } from '../db/schema/bills'; // Helper function to detect section headers function detectSection(content: string): string | null { @@ -143,7 +144,7 @@ export const uploadDocument = async ( originalFileName: file.name, }); - // If it's a parliamentary bulletin, create the summary + // Handle different document types if (type === 'parliamentary_bulletin') { if (!bulletinDate) { throw new Error('Bulletin date is required'); @@ -153,10 +154,27 @@ export const uploadDocument = async ( await createProceeding({ title, - date: bulletinDate, // Use the date from the form input + date: bulletinDate, summary, originalText: text, }); + } else if (type === 'bill') { + const billNumber = formData.get('billNumber') as string; + const sessionNumber = formData.get('sessionNumber') as string; + const status = formData.get('status') as string; + const passageDate = formData.get('passageDate') as string; + + const summary = await generateBillSummary(text); + + await db.insert(bills).values({ + title, + status, + summary, + originalText: text, + billNumber, + sessionNumber, + passageDate: passageDate ? new Date(passageDate) : null, + }); } return { @@ -171,4 +189,41 @@ export const uploadDocument = async ( message: 'Error uploading document: ' + (error as Error).message }; } -}; \ No newline at end of file +}; + +async function generateBillSummary(text: string): Promise { + try { + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'system', + content: 'You are a legal expert specializing in summarizing legislative bills. Create clear, concise summaries that highlight the key points, main objectives, and potential impacts of the bill.' + }, + { + role: 'user', + content: `Please provide a concise summary of this bill: ${text}` + } + ], + temperature: 0.3, + max_tokens: 500, + }), + }); + + if (!response.ok) { + throw new Error('Failed to generate summary'); + } + + const data = await response.json(); + return data.choices[0].message.content.trim(); + } catch (error) { + console.error('Error generating bill summary:', error); + return 'Error generating summary. Please try again later.'; + } +} \ No newline at end of file diff --git a/lib/db/index.ts b/lib/db/index.ts index bc1e8e9..278097a 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -1,7 +1,18 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import * as documents from './schema/documents'; +import * as embeddings from './schema/embeddings'; +import * as bills from './schema/bills'; +import * as chatThreads from './schema/chat-threads'; import { env } from "@/lib/env.mjs"; const client = postgres(env.DATABASE_URL); -export const db = drizzle(client); +export const db = drizzle(client, { + schema: { + ...documents, + ...embeddings, + ...bills, + ...chatThreads, + }, +}); diff --git a/lib/db/migrations/0002_open_guardian.sql b/lib/db/migrations/0002_open_guardian.sql new file mode 100644 index 0000000..caced22 --- /dev/null +++ b/lib/db/migrations/0002_open_guardian.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "bills" ( + "id" varchar(191) PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "status" varchar(50) NOT NULL, + "summary" text NOT NULL, + "original_text" text NOT NULL, + "passage_date" timestamp, + "session_number" text, + "bill_number" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); diff --git a/lib/db/migrations/meta/0002_snapshot.json b/lib/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..7715a71 --- /dev/null +++ b/lib/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,351 @@ +{ + "id": "431ede29-a6f1-4302-929b-dc67169f9a75", + "prevId": "1da3f21a-e433-49bd-bc00-056af3f2c428", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.bills": { + "name": "bills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_text": { + "name": "original_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passage_date": { + "name": "passage_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "session_number": { + "name": "session_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bill_number": { + "name": "bill_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.chat_threads": { + "name": "chat_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pehchan_id": { + "name": "pehchan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_file_name": { + "name": "original_file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.embeddings": { + "name": "embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "embeddingIndex": { + "name": "embeddingIndex", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "embeddings_resource_id_documents_id_fk": { + "name": "embeddings_resource_id_documents_id_fk", + "tableFrom": "embeddings", + "tableTo": "documents", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.parliamentary_proceedings": { + "name": "parliamentary_proceedings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_text": { + "name": "original_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.resources": { + "name": "resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 241fbdf..b77354f 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1733227506159, "tag": "0001_mushy_quentin_quire", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1734341801203, + "tag": "0002_open_guardian", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/schema/bills.ts b/lib/db/schema/bills.ts new file mode 100644 index 0000000..12d00f6 --- /dev/null +++ b/lib/db/schema/bills.ts @@ -0,0 +1,32 @@ +import { sql } from "drizzle-orm"; +import { text, timestamp, pgTable, varchar } from "drizzle-orm/pg-core"; +import { createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { nanoid } from '@/lib/utils' + +export const bills = pgTable("bills", { + id: varchar("id", { length: 191 }).primaryKey().$defaultFn(() => nanoid()), + title: text("title").notNull(), + status: varchar("status", { length: 50 }).notNull(), // pending, passed, rejected + summary: text("summary").notNull(), + originalText: text("original_text").notNull(), + passageDate: timestamp("passage_date"), + sessionNumber: text("session_number"), + billNumber: text("bill_number"), + createdAt: timestamp("created_at") + .notNull() + .default(sql`now()`), + updatedAt: timestamp("updated_at") + .notNull() + .default(sql`now()`), +}); + +export const insertBillSchema = createSelectSchema(bills) + .omit({ + id: true, + createdAt: true, + updatedAt: true, + }); + +export type Bill = typeof bills.$inferSelect; +export type NewBillParams = typeof bills.$inferInsert; \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03..40c3d68 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/tsconfig.json b/tsconfig.json index 1b03bc2..22092f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -15,15 +19,25 @@ "jsx": "preserve", "baseUrl": ".", "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] }, "plugins": [ { "name": "next" } ], - "strictNullChecks": true + "strictNullChecks": true, + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }