Studio
Studio
Shared across every PremiumPress product. 15 classes in inc/Studio.
AI AI.php
AI site generation — turns a plain-English brief into a complete, validated working plan by asking an AI model to choose blocks from the live registry and write per-block copy. Ported clean from DT10's Generator\AI. The model picks one of three intents each turn: CHAT (greetings/questions), GENERATE (build a whole site from a description), or EDIT (change the site the visitor is already previewing — swap/add/remove a section, recolour, reword). EDIT ops are applied through the same registry-validated Studio\Command mutations the visual picker uses; the current plan is fed to the model as context so it knows which slot the hero is in, the current colours, and what to change. PROVIDERS - Two backends are supported: Anthropic (Claude) and OpenAI (GPT). The owner enters either key via the connectors plugins, and provider() picks which to use — Anthropic when its key is present, otherwise OpenAI (auto). The choice can be forced with the PPT_AI_PROVIDER constant, the ppt_generator_ai_provider option, or the same-named filter. Both backends return ONE JSON plan object, so everything downstream of the model call is provider-agnostic. SAFETY - The API key is read at request time (connectors option, a PPT constant, or a filter) and used only to authenticate the server-side request via the WordPress HTTP API — never logged or echoed. - The model may only pick block keys we send it (the live registry). Everything it returns is coerced to real blocks; a hallucinated key can never reach a slot. - Images are never invented by the model; we seed niche demo images and overlay only the model's text. generate() returns array('ok'=>bool, 'reply'=>string, 'changed'=>bool[, 'reload'=>bool]).
Catalog Catalog.php
Studio catalogue — the studio/exporter's read-only view of the block library. DT10 had a rich Catalog metadata layer over the legacy nb_* blocks; PPT derives everything from the clean Blocks\Registry + each Block's declared defaults(), so there is a single source of truth. Every registered block is a "new" block here.
Command Command.php
Studio mutations — place/clear a block, set a brand colour, set one text field, build a random page. Each edits the WorkingPlan and validates against the live Registry, so it can never set an unknown block. Also serves as the deterministic fallback for the chat box until the AI phase lands. Every method returns array('reply' => string, 'changed' => bool); when changed is true the studio reloads the preview.
DemoCopy DemoCopy.php
Take a source design's own DEMO COPY out of a GENERATED page. Every design in the catalogue is a finished demo of one business — Bazaar is a city guide to India, Calm is a spa, Collage is a builder's directory. The generator reuses those blocks, and a block carries its demo copy in its own defaults(). The model is asked for a heading and a handful of named fields; EVERY other field on the block it lands in keeps the demo's subject. On 2026-09-10 a brief for "a directory of independent record shops and vinyl fairs" rendered: <h1>Find Your Next Favorite Record incredible India.</h1> Your guide to India</span> Delhi · Mumbai · Jaipur · Goa · Kerala — the model's headline, then two leftover pieces of Bazaar's own headline, then its eyebrow and its city chips. The same shape produced "Your Game. Your Arena. pushes you." (hero_impact) and "Explore the Web, Human-Curated you live." (hero_edit). Patching field names one at a time — which is what ExampleContent::nicheHeadings() did — only ever reaches the names someone thought of: the 44 generatable hero blocks carry 60+ text fields between them that no such list mentions. So this class works by ROLE instead, matched on the field NAME PATTERN, and its DEFAULT for an unrecognised text field on a generated page is to BLANK it. A field is kept only when it is structural (an image, a link, a count) or when someone actually wrote it for THIS site. Two things make blanking safe and correct: 1. Blocks\Renderer::render() merges $block->defaults() UNDER the plan's data, and array_merge lets an empty string win. A field blanked here therefore stays blank on the page — but only while the key is still PRESENT. Never unset(); always set ''. 2. A hero's headline is usually SPLIT across two fields ("Find trusted" + "car pros near you."), and the model writes ONE complete headline. Blanking the leftover fragment is the only correct outcome — filling it duplicates the sentence, which is exactly what "Your Game. Your Arena. pushes you." was. The exception to blanking is a field whose RENDERER substitutes its own demo string when the value is empty: HeroCalm puts "Massage & spa near you" back, HeroCollage puts "Find a trade" back, and every Popular::render() call carries a hard-coded chip list. Those roles are SET to a site-derived or subject-neutral value instead of blanked; the harness (tools/qa/verify-demo-copy.php) renders every hero and fails if any default string reappears, so a new block with a hidden fallback is caught rather than shipped.
ExampleContent ExampleContent.php
Baseline content for a generated homepage block — complete, directory-generic copy + niche demo images for each of the 8 content blocks, so an AI-generated design renders fully populated. The AI step overlays its tailored title / subtitle / desc (and category names) on top of this.
ImageGen ImageGen.php
PPT — Site Generator hero image generation via OpenAI (gpt-image-1). One image per generate: a photoreal hero matching the brief/niche, sideloaded into the media library and set on the hero block. SAFETY: the key is read at request time (connectors option, a PPT constant, or the ppt_openai_key filter) and used only to authenticate the server-side request — never logged or echoed.
NicheDemand NicheDemand.php
What people ask the demo for that the catalog has no niche for. The generator recognises a fixed set of niches by keyword (Studio\AI::nicheKeywords), and each one owns real curated listings and a real photo folder. A brief outside that set — "chess clubs and coaching", "pig farms" — matches nothing, so the model is asked for its closest pick and the site gets dressed as something adjacent. That guess is recorded here, brief and all, so the gap between what the demo is asked for and what the product actually covers is a list you can read rather than a hunch. This is demand data, not an error log: a brief showing up here means someone wanted a directory the catalog can't dress properly yet. The counts say which niches to build next. Storage: option ppt_niche_demand (never autoloaded) — one row per distinct brief, capped at MAX by least-recently-seen, so a busy demo can't grow it without bound.
Picker Picker.php
Describes the working plan as editable "regions" (content slots, plus header / footer when those block categories exist) and the browse tree of categories → blocks the picker navigates. Also lists the editable text/image fields so the studio can wire in-place editing in the preview. Ported from DT10's Generator\Picker, driven by the PPT Blocks\Registry.
Plan Plan.php
Working-plan schema helpers. A "plan" is the studio's editable description of a page: brand colours, scheme, an ordered list of content slots, and per-block content. Mirrors DT10's SitePlan shape, trimmed to what PPT needs. array( 'meta' => array('title' => ''), 'design' => array( 'colors' => array('color_primary' => '#...', 'color_secondary' => '#...'), 'scheme' => 'light', 'slots' => array('<block>' => array('field' => value))), ), )
Preview Preview.php
Studio live-preview: renders the working plan's blocks on the front end when ?ppt_preview=1 (admins only). The studio iframe points here; the plan's brand colours are stamped as a :root override so the preview matches the studio's swatches, and eye-hidden blocks (?ppt_hide=csv) are omitted. With &ppt_snapshot=<prompt turn id> (admins only, nonce-signed) it renders a STORED plan instead — the site a past prompt built (Snapshots) — so Settings ▸ Prompts can show what someone's prompt actually produced. That path is read-only: it never reads or writes the working plan, so previewing an old prompt cannot disturb whatever is on the studio canvas now.
SavedDesign SavedDesign.php
Published Site-Generator designs — a per-slot SNAPSHOT of the studio working plan that the theme renders directly (via Blocks\Renderer), so an admin can put a generated layout live without exporting to Elementor/Divi. "Save Design" writes one of these; the front end renders it through Admin\DesignPage::renderAssigned(). Each snapshot is the exact render configs + brand colours + scheme at save time, so later studio edits don't change the live slot until it is re-saved. Rendering mirrors Studio\Preview::render() (scheme-token :root override + .ppt-design-root wrapper).
Session Session.php
Studio session context. The Site Generator is available to administrators everywhere, and — when the site is in DEMO mode (no active design installed) — to anonymous visitors too, so they can try building a site from the home-demo. Storage is isolated per visitor: admins edit the shared working-plan option (so the editor → Elementor export flow is unchanged), while demo visitors get their own plan in a transient keyed by a random per-visitor cookie token, so they never clobber each other or the real site. Privileged actions (Elementor export / persisting to a real page) stay admin-only.
SnapshotCards SnapshotCards.php
The curated cards a prompt's generated site was actually built with. A snapshot (Studio\Snapshots) stores the PLAN — layout, headings, colours, brand, hero image. It does not store the listing cards, because nothing does: every listings/gallery block derives its cards at render time from the plan's niche (Content\SampleListings::cards), and a generated site's card PHOTOS are matched at render time against the image collection using the brief. Both of those are moving targets, which is why every preview looked like the same site (2026-09-10). Same niche, same cards: seven different "ESPN design directory" prompts all replayed Apex Strength Lab / Southpaw Boxing / Baseline Tennis Academy, and a build whose brief matched no curated niche replayed some other niche's cards entirely. The photos drift too — the collection grows with every generation, so a preview opened a week later shows pictures the visitor never saw. So each snapshot records its resolved cards here, keyed <niche>`|`<limit>, and the preview hands them straight back. Recording is a real render of the plan under the same filters the visitor's preview ran under (Preview::addGeneratedFilters), so what is stored is what was shown, not a re-derivation that hopes to match. Snapshots taken before this existed simply have no cards and derive as they always did — a preview that is a bit generic beats no preview at all.
Snapshots Snapshots.php
Snapshots of what the site-generator actually BUILT for a given prompt. The prompt log (Admin\Prompts) records the words — what the visitor asked and how the AI replied — but the page itself lives in the working plan, which is a single shared option for admins and a one-day transient for demo visitors. Both are overwritten by the very next generation, so by the time anyone reads the log the site that prompt produced is gone. So every turn that CHANGED the canvas stores a copy of the resulting plan here, keyed by the prompt turn's id. Settings ▸ Prompts renders it back through the studio preview (Studio\Preview::snapshotUrl) — read-only: viewing a snapshot never touches the working plan, the live site, or the visitor whose session made it. Storage: option ppt_prompt_snapshots (never autoloaded) — id => { t, plan, cards }, capped at MAX newest AND at MAX_TOTAL_BYTES so it can't grow with the 500-turn log behind it. A capped-off or deleted turn simply loses its Preview button; the log entry itself is unaffected. cards is what the generated site's listing/gallery blocks actually rendered — see Studio\SnapshotCards for why a snapshot that stores only the plan replays as a different site.
WorkingPlan WorkingPlan.php
The current, editable plan for the studio session. Commands mutate it; the preview renders it. Persisted as a WP option so edits survive across requests. Falls back to a blank canvas when nothing has been built yet.