From 9c1d9a9b94c3911bb54b018b1be745d13b676ac0 Mon Sep 17 00:00:00 2001 From: v0 Date: Sat, 14 Mar 2026 16:46:55 +0000 Subject: [PATCH 1/5] feat: set up Streamlit app for research paper analysis Add environment variable setup for GEMINI_API_KEY. --- scripts/run-app.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 scripts/run-app.sh diff --git a/scripts/run-app.sh b/scripts/run-app.sh new file mode 100644 index 0000000..5faf855 --- /dev/null +++ b/scripts/run-app.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd /vercel/share/v0-project +pip install -r requirements.txt +streamlit run app.py --server.port 8501 --server.headless true From ab733735a87ea620f25ff5b8b334708d2e217f3a Mon Sep 17 00:00:00 2001 From: v0 Date: Sat, 14 Mar 2026 16:50:29 +0000 Subject: [PATCH 2/5] feat: convert Streamlit app to Next.js with same functionality Migrate research paper analysis app to Next.js with full features --- app/api/citations/route.ts | 92 +++ app/api/compare/route.ts | 68 ++ app/api/parse-pdf/route.ts | 28 + app/api/summarize/route.ts | 52 ++ app/globals.css | 29 + app/layout.tsx | 24 + app/page.tsx | 177 +++++ components/citation-finder.tsx | 194 ++++++ components/paper-compare.tsx | 172 +++++ components/paper-workspace.tsx | 240 +++++++ lib/utils.ts | 6 + next.config.ts | 7 + package.json | 32 + pnpm-lock.yaml | 1179 ++++++++++++++++++++++++++++++++ postcss.config.mjs | 7 + tsconfig.json | 27 + 16 files changed, 2334 insertions(+) create mode 100644 app/api/citations/route.ts create mode 100644 app/api/compare/route.ts create mode 100644 app/api/parse-pdf/route.ts create mode 100644 app/api/summarize/route.ts create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components/citation-finder.tsx create mode 100644 components/paper-compare.tsx create mode 100644 components/paper-workspace.tsx create mode 100644 lib/utils.ts create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 tsconfig.json 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..d9e8873 --- /dev/null +++ b/app/api/parse-pdf/route.ts @@ -0,0 +1,28 @@ +import pdf from "pdf-parse"; + +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 buffer = Buffer.from(arrayBuffer); + const data = await pdf(buffer); + + // Clean and join text + const cleanedText = data.text + .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. +

+
+ +