<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arham_Q</title>
    <description>The latest articles on DEV Community by Arham_Q (@arhamqureshi).</description>
    <link>https://dev.to/arhamqureshi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890244%2F5d4fe702-8bd6-4378-b88e-2a5cb366e74a.jpeg</url>
      <title>DEV Community: Arham_Q</title>
      <link>https://dev.to/arhamqureshi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arhamqureshi"/>
    <language>en</language>
    <item>
      <title>your feedback will keep me motivated to work more</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Thu, 30 Apr 2026 10:49:05 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/your-feedback-will-keep-me-motivated-to-work-more-30b6</link>
      <guid>https://dev.to/arhamqureshi/your-feedback-will-keep-me-motivated-to-work-more-30b6</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj" class="crayons-story__hidden-navigation-link"&gt;I was tired of losing track of my AI conversations, so I built a Chrome extension&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/arhamqureshi" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890244%2F5d4fe702-8bd6-4378-b88e-2a5cb366e74a.jpeg" alt="arhamqureshi profile" class="crayons-avatar__image" width="300" height="300"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/arhamqureshi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Arham_Q
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Arham_Q
                
              
              &lt;div id="story-author-preview-content-3562148" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/arhamqureshi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890244%2F5d4fe702-8bd6-4378-b88e-2a5cb366e74a.jpeg" class="crayons-avatar__image" alt="" width="300" height="300"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Arham_Q&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 28&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj" id="article-link-3562148"&gt;
          I was tired of losing track of my AI conversations, so I built a Chrome extension
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sideprojects"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sideprojects&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/extensions"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;extensions&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/browser"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;browser&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I was tired of losing track of my AI conversations, so I built a Chrome extension</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Tue, 28 Apr 2026 11:31:45 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj</link>
      <guid>https://dev.to/arhamqureshi/i-was-tired-of-losing-track-of-my-ai-conversations-so-i-built-a-chrome-extension-16cj</guid>
      <description>&lt;p&gt;TL;DR: I built  &lt;strong&gt;Dendrite&lt;/strong&gt; a Chrome extension that reads your live Claude and ChatGPT conversations and auto-saves every question, code block, and link into a sidebar. No copy-paste. No Notion. It just works.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The Problem Nobody Talks About (But Everyone Has)&lt;/strong&gt;&lt;br&gt;
You ask ChatGPT to write a Python function. It does. You keep chatting.&lt;br&gt;
Twenty minutes later, you need that function. You scroll. And scroll. Past the small talk, the wrong answers, the “here’s a revised version,” the “actually, let me correct that.”&lt;br&gt;
It’s gone. Buried. You’re now a digital archaeologist excavating your own conversation.&lt;br&gt;
This happens every single day to developers using AI tools. And the worst part? The AI already did the work. You just can’t find it.&lt;br&gt;
I’ve lost:&lt;br&gt;
    • A regex pattern for parsing dates (took 3 back-and-forths to get right)&lt;br&gt;
    • A CSS trick for sticky footers that actually worked&lt;br&gt;
    • A link to a GitHub repo the AI recommended&lt;br&gt;
All buried in chat history I’ll never scroll to again.&lt;br&gt;
So I stopped complaining and built Dendrite.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;What Dentrite does&lt;/strong&gt;&lt;br&gt;
Dendrite is a Chrome extension that adds a collapsible sidebar to your Claude and ChatGPT tabs. As you chat, it watches the conversation and automatically extracts:&lt;br&gt;
    • ✅ Every question you asked&lt;br&gt;
    • ✅ Every code block the AI returned&lt;br&gt;
    • ✅ Every link that was dropped&lt;br&gt;
    • ✅ A running summary of the conversation&lt;br&gt;
