This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
Opening a 40-page lecture slide deck the night before class is pure dread. You skipped the lecture, you did not do the reading, and you know opening that PDF will take forty minutes of skimming through title cards, bullet points, and derivations you do not need right now.
Last Slide is a skip card for that unread deck. It is not a summarizer. It does not produce study packs, flashcard sets, or WhatsApp digests. When you skip class, you do not want an AI tutor explaining the entire curriculum. You just want the leftover slide still projected on the whiteboard after everyone left the room.
Last Slide extracts the native text from your PDF directly in your browser tab, passes a budgeted slice to Gemma 3 4B running on your local machine via Ollama, and fills exactly four fields:
- What you missed: exactly two sentences explaining what the lecture covered.
- Due: the one homework assignment or due date if it is actually written on a slide, complete with the slide number. If nothing is due, it explicitly says so.
- They will ask: one exam-style question grounded in the slides, so you are ready if called on.
- You can skip: one sentence pointing out the appendix, optional reading, or repeated examples you can safely ignore.
No files leave your computer. Scanned PDFs without text layers are refused immediately.
Demo
- Live UI: https://last-slide.onrender.com/
- Source Code: https://github.com/Faris0520/last-slide
Running locally
- Install Ollama and pull Gemma 3 4B:
ollama pull gemma3:4b
ollama serve
- Clone and start the client:
pnpm install
pnpm sample
pnpm dev
Open the local Vite URL in your browser. You can click Load sample to run the bundled 12-page sample deck (CS 101 Hash Tables), or drag and drop your own lecture PDF.
If you access a deployed frontend over HTTPS, the browser blocks requests to localhost:11434 by default. Start Ollama with the origin flag to allow cross-origin requests:
OLLAMA_ORIGINS="https://last-slide.onrender.com" ollama serve
On Windows PowerShell:
$env:OLLAMA_ORIGINS="https://last-slide.onrender.com"; ollama serve
Code
The project is open source under the MIT license:
Last Slide
A skip card for the lecture PDF you never opened.
Drop a native-text slide deck. Last Slide reads it in this browser tab (pdf.js), keeps only a budget of pages (deadline language always included), and asks Gemma 3 4B via local Ollama for four fields: two sentences of what you missed, the one due date if it is actually on a slide, one exam question, and what you can skip. Scans are refused. Nothing is uploaded.
Setup
- Install Ollama and pull the model:
ollama pull gemma3:4b
ollama serve
- Install and run the app:
pnpm install
pnpm sample
pnpm dev
Open the URL Vite prints. Load the bundled sample (CS 101 Hash Tables) or drop your own PDF. Confirm before anything is sent to http://localhost:11434.
If you open a deployed https page (Render), the browser will block localhost unless Ollama allows that origin:
OLLAMA_ORIGINS=https://YOUR-APP.onrender.com…The core logic is divided into three focused TypeScript modules:
-
src/lib/pdf.ts: Extracts text per page in the browser tab usingpdfjs-dist. Refuses scans if total text is under 50 characters. -
src/lib/ingest.ts: Manages a strict 4,500-character budget. Runs a regex pre-pass for deadline keywords (tugas,homework,due,deadline,submit) so homework slides are never cut off by early filler slides. -
src/lib/card.ts: Defines the strict JSON schema and system prompt that confines Gemma 3 4B to four fields. -
src/lib/ollama.ts: Probes local Ollama health and posts to/api/chatwith schema constraints.
Here is the keyword-first budgeting from src/lib/ingest.ts:
export const CHAR_BUDGET = 4_500
export const DEADLINE_RE =
/\b(tugas|pengumpulan|homework|assignment|due\s+date|\bdue\b|deadline|uts|uas|submit)\b/i
export function planIngest(pages: PdfPage[]): IngestPlan {
const keyword = pages.filter((p) => DEADLINE_RE.test(p.text))
const keywordNs = new Set(keyword.map((p) => p.n))
const sent: PdfPage[] = []
let chars = 0
function take(page: PdfPage) {
if (chars >= CHAR_BUDGET) return
const room = CHAR_BUDGET - chars
const text = page.text.length > room ? page.text.slice(0, room) : page.text
sent.push({ n: page.n, text })
chars += text.length
}
// Prioritize pages containing deadline language
for (const page of keyword) take(page)
// Fill remaining budget with sequential slides
for (const page of pages) {
if (keywordNs.has(page.n)) continue
take(page)
}
sent.sort((a, b) => a.n - b.n)
const sentNs = new Set(sent.map((p) => p.n))
const skipped = pages.filter((p) => !sentNs.has(p.n)).map((p) => p.n)
return { sent, skipped, keywordPages: keyword.map((p) => p.n), chars }
}
And the Ollama call in src/lib/ollama.ts sending the raw JSON schema directly into format:
export async function completeViaOllama(
url: string,
model: string,
messages: ChatTurn[],
schema: Record<string, unknown>,
): Promise<string> {
const res = await fetch(`${base(url)}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages,
format: schema,
stream: false,
options: { temperature: 0.2, top_p: 0.9 },
}),
})
if (!res.ok) {
const detail = await res.text().catch(() => '')
throw new Error(`Ollama answered ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`)
}
const data = (await res.json()) as { message?: { content?: string } }
return data.message?.content ?? ''
}
How I Built It
The stack is intentionally lean: React 19, TypeScript, Vite, pdfjs-dist, and plain CSS. There is no custom backend server.
1. Extracting text locally with pdf.js
Instead of uploading files to a cloud document processor, the user drops their PDF directly into the browser tab. pdfjs-dist runs in a web worker, walks through each page, and collects the text stream.
If total extractable text across all pages is under 50 characters, Last Slide stops immediately and shows an error. There is no hidden OCR fallback. Lecture slides with real text parse in milliseconds; scanned photo decks are rejected honestly.
This approach was informed by community discussions on client-side PDF handling, particularly Shadman's writeup on Comparing open source PDF libraries (2025 edition), which details how pdf.js excels at client-side extraction without server overhead.
2. The 4,500-character budget
Language models slow down when fed entire 60-page PDF transcripts, especially on consumer laptops running on CPU. To keep inference practical, I capped the ingest at 4,500 characters.
The challenge with a simple character cutoff is that professors almost always put the homework assignment on the penultimate slide. If a student drops a 25-slide deck and you take the first 4,500 characters, slide 24 gets cut off, and the student misses their assignment.
The solution in planIngest is a keyword-first budget plan. It scans for words like homework, assignment, due, deadline, and tugas. Pages matching those words get top priority in the character budget. The remaining character allowance is then filled by the earlier slides in sequential order.
3. Taming Gemma 3 4B with constrained decoding
Small models like Gemma 3 4B are fast and capable of running locally, but when asked for open-ended summaries, they tend to ramble, parrot slide headers, or invent extra homework tasks.
In my earlier projects (Touch Soil and Unramble), I learned that small open-weight models become remarkably reliable when their decoding is strictly constrained.
Rather than asking the model to write paragraphs and parsing with regular expressions, Last Slide passes a strict JSON schema (CARD_SCHEMA) directly into Ollama's format parameter. Under the hood, this enforces grammar-constrained decoding: the runtime physically zeroes out token probabilities that violate the schema. The model cannot output markdown fences, conversational commentary, or unexpected JSON keys.
4. Intentional friction and honest CPU wait times
Before any text is sent to Ollama, Last Slide displays an explicit confirmation checkbox stating exactly how many characters will be sent, which slides are included, and which slides were held back. The user must check this box before generating.
When running on CPU without a dedicated GPU, generation takes between one and two minutes. Last Slide states this honestly on screen while the status indicator runs, rather than hiding the wait behind fake progress bars.
5. Projector slide aesthetic
The design does not look like a SaaS dashboard or a Notion document. It looks like a lecture slide left on an overhead projector after class:
- Warm beige paper background (
#F3EDE0) - Deep ink typography (
#1A1612) - Newsreader for titles, IBM Plex Sans for body text
- One red pen accent (
#B42318) used strictly once: on the homework due date.
Why Does Open Innovation Matter?
This project could not exist in this form on a closed commercial API:
- Course material privacy: Lecture slides, unpublished problem sets, and mid-term exam hints belong to students and universities. Asking students to upload their course PDFs to third-party cloud servers creates data leakage risks. With Ollama and Gemma, the entire workflow remains on the user's laptop.
- Zero per-token costs: Commercial APIs charge per input token. When students ingest dozens of slide decks across a semester, API costs accumulate quickly. Running Gemma locally costs nothing beyond electricity.
-
Architecture decoupling: The model tag in Last Slide is a plain configuration string (
gemma3:4b). If a user has a laptop with more VRAM, they can switch to an 8B or 12B variant without changing a single line of backend logic. - Resisting feature bloat: Cloud AI providers encourage complex multi-step pipelines because their business model relies on API volume. Open-weight models allowed me to build an ultra-focused, four-field skip card designed specifically for students who want to do less work, not more.
The lazy payoff
The Week 3 theme is Slow and Steady: tools that reward laziness.
Many AI study tools try to make students study harder by generating twenty flashcards, multi-chapter summaries, and interactive quizzes. Last Slide does the exact opposite: it accepts that you skipped the lecture, and it lets you stay lazy without failing.
Consider the bundled sample deck, CS 101 Hash Tables (12 slides):
- Slides 1 to 10 explain dictionaries, collision handling, chaining, linear probing, and load factors.
- Slide 11 is the homework: implement a chain-hash map, due Friday 24 October at 17:00.
- Slide 12 is an appendix containing optional readings from Knuth and office hour schedules.
If you skip opening this deck, Last Slide produces:
- What you missed: "Hash tables provide expected O(1) dictionary operations by hashing keys to bucket indices. Collisions are inevitable and are handled through chaining or open addressing."
- Due: "Implement a chain-hash map in the language of the course. Due Friday 24 October, 17:00 (Slide 11)."
- They will ask: "Explain how load factor determines when a hash table must rehash, and why tombstones are necessary when deleting in open addressing."
- You can skip: "The optional Knuth reading in the appendix and the previous year's binary search tree quiz."
You skip reading 12 slides, you skip the optional appendix, you never miss the assignment, and you have an answer ready if the professor asks you about hash collisions. That is the lazy payoff.
Prize Categories
Gemma
Last Slide runs Gemma 3 4B locally via Ollama (gemma3:4b). It leverages Ollama's schema-constrained decoding (format: CARD_SCHEMA) to produce a reliable four-field JSON payload on consumer hardware without hallucinating deadlines or rambling.
Best Use of Render
The static frontend of Last Slide is hosted on Render (https://last-slide.onrender.com/). The architecture separates the user interface from inference: the UI is served globally via Render's static site network, while all document parsing and model inference stay entirely local on the visitor's machine via OLLAMA_ORIGINS.


Top comments (0)