OzMath (weblink) is a website I built to help students in high school revise for their final exam. Key features include a mock test feature, AI autograder and visual math editor.
For devtools, I used bolt.new for the first ~4 months of development, then pivoted to Cursor which has been my main ide till this day. I used a lot of models but mostly GPT 5.1 Codex Max.
For data persistance, I used to use Supabase (back when I tried to profit from ozmath). But now I just use caching so that users dont have to sign up. The concern with this is API costs but I'm not too concerned as I'm only using vision models on a select few problems with extended solutions, so costs are cheap (added $10aud a year ago and havent had to top up).
As for hosting, I use framer for the landing page and reroute to the Vue app to Netlify.
While developing the UI, I quickly found out that LaTeX does not wrap containers well at all. Since all my question descriptions are in LaTeX, when I implemented Shadcn resizable in the ProblemView UI, the LaTeX didnt wrap the container, and I couldnt work around it by manually creating new lines due to screen sizing variations. It took me ages but I eventually noticed that the latex operator \to (which is a right pointing arrow) actually had a text-wrapping property to it.
From this, I found a way to brute force a solution by seperating every character with \to, then making the element invisible with \phantom, and pushing the (now) invisible space back with \mkern:
/**
* Adds proper spacing between words in LaTeX text mode
* @param text The LaTeX text to process
* @returns The text with proper spacing between words
*/
export function addLatexTextSpacing(text: string): string {
// Early return if text is empty or not a string
if (!text || typeof text !== 'string') return text;
// Regular expression to match text mode content, but not inside array environments
const textModeRegex = /\\text\{([^}]+)\}/g;
let inArray = false;
return text.replace(textModeRegex, (match, textContent) => {
// Don't process text inside array environments
if (textContent.includes('\\begin{array}') || textContent.includes('\\end{array}')) {
return match;
}
// Split by spaces while preserving LaTeX commands
const words = textContent.split(/\s+/).filter(word => word.length > 0);
// Add phantom spacing between text words and \; after each text block
// Use \mkern-6mu for precise negative space (equivalent to \!\!)
return words
.map(word => `\\text{\\sf ${word}}\\;`)
.join('\\mathrel{\\vphantom{\\to}}\\mkern-8mu');
});
}