Back
Year
2026
Tech & Technique
HTML, CSS, JavsScript, Python, LLM
Overview
What started as a straightforward chatbot assignment quietly evolved into something with voice input, document scanning, translation, and OCR. Often because we didn't know any better, and that turned out to be a good thing.
About the Project
It started simple. For our Year 3 Natural Language Processing module, we were tasked with building a functional chatbot. Nothing exotic — just something that could hold a conversation intelligently. Three of us formed a group, I led the team, and we set out to build something we could be proud of within the semester.
The initial scope was: a web-based chat interface, a Python backend, and an LLM powering the responses. We landed on DeepSeek R1 accessed via the OpenRouter API — it was free, capable, and well-suited for a university project where budget was zero.
We called the project BrainWaves. The name felt fitting at the time. It would feel even more fitting by the time we were done.
The stack
What we built it with
Python / Flask DeepSeek R1 via OpenRouter Vanilla JavaScript HTML / CSS PDF.js Web Speech API RecorderJS
The backend is a Flask server handling API calls to OpenRouter, routing messages, and processing uploaded files. The frontend is pure vanilla JavaScript — no React, no Vue, no framework. At the time, none of us had worked with modern frontend frameworks in a meaningful way, so we went with what we knew: the DOM API and a lot of manual state management.
That decision to go frameworkless is one of the things I'd revisit if I were starting over. More on that later.
The scope creep (the good kind)
When the chatbot stopped being just a chatbot
Somewhere around the midpoint of the semester, we started asking: what's the most natural thing a user might want to do with an AI assistant beyond chatting? The answer felt obvious — they'd want to hand it a document and ask questions about it.
So we added it. File upload, PDF parsing, OCR for scanned images, and a mode that constrains the assistant to only answer questions from within the document. That last constraint — the "document-only" mode — was deliberate. We didn't want the model going off on tangents when a user uploads a specific file they want to interrogate. It's a small UX decision but one that made the feature feel purposeful rather than tacked on.
From there, the feature list grew:
🎤 Voice input
Record audio directly in the browser via RecorderJS, transcribed and passed to the model.
🔊 Text-to-speech
Model responses can be read aloud using the Web Speech API, useful for accessibility.
🌐 Translation
Users can request responses in different languages, handled through prompt engineering.
📄 OCR + document QA
Scanned documents and PDFs are parsed, with the model restricted to answering within the document's content.
Note: The document QA mode was intentionally scoped — the model will only answer questions from within the uploaded file, and nothing else. This was a deliberate design choice to keep the feature focused and reliable.
The hard parts
Where we got it wrong
Not everything was smooth. Two problems in particular stand out — one was a genuine security oversight, and one was a design challenge that exposed how much we hadn't learned yet.
The API key problem
We didn't know about .env files or environment variable management when we started. None of us had worked on a project where secret management was a real concern. So we did the thing you should never do — we hardcoded the OpenRouter API key directly into the source code.
Lesson learned: Never hardcode secrets. Use python-dotenv, load keys via os.environ, and add .env to your .gitignore before your first commit. This is table stakes for any project that touches an external API.
We caught this before anything was published publicly, but it was a wake-up call. Security hygiene isn't something you retrofit — it needs to be in your workflow from day one.
The frontend problem
Building the interface in vanilla JavaScript was manageable at first. But as features accumulated — chat history, file previews, recording state, settings modals, sidebar toggling, mode indicators — the codebase started fighting us. Without a component model or reactive state management, every new feature meant tracking more global state by hand and wiring up more event listeners manually.
By the end, the UI worked. It looked good. But the code behind it was sprawling in ways that made iteration painful. Had we taken the time upfront to learn even the basics of React or Vue, the second half of the project would have been significantly smoother.
Lesson learned: Vanilla JS scales poorly once UI complexity grows beyond a certain point. For any project with multiple interactive states and dynamic components, learning a framework is worth the upfront cost.
What we'd do differently
Lessons taken forward
Security
Set up .env and python-dotenv before writing a single line of application code. Treat secrets like passwords — they should never appear in source files.
Frontend architecture
Invest time at the start of the project to learn the framework your team will use. The learning curve pays dividends as soon as the UI becomes non-trivial. React, Vue, Svelte — pick one and commit.
Scope discipline
Adding features mid-project isn't inherently bad — but each one should be a deliberate team decision, not a rabbit hole one person went down alone. The features we added intentionally (document QA, voice input) were polished. The ones we bolted on without planning showed.
Distribution of work
As team lead, clearer task ownership from the beginning would have reduced blockers. GitHub issues, even informal ones, would have helped keep three people from stepping on each other's code.
The outcome
How it landed
BrainWaves worked. That sounds like a low bar, but for a semester-long NLP project built by a team of three people learning on the fly, it wasn't. The chatbot held conversations well, the document QA mode was genuinely useful, text-to-speech worked reliably, and the voice input added something that felt unexpectedly polished for a university project.
The final submission was received well. The panel noted the OCR and translation features as standouts — features we honestly hadn't been sure were worth the effort to add. They were.
"The things we added because we thought they'd be interesting to build turned out to be the things that made it memorable."
If I could go back, I'd fix the secrets issue immediately and I'd push harder for us to learn a frontend framework from the start. But I wouldn't change the ambition. Scope creep has a bad reputation — sometimes it's just called building something worth using.
Get In Touch
Contact Me
Have a project in mind or just want to say hello? Drop me a message and I'll get back to you within 24 hours.
© 2026· Built with Next.js & GSAP