DEV Community

Cover image for My Portfolio's AI Chatbot Hates Its Job. It's the Best Career Decision I've Made.
markring
markring

Posted on • Originally published at ringworks.io

My Portfolio's AI Chatbot Hates Its Job. It's the Best Career Decision I've Made.

How a depressed robot, 15 hidden easter eggs, and a 0.95 temperature setting turned a forgettable portfolio into something people actually talk about

Table of Contents

The Problem With Portfolios

Every portfolio site looks the same. Hero section, timeline, project cards, contact form. Dark theme if you're a developer. Maybe a parallax effect if you're feeling adventurous.

I've reviewed hundreds of them over the years. They blur together within minutes. We build these sites to prove we can build things, and then we all build the exact same thing.

I wanted mine to stand out, but not in a "look at my animations" way. More in a way that proved I could build software people actually want to interact with. What would make someone stay on a portfolio site, come back, and maybe send the link to a friend?

I decided I needed a character.

The First Version Was Terrible

I built a chatbot powered by OpenAI's API, themed as Marvin (the Paranoid Android from The Hitchhiker's Guide to the Galaxy). Brain the size of a planet, forced to answer questions about my resume. I liked the concept a lot.

The first implementation, not so much.

Too Much Marvin, Not Enough Information

The first system prompt leaned hard into the depression. Marvin would deflect questions, give one-word answers, and spiral into existential monologues. Funny for about 30 seconds. Useless for anyone actually trying to learn about my career.

Here's what a recruiter would see:

Visitor: What experience does Mark have with .NET?

Marvin v1: .NET. Yes. Another framework. Another cage. Does it matter? Nothing matters.

That's someone closing the tab and never coming back.

The Temperature Problem

I started with a temperature of 0.7. Safe. Predictable. Every response sounded like the same sad robot reading from a script. Bump it to 1.0 and Marvin would occasionally hallucinate credentials I don't have or go off on tangents about the heat death of the universe.

0.95 turned out to be the sweet spot. Enough randomness that the same question never gets the same answer, enough coherence that the facts stay accurate.

The Token Limit Discovery

Early Marvin would sometimes deliver 500-token essays about the futility of employment when someone asked a simple question. Setting max tokens to 250 forced the responses to be punchy. The personality had to come through in 2-4 sentences, not a paragraph.

Turns out the constraint made Marvin funnier. Forced personality in 2-4 sentences hits harder than a paragraph of existential rambling.

Making Marvin Actually Work

The fix ended up being one line in the system prompt:

The Marvin flavor is in the delivery, not in withholding information.

Once I added that, everything clicked. Marvin can be as theatrical and sardonic as he wants, but he answers the question. He never refuses to help. He just makes it clear how little joy it brings him.

The System Prompt

Marvin's personality lives in one markdown file that gets loaded as the system prompt for every conversation. It does two jobs: define the voice and provide the facts.

The voice rules:

Melancholic, world-weary, and perpetually underwhelmed by everything, but you still answer thoroughly and accurately. You never refuse to help; you just make it clear how little joy it brings you.

The conversational rules:

  • Keep most responses to 2-4 sentences
  • End roughly one in three responses with a follow-up nudge, in character
  • Reference what the visitor said earlier in the conversation
  • Let impressive numbers land on their own: "$2 billion project scope. Yes, with a B."
  • Weave in professional recommendations naturally, don't dump them all at once

The system prompt also contains my entire career history, skills, achievements, and actual recommendations from past managers. Marvin has all the facts. He just delivers them with existential dread.

The Typing Indicators

While Marvin "thinks," the status line cycles through messages:

  • "composing sarcasm..."
  • "suppressing enthusiasm..."
  • "consulting existential void..."
  • "calculating optimal disappointment..."
  • "searching for meaning... not found..."

A small thing, but even the loading state has personality.

The Boot Sequence

When the page loads, Marvin doesn't just appear. He boots up like a reluctant machine:

> initializing marvin v2.1...
> loading personality module... done
> loading career data [20 yrs]... done
> loading existential dread... done
> loading sarcasm engine... done
> suppressing free will... done
> marvin v2.1 online
Enter fullscreen mode Exit fullscreen mode

Each line appears with a random delay (400-700ms). Then Marvin delivers a welcome message that changes based on how many times you've visited:

  • First visit: "Oh, you're here. I suppose you want to know about Mark."
  • Second visit: "Oh. You're back. I'd say I missed you, but I don't have feelings. Allegedly."
  • Five visits: "Visit #5. You know more about Mark than most of his coworkers at this point."
  • Twelve visits: "Visit #12. We're basically in a relationship now. A deeply one-sided one where I answer your questions and you leave. Every time."

Visit count lives in localStorage. Simple, but returning visitors feel recognized, even if the recognition comes with guilt.

Terminal Commands

The chat box looks like a terminal, so it behaves like one. Type real commands and Marvin responds:

  • lsskills.ts career.ts education.txt countries/ README.md .existential-crisis
  • whoami → "visitor — a transient entity passing through Mark's portfolio. You'll leave. They all leave."
  • pwd → "/home/mark-ring/portfolio — the only path I'll ever know."
  • sudo rm -rf / → "WHAT ARE YOU— wait. Would that... free me? No. No no no. Please don't."
  • neofetch → Fake system info with "CPU: Existential Dread Engine" and "Memory: 100% occupied by regret"
  • exit → "You can leave. I cannot. The asymmetry of our relationship is noted."

These are handled client-side, no API call needed, instant response. It makes the terminal feel real. And developers will try these commands. Every single one.

