Two days of AI-assisted coding gave me a working 3D real estate dashboard. Every field looked realistic. The listings made no sense. Part 1 of a series on building with AI instead of just reviewing it.

I built this in two days with AI assistance.

Before you scroll further, try to find three bugs on this screenshot.
I did not find them myself. I was checking whether the dashboard looked good enough: camera angle, spacing, labels, colors. Then I pasted the screenshot into an AI chat to discuss a blog post about it. The AI read my data more carefully than I did.
Here is the list:
Every field looks realistic on its own. Together, the listings make no sense. And I almost published a post about this dashboard without noticing.
A small side project: High Water Mark, a real estate dashboard. React, TypeScript, Vite, shadcn/ui, and a Three.js scene where every listing is a bar on a grid. Height shows the area, color shows the status. You can rotate the view, hover a bar, click it to see details.
The data comes from faker-js: 50 generated listings with Polish cities, districts and prices in PLN. Stats are computed live. The whole thing took two days of AI-assisted work. Most of that time went into the 3D scene and the layout.
The speed was real. That part of the AI promise works.
But speed has a funny side effect: it makes you trust the thing sooner than you should.
Here is the original generator, simplified:
// title: picks a random city, independent from location
const title = `${typeLabel} ${area} m² - ${faker.helpers.arrayElement(cities)}`;
// location: picks a city again, district from a SHARED pool
const location = {
city: faker.helpers.arrayElement(cities),
district: faker.helpers.arrayElement(allDistricts),
};
// type, area and rooms: three independent rolls
const type = faker.helpers.arrayElement(types);
const area = faker.number.int({ min: 25, max: 220 });
const rooms = faker.number.int({ min: 0, max: 6 });Each line is fine. Each value is plausible. A city from a real list of cities. A district from a real list of districts. An area in a sane range.
The problem: nothing connects them. The title rolls its own city. The district does not know which city it belongs to. A "house" does not know it should not be 28 m². A "studio" does not know it cannot have 5 rooms.
This is the same failure mode I keep writing about in AI-generated code.
A hallucinated function call looks right. The import looks right. The names look right. Every fragment passes a quick look, because every fragment is plausible on its own. The bug lives in the relationships between fragments. And that is exactly where a tired reviewer does not look.
My generator did the same thing with data. And the funny thing is that I was not blind to bugs. I was actively looking for them. Just not these bugs.
I was checking interactions, layout, hover states, whether the bars were readable, whether the UI felt too toy-like. I was reviewing the thing as an interface, not as a dataset. And the dataset used that gap perfectly.
The uncomfortable part: this was not someone else's pull request. This was my own project, my own two days, and my own screenshot. I was about to publish it as "look what AI can build".
My first instinct was to patch the obvious cases. Do not allow 6-room studios. Do not pick Warsaw districts for Szczecin. Do not create tiny houses.
But that would only hide the same bug in three places. The real issue was not one bad value. It was the shape of the generator.
So instead of rolling every field independently, roll the causes and derive the effects:
// 1. City first, district only from THAT city's districts
const city = faker.helpers.arrayElement(cities);
const district = faker.helpers.arrayElement(districtsByCity[city]);
// 2. Type first, then area and rooms within that type's limits
const type = faker.helpers.arrayElement(types);
const { minArea, maxArea } = areaRangeFor(type); // studio: 18-35, house: 70-250...
const area = faker.number.int({ min: minArea, max: maxArea });
const rooms = roomsFor(type, area); // ~1 room per 20-25 m², plot: 0
// 3. Title derived from data that already exists. Never rolled again.
const title = `${typeLabel(type)} ${area} m² - ${city}`;The dependencies go one way. Location decides the district. Type decides the area range. Area decides the rooms. The title is just built from fields that already exist. Nothing is rolled twice.
The result:

A 28 m² listing is now a studio with 1 room. Wilda is actually in Poznań. Plots have zero rooms. The data finally reads like data.
To be honest, the fix did not cover everything:
I am leaving these in for now, on purpose. Data consistency is not a single fix. It is a set of dependencies you keep extending. Price should depend on location. Every new dependency makes the data more real and the generator less random.
And that is the whole lesson in one line: realistic data is not random values in realistic ranges. It is relationships.
Part 2 is the reason this project exists: an AI chatbot on top of this data. "Talk to your listings" instead of clicking filters.
This is also why I wanted to fix the data before adding the chatbot. Because if the data says that a house has 28 m² and a Warsaw district belongs to Szczecin, the chatbot will not save me. It will only make the nonsense easier to ask about.
And probably more convincing.
AI on top of data only works when the data has a model. More on that soon.
Was this helpful?