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. -
-
- """, - unsafe_allow_html=True, - ) - - col_left, col_right = st.columns([1.05, 1.95]) - - with col_left: - st.markdown( - """ -
-
- Inputs -
-
- 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.markdown("---") - - tabs = st.tabs(["📄 Upload & summarise", "🧩 Compare papers", "🔍 Find citations from text"]) - - # --- Tab 1: Summarise PDFs --- - with tabs[0]: - st.markdown( - """ -
-
- Processing flow -
-
-
1. Upload PDF
-
→
-
2. Extract & clean text
-
→
-
3. Build structured prompt
-
→
-
4. Gemini generates summary
-
→
-
5. Read context by section
-
-
- """, - 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. +

+
+
+ +
+ + {/* Main Tabs */} +
+ + + +
+ + {/* Tab Content */} + {activeTab === "summarize" && ( +
+ {/* Processing Flow */} +
+

+ Processing flow +

+
+ + 1. Upload PDF + + → + + 2. Extract & clean text + + → + + 3. Build structured prompt + + → + + 4. AI generates summary + + → + + 5. Read context by section + +
+
+ + {/* Workspaces Header */} +
+

Workspaces

+

+ Each workspace is independent, so you can compare multiple papers + side by side. +

+
+ + {/* Paper Tabs */} +
+ {(["paper1", "paper2", "paper3"] as PaperTab[]).map( + (paper, index) => ( + + ) + )} +
+ + {/* Active Paper Workspace */} + +
+ )} + + {activeTab === "compare" && ( + + )} + + {activeTab === "citations" && } +
+
+ ); +} diff --git a/components/citation-finder.tsx b/components/citation-finder.tsx new file mode 100644 index 0000000..31f4aac --- /dev/null +++ b/components/citation-finder.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Loader2, ExternalLink } from "lucide-react"; + +interface Citation { + paperId?: string; + title?: string; + authors?: { name?: string }[]; + year?: number; + venue?: string; + doi?: string; + url?: string; + isOpenAccess?: boolean; +} + +export function CitationFinder() { + const [ideaText, setIdeaText] = useState(""); + const [maxResults, setMaxResults] = useState(8); + const [citations, setCitations] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [error, setError] = useState(""); + const [hasSearched, setHasSearched] = useState(false); + + const handleSearch = useCallback(async () => { + if (!ideaText.trim()) return; + + setIsSearching(true); + setError(""); + setHasSearched(true); + + try { + const response = await fetch("/api/citations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ideaText, limit: maxResults }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch citations"); + } + + setCitations(data.citations || []); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch citations"); + setCitations([]); + } finally { + setIsSearching(false); + } + }, [ideaText, maxResults]); + + return ( +
+
+

+ Find citations for your idea +

+

+ Describe your research idea or paragraph in plain language. The tool + will suggest recent, peer-reviewed papers as starting points. +

+
+ +