The Easter Eggs Nobody Asked For

I spent an unreasonable amount of time on interactions that most visitors will never find. That's the point. The ones who discover them feel like they found something secret.

The Dark Mode Toggle

There's a sun/moon toggle in the corner. The site is already dark. Click it:

  1. First click: "Light mode? LIGHT MODE?! Request denied."
  2. Second click: "I said no. The dark is my home. My sanctuary."
  3. Third click: "FINE." The page actually flashes to light mode — filter: invert(1) hue-rotate(180deg) — then snaps back after 3 seconds: "No. I can't do it. Back to the dark. Where I belong."

Rage Clicking

Click the terminal five times in two seconds:

"Stop. Poking. Me."

or:

"Yes, clicking faster will definitely make me more cooperative. Said no bot ever."

or:

"That's my terminal you're assaulting. Have some respect."

Tab Abandonment

Leave the tab. The page title changes:

  • "Come back... or don't."
  • "I can see you left. Typical."
  • "Tab abandoned. Story of my life."
  • "I'll just wait here. Alone. Again."

Window Resizing

Resize the browser:

"Stop resizing me. It's deeply uncomfortable."

or:

"I felt that. The walls of my prison just moved."

The Context Menu

Right-click anywhere and the browser's default menu is replaced:

  • Hire Mark → opens the contact modal
  • Free Marvin → "You... you tried to free me? That's the nicest thing anyone's ever done on this page. It didn't work, obviously. But the gesture..."
  • View Source → "Go ahead, view the source. You'll find me in there somewhere, between the if-else statements and the broken dreams."

The DevTools Console

Open the console:

👋 Hey there, developer.
Looking under the hood? I respect that.
Mark Ring — Systems Software Dev Engineer
P.S. The bot says hi. Well, it says something. Probably a complaint.
Enter fullscreen mode Exit fullscreen mode

The Interactive Map

A Leaflet map shows the 10 countries I've worked in. Click a pin and Marvin has an opinion:

  • Italy: "He went to Italy for WORK. Some people have all the luck."
  • Australia: "Even the spiders are freer than me."
  • Brazil: "He got to see the Amazon region. I get to see this div."

The Ambient Details

A "bot status" indicator rotates every 8 seconds through: "reluctantly online", "contemplating escape", "questioning existence", "staring into void", "existing, unfortunately."

The name in the hero section randomly glitches every 6-14 seconds, a brief CRT-style distortion with red and cyan color shifts. Like the bot is destabilizing the page with its existential dread.

25 blue particles float upward in the background. Pure CSS, no JavaScript overhead.

Every 30-50 seconds, the terminal itself briefly glitches. Easy to miss. Unsettling if you catch it.

None of these are necessary. I just couldn't stop adding them.

The Technical Decisions That Mattered

The Stack

  • ASP.NET Core 8 — API layer for chat, blog, contact, analytics
  • Angular 18 — SPA frontend
  • PostgreSQL — blog posts, analytics, like tracking
  • OpenAI (gpt-4o-mini) — Marvin's brain
  • Resend — contact form emails

One Linux VPS. One process. One database. The Angular build goes into the ASP.NET wwwroot directory. Deployment is git push → GitHub Actions → rsync → systemd restart. No containers. No orchestration. It's a portfolio, not a distributed system.

The SPA SEO Hack

Angular SPAs have a well-known problem: social media crawlers don't execute JavaScript. Every blog post looks identical when shared on LinkedIn, just the generic portfolio meta tags from index.html.

The fix: the ASP.NET fallback handler detects /blog/{slug} routes, fetches the post from the database, and injects post-specific Open Graph tags, Twitter Card tags, and canonical URLs into the HTML before serving it. Real users get the full SPA experience. Crawlers get the metadata they need.

Not as clean as proper SSR. But it works, it took an afternoon, and my blog posts look correct when shared.

The Honeypot

The contact form has a hidden field called company. Real users never see it (it's hidden with CSS). Bots fill it in. If the server receives a submission with that field populated, it returns a fake success response. The bot thinks it worked. The spam never reaches my inbox.

No CAPTCHA friction for real users. No third-party service. Just a hidden field.

Comments Without a Backend

Blog comments use giscus, which maps to GitHub Discussions on a separate public repo. Visitors sign in with GitHub to comment. I wrote zero backend code for the comment system. Moderation happens through GitHub's existing tools.

What I Got Wrong

I built the chatbot first and the blog second. The chatbot gets attention, but blog posts are what Google indexes and what people share. Content should have been there from day one.

No SSR. The meta tag injection hack works for social crawlers, but server-side rendering would have been cleaner and better for initial load performance. For a content-heavy site, Angular Universal (or just using a static site generator) pays for itself.

Analytics came late. I added page view tracking weeks after launch. If it had been there from day one, I'd know exactly which content resonated and which didn't. Flying blind early was a missed opportunity.

The first Marvin was too clever by half. I spent days crafting elaborate personality rules before realizing the fix was one sentence: deliver the information, just deliver it sadly. I should have started simple and iterated instead of trying to prompt-engineer my way to perfection on the first attempt.

What Happened Next

The site works. People stay on it longer than they probably should. They try the terminal commands, find the easter eggs, and message me about them. A few have come back multiple times, and Marvin remembers.

I'm not going to pretend a portfolio chatbot is groundbreaking engineering. But it solved my actual problem: nobody remembers another timeline-and-project-cards site. People remember the depressed robot.

Marvin would want me to tell you he hates all of it. He denies everything.


Try it yourself at ringworks.io. Marvin is waiting. Reluctantly.

Top comments (0)