Skip to content
Open
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
35 changes: 35 additions & 0 deletions docs/api-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# API Contracts

## POST /upload
Uploads a single PDF file.

**Request:** multipart/form-data with a "file" field.

**Response (200):**
{
"filename": "notes.pdf",
"upload_date": "2026-07-19T10:15:00",
"file_type": "pdf",
"status": "pending",
"metadata": null
}

**Response (400):** if file type is invalid
{
"detail": "Only PDF files are accepted."
}

## GET /uploads
Returns a list of all uploaded files.

**Response (200):**
[
{
"filename": "notes.pdf",
"upload_date": "2026-07-19T10:15:00",
"file_type": "pdf",
"status": "pending",
"metadata": null
},
...
]
6 changes: 4 additions & 2 deletions web/backend/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes.upload import router as upload_router # ADD THIS

app = FastAPI(title="StudyMind AI Backend")

# Allow the React dev server to talk to this API
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # Vite's default port
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(upload_router) # ADD THIS

@app.get("/health")
def health_check():
return {"status": "ok"}
50 changes: 50 additions & 0 deletions web/backend/routes/upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os
from datetime import datetime
from fastapi import APIRouter, UploadFile, File, HTTPException

from database import get_uploads_collection
from models import Upload

router = APIRouter()

UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "uploads")


@router.post("/upload")
async def upload_file(file: UploadFile = File(...)):
# Stretch goal: only accept PDFs
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="Only PDF files are accepted")

# Avoid overwriting files with the same name
timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
safe_filename = f"{timestamp}_{file.filename}"
file_path = os.path.join(UPLOAD_DIR, safe_filename)

os.makedirs(UPLOAD_DIR, exist_ok=True)
with open(file_path, "wb") as f:
content = await file.read()
f.write(content)

upload_record = Upload(
filename=safe_filename,
file_type=file.content_type or "application/pdf",
status="pending",
)

collection = get_uploads_collection()
await collection.insert_one(upload_record.model_dump())

return {
"filename": safe_filename,
"status": "pending",
"message": "File uploaded successfully",
}


@router.get("/uploads")
async def list_uploads():
collection = get_uploads_collection()
cursor = collection.find({}, {"_id": 0})
uploads = await cursor.to_list(length=100)
return {"uploads": uploads}
Empty file added web/backend/uploads/.gitkeep
Empty file.
5 changes: 5 additions & 0 deletions web/frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import './App.css';
import UploadView from './components/UploadView';
import Dashboard from './components/Dashboard';

function App() {
const [status, setStatus] = useState('checking...');
Expand All @@ -15,6 +17,9 @@ function App() {
<div style={{ textAlign: 'center', marginTop: '4rem' }}>
<h1>Quantum Learning Workspace</h1>
<p>Backend status : <strong>{status}</strong></p>

<UploadView />
<Dashboard />
</div>
);
}
Expand Down
37 changes: 37 additions & 0 deletions web/frontend/src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState } from "react";

function Dashboard() {
// This is our FAKE list of uploads, just for now.
// It has the exact same shape we expect the real /uploads
// endpoint to send us later (filename, upload_date, etc.)
const [uploads, setUploads] = useState([
{
filename: "chapter1-notes.pdf",
upload_date: "2026-07-15T10:00:00",
},
{
filename: "physics-lecture.pdf",
upload_date: "2026-07-16T14:30:00",
},
{
filename: "research-paper.pdf",
upload_date: "2026-07-17T09:15:00",
},
]);

return (
<div style={{ padding: "2rem" }}>
<h2>Dashboard — Uploaded Files</h2>

<ul>
{uploads.map((upload) => (
<li key={upload.filename}>
{upload.filename} — {upload.upload_date}
</li>
))}
</ul>
</div>
);
}

export default Dashboard;
44 changes: 44 additions & 0 deletions web/frontend/src/components/UploadView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from "react";

function UploadView() {
// This "box" remembers which file the user picked.
// Right now it's empty (null) because no file is chosen yet.
const [selectedFile, setSelectedFile] = useState(null);

// This function runs automatically when the user picks a file
// using the file picker window.
function handleFileChange(event) {
const file = event.target.files[0]; // grab the first picked file
setSelectedFile(file); // save it into our "box"
}

// This function runs when the user clicks the Upload button.
// For now, it just shows the file name in the console —
// we are NOT sending it to the backend yet.
function handleUploadClick() {
if (!selectedFile) {
alert("Please choose a file first.");
return;
}
console.log("File ready to upload:", selectedFile.name);
}

return (
<div style={{ padding: "2rem" }}>
<h2>Upload a File</h2>

{/* This is the file picker itself */}
<input type="file" onChange={handleFileChange} />

{/* This button triggers the upload action */}
<button onClick={handleUploadClick} style={{ marginLeft: "1rem" }}>
Upload
</button>

{/* A little message showing what file is currently picked */}
{selectedFile && <p>Selected file: {selectedFile.name}</p>}
</div>
);
}

export default UploadView;