No button clicks. No copy-paste ritual. It just syncs live as the conversation updates.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;How It Actually Works (The Interesting Part)&lt;/strong&gt;&lt;br&gt;
Here’s where it gets technically spicy. Claude and ChatGPT have no public API for reading conversation content. So how do you get the data out?&lt;br&gt;
MutationObserver — the browser’s built-in way of watching the DOM for changes.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;// Watching for new messages as they appear&lt;br&gt;
const observer = new MutationObserver((mutations) =&amp;gt; {&lt;br&gt;
  for (const mutation of mutations) {&lt;br&gt;
    if (mutation.addedNodes.length &amp;gt; 0) {&lt;br&gt;
      debounce(captureMessages, 400)();&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}); &lt;br&gt;
observer.observe(document.body, {&lt;br&gt;
  childList: true,&lt;br&gt;
  subtree: true&lt;br&gt;
});&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;




&lt;p&gt;Every time a new message appears in the chat, our observer fires. We then scrape the DOM for structured content.&lt;br&gt;
Extracting code blocks, for example:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;function extractCodeBlocks(messageEl) {&lt;br&gt;
  const codeBlocks = messageEl.querySelectorAll('pre code');&lt;br&gt;
  return Array.from(codeBlocks).map(block =&amp;gt; ({&lt;br&gt;
    language: block.className.replace('language-', '') || 'text',&lt;br&gt;
    content: block.innerText.trim(),&lt;br&gt;
    timestamp: Date.now()&lt;br&gt;
  }));&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;And Links:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;function extractLinks(messageEl) {&lt;br&gt;
  const anchors = messageEl.querySelectorAll('a[href]');&lt;br&gt;
  return Array.from(anchors)&lt;br&gt;
    .map(a =&amp;gt; ({ href: a.href, text: a.innerText.trim() }))&lt;br&gt;
    .filter(link =&amp;gt; link.href.startsWith('http'));&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The tricky part? Debouncing. ChatGPT and Claude stream their responses token by token. Without debouncing, you’d capture 200 half-finished versions of the same message. The 400ms debounce lets the message finish before we snapshot it.&lt;br&gt;
———&lt;br&gt;
&lt;strong&gt;The Hardest Part:&lt;/strong&gt; Selectors That Break&lt;br&gt;
Here’s what nobody tells you about building browser extensions on top of AI chat apps: the DOM changes without warning.&lt;br&gt;
One day “div.message-content” works. A frontend deploy later, it’s “article[data-testid="conversation-turn"]”. Selectors that worked Tuesday are dead by Friday.&lt;br&gt;
My current approach: use multiple fallback selectors and log which one succeeds.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const MESSAGE_SELECTORS = [&lt;br&gt;
  '[data-message-author-role]',     // ChatGPT (current)&lt;br&gt;
  '.human-turn, .assistant-turn',   // Claude (current)&lt;br&gt;
  '.message-content',               // fallback&lt;br&gt;
];&lt;br&gt;
function findMessages() {&lt;br&gt;
  for (const selector of MESSAGE_SELECTORS) {&lt;br&gt;
    const els = document.querySelectorAll(selector);&lt;br&gt;
    if (els.length &amp;gt; 0) {&lt;br&gt;
      console.debug('[Dendrite] Using selector:', selector);&lt;br&gt;
      return Array.from(els);&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
  return [];&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;It’s fragile by nature. That’s the cost of building on platforms you don’t control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Stack (Intentionally Boring)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extension API&lt;/td&gt;
&lt;td&gt;Manifest V3&lt;/td&gt;
&lt;td&gt;Required by Chrome Web Store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic&lt;/td&gt;
&lt;td&gt;Vanilla JavaScript&lt;/td&gt;
&lt;td&gt;Zero dependencies, instant load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;HTML + CSS&lt;/td&gt;
&lt;td&gt;No framework overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chrome.storage.local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistent, private, no server needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM Watching&lt;/td&gt;
&lt;td&gt;MutationObserver&lt;/td&gt;
&lt;td&gt;Only way to read live chat updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundler&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Ships as-is, no build step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Why “Dendrite”?&lt;/strong&gt;&lt;br&gt;
Dendrites are the branching receivers of a neuron — they collect incoming signals and feed them into the cell body.&lt;br&gt;
That’s exactly what this extension does: it receives the signals from your AI conversations and feeds them somewhere useful.&lt;br&gt;
But the name also hints at where this is going. V1 is a flat list. V2 will be a topic graph — a visual tree showing how questions across different chats connect. Ask about React in five different conversations? Dendrite will surface that pattern.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Current Limitations (Honesty Section)&lt;/strong&gt;&lt;br&gt;
I’d rather tell you now than have you find out:&lt;br&gt;
    • Selectors break on site updates — I patch them when I catch it, but there’s a delay&lt;br&gt;
    • No cloud sync — everything lives in chrome.storage.local. Close the browser, data stays. Uninstall, data’s gone.&lt;br&gt;
    • No Firefox support — Manifest V3 differences make it non-trivial. It’s on the list.&lt;br&gt;
    • Long chats get heavy — Haven’t optimized storage for 100+ message conversations ye&lt;/p&gt;




&lt;p&gt;What I Want to Build Next&lt;br&gt;
    • Export to Markdown / Notion / Obsidian&lt;br&gt;
    • Keyword search across all saved conversations&lt;br&gt;
    • Topic clustering (the actual dendrite graph)&lt;br&gt;
    • Firefox port&lt;br&gt;
    • Highlight and manually save specific messages&lt;br&gt;
What would you want it to track? Drop it in the comments — I’m actively building and reading every reply.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Follow Along&lt;/strong&gt;&lt;br&gt;
The extension is in active development. If you want early access or want to follow the build:&lt;br&gt;
    • Follow me here for V2 updates&lt;br&gt;
        •       Comment down your thoughts it’ll be helpful&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with zero frameworks and a lot of frustration. If you’ve ever lost a good code snippet to the AI chat void, you know why this exists.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>extensions</category>
      <category>browser</category>
      <category>llm</category>
    </item>
    <item>
      <title>#1 DevLog Meta-research: I Got Tired of Tab Chaos While Reading Research Papers.</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Sat, 25 Apr 2026 16:03:36 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/1-devlog-meta-research-i-got-tired-of-tab-chaos-while-reading-research-papers-3alm</link>
      <guid>https://dev.to/arhamqureshi/1-devlog-meta-research-i-got-tired-of-tab-chaos-while-reading-research-papers-3alm</guid>
      <description>&lt;p&gt;Every time I sit down to explore a research topic, the same thing happens.&lt;/p&gt;

&lt;p&gt;I open arXiv for preprints. Then Semantic Scholar for citations. Then Crossref to verify a reference. Then back to arXiv because I forgot the paper I was on. Then I lose the thread entirely.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;That frustration is why I started building &lt;strong&gt;Meta-Research&lt;/strong&gt; an AI-powered web platform for academic literature search, analysis, and management. It's still in active development, but I wanted to share the problem it's trying to solve and what I've built so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;Researching a topic today means juggling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple search engines with overlapping but non-identical indexes&lt;/li&gt;
&lt;li&gt;No way to see &lt;em&gt;how&lt;/em&gt; papers connect to each other visually&lt;/li&gt;
&lt;li&gt;PDFs you can read but can't &lt;em&gt;talk to&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;No single place to save, organize, and revisit papers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing tools are either paywalled, too broad, or don't integrate AI in a meaningful way. I wanted one workspace that handles all of it.&lt;/p&gt;




&lt;p&gt;&lt;u&gt;&lt;strong&gt;What I've built so far&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Unified search across major databases
&lt;/h3&gt;

&lt;p&gt;Instead of running the same query on four different sites, Meta-Research hits them all at once, arXiv, Crossref, OpenAlex, and Semantic Scholar and surfaces results in a single view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified example of a unified search call
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unified_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;search_arxiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;search_crossref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;search_openalex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;search_semantic_scholar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;deduplicate_and_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each source has its own API quirks, rate limits, and response formats normalizing them into a consistent schema was one of the trickier early problems.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Chat with research papers using LLMs
&lt;/h3&gt;

&lt;p&gt;This is the feature I'm most excited about. You can load a paper and ask it questions directly "What methodology did they use?", "Summarize the limitations", "How does this compare to X?"&lt;/p&gt;

&lt;p&gt;Under the hood it's using &lt;strong&gt;Groq (Llama)&lt;/strong&gt; and &lt;strong&gt;Google Gemini&lt;/strong&gt;, depending on the task. Groq is fast for quick Q&amp;amp;A; Gemini handles longer context well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_with_paper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paper_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    You are a research assistant. Based on the paper below, answer the question.

    Paper:
    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;paper_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;query_groq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;query_gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For cases where I don't want to hit an API, I also integrated &lt;strong&gt;Sumy&lt;/strong&gt; for local extractive summarization useful for quick overviews without burning tokens.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Citation graph visualization
&lt;/h3&gt;

&lt;p&gt;This one changes how you explore literature. Instead of manually chasing citations, Meta-Research generates an interactive graph showing how papers reference each other.&lt;/p&gt;

&lt;p&gt;You can see clusters, find highly-cited hubs, and spot gaps — papers that cite each other a lot but aren't directly connected, which often points to an interesting research gap.&lt;/p&gt;

&lt;p&gt;It's built dynamically on the frontend using JavaScript, with the graph data computed server-side in Flask.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Library and collection management
&lt;/h3&gt;

&lt;p&gt;Users can save papers, create named collections ("Transformer architectures", "My thesis sources"), and pick up where they left off. Auth is handled with Flask-Login, passwords hashed via Werkzeug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/save_paper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@login_required&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_paper&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;paper_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paper_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;collection&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SavedPaper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;saved&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Python, Flask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite via Flask-SQLAlchemy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Flask-Login + Werkzeug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Groq API (Llama), Google Gemini API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NLP (local)&lt;/td&gt;
&lt;td&gt;Sumy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;HTML5, CSS3, Vanilla JS, Jinja2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I deliberately kept the frontend framework-free for now. Vanilla JS keeps the complexity low while the core features are still taking shape.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's still rough (being honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The citation graph can get slow with large paper sets need to add pagination or lazy loading&lt;/li&gt;
&lt;li&gt;Multi-source deduplication isn't perfect, the same paper from arXiv and Crossref sometimes shows up twice&lt;/li&gt;
&lt;li&gt;The chat feature works well on shorter papers but struggles with very long PDFs due to context limits&lt;/li&gt;
&lt;li&gt;No collaborative features yet it's fully single-user right now&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Smarter deduplication using DOI matching&lt;/li&gt;
&lt;li&gt;Streaming responses for the paper chat (so it feels faster)&lt;/li&gt;
&lt;li&gt;A recommendation engine based on your saved papers&lt;/li&gt;
&lt;li&gt;Maybe: export to BibTeX / Zotero&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I'm sharing this now
&lt;/h2&gt;

&lt;p&gt;Mostly because building in public keeps me accountable. And because if you've felt the same tab-switching pain, I'd love to hear what features would actually matter to you.&lt;/p&gt;

&lt;p&gt;Follow along if you're curious.&lt;/p&gt;

&lt;p&gt;What's the most annoying part of your research or paper-reading workflow? Drop it in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>computerscience</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a PDF Toolkit as a Student (And Deployed It for Free)</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Fri, 24 Apr 2026 12:23:09 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/i-built-a-pdf-toolkit-as-a-student-and-deployed-it-for-free-1oel</link>
      <guid>https://dev.to/arhamqureshi/i-built-a-pdf-toolkit-as-a-student-and-deployed-it-for-free-1oel</guid>
      <description>&lt;p&gt;&lt;em&gt;Flask, PyMuPDF, Groq, and a lot of jugaad engineering&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every student has been there. It's 11 PM. You need to compress a PDF before submitting it, convert a JPEG to PDF for a form, or quickly summarize a 40-page document before an exam. You open some sketchy website, it watermarks your file, asks you to pay, and uploads your documents to who-knows-where.&lt;/p&gt;

&lt;p&gt;I got tired of it. So I built my own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DocFlask&lt;/strong&gt; is an all-in-one document toolkit built with Flask. It handles PDF merging, splitting, conversion, compression, image conversion, and even AI-powered summarization and quiz generation — all for free, hosted on the internet for other broke students.&lt;/p&gt;

&lt;p&gt;Here's the honest story of how it got built, the problems I ran into, and the "good enough" solutions I used to ship it anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Before the war stories, here's the feature set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge &amp;amp; Split PDFs&lt;/strong&gt; — combine multiple PDFs or split by page ranges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert&lt;/strong&gt; — PDF ↔ DOCX and DOCX ↔ PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress&lt;/strong&gt; — reduce file size for both PDFs and DOCX files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Summarize&lt;/strong&gt; — structured summary from any PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiz Generator&lt;/strong&gt; — flashcards and MCQs generated from PDF content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG to PDF&lt;/strong&gt; — batch convert up to 30 images into one PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Convert&lt;/strong&gt; — JPEG ↔ PNG with alpha-safe handling&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;Flask&lt;/span&gt;
&lt;span class="na"&gt;PDF engine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;PyMuPDF (fitz)&lt;/span&gt;
&lt;span class="na"&gt;PDF→DOCX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;pdf2docx&lt;/span&gt;
&lt;span class="na"&gt;DOCX→PDF&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;python-docx + fpdf2&lt;/span&gt;
&lt;span class="na"&gt;NLP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;sumy + nltk&lt;/span&gt;
&lt;span class="na"&gt;AI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="s"&gt;Groq API&lt;/span&gt;
&lt;span class="na"&gt;Images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;Pillow&lt;/span&gt;
&lt;span class="na"&gt;Frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;Jinja2 + Vanilla JS + Tailwind CDN&lt;/span&gt;
&lt;span class="na"&gt;Hosting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;Vercel (free)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy. No Docker, no Celery, no Redis. Just Flask doing Flask things.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 1: The Ghostscript Problem
&lt;/h2&gt;

&lt;p&gt;PDF compression was supposed to use Ghostscript — a battle-tested tool that gives you real compression presets (low, medium, high quality). The plan was clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compress_with_ghostscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ebook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-sDEVICE=pdfwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-dCompatibilityLevel=1.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-dPDFSETTINGS=/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-dNOPAUSE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-dBATCH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-dQUIET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-sOutputFile=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;input_path&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? Vercel's serverless runtime doesn't have Ghostscript installed. And installing system packages on Vercel isn't really a thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The jugaad:&lt;/strong&gt; Silent fallback to PyMuPDF compression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compress_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;compress_with_ghostscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CalledProcessError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Ghostscript not available, fall back to PyMuPDF
&lt;/span&gt;        &lt;span class="nf"&gt;compress_with_pymupdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is PyMuPDF compression as good as Ghostscript? No. Is it good enough for a student compressing a form submission? Yes. The tradeoff was acceptable for the target use case.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lesson: Know your user. A student compressing a 5-page form doesn't need the same quality as a print shop.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 2: Async Tasks on a Serverless Platform
&lt;/h2&gt;

&lt;p&gt;Summarization and quiz generation take time — sometimes 15-30 seconds depending on PDF size. My original plan used a &lt;code&gt;TaskManager&lt;/code&gt; with background threads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_in_background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works perfectly on a real server. On Vercel's serverless functions? The thread gets killed the moment the initial HTTP response is sent. The polling endpoint returns nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The jugaad:&lt;/strong&gt; Switch summarize and quiz to synchronous execution on Vercel. The user waits. The UI shows a spinner. The function either completes or hits Vercel's 60-second timeout.&lt;/p&gt;

&lt;p&gt;For small PDFs (under ~15 pages), it completes fine. For large ones, it times out. The fix? Enforce a soft page limit on upload and set honest expectations in the UI.&lt;/p&gt;

&lt;p&gt;Not elegant. Ships though.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 3: DOCX → PDF Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;I assumed converting a DOCX to PDF would be straightforward. &lt;code&gt;python-docx&lt;/code&gt; reads the file, &lt;code&gt;fpdf2&lt;/code&gt; renders it. Simple.&lt;/p&gt;

&lt;p&gt;It is not simple.&lt;/p&gt;

&lt;p&gt;The combination of python-docx + fpdf2 produces acceptable output for plain text documents. The moment your DOCX has tables, custom fonts, images, or complex formatting — it falls apart. Columns collapse, fonts substitute weirdly, images disappear.&lt;/p&gt;

&lt;p&gt;The honest truth: &lt;strong&gt;good DOCX→PDF conversion requires either LibreOffice (headless) or a paid API&lt;/strong&gt;. Neither was available to me for free on Vercel.&lt;/p&gt;

&lt;p&gt;What I did: kept the feature, documented the limitation clearly. For simple documents it works. For complex ones, the README tells users to use LibreOffice locally.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sometimes the right answer is just being transparent about what your tool can't do.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Async Polling Flow (For Features That Need It)
&lt;/h2&gt;

&lt;p&gt;For quiz and summarize, even in sync mode, the frontend uses a polling pattern that was originally designed for async. Here's the simplified version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pollStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/status/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;fetchResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even running synchronously, the task ID pattern means the frontend and backend are cleanly decoupled. If I ever move to a real server with proper async, the frontend needs zero changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment: Why Vercel (And Why It Kind Of Works)
&lt;/h2&gt;

&lt;p&gt;Everyone told me to use Render or Railway for a Flask app. They're right — those platforms give you a real Linux environment with persistent processes, system packages, and no cold start issues.&lt;/p&gt;

&lt;p&gt;But Render's free tier sleeps after 15 minutes of inactivity. Railway has usage limits. For a portfolio project targeting last-minute student use cases, I needed something that just stays up.&lt;/p&gt;

&lt;p&gt;Vercel with a &lt;code&gt;vercel.json&lt;/code&gt; config works for Flask if you accept the constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No system packages (hence the Ghostscript fallback)&lt;/li&gt;
&lt;li&gt;No persistent background threads (hence synchronous AI features)&lt;/li&gt;
&lt;li&gt;60-second function timeout (hence the page limits)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small files and quick tasks? It handles it fine. That's exactly the use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Use LibreOffice headless for DOCX→PDF&lt;/strong&gt;&lt;br&gt;
It produces near-perfect output. The challenge is hosting — it's a heavy dependency. But for a proper deployment, it's worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add explicit file size and page limits on every route&lt;/strong&gt;&lt;br&gt;
I added them on some routes (compression: 12 pages, quiz: similar). I should have added them everywhere with clear user-facing messages from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Show compression method in the UI&lt;/strong&gt;&lt;br&gt;
When Ghostscript falls back to PyMuPDF, the user should know. Silent fallbacks that return a different quality than advertised are a trust issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use a proper task queue&lt;/strong&gt;&lt;br&gt;
Redis + Celery or even a simple SQLite-backed queue would make the async story clean. In-memory task state means a server restart wipes all pending tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://ihatepdf-tau.vercel.app" rel="noopener noreferrer"&gt;https://ihatepdf-tau.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Arham-Qureshi/I-hate-PDFs" rel="noopener noreferrer"&gt;https://github.com/Arham-Qureshi/I-hate-PDFs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for files under 10-15 pages. Free hosted, so the first load might take a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;This project taught me that shipping something imperfect but functional is better than architecting something perfect that never ships. Every "jugaad" in this codebase is a real constraint I hit, a decision I made, and a tradeoff I understood.&lt;/p&gt;

&lt;p&gt;That's engineering. Especially when you're broke.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Flask, PyMuPDF, Groq, and the spirit of jugaad. If you found this useful, drop a ⭐ on GitHub.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#python&lt;/code&gt; &lt;code&gt;#flask&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#beginners&lt;/code&gt;&lt;/p&gt;

</description>
      <category>flask</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>Devlog #2 — I Hate PDFs: Why I Never Save Uploaded Files to Disk</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Wed, 22 Apr 2026 19:56:38 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/devlog-2-i-hate-pdfs-why-i-never-save-uploaded-files-to-disk-1dnn</link>
      <guid>https://dev.to/arhamqureshi/devlog-2-i-hate-pdfs-why-i-never-save-uploaded-files-to-disk-1dnn</guid>
      <description>&lt;p&gt;&lt;u&gt;&lt;strong&gt;If you missed Devlog #1, I built an AI-powered Quiz &amp;amp; Flashcard generator that turns any PDF into a quiz using Groq AI. This is what came next.&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Decision Before Writing a Single Line&lt;/strong&gt;&lt;br&gt;
When I started wiring up the file upload logic for I Hate PDFs my student-focused PDF toolkit.I had an obvious plan:&lt;br&gt;
User uploads PDF → save it to disk → do the operation → return the result.&lt;/p&gt;

&lt;p&gt;Simple. Familiar. What most tutorials show.&lt;br&gt;
But before I actually wrote that code, I paused and researched a bit. The questions I had were straightforward, what happens when two users upload at the same time? Who cleans up the saved files? What if I deploy this on a server with limited storage?&lt;br&gt;
That research led me to something I hadn't used before: Python's BytesIO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Even Is BytesIO?&lt;/strong&gt;&lt;br&gt;
BytesIO lives in Python's built-in io module. The simplest way to think about it.It's a file that exists only in memory.&lt;br&gt;
It behaves exactly like a real file. You can read from it, write to it, pass it around, but it never touches your disk. The moment it's no longer needed, Python clears it automatically.&lt;br&gt;
pythonfrom io import BytesIO&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhs6kefdidzsrs5uqopjm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhs6kefdidzsrs5uqopjm.png" alt="sample code" width="708" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Use It in I Hate PDFs&lt;/strong&gt;&lt;br&gt;
Almost every feature in this project involves a user uploading a PDF  merge, split, compress, extract text. BytesIO handles all of it without ever writing to disk.&lt;/p&gt;

&lt;p&gt;Text extraction for the Quiz Generator:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdp4kvwydka4gpz3lq638.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdp4kvwydka4gpz3lq638.png" alt="code about operation on RAM" width="724" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The uploaded PDF goes straight into a BytesIO buffer, gets passed to PyMuPDF's fitz, and the text comes out no file ever saved.&lt;br&gt;
Sending a processed PDF back to the user:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogvca0kdemo9yeo4qhqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogvca0kdemo9yeo4qhqy.png" alt="last wrap-up" width="701" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's Next&lt;br&gt;
The core operations and core features are coming together. Next, I’ll focus on refining the user interface and ensuring the tool is shareable, stay tuned for a live demo soon.&lt;br&gt;
If you're developing an application with file uploads in Flask or any Python backend, consider using BytesIO instead of saving files to disk. This small choice can significantly enhance the cleanliness of your code from the outset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I Hate PDFs is still evolving, building in public.&lt;br&gt;
building in public. Follow along for Devlog #3.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>backend</category>
      <category>devjournal</category>
      <category>python</category>
    </item>
    <item>
      <title>Devlog #1- I Hate PDFs: How I Used Groq and PyMuPDF to Make an AI-Powered Quiz Maker from PDFs</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Wed, 22 Apr 2026 11:14:54 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/how-i-used-groq-and-pymupdf-to-make-an-ai-powered-quiz-maker-from-pdfs-25p4</link>
      <guid>https://dev.to/arhamqureshi/how-i-used-groq-and-pymupdf-to-make-an-ai-powered-quiz-maker-from-pdfs-25p4</guid>
      <description>&lt;p&gt;***&lt;em&gt;**Why I Made This&lt;/em&gt;*&lt;br&gt;
Every student has felt this way: it's late, you have a lot of reading to do, and you have an exam tomorrow. You read it once or twice, but you're not sure if you got it.&lt;br&gt;
I'm working on I Hate PDFs, a PDF toolkit for students that works in a web browser. It has the usual tools like merge, split, convert, and summarize, but the one thing I wanted most was something that could test your understanding instead of just giving you information.&lt;br&gt;
That's why I made a Flashcard and Quiz maker. With Groq AI, you can upload any PDF and choose how many questions you want (5, 10, or 15). The AI will make a multiple choice quiz for you in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Feature in Action&lt;/strong&gt;&lt;br&gt;
To utilize this feature, one must upload a PDF document, select the desired number of questions, and then click on the Generate button. The application will extract the text from the PDF, process it through Groq, and subsequently produce a fully interactive quiz.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk205btzhwmo73tijc2jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk205btzhwmo73tijc2jn.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;br&gt;
Each question is accompanied by four answer options, providing immediate feedback: a green indicator for the correct answer, a red indicator for incorrect responses, and the correct answer is displayed promptly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Feature in Action&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6js0kkz69c0anxoqdyj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6js0kkz69c0anxoqdyj2.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;br&gt;
To utilize this feature, users must upload a PDF document, specify the desired number of questions, and then click the Generate button. The application will extract the text from the PDF, process it through Groq, and subsequently produce a fully interactive quiz. &lt;br&gt;
Each question is presented with four answer options, providing immediate feedback: a green indicator signifies the correct answer, while a red indicator denotes incorrect responses. The correct answer is displayed promptly for the user's reference.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;How It Works — Step by Step&lt;/strong&gt;&lt;/u&gt;&lt;br&gt;
&lt;strong&gt;Step 1 — Extract text from the PDF&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04u2uxm99qrh9davc6qr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04u2uxm99qrh9davc6qr.png" alt=" " width="795" height="255"&gt;&lt;/a&gt;&lt;br&gt;
PyMuPDF's fitz is fast and reliable for text-based PDFs. It goes page by page and concatenates the content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Send to Groq with a structured prompt&lt;/strong&gt;&lt;br&gt;
&lt;u&gt;The prompt is the most important part. You can't just say "make a quiz" — you need to tell the model exactly what format to return, otherwise parsing becomes a nightmare.&lt;/u&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F12b3raqcq4112bzwu74z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F12b3raqcq4112bzwu74z.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
See the text[:4000]? This is a hard trim to keep from going over the token limit. It's a known limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Parse the response&lt;/strong&gt;&lt;br&gt;
Groq occasionally provides JSON formatted with markdown, which you need to remove before parsing:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpey4s3xdbrsjja0gbeij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpey4s3xdbrsjja0gbeij.png" alt=" " width="788" height="186"&gt;&lt;/a&gt;&lt;br&gt;
This gives you a clean Python list of question objects ready to send to the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Render on the frontend&lt;/strong&gt;&lt;br&gt;
Each question is shown as a card with four choices that you can click on. When you make a choice, the app compares it to the answer field and gives you feedback right away, without having to reload the page.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;NOTE:&lt;/u&gt; Still in the development stage, will be happy to get feedback and suggestions for improvements.&lt;br&gt;
I will soon tell everyone about this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FOLLOW FOR MORE&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>I couldn't find a fun way to learn CPU scheduling so I built one</title>
      <dc:creator>Arham_Q</dc:creator>
      <pubDate>Tue, 21 Apr 2026 07:29:35 +0000</pubDate>
      <link>https://dev.to/arhamqureshi/i-couldnt-find-a-fun-way-to-learn-cpu-scheduling-so-i-built-one-5h39</link>
      <guid>https://dev.to/arhamqureshi/i-couldnt-find-a-fun-way-to-learn-cpu-scheduling-so-i-built-one-5h39</guid>
      <description>&lt;p&gt;&lt;strong&gt;What Happens When You Don't Find a Perfect Tool for Learning!&lt;/strong&gt;&lt;br&gt;
Start with the exact moment you got frustrated, the lecture, the textbook diagram that made no sense, or whatever it was. Personal and specific beats generic every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The issue&lt;/strong&gt;&lt;br&gt;
Every CS student has felt this way. Your teacher draws a Gantt chart on the board and talks about FCFS and Round Robin. You nod along as if you understand. You open your textbook, and all you see are more static diagrams.&lt;br&gt;
I wanted something that I could do. Something that really showed me what was going on step by step. I looked around and couldn't find anything that didn't look like it was made in 2003.&lt;br&gt;
I made it myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thought&lt;/strong&gt;&lt;br&gt;
What if algorithms could battle each other? Algo Wars is a game-like CPU scheduling simulator where you can put two algorithms against each other and watch them compete in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke first, the Gantt chart&lt;/strong&gt;&lt;br&gt;
The first version of mine loaded the Gantt chart in a static way. The whole thing just showed up at once, which ruined the point. You couldn't see the scheduling happen; you could only see the end result.&lt;br&gt;
So I completely changed it. Changed the output to JSON and sent it to the chart one block at a time. It came to life all of a sudden. You could see the CPU choose which process to run next.&lt;br&gt;
That one change made the whole thing seem real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The last boss is MLQ&lt;/strong&gt;&lt;br&gt;
The Gantt chart was like a mid-level enemy, and multilevel queue scheduling was the final boss. There are multiple queues, each with its own priority and running a different algorithm at the same time. They all work together without getting in each other's way.&lt;br&gt;
It took the longest to get that logic right. The best :( part of the whole build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A peek at the piece of Code&lt;/strong&gt;&lt;br&gt;
This code show the star feature, How the two algorithms run side by side and compare the efficiency&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo64cxvjhfqwkvquarm7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo64cxvjhfqwkvquarm7d.png" alt="This code show the star feature, How the two algorithms run side by side and compare the efficiency" width="551" height="692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it yourself&lt;br&gt;
The repo is open. Feedback, bug reports, pull requests, all welcome.&lt;br&gt;
Visit &lt;a href="https://github.com/Arham-Qureshi/Algo-wars/" rel="noopener noreferrer"&gt;https://github.com/Arham-Qureshi/Algo-wars/&lt;/a&gt;&lt;br&gt;
live: &lt;a href="https://algo-wars-rust.vercel.app/" rel="noopener noreferrer"&gt;https://algo-wars-rust.vercel.app/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>webdev</category>
      <category>python</category>
    </item>
  </channel>
</rss>
