const { useEffect, useMemo, useRef, useState } = React;
const CFG = window.OLYARI_CONFIG || { API_BASE: "", PRICE_USD: "9.99", APP_NAME: "Olyari" };
function cx(...xs){ return xs.filter(Boolean).join(" "); }
function parseHash(){
const h = (location.hash || "#/dashboard").replace(/^#/, "");
const [path, query] = h.split("?");
const parts = path.split("/").filter(Boolean);
const q = new URLSearchParams(query || "");
return { parts, q, raw: h };
}
function useRoute(){
const [r, setR] = useState(parseHash());
useEffect(() => {
const on = () => setR(parseHash());
window.addEventListener("hashchange", on);
return () => window.removeEventListener("hashchange", on);
}, []);
return r;
}
async function api(path, opts={}){
const url = (CFG.API_BASE || "").replace(/\/$/, "") + path;
const res = await fetch(url, {
credentials: "include",
...opts,
headers: {
"Content-Type": "application/json",
...(opts.headers || {})
}
});
const text = await res.text();
let json = null;
try { json = text ? JSON.parse(text) : null; } catch {}
if(!res.ok){
const msg = (json && (json.error || json.message)) || text || `HTTP ${res.status}`;
throw new Error(msg);
}
return json;
}
function TopBar({ me }){
return (
⟁
{CFG.APP_NAME} Game Creator OS
Forge • Variants • Canon • Regions • OS • NPCs
{me?.active ? "Subscribed" : "Locked"} • {me?.email || "Guest"}
Live Monitor
);
}
const NAV = [
{ to:"#/dashboard", label:"Dashboard", icon:"⌂" },
{ to:"#/engine/pixel", label:"Pixel Forge", icon:"🎨" },
{ to:"#/engine/variants", label:"Variants", icon:"🧬" },
{ to:"#/engine/narrative", label:"Quest + Dialogue", icon:"🧩" },
{ to:"#/engine/world", label:"Region Kit", icon:"🌍" },
{ to:"#/engine/os", label:"Game OS Templates", icon:"🧱" },
{ to:"#/engine/npc", label:"NPC Engine", icon:"🧠" },
{ to:"#/profile", label:"Profile", icon:"👤" },
{ to:"#/forum", label:"Community", icon:"💬" },
{ to:"#/billing", label:"Billing", icon:"💳" },
];
function SideNav({ current }){
return (
);
}
function Shell({ me, children }){
return (
);
}
function Card({ title, subtitle, children, right }){
return (
{title}
{subtitle &&
{subtitle}
}
{right}
{children}
);
}
function KVPill({ k, v }){
return (
{k}
{v}
);
}
function useMe(){
const [me, setMe] = useState(null);
const [loading, setLoading] = useState(true);
const refresh = async () => {
setLoading(true);
try {
const data = await api("/me");
setMe(data);
} catch {
setMe(null);
} finally {
setLoading(false);
}
};
useEffect(() => { refresh(); }, []);
return { me, loading, refresh };
}
const ENGINE_DEFS = {
pixel: {
title: "Pixel-Art Asset Generator",
tagline: "Style DNA + placement prompt → modular prompt pack + palette + constraints.",
settings: [
{ key:"styleProfile", label:"Style Profile", type:"select", options:["Olyari Noir", "High-Fantasy Grit", "Cozy Medieval", "Gothic Silver"], default:"Olyari Noir" },
{ key:"tileSize", label:"Tile Size", type:"select", options:["16", "24", "32", "48"], default:"32" },
{ key:"dither", label:"Dithering (0–1)", type:"range", min:0, max:1, step:0.05, default:0.35 },
{ key:"shade", label:"Shading (0–1)", type:"range", min:0, max:1, step:0.05, default:0.55 },
{ key:"palette", label:"Palette Hints", type:"text", default:"dark navy, silver highlights, warm orange torchlight, muted stone" },
{ key:"negatives", label:"Avoid", type:"text", default:"photorealism, blurry, noisy JPEG, text, watermark" },
],
example: "A medieval apothecary shop sign hanging from a wrought-iron bracket; readable silhouette; 3/4 view; warm lantern glow."
},
variants: {
title: "Recoloring + Variant Engine",
tagline: "One base asset → controlled variants by biome, season, faction, rarity.",
settings: [
{ key:"variantCount", label:"Variants", type:"select", options:["6","8","12"], default:"8" },
{ key:"variantAxes", label:"Axes", type:"text", default:"biome, season, rarity, faction" },
{ key:"keepSilhouette", label:"Keep silhouette?", type:"select", options:["yes","no"], default:"yes" },
{ key:"paletteRules", label:"Palette Rules", type:"text", default:"keep values consistent; swap hue families only; preserve highlight/shadow ratios" },
],
example: "Base asset: leather satchel. Generate variants for: swamp, tundra, desert, city, royal, bandit."
},
narrative: {
title: "Quest / Dialogue Generator (Canon Memory)",
tagline: "Quest structure + dialogue beats + canon rules + tags you can store.",
settings: [
{ key:"tone", label:"Tone", type:"select", options:["mysterious","cozy","dark","epic"], default:"mysterious" },
{ key:"length", label:"Quest Length", type:"select", options:["short","medium","long"], default:"medium" },
{ key:"canonStrict", label:"Canon Strictness", type:"select", options:["high","medium","low"], default:"high" },
{ key:"characters", label:"Key Characters", type:"text", default:"Maeve-type protagonist, archivist NPC, vendor NPC" },
],
example: "Quest about a town fountain that whispers at night; player must collect 3 glyph fragments and confront a memory-eating echo."
},
world: {
title: "Region-by-Region Worldbuilding Kit",
tagline: "Biome schema + palette + tileset list + landmarks + encounter table.",
settings: [
{ key:"biome", label:"Biome", type:"select", options:["coastal","mountain","forest","swamp","tundra","desert"], default:"forest" },
{ key:"mood", label:"Mood", type:"select", options:["dreamlike","ominous","peaceful","decayed","mythic"], default:"dreamlike" },
{ key:"scale", label:"Scale", type:"select", options:["small","medium","large"], default:"medium" },
{ key:"tileNeeds", label:"Tile Needs", type:"text", default:"ground, path, water edge, cliff, wall, roof, door, window, foliage" },
],
example: "Design a fog-draped coastal hamlet with silver rock beaches and a plateau city above it."
},
os: {
title: "Modular Game OS Templates",
tagline: "Starter folder tree + manifests + systems list + starter content pack.",
settings: [
{ key:"engine", label:"Target Stack", type:"select", options:["Web (Canvas)","Unity 2D","Godot 2D","Custom"], default:"Web (Canvas)" },
{ key:"systems", label:"Core Systems", type:"text", default:"inventory, quests, dialogue, crafting, save/load, map, time/weather" },
{ key:"dataFormat", label:"Data Format", type:"select", options:["JSON","YAML"], default:"JSON" },
],
example: "Generate a starter OS template for a cozy mystery farm RPG with a glyph puzzle layer."
},
npc: {
title: "NPC Personality + Behavior Engine",
tagline: "Sliders + schedules + memory fragments + dialogue style + hooks.",
settings: [
{ key:"npcCount", label:"NPCs", type:"select", options:["1","3","5"], default:"3" },
{ key:"complexity", label:"Complexity", type:"select", options:["lite","standard","deep"], default:"standard" },
{ key:"memoryMode", label:"Memory Mode", type:"select", options:["none","local","canon"], default:"canon" },
{ key:"themes", label:"Themes", type:"text", default:"loyalty, secrecy, folklore, guilt, tenderness" },
],
example: "Create an apothecary mentor who is ancient, calm, and slightly terrifying when truth is threatened."
}
};
function Field({ def, value, onChange }){
const common = "oly-input w-full px-3 py-2 text-sm";
if(def.type === "select"){
return (
onChange(e.target.value)}>
{def.options.map(o => {o} )}
);
}
if(def.type === "range"){
return (
onChange(parseFloat(e.target.value))} />
{String(value)}
);
}
return (
onChange(e.target.value)} placeholder={def.default || ""} />
);
}
function EnginePage({ engineKey, me }){
const def = ENGINE_DEFS[engineKey];
const [prompt, setPrompt] = useState(def.example);
const [settings, setSettings] = useState(() => {
const o = {};
def.settings.forEach(s => o[s.key] = s.type==="range" ? s.default : (s.default ?? ""));
return o;
});
const [busy, setBusy] = useState(false);
const [out, setOut] = useState(null);
const [history, setHistory] = useState([]);
const [err, setErr] = useState("");
const load = async () => {
try {
const data = await api(`/engine/${engineKey}/list?limit=12`);
setHistory(data.items || []);
} catch {}
};
useEffect(() => { load(); }, [engineKey]);
const generate = async () => {
setErr("");
setBusy(true);
try {
const data = await api(`/engine/${engineKey}/generate`, {
method:"POST",
body: JSON.stringify({ prompt, settings })
});
setOut(data);
await load();
} catch(e){
setErr(e.message || "Generation failed");
} finally {
setBusy(false);
}
};
const downloadJSON = () => {
const blob = new Blob([JSON.stringify(out, null, 2)], { type:"application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `olyari_${engineKey}_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(a.href);
};
return (
Engine: {engineKey}}
>
{def.settings.map(s => (
{s.label}
setSettings(prev=>({...prev,[s.key]:v}))} />
))}
{busy ? "Forging…" : "Generate"}
Monitor
{out &&
Export JSON }
{err && (
)}
{history.length === 0 &&
No outputs yet.
}
{history.map(it => (
{it.title || it.id}
{new Date(it.createdAt).toLocaleString()}
setOut(it.data)}>Load
))}
{!out ? (
Generate once and your forged output appears here: prompt pack, palette, modules, rules, manifests, etc.
) : (
{JSON.stringify(out, null, 2)}
)}
);
}
function Dashboard({ me }){
return (
{[
["#/engine/pixel","🎨","Pixel Forge"],
["#/engine/variants","🧬","Variants"],
["#/engine/narrative","🧩","Quest+Dialogue"],
["#/engine/world","🌍","Region Kit"],
["#/engine/os","🧱","OS Templates"],
["#/engine/npc","🧠","NPC Engine"],
].map(([to,icon,label])=>(
{icon}
{label}
Open engine
))}
Monitor gives you health + logs so you can debug without DevTools.
Your engines are wired so the backend returns structured outputs first.
Image generation (pixel renders) can be swapped in later without changing the UI.
);
}
function Profile({ me }){
return (
Next step: show your saved outputs by engine + allow “pin to canon” tags.
);
}
function Forum(){
const [posts, setPosts] = useState([]);
const [text, setText] = useState("");
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
const load = async () => {
try {
const data = await api("/forum/posts?limit=50");
setPosts(data.posts || []);
} catch {}
};
useEffect(()=>{ load(); }, []);
const submit = async () => {
setErr("");
setBusy(true);
try{
await api("/forum/post", { method:"POST", body: JSON.stringify({ text }) });
setText("");
await load();
}catch(e){
setErr(e.message || "Post failed");
}finally{
setBusy(false);
}
};
return (
{posts.length === 0 &&
No posts yet.
}
{posts.map(p => (
{new Date(p.createdAt).toLocaleString()} • {p.author || "anon"}
{p.text}
))}
);
}
function Monitor({ refreshMe }){
const [health, setHealth] = useState(null);
const [logs, setLogs] = useState([]);
const [err, setErr] = useState("");
const load = async () => {
setErr("");
try{
const h = await api("/health");
setHealth(h);
const l = await api("/logs?limit=120");
setLogs(l.logs || []);
}catch(e){
setErr(e.message || "Monitor failed");
}
};
useEffect(()=>{ load(); }, []);
return (
Refresh}
>
{err && {err}
}
Tip: If Health works but engines fail, your OpenAI/Stripe env vars are the usual culprit.
{(logs || []).map(l => `[${l.ts}] ${l.level.toUpperCase()} ${l.msg} ${l.data ? JSON.stringify(l.data) : ""}`).join("\n")}
Refresh /me
);
}
function Billing({ me, refreshMe, route }){
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
const startCheckout = async () => {
setErr("");
setBusy(true);
try{
const data = await api("/stripe/checkout", { method:"POST", body: JSON.stringify({}) });
if(!data?.url) throw new Error("No checkout URL returned");
location.href = data.url;
}catch(e){
setErr(e.message || "Checkout failed");
setBusy(false);
}
};
const verifyIfNeeded = async () => {
const parts = route.parts;
const isSuccess = parts[0]==="billing" && parts[1]==="success";
if(!isSuccess) return;
const sessionId = route.q.get("session_id");
if(!sessionId) return;
setBusy(true);
setErr("");
try{
await api(`/stripe/verify?session_id=${encodeURIComponent(sessionId)}`);
await refreshMe();
location.hash = "#/dashboard";
}catch(e){
setErr(e.message || "Verify failed");
setBusy(false);
}
};
useEffect(()=>{ verifyIfNeeded(); }, [route.raw]);
return (
This MVP uses Stripe Checkout to create a monthly subscription, then sets an app session cookie after verification.
Webhooks keep your subscription status synced on renewals/cancellations.
{!me?.active && (
{busy ? "Opening Stripe…" : "Subscribe & Unlock"}
)}
Back
{err && {err}
}
Success URL auto-verifies: #/billing/success?session_id=...
);
}
function LockedOverlay(){
return (
Olyari is sealed.
Subscribe to unlock the engines. Your outputs will then store server-side and appear in your library.
);
}
function App(){
const route = useRoute();
const { me, loading, refresh } = useMe();
const view = () => {
const p = route.parts;
if(p[0] === "dashboard") return ;
if(p[0] === "engine") {
const key = p[1];
return ;
}
if(p[0] === "profile") return ;
if(p[0] === "forum") return ;
if(p[0] === "monitor") return ;
if(p[0] === "billing") return ;
return ;
};
const requiresActive =
route.parts[0] === "engine" ||
route.parts[0] === "forum" ||
route.parts[0] === "profile";
return (
{view()}
{!loading && requiresActive && !me?.active && }
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );