diff --git a/README.md b/README.md
index 42121c6..8b365d4 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,30 @@
-## Research Paper Context Builder
+# Research Paper Context Builder
-A small web app that helps you quickly understand research papers by turning a PDF into a concise, structured context summary.
+A Next.js web application that helps you quickly understand research papers by generating structured AI-powered summaries.
-The app extracts text from a PDF and asks an LLM to summarise it into clear sections like **Key Findings**, **Evidence & Methodology**, **Limitations & Improvements**, **Future Work**, and **Practical Implications**.
+## Features
-### Features
+- **PDF Upload & Summarize**: Upload research papers and get AI-generated structured summaries with Key Findings, Evidence & Methodology, Limitations, Future Work, and Practical Implications
+- **Compare Papers**: Compare multiple papers side-by-side to identify similarities, differences, and complementary insights
+- **Find Citations**: Search for related academic papers using the Semantic Scholar API
-- **PDF upload**: Drop in any research paper in PDF format.
-- **Automatic text extraction**: Uses `PyPDF2` to read the text from each page.
-- **Structured summary**: Short bullet-point sections designed to be skimmable.
-- **User‑friendly UI**: Built with Streamlit; runs locally in your browser.
-
-### 1. Prerequisites
-
-- Python 3.9+ installed
-- An OpenAI API key (or compatible API) with access to the specified model
-
-### 2. Installation
-
-From the project folder (`Context_builder`):
-
-```bash
-python -m venv .venv
-.venv\Scripts\activate
-pip install -r requirements.txt
-```
-
-### 3. Environment variables
-
-Create a `.env` file in the project root with:
-
-```bash
-OPENAI_API_KEY=your_api_key_here
-# Optional – override the default model:
-OPENAI_MODEL=gpt-4.1-mini
-```
-
-> You can also set these as normal environment variables instead of using a `.env` file.
-
-### 4. Run the app
-
-From the project root:
+## Getting Started
```bash
-streamlit run app.py
+pnpm install
+pnpm dev
```
-Then open the URL shown in the terminal (usually `http://localhost:8501`) in your browser.
+Open [http://localhost:3000](http://localhost:3000) in your browser.
-### 5. Usage
+## Environment Variables
-1. Upload a research paper PDF from the left sidebar.
-2. (Optional) Toggle **Show extracted text preview** to inspect what was read from the PDF.
-3. Click **Generate Context Summary**.
-4. Read the structured summary on the right side of the page.
+The app uses the Vercel AI Gateway by default, which requires no additional configuration when deployed on Vercel.
-Each segment is intentionally short and concise, making it easy to build context quickly and compare multiple papers.
+## Tech Stack
+- Next.js 16
+- React 19
+- AI SDK 6
+- Tailwind CSS 4
+- PDF.js for PDF parsing
diff --git a/app.py b/app.py
deleted file mode 100644
index 9c0481e..0000000
--- a/app.py
+++ /dev/null
@@ -1,586 +0,0 @@
-import os
-from textwrap import shorten
-
-import streamlit as st
-from dotenv import load_dotenv
-from PyPDF2 import PdfReader
-import google.generativeai as genai
-import requests
-
-
-load_dotenv()
-
-GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
-GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
-
-
-@st.cache_data(show_spinner=False)
-def extract_text_from_pdf(uploaded_file) -> str:
- reader = PdfReader(uploaded_file)
- pages_text = []
- for page in reader.pages:
- text = page.extract_text() or ""
- cleaned = " ".join(text.split())
- if cleaned:
- pages_text.append(cleaned)
- return "\n\n".join(pages_text)
-
-
-def build_prompt(paper_text: str) -> str:
- # Truncate very long papers to keep within model limits
- # (rough heuristic; you can adjust this)
- max_chars = 12000
- trimmed_text = paper_text[:max_chars]
-
- return f"""
-You are a research assistant. Read the following research paper text and create a **very concise, well‑written context summary**.
-
-Summarise into the following sections. Each bullet should be short, specific, and easy to scan:
-
-1. Key Findings
-2. Evidence & Methodology
-3. Limitations & Improvements
-4. Future Work / Open Questions
-5. Practical Implications / Applications
-
-Rules:
-- Use plain, grammatical English; avoid heavy jargon where possible.
-- Prefer 3–6 bullets per section.
-- Each bullet should be one short, complete sentence (not fragments).
-- Do NOT restate the full abstract; focus on the most important points.
-- If the information for a section is missing, write "Not clearly specified in the provided text."
-
-Return the answer in **Markdown** with `##` headings for each section, clean spacing, and no duplicated headings.
-
-Paper text:
-\"\"\"{trimmed_text}\"\"\"
-"""
-
-
-def generate_structured_summary(paper_text: str) -> str:
- if not GEMINI_API_KEY:
- return "Error: GEMINI_API_KEY is not set. Please add it to a .env file or your environment variables."
-
- genai.configure(api_key=GEMINI_API_KEY)
-
- prompt = build_prompt(paper_text)
-
- # #region agent log
- try:
- import json
- from datetime import datetime
-
- log_entry = {
- "sessionId": "e2eb50",
- "runId": "pre-fix",
- "hypothesisId": "H_model_name",
- "location": "app.py:generate_structured_summary",
- "message": "About to call Gemini model",
- "data": {
- "model": GEMINI_MODEL,
- "paper_text_chars": len(paper_text or ""),
- },
- "timestamp": int(datetime.utcnow().timestamp() * 1000),
- }
- with open("debug-e2eb50.log", "a", encoding="utf-8") as f:
- f.write(json.dumps(log_entry) + "\n")
- except Exception:
- pass
- # #endregion agent log
-
- model = genai.GenerativeModel(GEMINI_MODEL)
- response = model.generate_content(
- prompt,
- generation_config={
- "temperature": 0.2,
- },
- )
-
- return (response.text or "").strip() if hasattr(response, "text") else "No text returned by Gemini."
-
-
-def build_compare_prompt(papers: list[dict]) -> str:
- """
- Build a prompt to compare multiple papers.
-
- Each item in `papers` should have: id, label, summary.
- """
- numbered_blocks = []
- for idx, p in enumerate(papers, start=1):
- numbered_blocks.append(
- f"Paper {idx} ({p['label']}):\n\n{p['summary']}\n"
- )
-
- joined = "\n\n".join(numbered_blocks)
-
- return f"""
-You are helping a researcher quickly understand **relationships between multiple research papers**.
-Each paper below is already summarised into key findings, evidence, limitations, and implications.
-
-Using only the information provided, create a clear, structured comparison.
-
-Required sections (in this order):
-1. Overall Topic Similarity
-2. Shared Ideas / Overlaps
-3. Key Differences in Findings
-4. Differences in Methods / Evidence
-5. Complementary Insights (how they reinforce each other)
-6. Conflicts or Tensions (where they disagree or diverge)
-7. Common Technologies / Techniques / Domains
-
-Rules:
-- Use skimmable bullet points for each section.
-- Keep language precise and neutral.
-- Call the papers "Paper 1", "Paper 2", "Paper 3" (matching the order below).
-- If something is not clear from the summaries, say "Not specified in the summaries."
-
-Return the answer in Markdown with `##` headings for each section.
-
-Paper summaries:
-{joined}
-"""
-
-
-def generate_comparison(papers: list[dict]) -> str:
- if not GEMINI_API_KEY:
- return "Error: GEMINI_API_KEY is not set. Please add it to a .env file or your environment variables."
-
- genai.configure(api_key=GEMINI_API_KEY)
-
- prompt = build_compare_prompt(papers)
-
- model = genai.GenerativeModel(GEMINI_MODEL)
- response = model.generate_content(
- prompt,
- generation_config={
- "temperature": 0.2,
- },
- )
-
- return (response.text or "").strip() if hasattr(response, "text") else "No text returned by Gemini."
-
-
-def fetch_citations_from_semantic_scholar(idea_text: str, limit: int = 8) -> list[dict]:
- """Fetch candidate citations from Semantic Scholar API with simple fallbacks.
-
- Strategy:
- - Try the full idea (trimmed) as a query.
- - If nothing is found, try the first sentence.
- - If still nothing is found, keep the first 10–15 content words.
- """
-
- base_url = "https://api.semanticscholar.org/graph/v1/paper/search"
-
- def _run_query(q: str, tag: str) -> list[dict]:
- q_clean = " ".join(q.split())
- if not q_clean:
- return []
-
- params = {
- "query": q_clean,
- "limit": limit,
- "fields": "title,authors,year,venue,doi,url,isOpenAccess",
- }
-
- # #region agent log
- try:
- import json
- from datetime import datetime
-
- log_entry = {
- "sessionId": "e2eb50",
- "runId": "citations",
- "hypothesisId": "H_semantic_scholar_query",
- "location": "app.py:fetch_citations_from_semantic_scholar",
- "message": "Calling Semantic Scholar",
- "data": {
- "query_tag": tag,
- "query_preview": q_clean[:120],
- },
- "timestamp": int(datetime.utcnow().timestamp() * 1000),
- }
- with open("debug-e2eb50.log", "a", encoding="utf-8") as f:
- f.write(json.dumps(log_entry) + "\n")
- except Exception:
- pass
- # #endregion agent log
-
- try:
- resp = requests.get(base_url, params=params, timeout=10)
- resp.raise_for_status()
- data = resp.json()
- return data.get("data", [])
- except Exception:
- return []
-
- # Candidate queries
- trimmed = idea_text.strip()
- queries: list[tuple[str, str]] = []
-
- # 1) Full idea, truncated to a reasonable length
- if trimmed:
- queries.append((trimmed[:400], "full_idea"))
-
- # 2) First sentence only
- sep_idx = min(
- [idx for idx in (trimmed.find("."), trimmed.find("?"), trimmed.find("!")) if idx != -1],
- default=-1,
- )
- if sep_idx != -1:
- first_sentence = trimmed[: sep_idx + 1]
- queries.append((first_sentence, "first_sentence"))
-
- # 3) Heuristic keywords (first ~15 words)
- words = trimmed.split()
- if words:
- keywords = " ".join(words[:15])
- queries.append((keywords, "first_keywords"))
-
- seen_ids = set()
- combined_results: list[dict] = []
-
- for q, tag in queries:
- if len(combined_results) >= limit:
- break
- results = _run_query(q, tag)
- for paper in results:
- paper_id = paper.get("paperId") or paper.get("doi") or paper.get("url")
- if not paper_id or paper_id in seen_ids:
- continue
- seen_ids.add(paper_id)
- combined_results.append(paper)
- if len(combined_results) >= limit:
- break
-
- return combined_results
-
-
-def format_citation_results(results: list[dict]) -> str:
- if not results:
- return "No matching papers were found for this query. Try rephrasing or broadening your idea."
-
- # Sort by year (desc) so recent work appears first when the field is present
- results_sorted = sorted(
- results,
- key=lambda r: r.get("year") or 0,
- reverse=True,
- )
-
- lines = ["## Suggested citations\n"]
- for paper in results_sorted:
- title = paper.get("title") or "Untitled"
- year = paper.get("year") or "n.d."
- venue = paper.get("venue") or "Venue not specified"
- authors = paper.get("authors") or []
- author_names = ", ".join(a.get("name") for a in authors[:4] if a.get("name"))
- if len(authors) > 4:
- author_names += " et al."
-
- doi = paper.get("doi")
- url = paper.get("url")
-
- lines.append(f"- **{title}** ({year})")
- if author_names:
- lines.append(f" - Authors: {author_names}")
- lines.append(f" - Venue: {venue}")
- if doi:
- lines.append(f" - DOI: `{doi}`")
- if url:
- lines.append(f" - Link: {url}")
- lines.append("") # blank line between entries
-
- lines.append(
- "> These suggestions come from the Semantic Scholar API and are intended as a starting point. "
- "Always read and verify each paper before citing it."
- )
-
- return "\n".join(lines)
-
-
-def render_paper_workspace(slot_id: str, title: str) -> None:
- """One independent 'window' for a single paper."""
- upload_key = f"upload_{slot_id}"
- toggle_key = f"show_raw_{slot_id}"
- summary_key = f"summary_md_{slot_id}"
-
- st.markdown(
- f"""
-
-
- {title}
-
-
- Upload a paper on the left and view its structured context on the right.
-
- Choose a PDF and (optionally) inspect the extracted text before creating the summary.
-
-
- """,
- unsafe_allow_html=True,
- )
-
- uploaded_file = st.file_uploader(
- "Upload a PDF",
- type=["pdf"],
- key=upload_key,
- help="Upload a research article in PDF format.",
- )
- show_raw_preview = st.toggle(
- "Show extracted text preview",
- value=False,
- key=toggle_key,
- )
-
- if not uploaded_file:
- st.info("Upload a PDF in this workspace to get started.")
- return
-
- with st.spinner("Extracting text from PDF..."):
- paper_text = extract_text_from_pdf(uploaded_file)
- # Remember raw text for possible future comparison
- st.session_state[f"text_{slot_id}"] = paper_text
-
- if not paper_text.strip():
- st.error("Could not extract text from this PDF. It may be scanned or image-only.")
- return
-
- if show_raw_preview:
- with st.expander("Extracted text (cleaned)"):
- st.text_area(
- "Extracted text",
- value=paper_text,
- height=350,
- )
-
- if st.button("Generate Context Summary", type="primary", key=f"generate_{slot_id}"):
- with st.spinner("Generating structured summary..."):
- summary_md = generate_structured_summary(paper_text)
- st.session_state[summary_key] = summary_md
-
- with col_right:
- st.markdown(
- """
-
- Context Summary
-
- """,
- unsafe_allow_html=True,
- )
-
- if summary_key in st.session_state:
- st.markdown(st.session_state[summary_key])
- else:
- st.info(
- "Click **Generate Context Summary** after uploading a PDF in this panel to see the structured summary here."
- )
-
-
-def main():
- st.set_page_config(
- page_title="Research Paper Context Builder",
- page_icon="📚",
- layout="wide",
- )
-
- # Global button style (lavender)
- st.markdown(
- """
-
- """,
- unsafe_allow_html=True,
- )
-
- st.markdown(
- """
-
-
- R
-
-
-
- Research Paper Context Builder
-
-
- Choose a tool below: summarise PDFs, compare papers, or find citations for your own idea.
-
- """,
- unsafe_allow_html=True,
- )
-
- st.subheader("Workspaces")
- st.caption("Each workspace is independent, so you can compare multiple papers side by side.")
-
- tab1, tab2, tab3 = st.tabs(["Paper 1", "Paper 2", "Paper 3"])
-
- with tab1:
- render_paper_workspace("paper1", "Workspace: Paper 1")
-
- with tab2:
- render_paper_workspace("paper2", "Workspace: Paper 2")
-
- with tab3:
- render_paper_workspace("paper3", "Workspace: Paper 3")
-
- # --- Tab 2: Compare existing summaries ---
- with tabs[1]:
- st.subheader("Compare papers")
- st.caption("Select at least two workspaces with generated summaries to see similarities and differences.")
-
- slots = [
- ("paper1", "Paper 1"),
- ("paper2", "Paper 2"),
- ("paper3", "Paper 3"),
- ]
- available = []
- for slot_id, label in slots:
- summary_key = f"summary_md_{slot_id}"
- if summary_key in st.session_state:
- available.append(
- {
- "id": slot_id,
- "label": label,
- "summary": st.session_state[summary_key],
- }
- )
-
- if len(available) < 2:
- st.info("Generate summaries for at least two papers in the 'Upload & summarise' tab first.")
- else:
- option_labels = [p["label"] for p in available]
- selected_labels = st.multiselect(
- "Choose which papers to compare",
- options=option_labels,
- default=option_labels,
- )
-
- label_to_paper = {p["label"]: p for p in available}
- selected_papers = [label_to_paper[lbl] for lbl in selected_labels if lbl in label_to_paper]
-
- if len(selected_papers) < 2:
- st.warning("Select at least two papers for a meaningful comparison.")
- else:
- if st.button("Compare selected papers", type="primary", key="compare_button"):
- with st.spinner("Generating comparison..."):
- comparison_md = generate_comparison(selected_papers)
- st.session_state["comparison_md"] = comparison_md
-
- if "comparison_md" in st.session_state:
- st.markdown(
- """
-
- Comparison overview
-
- """,
- unsafe_allow_html=True,
- )
- st.markdown(st.session_state["comparison_md"])
-
- # --- Tab 3: Find citations for a free‑text idea ---
- with tabs[2]:
- st.subheader("Find citations for your idea")
- st.caption(
- "Describe your research idea or paragraph in plain language. "
- "The tool will suggest recent, peer‑reviewed papers as starting points."
- )
-
- idea_text = st.text_area(
- "Describe your research idea or write a short paragraph:",
- height=180,
- placeholder="Example: Investigating how large language models can help clinicians summarise patient histories more accurately...",
- )
-
- col_a, col_b = st.columns([1, 3])
- with col_a:
- max_results = st.slider("Number of suggested papers", 3, 15, 8)
- with col_b:
- st.write("")
-
- if st.button("Find citations", type="primary", key="find_citations"):
- if not idea_text.strip():
- st.warning("Please enter a short description of your idea first.")
- else:
- with st.spinner("Searching for relevant papers..."):
- raw_results = fetch_citations_from_semantic_scholar(idea_text, limit=max_results)
- citations_md = format_citation_results(raw_results)
- st.session_state["citations_md"] = citations_md
-
- if "citations_md" in st.session_state:
- st.markdown(st.session_state["citations_md"])
-
-
-if __name__ == "__main__":
- main()
-
diff --git a/app/api/citations/route.ts b/app/api/citations/route.ts
new file mode 100644
index 0000000..70603d4
--- /dev/null
+++ b/app/api/citations/route.ts
@@ -0,0 +1,92 @@
+interface SemanticScholarPaper {
+ paperId?: string;
+ title?: string;
+ authors?: { name?: string }[];
+ year?: number;
+ venue?: string;
+ doi?: string;
+ url?: string;
+ isOpenAccess?: boolean;
+}
+
+async function runQuery(
+ query: string,
+ limit: number
+): Promise {
+ const cleanQuery = query.split(/\s+/).join(" ").trim();
+ if (!cleanQuery) return [];
+
+ const params = new URLSearchParams({
+ query: cleanQuery,
+ limit: String(limit),
+ fields: "title,authors,year,venue,doi,url,isOpenAccess",
+ });
+
+ try {
+ const resp = await fetch(
+ `https://api.semanticscholar.org/graph/v1/paper/search?${params}`,
+ { signal: AbortSignal.timeout(10000) }
+ );
+
+ if (!resp.ok) return [];
+ const data = await resp.json();
+ return data.data || [];
+ } catch {
+ return [];
+ }
+}
+
+export async function POST(req: Request) {
+ const { ideaText, limit = 8 } = await req.json();
+
+ if (!ideaText || typeof ideaText !== "string") {
+ return Response.json({ error: "No idea text provided" }, { status: 400 });
+ }
+
+ const trimmed = ideaText.trim();
+ const queries: string[] = [];
+
+ // Full idea (truncated)
+ if (trimmed) {
+ queries.push(trimmed.slice(0, 400));
+ }
+
+ // First sentence
+ const sepIdx = Math.min(
+ ...[trimmed.indexOf("."), trimmed.indexOf("?"), trimmed.indexOf("!")].filter(
+ (i) => i !== -1
+ )
+ );
+ if (sepIdx !== Infinity && sepIdx !== -1) {
+ queries.push(trimmed.slice(0, sepIdx + 1));
+ }
+
+ // First 15 words
+ const words = trimmed.split(/\s+/);
+ if (words.length > 0) {
+ queries.push(words.slice(0, 15).join(" "));
+ }
+
+ const seenIds = new Set();
+ const combinedResults: SemanticScholarPaper[] = [];
+
+ for (const q of queries) {
+ if (combinedResults.length >= limit) break;
+
+ const results = await runQuery(q, limit);
+ for (const paper of results) {
+ const paperId = paper.paperId || paper.doi || paper.url;
+ if (!paperId || seenIds.has(paperId)) continue;
+
+ seenIds.add(paperId);
+ combinedResults.push(paper);
+
+ if (combinedResults.length >= limit) break;
+ }
+ }
+
+ // Sort by year descending
+ combinedResults.sort((a, b) => (b.year || 0) - (a.year || 0));
+
+ return Response.json({ citations: combinedResults });
+}
diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts
new file mode 100644
index 0000000..e7571c7
--- /dev/null
+++ b/app/api/compare/route.ts
@@ -0,0 +1,68 @@
+import { generateText } from "ai";
+
+interface Paper {
+ id: string;
+ label: string;
+ summary: string;
+}
+
+export async function POST(req: Request) {
+ const { papers } = await req.json();
+
+ if (!papers || !Array.isArray(papers) || papers.length < 2) {
+ return Response.json(
+ { error: "At least two papers are required" },
+ { status: 400 }
+ );
+ }
+
+ const numberedBlocks = papers
+ .map(
+ (p: Paper, idx: number) =>
+ `Paper ${idx + 1} (${p.label}):\n\n${p.summary}\n`
+ )
+ .join("\n\n");
+
+ const prompt = `
+You are helping a researcher quickly understand **relationships between multiple research papers**.
+Each paper below is already summarised into key findings, evidence, limitations, and implications.
+
+Using only the information provided, create a clear, structured comparison.
+
+Required sections (in this order):
+1. Overall Topic Similarity
+2. Shared Ideas / Overlaps
+3. Key Differences in Findings
+4. Differences in Methods / Evidence
+5. Complementary Insights (how they reinforce each other)
+6. Conflicts or Tensions (where they disagree or diverge)
+7. Common Technologies / Techniques / Domains
+
+Rules:
+- Use skimmable bullet points for each section.
+- Keep language precise and neutral.
+- Call the papers "Paper 1", "Paper 2", "Paper 3" (matching the order below).
+- If something is not clear from the summaries, say "Not specified in the summaries."
+
+Return the answer in Markdown with \`##\` headings for each section.
+
+Paper summaries:
+${numberedBlocks}
+`;
+
+ try {
+ const result = await generateText({
+ model: "openai/gpt-4o-mini",
+ prompt,
+ temperature: 0.2,
+ });
+
+ return Response.json({ comparison: result.text });
+ } catch (error) {
+ console.error("Error generating comparison:", error);
+ return Response.json(
+ { error: "Failed to generate comparison" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/parse-pdf/route.ts b/app/api/parse-pdf/route.ts
new file mode 100644
index 0000000..f5067b7
--- /dev/null
+++ b/app/api/parse-pdf/route.ts
@@ -0,0 +1,48 @@
+import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
+
+// Disable worker in serverless environment
+GlobalWorkerOptions.workerSrc = "";
+
+export async function POST(req: Request) {
+ const formData = await req.formData();
+ const file = formData.get("file") as File | null;
+
+ if (!file) {
+ return Response.json({ error: "No file provided" }, { status: 400 });
+ }
+
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const uint8Array = new Uint8Array(arrayBuffer);
+
+ const pdf = await getDocument({
+ data: uint8Array,
+ useSystemFonts: true,
+ disableFontFace: true,
+ }).promise;
+
+ const textParts: string[] = [];
+
+ for (let i = 1; i <= pdf.numPages; i++) {
+ const page = await pdf.getPage(i);
+ const content = await page.getTextContent();
+ const pageText = content.items
+ .map((item) => ("str" in item ? item.str : ""))
+ .join(" ");
+ textParts.push(pageText);
+ }
+
+ // Clean and join text
+ const cleanedText = textParts
+ .join("\n\n")
+ .split(/\n+/)
+ .map((line: string) => line.trim())
+ .filter((line: string) => line.length > 0)
+ .join("\n\n");
+
+ return Response.json({ text: cleanedText });
+ } catch (error) {
+ console.error("Error parsing PDF:", error);
+ return Response.json({ error: "Failed to parse PDF" }, { status: 500 });
+ }
+}
diff --git a/app/api/summarize/route.ts b/app/api/summarize/route.ts
new file mode 100644
index 0000000..605b061
--- /dev/null
+++ b/app/api/summarize/route.ts
@@ -0,0 +1,52 @@
+import { generateText } from "ai";
+
+export async function POST(req: Request) {
+ const { text } = await req.json();
+
+ if (!text || typeof text !== "string") {
+ return Response.json({ error: "No text provided" }, { status: 400 });
+ }
+
+ const maxChars = 12000;
+ const trimmedText = text.slice(0, maxChars);
+
+ const prompt = `
+You are a research assistant. Read the following research paper text and create a **very concise, well‑written context summary**.
+
+Summarise into the following sections. Each bullet should be short, specific, and easy to scan:
+
+1. Key Findings
+2. Evidence & Methodology
+3. Limitations & Improvements
+4. Future Work / Open Questions
+5. Practical Implications / Applications
+
+Rules:
+- Use plain, grammatical English; avoid heavy jargon where possible.
+- Prefer 3–6 bullets per section.
+- Each bullet should be one short, complete sentence (not fragments).
+- Do NOT restate the full abstract; focus on the most important points.
+- If the information for a section is missing, write "Not clearly specified in the provided text."
+
+Return the answer in **Markdown** with \`##\` headings for each section, clean spacing, and no duplicated headings.
+
+Paper text:
+"""${trimmedText}"""
+`;
+
+ try {
+ const result = await generateText({
+ model: "openai/gpt-4o-mini",
+ prompt,
+ temperature: 0.2,
+ });
+
+ return Response.json({ summary: result.text });
+ } catch (error) {
+ console.error("Error generating summary:", error);
+ return Response.json(
+ { error: "Failed to generate summary" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..e9515b5
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,29 @@
+@import "tailwindcss";
+
+:root {
+ --background: #ffffff;
+ --foreground: #0f172a;
+ --card: #ffffff;
+ --card-foreground: #0f172a;
+ --primary: #4f46e5;
+ --primary-foreground: #ffffff;
+ --secondary: #f1f5f9;
+ --secondary-foreground: #0f172a;
+ --muted: #f8fafc;
+ --muted-foreground: #64748b;
+ --accent: #e9d5ff;
+ --accent-foreground: #312e81;
+ --border: #e2e8f0;
+ --ring: #4f46e5;
+ --radius: 0.75rem;
+}
+
+body {
+ background-color: var(--background);
+ color: var(--foreground);
+ font-family: system-ui, -apple-system, sans-serif;
+}
+
+* {
+ border-color: var(--border);
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..3cdac6e
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,24 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+
+const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
+
+export const metadata: Metadata = {
+ title: "Research Paper Context Builder",
+ description: "Summarize PDFs, compare papers, and find citations for your research ideas",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..d342aad
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,177 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import { FileText, GitCompare, Search } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { PaperWorkspace } from "@/components/paper-workspace";
+import { PaperCompare } from "@/components/paper-compare";
+import { CitationFinder } from "@/components/citation-finder";
+
+type Tab = "summarize" | "compare" | "citations";
+type PaperTab = "paper1" | "paper2" | "paper3";
+
+interface PaperSummary {
+ id: string;
+ label: string;
+ summary: string;
+}
+
+export default function Home() {
+ const [activeTab, setActiveTab] = useState("summarize");
+ const [activePaper, setActivePaper] = useState("paper1");
+ const [summaries, setSummaries] = useState>({});
+
+ const handleSummaryGenerated = useCallback(
+ (id: string, summary: string, label: string) => {
+ setSummaries((prev) => ({
+ ...prev,
+ [id]: { id, label, summary },
+ }));
+ },
+ []
+ );
+
+ const paperSummariesArray = Object.values(summaries);
+
+ return (
+
+
+ {/* Header */}
+
+
+ R
+
+
+
+ Research Paper Context Builder
+
+
+ Choose a tool below: summarise PDFs, compare papers, or find
+ citations for your own idea.
+