A demo page is a complete, working Ambasdr page built from authored content: a real identity, real documents, real photographs and a real conversation history that the production insights pipeline then summarises.
This is the single most important thing to understand before changing anything here.
A persona declares scripted conversations. The pipeline writes them as ordinary chat threads, and then runs the production digest and rollup over those rows — through the same service method the admin panel's force button calls, not the scheduled jobs. Every dashboard number on a demo page is therefore computed, not authored.
The practical consequence: the lever for a panel is what a visitor says, several steps upstream of the panel. Someone looking for a field to set will not find one.
| Stage | What it produces |
|---|---|
| Persona | Identity, bio, documents, social links, knowledge gaps and scripted conversations. |
| Provision | Auth0 identity, user row, page row. Resuming finds an existing page by slug rather than duplicating it. |
| Documents | Prose documents uploaded as files; image documents generated as real photographs. Diffed by filename, so a resume skips what exists. |
| Conversations | Scripted threads written with timestamps derived from a recency bucket. Diffed by title. |
| Insights | The production digest and rollup, run until every window has a row. |
Written by hand in internal/demokit/seed. Held to their contract by the test suite beside them, which asserts properties like every declared gap being asked and honestly declined.
An LLM invents a persona from free text. Held to its contract by a validator: blocking findings are fed back to the model for another attempt, warnings only reach the operator.
A requirement enforced on one path and not the other is the failure mode this system keeps producing. A rule stated only in the generator prompt is a suggestion, not a contract.
Conversations declare a bucket rather than a date, because content is written once and read for years — an absolute date would age out of its window, a bucket cannot. The timeline maps a bucket to a concrete moment at seed time.
| Bucket | Maps to |
|---|---|
today | A hashed moment inside today, always in the past. |
this_week | 24 to 166 hours back — provably before today's midnight and provably inside the rolling seven days. |
this_month | Eight to twenty-seven days back — strictly outside the week bucket, inside the 30-day window. |
The mobile Meeting-requests tile counts one intent over the last seven days. A persona carrying a meeting request filed under this_month satisfies "the page has one" and still leaves the tile reading zero. That happened: a live demo page showed zero for weeks while a correct-looking conversation sat outside the window.
Some panels read a classification. Personas declare what they intend to produce so a test can hold them to it.
A conversation may carry an ExpectedIntent — the intent the digest should assign it. It is declared by the author, not derived from the transcript, and that distinction is the whole design. The real classification is made by a model at digest time against a strict enum; re-deriving it in a test would mean writing a second classifier free to disagree with the first, and two classifiers that disagree are worse than one that is occasionally wrong.
So the test pins the thing an author controls: that a recent conversation is intended to be a meeting request. Whether the pipeline agrees is settled by the digest eval harness, which runs the real prompt against a real model, and ultimately by seeding a page and reading the stored classification.
Authored personas are held by a test in the seed package; generated personas by two blocking validator findings, kept separate because "you have none" and "yours is outside the window" need different fixes. A third finding blocks the opposite failure — marking every conversation a meeting request, which would satisfy both of the first two and still produce a broken page.
Image documents are real generated photographs, prompted from the persona and the image's caption, with hard guardrails: no rendered text, no logos or brand marks, no likeness of a real person. With no image model configured the pipeline falls back to legible caption-card placeholders.
A generation failure is a step failure, not a silent placeholder. The model being configured means real images are the contract, and a resume retries only what is missing.
| Setting | Note |
|---|---|
LLM_IMAGE_MODEL | Defaults to gpt-image-2. Its predecessor produced anatomically incoherent results on the same prompts — a forearm with no body attached, a second bucket levitating mid-pour. |
LLM_IMAGE_QUALITY | Defaults to high. These images are opened full-size from the Files tab and shown by the ambassador on request, and low-fidelity sampling is where incoherence comes from. |
LLM_IMAGE_SIZE | Landscape 3:2, the same shape the placeholder uses. Nothing validates it locally; an unsupported value arrives as a rejection at seed time. |
Image generation retries transport faults, because a connection reset may not recur. It does not retry its own deadline: nothing about a second attempt makes the model faster, so three attempts would be three deadlines and most of the create job's budget spent to reach the same answer.
The two are indistinguishable by inspecting the error — a dial timeout and a client timeout both report as timeouts and both wrap the same cause. The distinction is made by asking a context the client owns, not by matching on error text.
The standing rule, recorded in CLAUDE.md as a sibling to the user-purge and harness rules.
A new UI surface fed by API data that no persona produces data for renders empty on every demo page, and no suite in this repo notices — because the panels are computed from a history rather than authored. Worse than empty is inconsistent: the panels are all reductions over the same history, so a topics list naming pricing beside a gap panel saying nobody could get a pricing answer reads as a broken product rather than a new page.
Which table and column does the surface read? Anything served from the API is in scope.
A lookup rather than a guess: the digest prompt specifies what each field means, and the eval corpus holds transcripts paired with the values they are expected to produce.
They differ in subject kind — one a person, one a business — so a surface can read correctly on one and wrongly on the other.
A prompt line for generated personas, a validator finding so it is a contract, and an assertion in the seed test suite so the committed personas are held to it too.
Check the new surface next to the ones around it. A panel that renders empty on a real seeded page is the only evidence that counts, and nothing in the repo produces that evidence for you.
If a surface has no honest demo data, the answer is a graceful empty state, not invented numbers. The rule against fabricated data applies to demo pages exactly as it applies everywhere else.
The digest and rollup jobs that turn these conversations into dashboard panels.