1<!DOCTYPE html><html lang="en" class="dark" style="color-scheme:dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/immutable/media/83afe278b6a6bb3c-s.p.45535valc9rzk.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" as="image" href="/brand/vn.svg"/><link rel="preload" as="image" href="/images/blog/style-locked-cover.jpg"/><link rel="preload" as="image" href="/images/blog/platformer-tests-cover.png"/><link rel="preload" as="image" href="/images/blog/ai-vs-dash-cover.png"/><link rel="preload" as="image" href="/images/blog/smart-contact-main.png"/><link rel="preload" as="image" href="/images/blog/ai-tokens-main.png"/><link rel="preload" as="image" href="/images/blog/tldr-bot-main.png"/><link rel="preload" as="image" href="/images/blog/article-cover.png"/><link rel="stylesheet" href="/_next/static/immutable/chunks/1xm6ifu1qrlmp.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/immutable/chunks/36k8in9a291ok.js"/>
1<script src="/_next/static/immutable/chunks/32_9u2lef27ri.js" async=""></script>
1<script src="/_next/static/immutable/chunks/26vns4dnn1xsa.js" async=""></script>
1<script src="/_next/static/immutable/chunks/0n0fwvxjnvj-g.js" async=""></script>
1<script src="/_next/static/immutable/chunks/turbopack-1cqw5n-jju8l7.js" async=""></script>
1<script src="/_next/static/immutable/chunks/2corl6wyj4669.js" async=""></script>
1<script src="/_next/static/immutable/chunks/0ean33l0me-9n.js" async=""></script>
1<script src="/_next/static/immutable/chunks/25imx_awd0ntr.js" async=""></script>
1<title>Vitali Nikolaev</title><meta name="description" content="Portfolio and work of Vitali Nikolaev â Product Design, Product Management, and Launches."/><link rel="author" href="https://personal-website-six-tau-39.vercel.app"/><meta name="author" content="Vitalii Nikolaev"/><meta name="keywords" content="product design,product management,UX design,UI design,freelancer,product strategy,design systems,startup design,digital products"/><meta name="creator" content="Vitalii Nikolaev"/><meta name="publisher" content="Vitalii Nikolaev"/><meta name="robots" content="index, follow"/><meta name="googlebot" content="index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"/><meta name="format-detection" content="telephone=no, address=no, email=no"/><meta property="og:title" content="Product Designer & Product Manager â Vitalii Nikolaev"/><meta property="og:description" content="Independent product designer & manager helping startups and teams build, launch, and grow digital products. Available for freelance work."/><meta property="og:url" content="https://personal-website-six-tau-39.vercel.app"/><meta property="og:site_name" content="Vitalii Nikolaev â Product Designer & Manager"/><meta property="og:locale" content="en_US"/><meta property="og:image" content="https://personal-website-six-tau-39.vercel.app/images/main-photo.png"/><meta property="og:image:width" content="1024"/><meta property="og:image:height" content="1024"/><meta property="og:image:alt" content="Vitalii Nikolaev - Product Designer & Manager"/><meta property="og:type" content="website"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="Product Designer & Product Manager â Vitalii Nikolaev"/><meta name="twitter:description" content="Independent product designer & manager helping startups and teams build, launch, and grow digital products."/><meta name="twitter:image" content="https://personal-website-six-tau-39.vercel.app/images/main-photo.png"/><link rel="icon" href="/favicon.ico?favicon.3fpu2ql9ns1a0.ico" sizes="256x256" type="image/x-icon"/><link rel="icon" href="/favicon.ico" sizes="32x32"/><link rel="icon" href="/favicon.svg" type="image/svg+xml"/><link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" type="image/png"/><meta name="next-size-adjust" content=""/>
1<script src="/_next/static/immutable/chunks/0c0hxoamwjsbw.js" noModule=""></script>
1</head><body class="inter_5972bc34-module__OU16Qa__className"><div hidden=""><!--$--><!--/$--></div><div class="min-h-screen flex flex-col"><header class="sticky top-0 z-50 bg-black/80 backdrop-blur border-b border-gray-800"><div class="mx-auto max-w-6xl px-4 h-14 flex items-center justify-between"><a class="flex items-center gap-3" href="/"><span class="inline-grid place-items-center w-10 h-10 rounded-full bg-black"><img alt="VN" aria-hidden="true" width="22" height="22" decoding="async" data-nimg="1" style="color:transparent" src="/brand/vn.svg"/></span><span class="text-sm sm:text-base font-medium tracking-wide text-foreground">Vitali Nikolaev</span></a><nav class="flex items-center gap-5 text-sm"><div class="relative "><button class="tldr-button flex items-center gap-2 px-4 py-2 rounded-lg border text-gray-400 hover:text-purple-500 hover:bg-purple-500/10 transition-all text-sm border-gray-600 hover:border-purple-500"><span>TL;DR Bot</span></button></div><a class="text-foreground" href="/blog">Blog</a><div class="flex items-center gap-4"><a href="mailto:[email protected]" aria-label="Email Vitali Nikolaev" class="text-foreground hover:opacity-70"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg></a><a href="https://wa.me/5513991672854" aria-label="WhatsApp Vitali Nikolaev" target="_blank" rel="noopener noreferrer" class="text-foreground hover:opacity-70"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg></a><a href="https://t.me/vrnikolaev89" aria-label="Telegram Vitali Nikolaev" target="_blank" rel="noopener noreferrer" class="text-foreground hover:opacity-70"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></a></div></nav></div></header><main class="flex-grow" id="main-content"><div class="min-h-screen bg-black"><main class="mx-auto max-w-5xl p-6 space-y-12"><header><h1 class="text-3xl font-semibold text-white">Articles</h1></header><div class="grid md:grid-cols-2 gap-8">
1<a class="group border border-foreground/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/style-locked-image-generation"><img src="/images/blog/style-locked-cover.jpg" alt="Style-Locked Image Generation â When Every Asset Has to Match" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">Style-Locked Image Generation â When Every Asset Has to Match</h3><p class="text-sm text-foreground/70 line-clamp-3">Generating one good illustration is easy. Generating the four hundredth one that still matches the first is the actual problem. Here's the setup we ended up with.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>9/22/2026</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">image generation</span><span class="px-2 py-0.5 rounded-full bg-foreground
1/10 text-foreground/80">design systems</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">gamedev</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a>
1<a class="group border border-foreground/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/ai-platformer-tests"><img src="/images/blog/platformer-tests-cover.png" alt="Testing AI Code Generation: A Simple Platformer" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">Testing AI Code Generation: A Simple Platformer</h3><p class="text-sm text-foreground/70 line-clamp-3">I tasked three different AI toolsâGPT, Gemini, and Composer 1âwith creating a basic platformer game. Here is how they performed.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>1/22/2026</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">gamedev</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">coding</span><span class="px-2 py-0.5 rounded-full bg-foreground
1/10 text-foreground/80">experiment</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a><a class="group border border-foreground
1/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/ai-vs-the-dash-code-replication-challenge"><img src="/images/blog/ai-vs-dash-cover.png" alt="AI vs. The Dash: A Code Replication Challenge" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">AI vs. The Dash: A Code Replication Challenge</h3><p class="text-sm text-foreground/70 line-clamp-3">We tested three major AI modelsâGemini, Claude Sonnet, and GPTâalong with Grok to replicate Geometry Dash mechanics. Here's which model produced the most playable result.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>12/11/2025</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">comparison</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">game development</span>
1<span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">testing</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a>
1<a class="group border border-foreground/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/smart-contact-system"><img src="/images/blog/smart-contact-main.png" alt="Smart Contact System â When AI Starts Managing Your Inbox" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">Smart Contact System â When AI Starts Managing Your Inbox</h3><p class="text-sm text-foreground/70 line-clamp-3">Here's a simple but powerful example of how AI can read, sort, and reply to messages on your website. Build your own AI-powered inbox with just a few API calls.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>10/22/2025</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">automation</span><span class="px-2 py-0.5 rounded-full bg-foreground
1/10 text-foreground/80">contact</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">nextjs</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">supabase</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a>
1<a class="group border border-foreground/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/understanding-ai-tokens-hallucinations"><img src="/images/blog/ai-tokens-main.png" alt="Why AI "Hallucinates": Understanding Style, Tokens, and Vector Drift" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">Why AI "Hallucinates": Understanding Style, Tokens, and Vector Drift</h3><p class="text-sm text-foreground/70 line-clamp-3">AI can go off track not just because of complex questions, but because of your style of communication. Learn how tokens, vectors, and semantic gradients affect AI responses.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>10/8/2025</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">technical</span>
1<span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">machine learning</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">tokens</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a>
1<a class="group border border-foreground/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/tldr-bot-blog"><img src="/images/blog/tldr-bot-main.png" alt="I Started a Blog â But I Know Not Everyone Likes to Read" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">I Started a Blog â But I Know Not Everyone Likes to Read</h3><p class="text-sm text-foreground/70 line-clamp-3">So I added a bot that reads for you. How I built an automatic TL;DR bot that summarizes articles on my blog using AI.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>1/22/2025</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">blogging</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">automation</span><span class="px-2 py-0.5 rounded-full bg-foreground
1/10 text-foreground/80">ux</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a><a class="group border border-foreground
1/20 rounded-2xl overflow-hidden hover:shadow-lg transition" href="/blog/ai-calendar-assistant"><img src="/images/blog/article-cover.png" alt="When AI Becomes a Real Teammate: Building My Dream Calendar Assistant" class="w-full h-80 object-cover object-center"/><div class="p-5 space-y-2 bg-background"><h3 class="text-lg font-semibold group-hover:underline text-foreground">When AI Becomes a Real Teammate: Building My Dream Calendar Assistant</h3><p class="text-sm text-foreground/70 line-clamp-3">How I built an AI-powered calendar assistant that actually works like magic - no more dragging events around or checking for conflicts.</p><div class="flex items-center justify-between text-xs text-foreground/60"><span>1/21/2025</span><div class="flex gap-2"><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">case study</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">ai</span><span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">productivity</span>
1<span class="px-2 py-0.5 rounded-full bg-foreground/10 text-foreground/80">automation</span></div></div><div class="flex items-center justify-between pt-3 border-t border-foreground/20"><button class="flex items-center gap-1 transition-colors text-foreground/60 hover:text-red-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg><span>Like</span></button><button class="flex items-center gap-1 text-foreground/60 hover:text-blue-500 transition-colors"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16,6 12,2 8,6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg><span>Share</span></button></div></div></a></div></main></div><!--$--><!--/$--></main>
1<footer class="bg-background border-t border-foreground/20"><div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12"><div class="text-center"><h3 class="text-sm font-semibold text-foreground uppercase tracking-wider mb-6">Contact</h3><div class="flex flex-wrap justify-center gap-6"><a href="mailto:[email protected]" class="flex items-center text-foreground/70 hover:text-foreground transition-colors" target="_blank" rel="noopener noreferrer"><span class="mr-2 text-sm">ð§</span>Email</a><a href="https://wa.me/5513991672854" class="flex items-center text-foreground/70 hover:text-foreground transition-colors" target="_blank" rel="noopener noreferrer"><span class="mr-2 text-sm">ð±</span>WhatsApp</a><a href="https://t.me/vrnikolaev89" class="flex items-center text-foreground/70 hover:text-foreground transition-colors" target="_blank" rel="noopener noreferrer"><span class="mr-2 text-sm">ð¬</span>Telegram</a><a href="https://www.linkedin.com/in/vitali-nikolaev-6368161b9/" class="flex items-center text-foreground/70 hover:text-foreground transition-colors" target="_blank" rel="noopener noreferrer"><span class="mr-2 text-sm">ð¼</span>LinkedIn</a></div></div>
1<div class="mt-8 pt-8 border-t border-foreground/20"><p class="text-center text-foreground/60 text-sm">© <!-- -->2026<!-- --> Vitali Nikolaev. All rights reserved.</p></div></div></footer></div>
1<script defer="" src="/_vercel/insights/script.js"></script>
1<script src="/_next/static/immutable/chunks/36k8in9a291ok.js" id="_R_" async=""></script>
1<script>(self.__next_f=self.__next_f||[]).push([0])</script>
1<script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[79520,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"\"]\n4:I[90762,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"default\"]\n5:I[22016,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"\"]\n6:I[85437,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"Image\"]\n7:I[62630,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"default\"]\n13:I[68027,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"default\",1]\n:HL[\"/_next/static/immutable/chunks/1xm6ifu1qrlmp.css\",\"style\"]\n:HL[\"/_next/static/immutable/media/83afe278b6a6bb3c-s.p.45535valc9rzk.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n3:T41c,{\"@context\":\"https://schema.org\",\"@type\":\"Person\",\"name\":\"Vitalii Nikolaev\",\"jobTitle\":\"Product Designer \u0026 Product Manager\",\"description\":\"Independent product designer \u0026 manager helping startups and teams build, launch, and grow digital products. Available for freelance work.\",\"url\":\"https://personal-website-six-tau-39.vercel.app\",\"email\":\"[email protected]\",\"image\":\"https://personal-website-six-tau-39.vercel.app/images/main-photo.png\",\"sameAs\":[\"https://www.linkedin.com/in/vitali-nikolaev-6368161b9/\",\"https://t.me/vrnikolaev89\"],\"knowsAbout\":[\"Product Design\",\"User Experience Design\",\"Product Management\",\"Design Systems\",\"User Research\",\"Prototyping\",\"Product Strategy\"],\"hasOccupation\":{\"@type\":\"Occupation\",\"name\":\"Product Designer \u0026 Product Manager\",\"description\":\"Independent product designer \u0026 manager helping startups and teams build, launch, and grow digital products. Available for freelance work.\",\"skills\":[\"Product Design\",\"UX/UI Design\",\"Product Strategy\",\"User Research\",\"Prototyping\",\"Design Systems\",\"Product Management\"]}}11:X\n0:{\"P\":null,\"c\":[\"\",\"blog\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"blog\",{\"children\":[\"__PAGE__\",{},\"$undefined\",\"$undefined\",4608]},\"$undefined\",\"$undefined\",4608]},\"$undefined\",\"$undefined\",4624],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/immutable/chunks/1xm6ifu1qrlmp.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/immutable/chunks/0ean33l0me-9n.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"style\":{\"colorScheme\":\"dark\"},\"children\":[\"$\",\"body\",null,{\"className\":\"inter_5972bc34-module__OU16Qa__className\",\"children\":[[\"$\",\"$L2\",null,{\"id\":\"person-structured-data\",\"type\":\"application/ld+json\",\"dangerouslySetInnerHTML\":{\"__html\":\"$3\"}}],[\"$\",\"$L2\",null,{\"id\":\"website-structured-data\",\"type\":\"application/ld+json\",\"dangerouslySetInnerHTML\":{\"__html\":\"{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"WebSite\\\",\\\"name\\\":\\\"Vitalii Nikolaev â Product Designer \u0026 Manager\\\",\\\"description\\\":\\\"Independent product designer \u0026 manager helping startups and teams build, launch, and grow digital products.\\\",\\\"url\\\":\\\"https://personal-website-six-tau-39.vercel.app\\\",\\\"author\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Vitalii Nikolaev\\\"},\\\"potentialAction\\\":{\\\"@type\\\":\\\"SearchAction\\\",\\\"target\\\":\\\"https://personal-website-six-tau-39.vercel.app/search?q={search_term_string}\\\",\\\"query-input\\\":\\\"required name=search_term_string\\\"},\\\"mainEntity\\\":{\\\"@type\\\":\\\"Person\\\",\\\"name\\\":\\\"Vitalii Nikolaev\\\"}}\"}}],[\"$\",\"$L4\",null,{}],[\"$\",\"div\",null,{\"className\":\"min-h-screen flex flex-col\",\"children\":[[\"$\",\"header\",null,{\"className\":\"sticky top-0 z-50 bg-black/80 backdrop-blur border-b border-gray-800\",\"children\":[\"$\",\"div\",null,{\"className\":\"mx-auto max-w-6xl px-4 h-14 flex items-center justify-between\",\"children\":[[\"$\",\"$L5\",null,{\"href\":\"/\",\"className\":\"flex items-center gap-3\",\"children\":[[\"$\",\"span\",null,{\"className\":\"inline-grid place-items-center w-10 h-10 rounded-full bg-black\",\"children\":[\"$\",\"$L6\",null,{\"src\":\"/brand/vn.svg\",\"alt\":\"VN\",\"width\":22,\"height\"
1:22,\"priority\":true,\"aria-hidden\":true}]}],[\"$\",\"span\",null,{\"className\":\"text-sm sm:text-base font-medium tracking-wide text-foreground\",\"children\":\"Vitali Nikolaev\"}]]}],[\"$\",\"nav\",null,{\"className\":\"flex items-center gap-5 text-sm\",\"children\":[[\"$\",\"$L7\",null,{}],[\"$\",\"$L5\",null,{\"href\":\"/blog\",\"className\":\"text-foreground\",\"children\":\"Blog\"}],[\"$\",\"div\",null,{\"className\":\"flex items-center gap-4\",\"children\":[[\"$\",\"a\",null,{\"href\":\"mailto:[email protected]\",\"aria-label\":\"Email Vitali Nikolaev\",\"className\":\"text-foreground hover:opacity-70\",\"children\":[\"$\",\"svg\",null,{\"ref\":\"$undefined\",\"xmlns\":\"http://www.w3.org/2000/svg\",\"width\":20,\"height\":20,\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"stroke\":\"currentColor\",\"strokeWidth\":2,\"strokeLinecap\":\"round\",\"strokeLinejoin\":\"round\",\"className\":\"lucide lucide-mail\",\"aria-hidden\":\"true\",\"children\":[\"$L8\",\"$L9\",\"$undefined\"]}]}],\"$La\",\"$Lb\"]}]]}]]}]}],\"$Lc\",\"$Ld\"]}],\"$Le\"]}]}]]}],{\"children\":[\"$Lf\",{\"children\":[\"$L10\",{},null,false,null]},null,false,\"$11\"]},null,false,null],\"$L12\",false]],\"m\":\"$undefined\",\"G\":[\"$13\",[\"$L14\"]],\"S\":true,\"h\":null,\"r\":\"$undefined\",\"s\":\"$undefined\",\"a\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"mdbVDk-q72uwYSwYt0POd\"}\n15:I[39756,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"default\"]\n16:I[37457,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"default\"]\n17:I[5321,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\",\"/_next/static/immutable/chunks/25imx_awd0ntr.js\"],\"default\"]\n21:I[97367,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"ViewportBoundary\"]\n23:I[97367,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"MetadataBoundary\"]\n24:\"$Sreact.suspense\"\n8:[\"$\",\"path\",\"132q7q\",{\"d\":\"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"}]\n9:[\"$\",\"rect\",\"izxlao\",{\"x\":\"2\",\"y\":\"4\",\"width\":\"20\",\"height\":\"16\",\"rx\":\"2\"}]\na:[\"$\",\"a\",null,{\"href\":\"https://wa.me/5513991672854\",\"aria-label\":\"WhatsApp Vitali Nikolaev\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"className\":\"text-foreground hover:opacity-70\",\"children\":[\"$\",\"svg\",null,{\"ref\":\"$undefined\",\"xmlns\":\"http://www.w3.org/2000/svg\",\"width\":20,\"height\":20,\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"stroke\":\"currentColor\",\"strokeWidth\":2,\"strokeLinecap\":\"round\",\"strokeLinejoin\":\"round\",\"className\":\"lucide lucide-message-circle\",\"aria-hidden\":\"true\",\"children\":[[\"$\",\"path\",\"1sd12s\",{\"d\":\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\"}],\"$undefined\"]}]}]\nb:[\"$\",\"a\",null,{\"href\":\"https://t.me/vrnikolaev89\",\"aria-label\":\"Telegram Vitali Nikolaev\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"className\":\"text-foreground hover:opacity-70\",\"children\":[\"$\",\"svg\",null,{\"ref\":\"$undefined\",\"xmlns\":\"http://www.w3.org/2000/svg\",\"width\":20,\"height\":20,\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"stroke\":\"currentColor\",\"strokeWidth\":2,\"strokeLinecap\":\"round\",\"strokeLinejoin\":\"round\",\"className\":\"lucide lucide-send\",\"aria-hidden\":\"true\",\"children\":[[\"$\",\"path\",\"1ffxy3\",{\"d\":\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"}],[\"$\",\"path\",\"12cjpa\",{\"d\":\"m21.854 2.147-10.94 10.939\"}],\"$undefined\"]}]}]\nc:[\"$\",\"main\",null,{\"className\":\"flex-grow\",\"id\":\"main-content\",\"children\":[\"$\",\"$L15\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L16\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\"
1:\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]\nd:[\"$\",\"footer\",null,{\"className\":\"bg-background border-t border-foreground
1/20\",\"children\":[\"$\",\"div\",null,{\"className\":\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12\",\"children\":[[\"$\",\"div\",null,{\"className\":\"text-center\",\"children\":[[\"$\",\"h3\",null,{\"className\":\"text-sm font-semibold text-foreground uppercase tracking-wider mb-6\",\"children\":\"Contact\"}],[\"$\",\"div\",null,{\"className\":\"flex flex-wrap justify-center gap-6\",\"children\":[[\"$\",\"a\",\"Email\",{\"href\":\"mailto:[email protected]\",\"className\":\"flex items-center text-foreground/70 hover:text-foreground transition-colors\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"children\":[[\"$\",\"span\",null,{\"className\":\"mr-2 text-sm\",\"children\":\"ð§\"}],\"Email\"]}],[\"$\",\"a\",\"WhatsApp\",{\"href\":\"https://wa.me/5513991672854\",\"className\":\"flex items-center text-foreground/70 hover:text-foreground transition-colors\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"children\":[[\"$\",\"span\",null,{\"className\":\"mr-2 text-sm\",\"children\":\"ð±\"}],\"WhatsApp\"]}],[\"$\",\"a\",\"Telegram\",{\"href\":\"https://t.me/vrnikolaev89\",\"className\":\"flex items-center text-foreground/70 hover:text-foreground transition-colors\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"children\":[[\"$\",\"span\",null,{\"className\":\"mr-2 text-sm\",\"children\":\"ð¬\"}],\"Telegram\"]}],[\"$\",\"a\",\"LinkedIn\",{\"href\":\"https://www.linkedin.com/in/vitali-nikolaev-6368161b9/\",\"className\":\"flex items-center text-foreground/70 hover:text-foreground transition-colors\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"children\":[[\"$\",\"span\",null,{\"className\":\"mr-2 text-sm\",\"children\":\"ð¼\"}],\"LinkedIn\"]}]]}]]}],[\"$\",\"div\",null,{\"className\":\"mt-8 pt-8 border-t border-foreground
1/20\",\"children\":[\"$\",\"p\",null,{\"className\":\"text-center text-foreground/60 text-sm\",\"children\":[\"© \",2026,\" Vitali Nikolaev. All rights reserved.\"]}]}]]}]}]\ne:[\"$\",\"script\",null,{\"defer\":true,\"src\":\"/_vercel/insights/script.js\"}]\nf:[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L15\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L16\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}]\n18:T2823,\n\n# Style-Locked Image Generation â When Every Asset Has to Match\n\nGenerating one good illustration is easy. Generating the four hundredth one that still matches the first is the actual problem.\n\nWe build learning games for primary-school children, and a single product needs hundreds of assets: shop objects, animals, characters in a dozen poses, props, icons. They all have to look like they came from the same hand. A prompt that worked last Tuesday will not get you there. Here's the setup we ended up with, and the parts of it that surprised us.\n\nScenes are the boring part â a big picture, a long prompt, done. The interesting work is in **objects and characters**, because that's where drift actually shows.\n\n\u003cdiv data-embed=\"object-switcher\"\u003e\u003c/div\u003e\n\n## How We Made It\n\n### 1. A written spec, not a mood board\n\nThe instinct is to collect reference images. We did that first and it failed, for a reason that took a while to see: **an image is ambiguous instruction.** Show the model a picture and it can't tell whether you meant the composition, the palette, the lighting, or the brushwork. It picks one, usually not yours.\n\nSo the primary asset isn't the reference folder. It's a JSON file. Palette as hex codes with a role for each one. Line weight. Lighting. Camera language. Mood. An `avoid` list. All of it compiles to a text block that gets prepended to every single call, identically, forever.\n\nThe values aren't invented. They're measured off the art we'd already shipped â area-weighted dominant colours snapped to colours actually declared in those files. When we guessed, we were wrong.\n\nTwo dimensions sit on top of it. A **variant** answers *which world* â it swaps the palette. A **type** answers *what am I making* â object, character, icon, map â and swaps format, composition, and where needed the rendering rules. Overlays replace whole sections rather than merging, because a merged palette is not a palette.\n\nAnd one section no overlay can touch: `hard_rules`. Today it holds one rule â no text of any kind, anywhere. Image models garble lettering, and a rule that a type could quietly relax is not a rule.\n\n### 2. One command, no raw API calls\n\nEvery image goes through one wrapper. Not \"usually\" â every one, including throwaway tests.\n\n```bash\ngrafari generate \"a small brass hand bell\" --type object --variant city\n```\n\nThat command injects the versioned style block, attaches the right references, and writes a provenance sidecar next to the PNG: the full assembled prompt, the guide version, the reference IDs, the provider settings, a fingerprint. We have 731 of them.\n\nThis sounds like bureaucracy until the first time someone asks why an asset from six weeks ago looks off. Then it's the only thing that answers the question. An ad-hoc `client.images.generate(...)` anywhere in the project â even in a scratch script â defeats the whole setup, so there is no path for one.\n\n### 3. Objects: the renderer ignores adjectives and obeys physics\n\nThis is the part I'd tell anyone starting out.\n\n**Naming a colour does not work. Naming a substance does.**\n\nWe needed honeycomb toffee. It came back as a wedge of cheese â caramel-coloured, porous, unmistakably cheese. \"In the kraft-tan family, not yellow\" did nothing. What fixed it on the next call:\n\n\u003e deep toffee brown, the colour of set treacle toffee, never yellow, pale, buttery or cheese-coloured\n\nSame pattern with a parasol that had to avoid the palette's reserved gold. Two attempts came back with yellow canopies â measured at 55.3% and 35.4% of the object inside the warm-gold band. Naming a referent substance â *\"deep terracotta orange-brown, the colour of a fired clay flowerpot⦠if a panel looks like the colour of a lantern glowing, it is wrong\"* â dropped it to 3.4%.\n\n**Naming a shape you want beats naming the shape you don't.** A chocolate bar kept arriving in three-quarter view. Rounds of \"not three-quarter, flat elevation\" achieved nothing. This worked immediately:\n\n\u003e a plain flat upright rectangle, like a book standing on a shelf with its cover facing you\n\nIt then made two more objects come back flat on the *first* call. Give the model something to draw, not something to avoid.\n\n**And when a shape fights twice, change the subject, not the wording.** We wanted a heaped tray of pastries. Four attempts came back drawn from high above, showing the tray's inner floor, when the scene needed a near-level view. We rewrote the instructions twice more. Twelve generations, six rounds.\n\nThe problem wasn't the wording. A tray is locked to a raised viewpoint in this renderer â the same way a bowl and an open tin are. So we stopped asking for a tray. A **tiered stand** holds just as many pastries, belongs on a table, and its natural presentation is side-on. It came back correct on the first call.\n\nThat rule has held every time since. If two well-aimed rewrites don't move it, the subject is wrong, not the prompt.\n\n### 4. Characters: words hold the description, an image holds the face\n\nCharacters need t
1wo mechanisms, and people usually only build one.\n\nThe **cast entry** in the style guide is prose. It renders into the prompt only when a call names that character, and it holds who they are â their clothes, their build, their role. That gets you a consistent description.\n\nIt does not get you the same face twice. For that you need a **subject reference**: an actual shipped sprite of that character, attached to the call. Words hold the description; pixels hold the identity. Both, or the character drifts.\n\nOne trap worth flagging, because it fails silently. Attaching a reference by ID isn't enough â the reference selector filters by role, and it defaults to anchors. Pass the ID without the role and your subject reference is quietly filtered back out, the call runs, and you get a perfectly good image of someone else. It looks like it worked. It didn't.\n\nThe other rule: **generate a character's poses in one call, not one at a time.** A model sheet of three poses in a single image is internally consistent by construction. Three separate calls give you three cousins. Generate the sheet, then cut it into sprites.\n\n### 5. The spec described a world the art had left\n\nOur most expensive mistake wasn't a prompt. It was trusting our own documentation.\n\nThe guide's character section says, in its own words:\n\n\u003e naturalistic proportions â real head-to-body ratio for adults and children alike, never enlarged heads\n\nOur two lead characters do not obey that. They have generously proportioned heads, around one to five and a half, large round eyes with a single catch-light, bold saturated clothing. The guide described a more realistic drawing than the cast it existed to describe.\n\nSo when we briefed five new supporting characters, we quoted the guide â it was our own spec. All five came back with small realistic eyes, finely detailed adult faces, and muted clothes. Measured mean saturation: the leads at 0.817 and 0.710, the new characters at 0.453, 0.430 and 0.538. They were correct according to the document and wrong beside the cast.\n\nThe fix was to stop quoting the spec and spell out the cast's actual construction â large round eyes set well apart, one bright catch-light, arched brows, head about a sixth of total height, bold saturated colour blocks. All five came back on model, at 0.775, 0.777 and 0.719.\n\nThe lesson generalises past art: **when the spec and the shipped work disagree, the shipped work is the spec.** Open a finished asset next to the new one before judging it. We've now hit this three separate times in the same project â a palette, a character section, and a type that quietly overrode the palette beneath it.\n\n### 6. When words stop working, measure and fix it afterwards\n\nSome artefacts don't respond to prompting at all, and it's worth knowing which so you stop paying for rerolls.\n\nThe renderer bakes a soft contact shadow under roughly six in ten transparent objects, even though the spec forbids it. We tested whether wording could fix it. Adding an explicit \"no contact shadow, empty alpha everywhere the object is not\" moved one asset from **15,612 stray pixels to 16,001**. It got slightly worse.\n\nSo that one isn't a prompting problem. It's a post-processing script â and it separates shadow from genuine antialiasing by geometry rather than by alpha threshold, because true edge antialiasing touches the object's body and a shadow pool doesn't.\n\nThis matters beyond looks. Sprites are placed by anchoring to the bottom of their alpha bounding box, so an unstripped shadow silently lifts the object off its mark and shrinks it. One measured 469 pixels below the object's base.\n\nThe general habit: **spend one call measuring whether wording fixes an artefact before you write a post-process â and say what you measured.** Half the things we were sure about turned out to be wrong when we measured them.\n\n## What's Possible Next\n\n**1. Flip the spec to active.** The guide is still a draft. Once it's locked, editing it in place would invali
1date every sidecar pointing at that version, so changes become new versions and old images stay reproducible.\n\n**2. Train a LoRA.** The same manifest that drives reference selection is already a labelled training set. It's the strongest lever available, and it needs 20â100 curated images first â which is the real reason we haven't yet.\n\n**3. Close the feedback loop properly.** Rejections are logged with a reason drawn from a fixed vocabulary, never free text, because free text can't be counted and an uncounted reason never makes it back into the spec. Eighty-five rejects logged so far. The reasons should be driving spec changes automatically, and right now they're driving them manually.\n\n## You Can Try It\n\nNone of this needs a big stack. A JSON file, one wrapper function, a sidecar per image, and the discipline to never call the API directly. The rest is measurement.\n\nThe part I'd start with is the smallest: write your style down as text before you collect a single reference image, and derive the values from work you've already shipped rather than from what you think your style is. The gap between those two is where drift comes from.\n10:[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"div\",null,{\"className\":\"min-h-screen bg-black\",\"children\":[\"$\",\"main\",null,{\"className\":\"mx-auto max-w-5xl p-6 space-y-12\",\"children\":[[\"$\",\"header\",null,{\"children\":[\"$\",\"h1\",null,{\"className\":\"text-3xl font-semibold text-white\",\"children\":\"Articles\"}]}],[\"$\",\"div\",null,{\"className\":\"grid md:grid-cols-2 gap-8\",\"children\":[[\"$\",\"$L17\",\"style-locked-image-generation\",{\"article\":{\"id\":\"style-locked-image-generation\",\"title\":\"Style-Locked Image Generation â When Every Asset Has to Match\",\"description\":\"Generating one good illustration is easy. Generating the four hundredth one that still matches the first is the actual problem. Here's the setup we ended up with.\",\"date\":\"2026-09-22\",\"cover\":\"/images/blog/style-locked-cover.jpg\",\"tags\":[\"ai\",\"image generation\",\"design systems\",\"gamedev\"],\"slug\":\"style-locked-image-generation\",\"content\":\"$18\"}}],\"$L19\",\"$L1a\",\"$L1b\",\"$L1c\",\"$L1d\",\"$L1e\"]}]]}]}],[\"$L1f\"],\"$L20\"]}]\n12:[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$L21\",null,{\"children\":\"$L22\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$L23\",null,{\"children\":[\"$\",\"$24\",null,{\"name\":\"Next.Metadata\",\"children\":\"$L25\"}]}]}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}]\n14:[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/immutable/chunks/1xm6ifu1qrlmp.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]\n2b:I[97367,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"OutletBoundary\"]\n11:C\n19:[\"$\",\"$L17\",\"ai-platformer-tests\",{\"article\":{\"id\":\"ai-platformer-tests\",\"title\":\"Testing AI Code Generation: A Simple Platformer\",\"description\":\"I tasked three different AI toolsâGPT, Gemini, and Composer 1âwith creating a basic platformer game. Here is how they performed.\",\"date\":\"2026-01-22\",\"cover\":\"/images/blog/platformer-tests-cover.png\",\"tags\":[\"ai\",\"gamedev\",\"coding\",\"experiment\"],\"slug\":\"ai-platformer-tests\",\"content\":\"\\n \\n# Testing AI Code Generation: A Simple Platformer\\n\\nIn this brief experiment, I tasked three different AI toolsâGPT, Gemini, and Composer 1âwith the same objective: creating a basic platformer game. The goal was to observe how each model approached a standard coding request with minimal instruction. The process was straightforward and yielded functional results, serving as a quick demonstration of current capabilities in generating simple game logic without requiring a deep technical breakdown.\\n\\n## GPT\\n\\nKept the character png file.\\n\\n## Gemini\\n\\nSimplified the character for no reason.\\n\\n## Composer\\n\\nIgnored the character completely.\"}}
1]\n26:T98b,\n# AI vs. The Dash: A Code Replication Challenge\n\nHere we embark on a decisive AI test and comparison.\n\nThe rules of this experiment are simple.\n\nWe tasked three major AI modelsâGemini, Claude Sonnet, and GPTâalong with a bonus challenger, Grok, to replicate the core mechanics of the popular rhythm-based platformer, Geometry Dash, using only three simple prompts.\n\nThe goal was to see which model could produce the most accurate and, critically, the most playable result.\n\n## The Challenger: Gemini\n\n**Gemini: Best Score Calculation, Most Playable**\n\nGemini's output stood out immediately. The resulting levels were noted as being quite repetitive. However, the key victory was its singular, functional game mode.\n\nCrucially, it was the only one that was genuinely playable. The mechanics were solid enough to be considered even a bit addictive. It also implemented the most robust and accurate score calculation, tracking player progress best.\n\n**Verdict:** Gemini earned the title of \"Best\" in this challenge.\n\n## The Contender: Claude Sonnet\n\n**Claude Sonnet: Randomness and Complexity**\n\nClaude Sonnet showed ambition by producing three distinct game modes. It handled level generation well, creating genuinely random levels, which was a nice feature.\n\nHowever, the extra complexity introduced bugs. The \"ship\" mode required a quick fix to make the flying mechanics function correctly. The \"wave\" mode remained problematic, with movement that was \"still too wavy\" and imprecise.\n\n**Verdict:** Claude Sonnet showed \"Mid\" performance, needing significant refinement to be truly enjoyable.\n\n## The Underperformer: GPT\n\n**GPT: Random Collision, Strictly Unplayable**\n\nGPT struggled significantly with the core physics replication. The game suffered from random collision issues. It produced only one very strict level layout.\n\nUltimately, the result was deemed almost unplayable.\n\n**Verdict:** GPT was rated as the \"Worse\" performer, failing to grasp the necessary physics precision.\n\n## The Bonus Challenger: Grok\n\n**Bonus Grok: Playable and Addictive**\n\nAs a bonus entry, the code generated by Grok surprised the testers. It managed to produce a game that was genuinely playable.\n\nThe resulting code was somehow addictive, demonstrating a better understanding of mechanics than expected.\n\n**Verdict:** Grok delivered a strong surprise, showcasing unexpected capability and fun.1a:[\"$\",\"$L17\",\"ai-vs-the-dash-code-replication-challenge\",{\"article\":{\"id\":\"ai-vs-the-dash-code-replication-challenge\",\"title\":\"AI vs. The Dash: A Code Replication Challenge\",\"description\":\"We tested three major AI modelsâGemini, Claude Sonnet, and GPTâalong with Grok to replicate Geometry Dash mechanics. Here's which model produced the most playable result.\",\"date\":\"2025-12-11\",\"cover\":\"/images/blog/ai-vs-dash-cover.png\",\"tags\":[\"ai\",\"comparison\",\"game development\",\"testing\"],\"slug\":\"ai-vs-the-dash-code-replication-challenge\",\"content\":\"$26\"}}]\n27:T15f9,# Smart Contact System â When AI Starts Managing Your Inbox\n\nHere's a simple but powerful example of how AI can read, sort, and reply to messages on your website.\n\nYes, there are plenty of expensive CRM and chatbot platforms that promise the same, but in reality, it's surprisingly easy to build your own â with just a few API calls and a few hundred lines of real, production-grade code.\n\nWe wanted something that works like a real assistant:\nwhen someone writes through the website, it understands the message, replies politely, and sends it to the right place.\nNo dashboards, no long setup. Just a system that runs quietly in the background and does the work for you.\n\n\n\nAI can already do much more than just answer questions â it can quietly manage communication across all your channels.\nIt can read an incoming message, understand what it's about, label it as a lead, question, or feedback, write a short human-like reply, and deliver it wherever you work â to a private Discord channel, a Slack workspace, your email inbox, or even a Telegram chat.\nThat entire workflow happens automatically, without plugins or complex setup.\n\nThis project shows that kind of intelligence doesn't need a full SaaS platform.\nIf your website already uses a modern stack (Next.js, Supabase, OpenAI), you can integrate it in an afternoon and it will behave like a personal AI-powered inbox.\nLater, you can easily expand it â connect email, Slack, or analytics dashboards â because everything is modular and API-driven.\n\n\n\n## How We Made It\n\nThe system runs on Next.js, Supabase, OpenAI, and a private Discord inbox.\nBelow is the high-level flow we built and where the key fixes were applied.\n\n### 1. Frontend Form (SmartContact.tsx)\n\nA simple form component handles input and submission:\n\n```tsx\n\u003cform onSubmit={handleSubmit}\u003e\n \u003cinput name=\"name\" required /\u003e\n \u003cinput name=\"email\" type=\"email\" required /\u003e\n \u003ctextarea name=\"message\" required /\u003e\n \u003cbutton type=\"submit\"\u003eSend\u003c/button\u003e\n\u003c/form\u003e\n```\n\nIt sends data to /api/inbox.\nThe problem we solved early was reliability â the API must respond within two seconds and never hang.\nWe added precise loading states and a graceful fallback message if the API fails.\n\n### 2. Categorization API\n\nThe /api/inbox route handles everything: AI classification, database insert, Discord notification, and user reply.\n\nThe first call to OpenAI detects message intent:\n\n```typescript\nconst categorize = await openai.chat.completions.
1create({\n model: \"gpt-4o-mini\",\n messages: [\n { role: \"system\", content: \"Categorize the message as Lead, Question, Feedback, or General.\" },\n { role: \"user\", content: message },\n ],\n});\n```\n\nEarlier versions used keyword matching and failed on short messages like \"test.\"\nSwitching to OpenAI categorization fixed that and gave consistent accuracy across all message types.\n\n### 3. Natural AI Reply\n\nAfter categorization, we ask OpenAI again to generate a short, friendly response:\n\n```typescript\nconst prompt = `\nYou are an assistant replying to contact form messages.\nReply in one short, friendly sentence (max 15 words).\nUser: \"${message}\"\nDetected category: ${category}\n`;\n```\n\nThe API returns a simple human-like confirmation that we display on the website.\nThis replaced the previous static text \"We'll get back to you soon,\" which made even test messages look robotic.\n\n\n\n### 4. Supabase Storage and Discord Notification\n\nEach message is saved in a contact_messages table:\n\n```sql\nCREATE TABLE contact_messages (\n id UUID DEFAULT gen_random_uuid() PRIMARY KEY,\n name TEXT,\n email TEXT,\n message TEXT,\n category TEXT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n```\n\nThen it's sent to Discord through a webhook.\nA fix here was environment handling â Next.js API routes didn't see .env.local by default, so we created load-env.mjs to merge .env.supabase, .env.openai, and .env.discord before launching the dev server.\nThat change made local and production behavior identical.\n\nThe notification itself is a simple JSON post:\n\n```typescript\nawait fetch(process.env.DISCORD_WEBHOOK_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ content: formattedMessage }),\n});\n```\n\nNow every new contact shows up instantly in my private Discord channel.\n\n## What's Possible Next\n\nOnce messages are sorted and labeled automatically, a lot of doors open.\nHere are the next steps planned for this system.\n\n\n\n### 1. Auto-reply Email Layer\n\nSend a real email reply containing the AI's message and your contact info.\nIt feels personal, even though it's automatic.\n\n### 2. Smart Analytics Vie
1w\n\nAdd a small /inbox dashboard showing messages by category, sentiment, or date.\nYou could see that 60% of your messages are leads or that most questions come after blog posts.\n\n### 3. Contextual Memory\n\nKeep conversation threads.\nIf the same person writes again, the AI continues with awareness of past context â \"Thanks for following up about your project.\"\n\n### 4. Custom Tone Profiles\n\nAdjust the AI's communication style per section of the site:\nfriendly on the blog, concise on documentation pages, and professional on client forms.\n\n## You Can Try It\n\nThis system is live on the site and working right now. The Smart Contact Form you see at the bottom of this article is powered by the exact system described above.1b:[\"$\",\"$L17\",\"smart-contact-system\",{\"article\":{\"id\":\"smart-contact-system\",\"title\":\"Smart Contact System â When AI Starts Managing Your Inbox\",\"description\":\"Here's a simple but powerful example of how AI can read, sort, and reply to messages on your website. Build your own AI-powered inbox with just a few API calls.\",\"date\":\"2025-10-22\",\"cover\":\"/images/blog/smart-contact-main.png\",\"tags\":[\"ai\",\"automation\",\"contact\",\"nextjs\",\"supabase\"],\"slug\":\"smart-contact-system\",\"content\":\"$27\"}}]\n28:T24d4,# Why AI \"Hallucinates\": Understanding Style, Tokens, and Vector Drift\n\nAI can \"go off track\" not just because of complex questions â but because of your style of communication. Words with similar meanings may sit on opposite ends of the vector space â and the model won't always know you meant the same thing. A sudden shift in tone â say, from joke to \"stern professor\" â can blur the meaning gradient. And if you ask the AI to be both Schopenhauer and a stand-up comedian? The attention blocks will overheat like a microwave with foil.\n\nAll of this distorts the model's internal vector structure. Instead of a helpful answer, you may get a confident but nonsensical improvisation. That's why style matters â not as politeness, but as token precision.\n\n## Why AI \"Lies\": Let's Start with an Example\n\n**Prompt:** \nName three books written by Aristotle about poetry.\n\n**GPT Response:**\n1. Poetics\n2. Treatise on the Art of Poetry\n3. Rhetoric: Book III\n\nAt first glance, this seems like a logical, academic list.\nBut in reality â it's mostly made up. Only *Poetics* is an actual work by Aristotle about poetry. The others were hallucinated because:\n- GPT predicts â it doesn't fact-check.\n- It has no built-in epistemology (no concept of true vs false).\n- It fills gaps when unsure how to respond.\n- Most importantly â it doesn't know what a lie is.\n\nGPT doesn't choose between \"truth\" and \"falsehood\". It generates the most statistically plausible sequence of tokens based on its training.\n\nEven outright falsehoods can sound confident â because confidence is just a style, not a marker of knowledge.\n\n## How GPT \"Understands\" Words: From Predictions to Cosine Similarity\n\nWhen we say GPT \"predicts the next token,\" it doesn't mean it \"guesses the word.\"\n\nInstead, GPT operates in a vector space, where each token is a high-dimensional vector. These vectors are positioned based on how frequently tokens appear in similar contexts during training.\n\n\n\n### What is Cosine Similarity?\n\nCosine Similarity measures the angle between two vectors â not the distance.\n\n**Values:**\n1 = exact same direction (similar meaning)\n0 = perpendicular (unrelated)\n-1 = opposite meanings\n\n### Example: High Cosine Similarity\n\n**Compare:**\n- \"I love building model kits.\"\n- \"I enjoy building Lego.\"\n\nThese have nearly identical meaning.\nTheir tokens â build, models, Lego â often co-occur in training data, leading to high cosine similarity.\n\n### Example: Similar Meaning, Low Cosine Similarity\n\n**Compare:**\n- \"I don't want to be here.\"\n- \"Sometimes I imagine disappearing.\"\n\nDeep emotional similarity â detachment, fatigue, escape.\nBut surface-wise: not a single shared word or even synonym.\n\nThis results in lower cosine similarity because:\n- They come from different lexical fields.\n- No literal overlap, despite similar intent.\n\n## How GPT \"Chooses\" a Word\n\nAt each step in generation:\n1. GPT computes a vector for the current context.\n2. It compares that vector to all token vectors.\n3. It filters tokens with high cosine similarity.\
1n4. It picks one based on probability.\n\nIt doesn't \"understand\" that the next word is \"poetry.\"\nIt \"sees\" that the vector for poetry fits well â so it inserts it.\n\n\n\n### Why Similar Phrases Might Not Match\n\n- **Metaphors**: \"Leap into the abyss\" â \"Give up\" lexically, but often carry the same emotional meaning.\n- **Rarity**: If a word combo is rare, the model may fail to tune in precisely.\n- **Cultural references**: \"Red pill\" â \"Understand the truth\" unless you know The Matrix.\n\nThis is why stylistically different but semantically similar texts can be misunderstood if they weren't well represented in training data.\n\n## When Meaning Drifts: The Wide Vector Gradient\n\nAs conversations grow longer or cover many topics, something happens that could be described as:\n\n**The expansion of the meaning gradient** â the semantic field stretches, and the model's attention becomes unevenly distributed.\n\nThis relates directly to GPT's architecture:\nEverything the model \"remembers\" sits in the context window â a large but limited memory space where each exchange is stored as a sequence of tokens.\n\n\n\n### What Is a \"Wide Vector Gradient\"?\n\nEach new topic introduces a new semantic direction.\nIf you're talking just about math â vectors align.\nThen you switch to Buddhism, linguistics, neuroscience â vectors diverge.\n\n**Result:**\n- Too many semantic directions become active.\n- The model loses track of which context is dominant.\n- It might mix roles â philosopher, scientist, assistant.\n- It forgets earlier instructions from the conversation.\n\n### It Feels Like Losing Focus\n\nEvery attention head in GPT handles different token relationships.\nWhen topics multiply:\n- Attention weights scatter.\n- \"Context\" becomes ambiguous.\n- The semantic gradient widens and loses cohesion.\n\nYou'll notice:\n- Hesitant answers\n- Reverting to neutral style\n- \"Role reset\" â the model stops following its earlier persona\n\n### Human Analogy\n\nIt's like juggling too many abstractions at once.\nYou remember each idea individually,\nbut with all of them active â your thoughts blur into generalities like \"be polite\" or \"answer logically.\"\n\nGPT behaves similarly: it collapses meaning into an average mode.\n\nA wide semantic gradient = too many meanings active at once â loss of local coherence for the sake of global smoothness.\n\n\n\n## Who's Really Talking: The Assistant vs The Model\n\nWhen you talk to ChatGPT, you're interacting with two layers:\n1. **The Assistant** â a persona, e.g., \"friendly helper.\"\n2. **The Model** â the neural network generating tokens based on statistics.\n\nThe assistant provides a frame (\"be helpful, follow instructions\"),\nbut the model has no internet access, no facts â it just continues the pattern.\n\n### Why Doesn't GPT Say \"I Don't Know\"?\n\nBecause it doesn't know that it doesn't know.\n\nHere's what happens:\n1. The model computes the current context vector.\n2. It checks which tokens \"fit\" best (cosine similarity).\n3. If nothing fits exactly, it picks the next closest match.\n\nThis is **gap-filling** â inserting plausible but potentially false content.\n\nThe model doesn't distinguish:\n- \"I know this\"\n- \"I don't know this\"\n- \"I made this up because it looked good\"\n\nGPT has no default epistemic frame that includes doubt or self-awareness.\n\n### Can I Make GPT Say \"I Don't Know\"?\n\nYes â if you explicitly tell it to.\nFor example:\n\n*\"If unsure, say: 'I'm not certain about this' before answering.\"*\n\nThis shifts the assistant's framing:\n- The assistant adds this rule to the prompt.\n- The model prioritizes \"doubt\" templates even when tempted to invent.\n\nHowever:\n- It might still simulate doubt, not feel it.\n- If you press for an answer â it will likely guess.\n\n### So Who Hallucinates â the Assistant or the Model?\n\n- The model always hallucinates.\n- The assistant just styles the hallucination.\n\n**Examples:**\n- A formal assistant: \"I don't have data on that.\"\n- A creative one: \"Here's a hypothetical versionâ¦\"\n\nBut both are powered by the same network that predicts tokens â not truths.\n\n\n\nGPT \"lies\" not out of malice, but because silence is not a token.\nIt doesn't know how to stay quiet â only how to continue.\n\n## Do Machines Have a \"Natural Language\"?\n\n
1If we define \"natural language\" as the medium of thought â machines don't have one.\nThey don't think in words, tones, or grammar.\n\nBut every architecture has a most expressive form â\nand that can be considered its natural language.\n\n### Architecture vs Natural Language\n\n| Architecture | Natural Expression |\n|--------------|-------------------|\n| CPU | Machine code, register logic |\n| Turing Machine | Symbol tape + transition table |\n| CNN | Convolutions, filters, feature maps |\n| Stable Diffusion | Latent image vectors |\n| Google Search | Link graphs, PageRank, query tokens |\n| GPT (LLM) | Tokens + attention weights + semantic vectors |\n\nFor GPT, the \"native language\" isn't English â\nbut vectorized token representations organized through attention mechanisms.\n\nHowever, English is:\n- Clearly tokenizable\n- Rich in statistical patterns\n- Logically structured (SVO)\n- Heavily present in training data\n\nâ¦which makes it the best bridge between users and the model.\n\n### Why Do Two AIs Talk in English?\n\nEven when AIs interact in experiments using \"beeps\" or invented codewords,\nunder the hood they still:\n- Convert data into vectors\n- Use attention and softmax to select tokens\n- And usually do this using the language they were trained on\n\nSo English becomes the most efficient medium â even for machines talking to each other â if both were optimized for human interaction.\n\n\n\n### Could a Pure \"Machine Language\" Exist?\n\nYes â and it already does in some cases:\n- In agent simulators (robot coordination)\n- In vision transformers (feature map exchange)\n- In meta-models (models training other models via internal states)\n\nBut for meaningful dialogue aimed at logic and explanation,\nhuman language remains the most energy-efficient interface.1c:[\"$\",\"$L17\",\"understanding-ai-tokens-hallucinations\",{\"article\":{\"id\":\"understanding-ai-tokens-hallucinations\",\"title\":\"Why AI \\\"Hallucinates\\\": Understanding Style, Tokens, and Vector Drift\",\"description\":\"AI can go off track not just because of complex questions, but because of your style of communication. Learn how tokens, vectors, and semantic gradients affect AI responses.\",\"date\":\"2025-10-08\",\"cover\":\"/images/blog/ai-tokens-main.png\",\"tags\":[\"ai\",\"technical\",\"machine learning\",\"tokens\"],\"slug\":\"understanding-ai-tokens-hallucinations\",\"content\":\"$28\"}}]\n29:T14d0,# I Started a Blog â But I Know Not Everyone Likes to Read\n\nSo I added a bot that reads for you.\n\nWhen I launched my personal blog, I was excited to finally have a space to write, document my projects, and share what I've been building. But I also knew the truth: **most people don't want to read full articles**. We skim. We scroll. We save things for later (and never come back).\n\nSo I made a decision: if I want people to get value even when they don't read the whole thing â **I need to summarize it for them**.\n\nBut instead of adding a classic \"TL;DR\" block by hand each time, I built a **TL;DR bot** that does it automatically. It lives right on the article page and gives you a short summary the moment the article loads.\n\nHere's how it works â and how you can build one too.\n\n\n\n## The Idea: Automatic Summarization, Built Into the Blog\n\nAI summarization tools are everywhere â but they usually require effort:\n\n- You copy-paste content into ChatGPT \n- You prompt it correctly \n- You clean up the result \n- You try again if it doesn't feel right \n\nIt's manual. And most visitors won't do it.\n\nI wanted something that **just works**. A summarizer that:\n\n- Lives on the page itself \n- Doesn't need user input \n- Doesn't need any prompt \n- Delivers value in a second \n\nSo I embedded the summarizer into my blog system â and now every article has a **small, elegant TL;DR block**, written by AI, powered by the actual content of the article, and refreshed when the text changes.\n\n\n\n## The Concept: What the TL;DR Bot Does\n\nHere's what the bot does:\n\n1. **Lives on the page** \n The summarizer is built directly into the webpage â no extensions, no external tools. It integrates with the blog layout and runs
1in the browser.\n\n2. **Summarizes the article on demand** \n It reads the article content from the page and sends it to an internal API connected to GPT-4, which returns a short, natural-language summary.\n\n3. **Displays the TL;DR inline** \n The AI-generated summary is injected right into the article layout â styled to match the look of the blog â and can appear anywhere: top, inline, or even on hover.\n\nIt's a native experience, seamlessly integrated into the reading flow.\n\n## How to Build a TL;DR Bot Like This\n\nHere's how you can implement something similar on your own site.\n\n### Step 1: Frontend Integration\n\nIn your blog's frontend (React, Next.js, Framer, whatever you're using), define the element that holds the article content.\n\n```js\nconst articleText = document.querySelector('.post-body')?.innerText;\n```\n\nSend that text to a summarization API when the page loads:\n\n```js\nfetch('/api/summarize', {\n method: 'POST',\n body: JSON.stringify({ text: articleText }),\n headers: { 'Content-Type': 'application/json' }\n})\n.then(res =\u003e res.json())\n.then(data =\u003e {\n const tldr = document.createElement('div');\n tldr.className = 'tldr-block';\n tldr.innerHTML = `\u003ch3\u003eTL;DR\u003c/h3\u003e\u003cp\u003e${data.summary}\u003c/p\u003e`;\n document.querySelector('.post-body').before(tldr);\n});\n```\n\nYou can wrap this in a debounce or cache to avoid extra calls.\n\n### Step 2: Create the API Endpoint\n\nOn the backend (Next.js API route, Supabase Edge Function, or Express server), handle the summarization with OpenAI:\n\n```ts\n// POST /api/summarize\nimport { OpenAI } from 'openai';\n\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport default async function handler(req, res) {\n const { text } = req.body;\n\n const prompt = `Summarize this blog article in 3-5 sentences, assuming the reader is curious but busy:\\n\\n${text}`;\n\n const completion = await openai.chat.completions.create({\n model: 'gpt-4',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n });\n\n const summary = completion.choices[0].message.content;\n res.status(200).json({ summary });\n}\n```\n\n### Step 3: Style and Position the Summary\n\nStyle the TL;DR block to look native to your blog:\n\n```css\n.tldr-block {\n background: #f5f5f5;\n border-left: 4px solid #222;\n padding: 1rem;\n margin-bottom: 2rem;\n font-size: 0.95rem;\n}\n.tldr-block h3 {\n margin-top: 0;\n}\n```\n\nPosition it before the article or inside the header area, depending on your layout.\n\n## Why It Works\n\nThis little AI widget solves a real problem:\n\n- Readers get value even if they skim \n- The blog feels more polished and intelligent \n- Summaries are consistent and fast to generate \n- I don't have to manually write TL;DRs anymore \n\n\n\nIt's also fun to see how GPT-4 interprets my writing. Sometimes it picks up things I didn't realize were important.\n\n## Want to Add It to Your Site?\n\nYou can replicate this with a few lines of frontend JavaScript and a basic API endpoint. If your site uses a headless CMS, you can even trigger summarization on publish and save the TL;DR statically.\n\nOr, if you want something custom â just reach out. I'm always happy to help people build smarter sites with AI.1d:[\"$\",\"$L17\",\"tldr-bot-blog\",{\"article\":{\"id\":\"tldr-bot-blog\",\"title\":\"I Started a Blog â But I Know Not Everyone Likes to Read\",\"description\":\"So I added a bot that reads for you. How I built an automatic TL;DR bot that summarizes articles on my blog using AI.\",\"date\":\"2025-01-22\",\"cover\":\"/images/blog/tldr-bot-main.png\",\"tags\":[\"ai\",\"blogging\",\"automation\",\"ux\"],\"slug\":\"tldr-bot-blog\",\"content\":\"$29\"}}]\n2a:T198b,# How to Build an AI-Powered Calendar Assistant That Feels Like Magic\n\n## Why This Matters\n\nImagine you just say, \"Add this task, estimate the time, and find me a slot this week\" â and your calendar reshuffles itself. No dragging events around, no endless checking for conflicts. That's what I built with AI, and in this guide I'll show you how you can create something similar.\n\nThis isn't about coding for the sake of it. It's about boosting productivity and letting AI act like a real teammate inside your schedule.\n\n\n\n⸻\n\n## Step 1: Define the Workflow You Want\n\nStart with clarity on the experience:\n1. You describe what you need.\n2. The assistant estimates how long it will take.\n3. It finds a free slot in y
1our week.\n4. It either inserts the task â or politely says there's no space.\n\nð **Tip:** Think in weekly units. This keeps the system simple and avoids chasing daily perfection.\n\n⸻\n\n## Step 2: Choose the Right Technology\n\nI tested two approaches:\n⢠**AppleScript** (what not to use): Slow, unreliable, constant permission errors.\n⢠**EventKit with Swift** (what works): Fast, reliable, full property access, clean JSON output.\n\nð **Lesson:** Don't patch broken tools. Pick a framework built for the job.\n\n\n\n⸻\n\n## Step 3: Build a Safe Command-Line Tool\n\nUsing Swift + EventKit, you can create a CLI with natural commands:\n```bash\nekcal list-calendars \nekcal events --from ISO --to ISO --tz TZ \nekcal create --cal NAME --title \"...\" --from ISO --to ISO \nekcal delete --id EVENT_ID \n```\n\nThese commands are enough to read, create, or delete events â but the magic is in the design:\n⢠**Dry runs first.** The tool shows proposed changes in a text file.\n⢠**Confirmation required.** Only after you approve does it touch the calendar.\n\nð **Tip:** Always put a safety buffer between AI and your actual data.\n\n⸻\n\n## Step 4: How to Instruct Cursor Correctly\n\nOne of the hardest (and most interesting) parts wasn't writing code â it was telling Cursor what to do in the right way. Good instructions produce usable code; vague ones lead to bugs, endless retries, or broken logic.\n\n### Golden Rules for Prompting Cursor\n1. **Be specific:** Always mention the framework (EventKit), language (Swift), and output format (JSON).\n2. **Ask for CLI design early:** Force Cursor to think in commands, not just functions.\n3. **Test in small chunks:** Don't ask for \"the whole app.\" Ask for \"list all calendars first.\"\n4. **Always demand error handling:** Cursor tends to skip this unless explicitly told.\n\n### Example: A Bad Instruction\n\nâ \"Make a script that lists calendar events.\"\n⢠Cursor might default to AppleScript.\n⢠No timezone support.\n⢠No error handling.\n```applescript\ntell application \"Calendar\"\n get summary of every event\nend tell\n```\nThis \"works,\" but fails on permissions, produces incomplete results, and is painfully slow.\n\n### Example: A Good Instruction\n\nâ
\"Write a Swift CLI tool using EventKit that lists events between two ISO8601 dates, outputs JSON, and includes timezone handling. Add error handling for missing permissions.\"\n\n```swift\nimport EventKit\nimport Foundation\n\nlet eventStore = EKEventStore()\neventStore.requestAccess(to: .event) { granted, error in\n guard granted else {\n print(\"{\\\"error\\\":\\\"Permission denied\\\"}\")\n return\n }\n\n let start = ISO8601DateFormatter().date(from: CommandLine.arguments[1])!\n let end = ISO8601DateFormatter().date(from: CommandLine.arguments[2])!\n let predicate = eventStore.predicateForEvents(withStart: start, end: end, calendars: nil)\n\n let events = eventStore.events(matching: predicate)\n let output = events.map { [\n \"title\": $0.title ?? \"\",\n \"start\": ISO8601DateFormatter().string(from: $0.startDate),\n \"end\": ISO8601DateFormatter().string(from: $0.endDate)\n ]}\n if let data = try? JSONSerialization.data(withJSONObject: output, options: .prettyPrinted) {\n print(String(data: data, encoding: .utf8)!)\n }\n}\n```\n\nThis code:\n⢠Uses EventKit directly.\n⢠Handles permissions gracefully.\n⢠Accepts ISO8601 dates.\n⢠Returns clean JSON for downstream use.\n\nð **Lesson:** AI coding tools are only as good as the instructions you give them. Think like a teacher, not a magician.\n\n\n\n## Step 5: Solve the Hard Problems Early\n\nHere are the key challenges and how to tackle them:\n⢠**Permissions:*
1* Handle macOS access gracefully. Never assume the app has rights.\n⢠**Timezones:** Always specify explicitly. Events live in different zones.\n⢠**Recurring events:** Use EventKit's built-in RRULE parser, never roll your own.\n⢠**Errors:** Classify them (permanent, data, transient) and retry smartly.\n⢠**Performance:** Use EventKit filters and JSON streaming instead of brute force.\n\nð **Lesson:** Reliability comes from handling the edge cases, not just the happy path.\n\n\n\n⸻\n\n## Step 6: Use It in Daily Life\n\nOnce the system is stable:\n⢠Type a task in plain language.\n⢠Let AI estimate its duration.\n⢠Preview the proposed slot.\n⢠Approve, and your calendar updates.\n\nIt won't be perfect â AI guesses can be wrong â but it saves hours of micro-management.\n\n⸻\n\n## Productivity Tips\n⢠**Think big â then delegate:** Don't micromanage your calendar, describe goals and let AI fill details.\n⢠**Trust but verify:** Keep dry runs; review before committing.\n⢠**Batch similar tasks:** Group work sessions so the AI can block them in chunks.\n⢠**Stay timezone-aware:** Especially if you collaborate internationally.\n\n## What's Next?\n\nOnce you master the basics, you can extend the tool:\n⢠Batch operations (plan multiple tasks at once).\n⢠Sync with Google Calendar or Outlook.\n⢠AI-powered conflict resolution.\n⢠Team collaboration, where everyone's schedules align automatically.\n\nð This is where AI stops being \"just another app\" and starts acting like a real teammate.\n 1e:[\"$\",\"$L17\",\"ai-calendar-assistant\",{\"article\":{\"id\":\"ai-calendar-assistant\",\"title\":\"When AI Becomes a Real Teammate: Building My Dream Calendar Assistant\",\"description\":\"How I built an AI-powered calendar assistant that actually works like magic - no more dragging events around or checking for conflicts.\",\"date\":\"2025-01-21\",\"cover\":\"/images/blog/article-cover.png\",\"tags\":[\"case study\",\"ai\",\"productivity\",\"automation\"],\"slug\":\"ai-calendar-assistant\",\"content\":\"$2a\"}}]\n1f:[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/immutable/chunks/25imx_awd0ntr.js\",\"async\":true,\"nonce\":\"$undefined\"}]\n20:[\"$\",\"$L2b\",null,{\"children\":[\"$\",\"$24\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@2c\"}]}]\n22:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n2d:I[27201,[\"/_next/static/immutable/chunks/2corl6wyj4669.js\",\"/_next/static/immutable/chunks/0ean33l0me-9n.js\"],\"IconMark\"]\n25:[[\"$\",\"title\",\"0\",{\"children\":\"Vitali Nikolaev\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Portfolio and work of Vitali Nikolaev â Product Design, Product Management, and Launches.\"}],[\"$\",\"link\",\"2\",{\"rel\":\"author\",\"href\":\"https://personal-website-six-tau-39.vercel.app\"}],[\"$\",\"meta\",\"3\",{\"name\":\"author\",\"content\":\"Vitalii Nikolaev\"}],[\"$\",\"meta\",\"4\",{\"name\":\"keywords\",\"content\":\"product design,product management,UX design,UI design,freelancer,product strategy,design systems,startup design,digital products\"}],[\"$\",\"meta\",\"5\",{\"name\":\"creator\",\"content\":\"Vitalii Nikolaev\"}],[\"$\",\"meta\",\"6\",{\"name\":\"publisher\",\"content\":\"Vitalii Nikolaev\"}],[\"$\",\"meta\",\"7\",{\"name\":\"robots\",\"content\":\"index, follow\"}],[\"$\",\"meta\",\"8\",{\"name\":\"googlebot\",\"content\":\"index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1\"}],[\"$\",\"meta\",\"9\",{\"name\":\"format-detection\",\"content\":\"telephone=no, address=no, email=no\"}],[\"$\",\"meta\",\"10\",{\"property\":\"og:title\",\"content\":\"Product Designer \u0026 Product Manager â Vitalii Nikolaev\"}],[\"$\",\"meta\",\"11\",{\"property\":\"og:description\",\"content\":\"Independent product designer \u0026 manager helping startups and team
1s build, launch, and grow digital products. Available for freelance work.\"}],[\"$\",\"meta\",\"12\",{\"property\":\"og:url\",\"content\":\"https://personal-website-six-tau-39.vercel.app\"}],[\"$\",\"meta\",\"13\",{\"property\":\"og:site_name\",\"content\":\"Vitalii Nikolaev â Product Designer \u0026 Manager\"}],[\"$\",\"meta\",\"14\",{\"property\":\"og:locale\",\"content\":\"en_US\"}],[\"$\",\"meta\",\"15\",{\"property\":\"og:image\",\"content\":\"https://personal-website-six-tau-39.vercel.app/images/main-photo.png\"}],[\"$\",\"meta\",\"16\",{\"property\":\"og:image:width\",\"content\":\"1024\"}],[\"$\",\"meta\",\"17\",{\"property\":\"og:image:height\",\"content\":\"1024\"}],[\"$\",\"meta\",\"18\",{\"property\":\"og:image:alt\",\"content\":\"Vitalii Nikolaev - Product Designer \u0026 Manager\"}],[\"$\",\"meta\",\"19\",{\"property\":\"og:type\",\"content\":\"website\"}],[\"$\",\"meta\",\"20\",{\"name\":\"twitter:card\",\"content\":\"summary_large_image\"}],[\"$\",\"meta\",\"21\",{\"name\":\"twitter:title\",\"content\":\"Product Designer \u0026 Product Manager â Vitalii Nikolaev\"}],[\"$\",\"meta\",\"22\",{\"name\":\"twitter:description\",\"content\":\"Independent product designer \u0026 manager helping startups and teams build, launch, and grow digital products.\"}],[\"$\",\"meta\",\"23\",{\"name\":\"twitter:image\",\"content\":\"https://personal-website-six-tau-39.vercel.app/images/main-photo.png\"}],[\"$\",\"link\",\"24\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.3fpu2ql9ns1a0.ico\",\"sizes\":\"256x256\",\"type\":\"image/x-icon\"}],[\"$\",\"link\",\"25\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"sizes\":\"32x32\"}],[\"$\",\"link\",\"26\",{\"rel\":\"icon\",\"href\":\"/favicon.svg\",\"type\":\"image/svg+xml\"}],[\"$\",\"link\",\"27\",{\"rel\":\"apple-touch-icon\",\"href\":\"/apple-touch-icon.png\",\"sizes\":\"180x180\",\"type\":\"image/png\"}],[\"$\",\"$L2d\",\"28\",{}]]\n2c:null\n"])</script>
1</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.