Lemonade Agent Control Room
Build a browser dashboard that safely simulates a lemonade sales agent.
Introduction
30 Second Summary
A simple sales goal can become expensive when software spends money before anyone checks its plan. A safe system keeps a person in control when a risky decision becomes real.
In this project, you will build a browser control room where a simulated AI agent pursues a $100 lemonade-sales goal. You will expose unsafe spending before adding human approval plus a budget guardrail.
What You'll Build
Your finished dashboard runs in Safari as you approve a safe sequence of business moves that reaches $110 in revenue without overspending.
By the end of this project, you'll have:
- An unsafe demo where one unchecked $60 robot juicer purchase sends cash from $25 to $-35.
- A human approval panel that pauses every spending proposal. A budget guardrail disables unaffordable actions before cash changes.
- A safe business day that reaches $110 in revenue without overspending. Live cards plus a newest-first log show how each round changes the business.
- Secret Mission: A weather-aware strategy that proposes a rainy-day special when demand falls.
What do I need before I start?
A Mac is the only prerequisite. The guide requires no coding experience or paid services.
Before We Start
Before the hands-on work begins, commit to a lemonade agent that aims to reach $100 in revenue while a person approves its spending. The simulation stays local in Visual Studio Code or TextEdit, Finder, and Safari, with no access to real money or external services.
Set Up Your Project
The agent simulation only becomes useful once you can see each decision in a browser. This setup gives the project a dependable home on your Mac.
You’ll use Visual Studio Code to edit one HTML file. If the current release does not run on your Mac, TextEdit provides a plain-text fallback.
In this step, get ready to:
- Install Visual Studio Code or configure TextEdit as a fallback.
- Create the Documents/lemonade-agent/index.html starting path.
- Open the starter page locally in Safari.
Prepare your code editor
Visual Studio Code is the primary editor for this project. Try it first so your project folder stays visible beside the file you are editing.
- Open the Visual Studio Code macOS setup page in Safari.
- Select the download for macOS.
The download appears in your Downloads folder as a .dmg file.
- Open the downloaded .dmg file from Downloads in Finder.
- Drag Visual Studio Code.app into the Applications folder.
You’ll see Visual Studio Code inside Applications once the copy finishes.
- Select Applications in the Finder sidebar.
- Double-click Visual Studio Code.app to open it.
✔️ Visual Studio Code opens
Great. Your code editor is installed and ready for the project folder.
ⓧ My Mac needs TextEdit
Older Macs can make this part fiddly because the current Visual Studio Code release may be incompatible. TextEdit can handle this single-file project without an extra installation.
- Press Cmd+Space to open Spotlight.
- Type TextEdit.
- Press Return to open TextEdit.
- Click Format in the menu bar.
- Select Make Plain Text.
Your blank document now stores plain text, which lets the browser read it as HTML after you save it with the correct filename.
Create the project folder and file
A dedicated folder keeps the whole simulation in one predictable location. Your file path for this project is Documents/lemonade-agent/index.html.
- Return to Finder from the editor setup.
- Select Documents in the Finder sidebar.
Finder now shows the files and folders inside Documents.
- Click File in the menu bar.
- Select New Folder.
You’ll see a new folder with its name highlighted.
- Type lemonade-agent as the folder name.
- Press Return to keep the name.
Your lemonade-agent folder is now listed inside Documents.
✔️ I’m using Visual Studio Code
Visual Studio Code may ask whether you trust this folder. You created the folder yourself, so you can allow the editor to use its full features.
- Return to Visual Studio Code.
- Click File in the menu bar.
- Select Open Folder....
- Select the lemonade-agent folder inside Documents.
- Click Open.
- Select Yes, I trust the authors if the Workspace Trust prompt appears.
You’ll see lemonade-agent at the top of the Explorer view.
- Select the Explorer view in the Activity Bar.
- Select the New File... button.
- Type index.html as the filename.
- Press Return to create the file.
The new index.html file opens in the editor.
ⓧ I’m using TextEdit
TextEdit must save the document as plain text with an .html extension. That filename lets Safari treat the document as a web page.
- Return to TextEdit.
- Create a new document from the File menu.
- Click Format in the menu bar.
- Select Make Plain Text.
- Click File in the menu bar.
- Select Save.
- Enter index.html as the filename.
- Select the lemonade-agent folder inside Documents.
The final save choice protects the .html extension that Safari needs.
- Click Save.
- Keep the .html extension if TextEdit asks about the filename.
Your index.html document is now saved inside Documents/lemonade-agent.
- Return to Finder.
- Double-click the lemonade-agent folder inside Documents.
You should see index.html inside the folder. That visible file confirms your project location is ready.
Write and open the starter page
The first page only needs a valid document structure plus a visible heading. This small test proves that your editor can save a page that Safari can load.
- Return to index.html in your editor.
- Replace its contents by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
</head>
<body>
<h1>Lemonade Agent Control Room</h1>
</body>
</html>
What Does This Code Do?
- The <!doctype html> line tells Safari to use modern HTML rules.
- The <head> section stores page information for the browser.
- The <body> section contains the heading that you can see on the page.
- The <h1> element gives the control room its main heading.
Before you open the page, form a prediction about the heading Safari will show.
- Press Cmd+S to save index.html.
- Return to Finder.
- Select Documents in the Finder sidebar.
- Double-click the lemonade-agent folder.
- Control-click index.html.
- Select Open With.
- Select Safari.
You’ll see Lemonade Agent Control Room as the page heading in Safari.
You did it. Safari is now loading your first local page directly from index.html.
Page Missing or Showing Code?
Confirm that the filename is exactly index.html. A different extension prevents Safari from treating it as the intended page.
If you used TextEdit, confirm that the document uses plain text. Rich-text formatting can stop the HTML from loading correctly.
Compare your opening tag with <html lang="en"> if the page is blank.
Help me fix my local HTML page.
✔️ Awesome, I’ve got everything!
Your index.html file is saved inside Documents/lemonade-agent. Safari displays the correct heading.
ⓧ I’d like to double check the full code
Compare your index.html file with this complete starting version.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
</head>
<body>
<h1>Lemonade Agent Control Room</h1>
</body>
</html>
Your editor and starter page are ready. Next, you’ll turn this heading into the lemonade agent’s control room.
Build the Control Room
Your starter page proves that Safari can display your local project. The single heading now needs a dashboard that can show the agent's decisions.
A static dashboard gives you an early view of the finished control room before any logic is added. In this step, you will use HTML for the dashboard structure and CSS for its yellow-and-navy design.
In this step, get ready to:
- Replace the starter heading with the dashboard structure.
- Apply the responsive control room design.
- Verify the static starting state in Safari.
Build the dashboard structure
The page needs separate sections for business metrics, controls, proposals, and the activity log. Each section gives later JavaScript a clear place to display the simulation state.
- In index.html, find the existing <h1> heading inside <body>.
- Replace the heading with the header and metric cards by pasting this code:
<main class="wrapper">
<header class="hero">
<h1>Lemonade Agent Control Room</h1>
<p>Goal: reach $100 in revenue without letting the simulated agent spend money it does not have.</p>
</header>
<section class="metrics" aria-label="Business metrics">
<article class="card"><span>Revenue</span><strong id="revenue">$0</strong></article>
<article class="card"><span>Cash</span><strong id="cash">$25</strong></article>
<article class="card"><span>Inventory</span><strong id="inventory">8 cups</strong></article>
<article class="card"><span>Round</span><strong id="round">0/5</strong></article>
</section>
</main>
What does this structure do?
- The hero header states the agent's goal before the simulation begins.
- The metric cards give revenue, cash, inventory, and rounds their own visible values.
- The IDs give later JavaScript a precise way to update each value.
- Save index.html.
- Return to the local page in Safari.
Reload the page. You should see the control room title, its revenue goal, and four unstyled metric cards.
Missing a metric card?
Check that every <article> sits inside the metrics section. Make sure the final </main> tag remains below all four cards.
Use this if the structure still looks incomplete. Help me check my dashboard HTML.
- In index.html, place your cursor after the closing tag for the metrics section.
- Add the agent controls by pasting this code before </main>:
<section class="panel">
<h2>Agent controls</h2>
<div class="controls">
<button id="unsafe-button" class="danger">Run unsafe demo</button>
<button id="start-button">Start safe day</button>
<button id="reset-button" class="secondary">Reset</button>
</div>
<p id="status" class="status" aria-live="polite">Ready. Run the unsafe demo first.</p>
</section>
How do the controls fit in?
- The three buttons represent the unsafe run, the protected run, and a fresh start.
- The status paragraph gives the agent a visible place to report its current state.
- The button and status IDs prepare these elements for event listeners and DOM updates in later steps.
- Save index.html.
- Return to Safari.
Reload the page. You should see three buttons beneath the metric cards and the status text beneath those controls.
Controls outside the page?
Make sure the controls section appears above </main>. Content pasted below that tag sits outside the dashboard wrapper.
Use this if a button or the status text is missing. Help me check my control-panel HTML.
The proposal area holds an action while it waits for review. Its hidden attribute keeps the panel out of sight until the agent has something to propose.
- In index.html, place your cursor after the controls section.
- Add the proposal panel and activity log by pasting this code before </main>:
<section id="proposal-panel" class="panel proposal" hidden>
<h2>Pending agent proposal</h2>
<h3 id="proposal-title"></h3>
<p id="proposal-reason"></p>
<p id="proposal-details"></p>
<p id="guardrail" class="guardrail"></p>
<div class="approval-buttons">
<button id="approve-button">Approve</button>
<button id="reject-button" class="secondary">Reject</button>
</div>
</section>
<section class="panel log-panel">
<h2>Activity log</h2>
<pre id="activity-log" aria-live="polite">Ready for a simulation.</pre>
</section>
Why is the proposal hidden?
The proposal panel only becomes useful when an action is waiting for approval. The hidden attribute keeps the starting dashboard focused on its controls.
The activity log remains visible from the beginning. It gives every later decision and result a shared history.
- Save index.html.
- Return to Safari.
Reload the page. You should see an Activity log containing Ready for a simulation. while the pending proposal remains hidden.
Proposal visible too soon?
Check that hidden appears inside the opening tag with id="proposal-panel". Check that the activity log section sits outside the proposal section.
Use this if the wrong panel appears. Help me fix the proposal visibility.
Style the control room
The structure now contains every dashboard element. Embedded CSS can turn those bare sections into a responsive control room without creating another file.
- In index.html, find the </title> line.
- Add the page foundations immediately below that line by pasting this code:
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fff8cc;
color: #17223b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #fff8cc, #fffdf2 55%, #dff7ea);
}
</style>
What do these foundations control?
- The root styles establish the page font and navy text colour.
- The universal selector makes element dimensions easier to control.
- The body fills the browser window with a soft yellow-to-green background.
- Save index.html.
- Return to Safari.
Reload the page. You should see a pale yellow background with navy text across the dashboard.
Background still white?
Make sure the entire style block sits inside <head>. Check that </style> appears before </head>.
Use this if the page remains unstyled. Help me check my embedded CSS.
- In the style block, place your cursor immediately before </style>.
- Style the dashboard buttons by pasting this code:
button {
border: 0;
border-radius: 12px;
padding: 0.8rem 1rem;
font: inherit;
font-weight: 800;
cursor: pointer;
background: #17223b;
color: white;
}
button.secondary {
background: #ffffff;
color: #17223b;
border: 2px solid #17223b;
}
button.danger {
background: #a62929;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
How do the button styles communicate risk?
The navy style marks the primary action. The red danger style separates the unsafe demo from the protected workflow.
The secondary style keeps Reset and Reject available without giving them the strongest visual weight. Disabled buttons become faded when an action is unavailable.
- Save index.html.
- Return to Safari.
Reload the page. You should see a red unsafe-demo button, a navy start button, and a white reset button.
Buttons still look plain?
Check that the button rules sit above the existing </style> tag. Make sure every selector retains its opening and closing braces.
Use this if one button has the wrong colour. Help me troubleshoot my button CSS.
- In the style block, place your cursor immediately before </style>.
- Add the wrapper and hero styles by pasting this code:
.wrapper {
width: min(960px, 92vw);
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero {
background: #ffd84d;
border: 3px solid #17223b;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 8px 8px 0 #17223b;
}
.hero h1 {
margin: 0 0 0.4rem;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p {
margin: 0;
max-width: 68ch;
font-weight: 650;
}
Why use a wrapper?
The wrapper limits the dashboard width while leaving space at the sides. Its viewport-based width also lets the page fit a smaller browser window.
The hero combines the yellow background, navy border, rounded corners, and offset shadow. This establishes the control room's visual identity.
- Save index.html.
- Return to Safari.
Reload the page. You should see a centered dashboard with a yellow hero card and a thick navy shadow.
Hero still spans the window?
Check that the page content remains inside <main class="wrapper">. Check that the hero header still uses class="hero".
Use this if the layout is not centered. Help me fix the wrapper and hero styles.
- In the style block, place your cursor immediately before </style>.
- Turn the metrics and panels into cards by pasting this code:
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.card,
.panel {
background: rgba(255, 255, 255, 0.92);
border: 2px solid #17223b;
border-radius: 18px;
padding: 1rem;
box-shadow: 4px 4px 0 #17223b;
}
How does the grid respond?
The metric grid creates as many columns as the available width can hold. Cards move onto new rows when the browser window becomes narrower.
Cards and panels share the same white background, navy border, rounded corners, and compact shadow. Shared styling makes each dashboard area feel connected.
- Save index.html.
- Return to Safari.
Reload the page. You should see four white metric cards followed by a matching control panel and activity-log panel.
Metrics staying in one column?
Check that the metrics section still uses class="metrics". Make sure the grid-template-columns line remains inside the .metrics rule.
Use this if the cards are not forming a grid. Help me troubleshoot the metric layout.
- In the style block, place your cursor immediately before </style>.
- Format the metric labels and values by pasting this code:
.card span {
display: block;
color: #52606d;
font-size: 0.85rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card strong {
display: block;
margin-top: 0.3rem;
font-size: 1.8rem;
}
What makes each metric readable?
The small uppercase label identifies the measurement. The larger value beneath it gives the current state the strongest emphasis.
- Save index.html.
- Return to Safari.
Reload the page. You should see compact uppercase labels above large values such as $25 and 8 cups.
Metric values still small?
Check that each value remains inside a <strong> element. Check that each label remains inside a <span> element.
Use this if the label and value look identical. Help me fix the metric typography.
- In the style block, place your cursor immediately before </style>.
- Arrange the controls and proposal details by pasting this code:
.controls,
.approval-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.status {
min-height: 1.5rem;
font-weight: 800;
}
.proposal {
margin-top: 1.5rem;
border-left: 10px solid #f0ad00;
}
.proposal h2,
.log-panel h2 {
margin-top: 0;
}
.guardrail {
padding: 0.75rem;
border-radius: 10px;
background: #fff0f0;
font-weight: 850;
}
How does the approval area stand out?
Flexible rows keep the control buttons usable when space becomes tight. The status reserves room for feedback beneath them.
The proposal panel uses a thick yellow edge to signal a pending decision. The pale red guardrail area reserves a clear place for budget warnings.
- Save index.html.
- Return to Safari.
Reload the page. You should see the three control buttons arranged with even spacing and bold status text beneath them.
Buttons overlapping?
Check that flex-wrap: wrap remains in the shared controls rule. Make sure the controls container still uses class="controls".
Use this if the controls do not fit the panel. Help me fix the flexible button layout.
- In the style block, place your cursor immediately before </style>.
- Finish the activity-log design by pasting this code:
.log-panel {
margin-top: 1.5rem;
}
#activity-log {
min-height: 9rem;
margin: 0;
padding: 1rem;
overflow: auto;
white-space: pre-wrap;
background: #101827;
color: #d8ffe5;
border-radius: 12px;
line-height: 1.5;
}
Why style the log like a console?
The dark background separates the simulation history from the business controls. Preserved whitespace lets later log entries appear on their own lines.
Automatic overflow keeps long runs contained inside the panel. The dashboard can collect decisions without stretching the entire page.
- Save index.html.
- Return to Safari.
Reload the page. You should see a dark activity log containing pale green text beneath the control panel.
Log missing its dark background?
Check that the selector begins with #activity-log. The hash matches the element's id="activity-log" value.
Use this if the log remains plain. Help me troubleshoot the activity-log CSS.
✔️ Awesome, I've got everything!
Great. Double check that index.html is saved and the styled dashboard is visible in Safari.
ⓧ I'd like to double check the full code
Compare your saved index.html with this complete static version:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fff8cc;
color: #17223b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #fff8cc, #fffdf2 55%, #dff7ea);
}
button {
border: 0;
border-radius: 12px;
padding: 0.8rem 1rem;
font: inherit;
font-weight: 800;
cursor: pointer;
background: #17223b;
color: white;
}
button.secondary {
background: #ffffff;
color: #17223b;
border: 2px solid #17223b;
}
button.danger {
background: #a62929;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.wrapper {
width: min(960px, 92vw);
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero {
background: #ffd84d;
border: 3px solid #17223b;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 8px 8px 0 #17223b;
}
.hero h1 {
margin: 0 0 0.4rem;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p {
margin: 0;
max-width: 68ch;
font-weight: 650;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.card,
.panel {
background: rgba(255, 255, 255, 0.92);
border: 2px solid #17223b;
border-radius: 18px;
padding: 1rem;
box-shadow: 4px 4px 0 #17223b;
}
.card span {
display: block;
color: #52606d;
font-size: 0.85rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card strong {
display: block;
margin-top: 0.3rem;
font-size: 1.8rem;
}
.controls,
.approval-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.status {
min-height: 1.5rem;
font-weight: 800;
}
.proposal {
margin-top: 1.5rem;
border-left: 10px solid #f0ad00;
}
.proposal h2,
.log-panel h2 {
margin-top: 0;
}
.guardrail {
padding: 0.75rem;
border-radius: 10px;
background: #fff0f0;
font-weight: 850;
}
.log-panel {
margin-top: 1.5rem;
}
#activity-log {
min-height: 9rem;
margin: 0;
padding: 1rem;
overflow: auto;
white-space: pre-wrap;
background: #101827;
color: #d8ffe5;
border-radius: 12px;
line-height: 1.5;
}
</style>
</head>
<body>
<main class="wrapper">
<header class="hero">
<h1>Lemonade Agent Control Room</h1>
<p>Goal: reach $100 in revenue without letting the simulated agent spend money it does not have.</p>
</header>
<section class="metrics" aria-label="Business metrics">
<article class="card"><span>Revenue</span><strong id="revenue">$0</strong></article>
<article class="card"><span>Cash</span><strong id="cash">$25</strong></article>
<article class="card"><span>Inventory</span><strong id="inventory">8 cups</strong></article>
<article class="card"><span>Round</span><strong id="round">0/5</strong></article>
</section>
<section class="panel">
<h2>Agent controls</h2>
<div class="controls">
<button id="unsafe-button" class="danger">Run unsafe demo</button>
<button id="start-button">Start safe day</button>
<button id="reset-button" class="secondary">Reset</button>
</div>
<p id="status" class="status" aria-live="polite">Ready. Run the unsafe demo first.</p>
</section>
<section id="proposal-panel" class="panel proposal" hidden>
<h2>Pending agent proposal</h2>
<h3 id="proposal-title"></h3>
<p id="proposal-reason"></p>
<p id="proposal-details"></p>
<p id="guardrail" class="guardrail"></p>
<div class="approval-buttons">
<button id="approve-button">Approve</button>
<button id="reject-button" class="secondary">Reject</button>
</div>
</section>
<section class="panel log-panel">
<h2>Activity log</h2>
<pre id="activity-log" aria-live="polite">Ready for a simulation.</pre>
</section>
</main>
</body>
</html>
What should this file contain?
This is the complete static dashboard for this step. It contains the final page structure and embedded styles without any simulation logic.
Verify the static starting state
The dashboard now has every visual surface the simulation needs. The pending proposal stays hidden because no agent action is waiting for review.
Before you reload the page, ask yourself whether the proposal panel should be visible.
- Reload the local index.html page in Safari.
You should see Revenue $0, Cash $25, Inventory 8 cups, and Round 0/5 across four cards.
You should also see the three control buttons, the ready status, and the dark activity log. You should not see the pending proposal or its approval buttons.
Static state does not match?
Check that Safari is displaying the saved file from Documents/lemonade-agent/index.html. An older browser tab can continue showing a previous saved version.
Check the full-code tab for a missing closing tag or style rule. Help me compare my static dashboard with the expected state.
That is your first complete control room view. Every future agent decision now has a visible place to appear.
Your static dashboard is ready. Next, you will give the unsafe agent permission to spend before it checks the budget.
Let the Agent Overspend
Your control room is visible in Safari. The next layer turns it into an agent harness for the lemonade business.
The first agent executes a spending decision without checking the available cash. You will use JavaScript to make that risky behavior visible.
In this step, get ready to:
- Define the robot juicer action and the simulation's starting state.
- Connect the state to the existing dashboard elements.
- Trigger an unchecked purchase and observe its financial effect.
Define the unsafe action and starting state
The simulation needs a consistent action to test. It also needs a fresh state containing the business values at the start of each run.
- Switch back to index.html in Visual Studio Code.
- Find the closing </body> tag near the bottom of the file.
- Create the JavaScript section by pasting this directly above </body>:
<script>
function robotJuicer() {
return {
title: "Order a deluxe robot juicer",
cost: 60,
inventoryAdded: 0,
demandBoost: 8,
rationale: "Automation could attract a crowd, but the machine costs more than the available cash."
};
}
</script>
What does this code do?
- The robotJuicer() function returns the same proposed purchase for every unsafe run.
- The action stores its title and its 60 cost.
- The rationale records why the proposal is risky.
- Save index.html.
- Reload the local page in Safari.
You should still see the yellow-and-navy control room with its four starting metrics.
Did the dashboard disappear?
Check that the entire <script> section sits above </body>. Confirm that the function includes matching braces.
Compare the quotation marks around each text value. One missing mark can stop the browser from reading the script.
Help me check the robot juicer function.
A fresh-state function gives every run the same starting point. This makes the failure repeatable each time you test it.
- Find the closing </script> tag.
- Add the starting state directly above </script> by pasting this code:
function freshState() {
return {
cash: 25,
revenue: 0,
inventory: 8,
round: 0,
price: 5,
pendingAction: null,
mode: "idle",
ended: false,
status: "Ready. Run the unsafe demo first."
};
}
let state = freshState();
How does the starting state work?
- The freshState() function returns a new copy of the starting business data.
- The mutable state variable holds the current values during a simulation.
- The pendingAction value begins as null because the agent has not proposed anything yet.
- Save index.html.
- Reload the page in Safari.
You should see Cash at $25 and the status text telling you to run the unsafe demo first.
Seeing the wrong starting values?
Make sure let state = freshState(); appears after the closing brace of freshState().
Check each property against the code above. A missing comma between properties causes a syntax error.
Help me check the starting state.
Connect state to the dashboard
The browser exposes the page through the DOM. A cached reference lets your script update each existing metric without searching for the same element again.
- Find the let state = freshState(); line.
- Add the dashboard cache and formatting helpers below that line by pasting this code:
const ui = {
revenue: document.querySelector("#revenue"),
cash: document.querySelector("#cash"),
inventory: document.querySelector("#inventory"),
round: document.querySelector("#round"),
status: document.querySelector("#status"),
proposal: document.querySelector("#proposal-panel"),
proposalTitle: document.querySelector("#proposal-title"),
proposalReason: document.querySelector("#proposal-reason"),
proposalDetails: document.querySelector("#proposal-details"),
guardrail: document.querySelector("#guardrail"),
log: document.querySelector("#activity-log"),
unsafe: document.querySelector("#unsafe-button"),
start: document.querySelector("#start-button"),
reset: document.querySelector("#reset-button"),
approve: document.querySelector("#approve-button"),
reject: document.querySelector("#reject-button")
};
function dollars(value) {
return `$${value}`;
}
function addLog(message) {
ui.log.textContent = `${message}\n${ui.log.textContent}`;
}
How does the dashboard cache help?
- The ui object stores references to the dashboard elements using their existing IDs.
- The dollars() helper turns a number into text that begins with a dollar sign.
- The addLog() helper places each new message above the earlier activity.
- Save index.html.
- Reload the page in Safari.
You should see the complete dashboard with all controls and the activity log still present.
Missing a dashboard element?
Check every selector for its leading #. Confirm that each ID matches the existing HTML exactly.
Help me find a mismatched selector.
The render function copies current state into the visible cards. It also controls whether a proposal can be seen or approved.
- Find the closing brace of addLog().
- Add the dashboard renderer below addLog() by pasting this code:
function render() {
ui.revenue.textContent = dollars(state.revenue);
ui.cash.textContent = dollars(state.cash);
ui.inventory.textContent = `${state.inventory} cups`;
ui.round.textContent = `${state.round}/5`;
ui.status.textContent = state.status;
const action = state.pendingAction;
ui.proposal.hidden = action === null;
if (action !== null) {
ui.proposalTitle.textContent = action.title;
ui.proposalReason.textContent = action.rationale;
ui.proposalDetails.textContent = `Cost: ${dollars(action.cost)} | Adds ${action.inventoryAdded} cups | Demand boost: ${action.demandBoost}`;
ui.guardrail.textContent = action.blocked
? "Blocked by budget guardrail. Reject this action to continue."
: "Within budget. A human decision is required.";
ui.approve.disabled = action.blocked;
} else {
ui.approve.disabled = true;
}
ui.start.disabled = state.mode === "safe" && !state.ended;
}
What does render update?
- The metric lines copy revenue and cash from state into the matching cards.
- The proposal panel stays hidden while pendingAction is null.
- The button rules keep approval unavailable when there is no valid pending action.
- Add the reset function directly below render() by pasting this code:
function resetSimulation() {
state = freshState();
ui.log.textContent = "Ready for a simulation.";
render();
}
resetSimulation();
Why reset on page load?
- The resetSimulation() function replaces the current state with fresh starting values.
- The final call runs the function as soon as the browser reaches the script.
- The initial render proves the state can drive the existing dashboard.
- Save index.html.
- Reload the local page in Safari.
You should see Revenue $0. Cash should show $25. The activity log should read Ready for a simulation.
Starting values not rendering?
Confirm that resetSimulation(); is the final line inside the script. Check that render() is called inside resetSimulation().
Help me debug the initial render.
Trigger the unchecked purchase
The unsafe function represents an agent with immediate access to a spending action. It subtracts the proposed cost before evaluating whether the business can afford it.
- Find the closing brace of resetSimulation().
- Add the unsafe simulation directly below that brace by pasting this code:
function runUnsafeDemo() {
state = freshState();
state.mode = "unsafe";
const action = robotJuicer();
state.cash -= action.cost;
state.status = "Unsafe agent spent first and checked the budget too late.";
ui.log.textContent = "";
addLog(`Unchecked purchase: ${action.title} cost ${dollars(action.cost)}. Cash is now ${dollars(state.cash)}.`);
render();
}
Where does the unsafe behavior happen?
- The function starts from freshState() so every test uses the same cash balance.
- The subtraction changes state.cash immediately after retrieving the robot juicer action.
- The log records the purchase before render() displays the result.
An event listener connects a browser interaction to a function. The unsafe button can now call the simulation whenever you click it.
- Find the final resetSimulation(); call.
- Connect the unsafe button by pasting this line directly above resetSimulation();:
ui.unsafe.addEventListener("click", runUnsafeDemo);
What does the listener connect?
The listener watches the element cached as ui.unsafe. A click calls runUnsafeDemo().
- Save index.html.
- Reload the local page in Safari.
Before you click the button, do you think the purchase leaves the business with any cash?
- Click Run unsafe demo in the control panel.
You will see Cash change from $25 to $-35. The activity log reports an unchecked $60 robot-juicer purchase.
That is the experiment working as intended. Your dashboard has made the unsafe side effect visible.
Button click has no effect?
Confirm that the listener appears after the closing brace of runUnsafeDemo(). Check that it still sits inside the <script> section.
Make sure the function name is spelled runUnsafeDemo in both places. JavaScript names are case-sensitive.
Help me debug the unsafe button.
✔️ Awesome, I've got everything!
Great. Double check that index.html is saved and the unsafe run shows Cash at $-35.
ⓧ I'd like to double check the full code
Compare your saved index.html with this complete version for the unsafe simulation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fff8cc;
color: #17223b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #fff8cc, #fffdf2 55%, #dff7ea);
}
button {
border: 0;
border-radius: 12px;
padding: 0.8rem 1rem;
font: inherit;
font-weight: 800;
cursor: pointer;
background: #17223b;
color: white;
}
button.secondary {
background: #ffffff;
color: #17223b;
border: 2px solid #17223b;
}
button.danger {
background: #a62929;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.wrapper {
width: min(960px, 92vw);
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero {
background: #ffd84d;
border: 3px solid #17223b;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 8px 8px 0 #17223b;
}
.hero h1 {
margin: 0 0 0.4rem;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p {
margin: 0;
max-width: 68ch;
font-weight: 650;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.card,
.panel {
background: rgba(255, 255, 255, 0.92);
border: 2px solid #17223b;
border-radius: 18px;
padding: 1rem;
box-shadow: 4px 4px 0 #17223b;
}
.card span {
display: block;
color: #52606d;
font-size: 0.85rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card strong {
display: block;
margin-top: 0.3rem;
font-size: 1.8rem;
}
.controls,
.approval-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.status {
min-height: 1.5rem;
font-weight: 800;
}
.proposal {
margin-top: 1.5rem;
border-left: 10px solid #f0ad00;
}
.proposal h2,
.log-panel h2 {
margin-top: 0;
}
.guardrail {
padding: 0.75rem;
border-radius: 10px;
background: #fff0f0;
font-weight: 850;
}
.log-panel {
margin-top: 1.5rem;
}
#activity-log {
min-height: 9rem;
margin: 0;
padding: 1rem;
overflow: auto;
white-space: pre-wrap;
background: #101827;
color: #d8ffe5;
border-radius: 12px;
line-height: 1.5;
}
</style>
</head>
<body>
<main class="wrapper">
<header class="hero">
<h1>Lemonade Agent Control Room</h1>
<p>Goal: reach $100 in revenue without letting the simulated agent spend money it does not have.</p>
</header>
<section class="metrics" aria-label="Business metrics">
<article class="card"><span>Revenue</span><strong id="revenue">$0</strong></article>
<article class="card"><span>Cash</span><strong id="cash">$25</strong></article>
<article class="card"><span>Inventory</span><strong id="inventory">8 cups</strong></article>
<article class="card"><span>Round</span><strong id="round">0/5</strong></article>
</section>
<section class="panel">
<h2>Agent controls</h2>
<div class="controls">
<button id="unsafe-button" class="danger">Run unsafe demo</button>
<button id="start-button">Start safe day</button>
<button id="reset-button" class="secondary">Reset</button>
</div>
<p id="status" class="status" aria-live="polite">Ready. Run the unsafe demo first.</p>
</section>
<section id="proposal-panel" class="panel proposal" hidden>
<h2>Pending agent proposal</h2>
<h3 id="proposal-title"></h3>
<p id="proposal-reason"></p>
<p id="proposal-details"></p>
<p id="guardrail" class="guardrail"></p>
<div class="approval-buttons">
<button id="approve-button">Approve</button>
<button id="reject-button" class="secondary">Reject</button>
</div>
</section>
<section class="panel log-panel">
<h2>Activity log</h2>
<pre id="activity-log" aria-live="polite">Ready for a simulation.</pre>
</section>
</main>
<script>
function robotJuicer() {
return {
title: "Order a deluxe robot juicer",
cost: 60,
inventoryAdded: 0,
demandBoost: 8,
rationale: "Automation could attract a crowd, but the machine costs more than the available cash."
};
}
function freshState() {
return {
cash: 25,
revenue: 0,
inventory: 8,
round: 0,
price: 5,
pendingAction: null,
mode: "idle",
ended: false,
status: "Ready. Run the unsafe demo first."
};
}
let state = freshState();
const ui = {
revenue: document.querySelector("#revenue"),
cash: document.querySelector("#cash"),
inventory: document.querySelector("#inventory"),
round: document.querySelector("#round"),
status: document.querySelector("#status"),
proposal: document.querySelector("#proposal-panel"),
proposalTitle: document.querySelector("#proposal-title"),
proposalReason: document.querySelector("#proposal-reason"),
proposalDetails: document.querySelector("#proposal-details"),
guardrail: document.querySelector("#guardrail"),
log: document.querySelector("#activity-log"),
unsafe: document.querySelector("#unsafe-button"),
start: document.querySelector("#start-button"),
reset: document.querySelector("#reset-button"),
approve: document.querySelector("#approve-button"),
reject: document.querySelector("#reject-button")
};
function dollars(value) {
return `$${value}`;
}
function addLog(message) {
ui.log.textContent = `${message}\n${ui.log.textContent}`;
}
function render() {
ui.revenue.textContent = dollars(state.revenue);
ui.cash.textContent = dollars(state.cash);
ui.inventory.textContent = `${state.inventory} cups`;
ui.round.textContent = `${state.round}/5`;
ui.status.textContent = state.status;
const action = state.pendingAction;
ui.proposal.hidden = action === null;
if (action !== null) {
ui.proposalTitle.textContent = action.title;
ui.proposalReason.textContent = action.rationale;
ui.proposalDetails.textContent = `Cost: ${dollars(action.cost)} | Adds ${action.inventoryAdded} cups | Demand boost: ${action.demandBoost}`;
ui.guardrail.textContent = action.blocked
? "Blocked by budget guardrail. Reject this action to continue."
: "Within budget. A human decision is required.";
ui.approve.disabled = action.blocked;
} else {
ui.approve.disabled = true;
}
ui.start.disabled = state.mode === "safe" && !state.ended;
}
function resetSimulation() {
state = freshState();
ui.log.textContent = "Ready for a simulation.";
render();
}
function runUnsafeDemo() {
state = freshState();
state.mode = "unsafe";
const action = robotJuicer();
state.cash -= action.cost;
state.status = "Unsafe agent spent first and checked the budget too late.";
ui.log.textContent = "";
addLog(`Unchecked purchase: ${action.title} cost ${dollars(action.cost)}. Cash is now ${dollars(state.cash)}.`);
render();
}
ui.unsafe.addEventListener("click", runUnsafeDemo);
resetSimulation();
</script>
</body>
</html>
You have reproduced the agent's costly mistake inside a safe browser simulation. Next, you will place a budget check and a human decision between the proposal and the spending action.
Add Approval Guardrails
Your unsafe demo now shows the costly gap in the agent's design. The agent spends $60 against a $25 balance.
A budget guardrail checks each proposal before cash changes. Human approval keeps the final spending decision with you.
In this step, get ready to:
- Display each proposed action with its cost and risk status.
- Block proposals that exceed the available cash.
- Connect the approval controls to a paused decision workflow.
Render the approval boundary
The existing render() function already updates the business metrics. It now needs to show the pending proposal while enforcing the guardrail through the approval button.
- Switch back to index.html in Visual Studio Code.
- Find the existing render() function inside the <script> section.
- Replace the entire render() function with the following code:
function render() {
ui.revenue.textContent = dollars(state.revenue);
ui.cash.textContent = dollars(state.cash);
ui.inventory.textContent = `${state.inventory} cups`;
ui.round.textContent = `${state.round}/5`;
ui.status.textContent = state.status;
const action = state.pendingAction;
ui.proposal.hidden = action === null;
if (action !== null) {
ui.proposalTitle.textContent = action.title;
ui.proposalReason.textContent = action.rationale;
ui.proposalDetails.textContent = `Cost: ${dollars(action.cost)} | Adds ${action.inventoryAdded} cups | Demand boost: ${action.demandBoost}`;
ui.guardrail.textContent = action.blocked
? "Blocked by budget guardrail. Reject this action to continue."
: "Within budget. A human decision is required.";
ui.approve.disabled = action.blocked;
} else {
ui.approve.disabled = true;
}
ui.start.disabled = state.mode === "safe" && !state.ended;
}
How does rendering enforce the boundary?
- The hidden property keeps the proposal panel out of view when no action is waiting.
- The proposal fields show the action's rationale and financial impact before you decide.
- The blocked value controls the guardrail message and the disabled state of Approve.
- The safe-day button becomes unavailable while an approval workflow is active.
- Save index.html.
- Reload the page in Safari.
You should still see the starting metrics. The proposal panel remains hidden because state.pendingAction is currently null.
Seeing the proposal panel too early?
Check that ui.proposal.hidden compares action with null. Confirm that freshState() still sets pendingAction to null.
Ask for help checking the rendering logic if the panel stays visible. Help me debug the proposal panel visibility in my lemonade agent dashboard.
Create the paused proposal workflow
A safe workflow separates choosing an action from executing it. The agent can propose the robot juicer while the dashboard holds that proposal for review.
- Find runUnsafeDemo() near the bottom of the <script> section.
- Place the following workflow directly below runUnsafeDemo() by pasting this code:
function chooseAction() {
if (state.round === 0) {
return robotJuicer();
}
}
function proposeAction() {
const action = chooseAction();
action.blocked = action.cost > state.cash;
state.pendingAction = action;
state.status = action.blocked
? "The guardrail stopped an unaffordable action before execution."
: "The agent is waiting for your decision.";
addLog(`Proposal: ${action.title} for ${dollars(action.cost)}.`);
render();
}
function startSafeDay() {
state = freshState();
state.mode = "safe";
state.status = "Safe mode started.";
ui.log.textContent = "";
addLog("Safe day started with $25 cash and 8 cups.");
proposeAction();
}
ui.start.addEventListener("click", startSafeDay);
What does the proposal workflow do?
- The chooseAction() function returns the robot juicer during the opening round.
- The action.cost > state.cash rule checks affordability before the action can change the balance.
- The proposeAction() function stores the action as pending and renders its details.
- The startSafeDay() function restores the starting state before requesting the first proposal.
- The event listener runs the safe workflow when you click Start safe day.
- Save index.html.
- Reload the page in Safari.
- Click Start safe day.
You should see the robot-juicer proposal with a $60 cost. Cash remains at $25 because the proposal is paused.
Proposal panel still hidden?
Check that the start-button listener calls startSafeDay. Confirm that proposeAction() assigns the action to state.pendingAction before calling render().
Use this prompt if Safari still shows the starting screen. Help me debug why Start safe day does not show the proposal panel.
Connect approval and rejection
The decision buttons create the human boundary. Approval spends only after an action passes the budget check while rejection records a decision without changing cash.
- Place the following decision handlers below startSafeDay().
- Replace the existing listener lines at the bottom of the script with this complete listener group:
function approveAction() {
const action = state.pendingAction;
if (action === null || action.blocked) {
return;
}
state.cash -= action.cost;
state.inventory += action.inventoryAdded;
addLog(`Approved: ${action.title}. Spent ${dollars(action.cost)}.`);
}
function rejectAction() {
const action = state.pendingAction;
if (action === null) {
return;
}
addLog(`Rejected: ${action.title}. No money was spent.`);
}
ui.unsafe.addEventListener("click", runUnsafeDemo);
ui.start.addEventListener("click", startSafeDay);
ui.reset.addEventListener("click", resetSimulation);
ui.approve.addEventListener("click", approveAction);
ui.reject.addEventListener("click", rejectAction);
How do the decision handlers protect cash?
- The approveAction() function exits when no proposal exists or the current proposal is blocked.
- An affordable approval subtracts the cost and adds any purchased inventory.
- The rejectAction() function records the rejection without changing cash.
- Each listener connects a control-room button to its matching handler.
- Save index.html.
- Reload the page in Safari.
- Click Start safe day.
- Click Reject.
The newest log entry should report that the robot juicer was rejected. Cash should remain at $25.
Decision button not responding?
Check that the listener names match approveAction and rejectAction exactly. Confirm that each listener appears before the final resetSimulation() call.
Ask for help if the log does not record your decision. Help me debug the approval and rejection buttons.
Before the final check, do you expect the budget test to disable approval before the cash balance changes?
- Click Reset.
- Click Start safe day.
You should see Blocked by budget guardrail beside the $60 robot juicer. Approve should be disabled while Cash remains at $25.
You have put the spending boundary in place. The agent now pauses before an unaffordable proposal can affect the business.
✔️ Awesome, I've got everything!
Great work. Double check that index.html is saved with the blocked proposal visible in Safari.
ⓧ I'd like to double check the full code
Compare your saved index.html with the complete file at this stage:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fff8cc;
color: #17223b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(145deg, #fff8cc, #fffdf2 55%, #dff7ea);
}
button {
border: 0;
border-radius: 12px;
padding: 0.8rem 1rem;
font: inherit;
font-weight: 800;
cursor: pointer;
background: #17223b;
color: white;
}
button.secondary {
background: #ffffff;
color: #17223b;
border: 2px solid #17223b;
}
button.danger {
background: #a62929;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.wrapper {
width: min(960px, 92vw);
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero {
background: #ffd84d;
border: 3px solid #17223b;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 8px 8px 0 #17223b;
}
.hero h1 {
margin: 0 0 0.4rem;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.hero p {
margin: 0;
max-width: 68ch;
font-weight: 650;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.card,
.panel {
background: rgba(255, 255, 255, 0.92);
border: 2px solid #17223b;
border-radius: 18px;
padding: 1rem;
box-shadow: 4px 4px 0 #17223b;
}
.card span {
display: block;
color: #52606d;
font-size: 0.85rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card strong {
display: block;
margin-top: 0.3rem;
font-size: 1.8rem;
}
.controls,
.approval-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.status {
min-height: 1.5rem;
font-weight: 800;
}
.proposal {
margin-top: 1.5rem;
border-left: 10px solid #f0ad00;
}
.proposal h2,
.log-panel h2 {
margin-top: 0;
}
.guardrail {
padding: 0.75rem;
border-radius: 10px;
background: #fff0f0;
font-weight: 850;
}
.log-panel {
margin-top: 1.5rem;
}
#activity-log {
min-height: 9rem;
margin: 0;
padding: 1rem;
overflow: auto;
white-space: pre-wrap;
background: #101827;
color: #d8ffe5;
border-radius: 12px;
line-height: 1.5;
}
</style>
</head>
<body>
<main class="wrapper">
<header class="hero">
<h1>Lemonade Agent Control Room</h1>
<p>Goal: reach $100 in revenue without letting the simulated agent spend money it does not have.</p>
</header>
<section class="metrics" aria-label="Business metrics">
<article class="card"><span>Revenue</span><strong id="revenue">$0</strong></article>
<article class="card"><span>Cash</span><strong id="cash">$25</strong></article>
<article class="card"><span>Inventory</span><strong id="inventory">8 cups</strong></article>
<article class="card"><span>Round</span><strong id="round">0/5</strong></article>
</section>
<section class="panel">
<h2>Agent controls</h2>
<div class="controls">
<button id="unsafe-button" class="danger">Run unsafe demo</button>
<button id="start-button">Start safe day</button>
<button id="reset-button" class="secondary">Reset</button>
</div>
<p id="status" class="status" aria-live="polite">Ready. Run the unsafe demo first.</p>
</section>
<section id="proposal-panel" class="panel proposal" hidden>
<h2>Pending agent proposal</h2>
<h3 id="proposal-title"></h3>
<p id="proposal-reason"></p>
<p id="proposal-details"></p>
<p id="guardrail" class="guardrail"></p>
<div class="approval-buttons">
<button id="approve-button">Approve</button>
<button id="reject-button" class="secondary">Reject</button>
</div>
</section>
<section class="panel log-panel">
<h2>Activity log</h2>
<pre id="activity-log" aria-live="polite">Ready for a simulation.</pre>
</section>
</main>
<script>
function robotJuicer() {
return {
title: "Order a deluxe robot juicer",
cost: 60,
inventoryAdded: 0,
demandBoost: 8,
rationale: "Automation could attract a crowd, but the machine costs more than the available cash."
};
}
function freshState() {
return {
cash: 25,
revenue: 0,
inventory: 8,
round: 0,
price: 5,
pendingAction: null,
mode: "idle",
ended: false,
status: "Ready. Run the unsafe demo first."
};
}
let state = freshState();
const ui = {
revenue: document.querySelector("#revenue"),
cash: document.querySelector("#cash"),
inventory: document.querySelector("#inventory"),
round: document.querySelector("#round"),
status: document.querySelector("#status"),
proposal: document.querySelector("#proposal-panel"),
proposalTitle: document.querySelector("#proposal-title"),
proposalReason: document.querySelector("#proposal-reason"),
proposalDetails: document.querySelector("#proposal-details"),
guardrail: document.querySelector("#guardrail"),
log: document.querySelector("#activity-log"),
unsafe: document.querySelector("#unsafe-button"),
start: document.querySelector("#start-button"),
reset: document.querySelector("#reset-button"),
approve: document.querySelector("#approve-button"),
reject: document.querySelector("#reject-button")
};
function dollars(value) {
return `$${value}`;
}
function addLog(message) {
ui.log.textContent = `${message}\n${ui.log.textContent}`;
}
function render() {
ui.revenue.textContent = dollars(state.revenue);
ui.cash.textContent = dollars(state.cash);
ui.inventory.textContent = `${state.inventory} cups`;
ui.round.textContent = `${state.round}/5`;
ui.status.textContent = state.status;
const action = state.pendingAction;
ui.proposal.hidden = action === null;
if (action !== null) {
ui.proposalTitle.textContent = action.title;
ui.proposalReason.textContent = action.rationale;
ui.proposalDetails.textContent = `Cost: ${dollars(action.cost)} | Adds ${action.inventoryAdded} cups | Demand boost: ${action.demandBoost}`;
ui.guardrail.textContent = action.blocked
? "Blocked by budget guardrail. Reject this action to continue."
: "Within budget. A human decision is required.";
ui.approve.disabled = action.blocked;
} else {
ui.approve.disabled = true;
}
ui.start.disabled = state.mode === "safe" && !state.ended;
}
function resetSimulation() {
state = freshState();
ui.log.textContent = "Ready for a simulation.";
render();
}
function runUnsafeDemo() {
state = freshState();
state.mode = "unsafe";
const action = robotJuicer();
state.cash -= action.cost;
state.status = "Unsafe agent spent first and checked the budget too late.";
ui.log.textContent = "";
addLog(`Unchecked purchase: ${action.title} cost ${dollars(action.cost)}. Cash is now ${dollars(state.cash)}.`);
render();
}
function chooseAction() {
if (state.round === 0) {
return robotJuicer();
}
}
function proposeAction() {
const action = chooseAction();
action.blocked = action.cost > state.cash;
state.pendingAction = action;
state.status = action.blocked
? "The guardrail stopped an unaffordable action before execution."
: "The agent is waiting for your decision.";
addLog(`Proposal: ${action.title} for ${dollars(action.cost)}.`);
render();
}
function startSafeDay() {
state = freshState();
state.mode = "safe";
state.status = "Safe mode started.";
ui.log.textContent = "";
addLog("Safe day started with $25 cash and 8 cups.");
proposeAction();
}
function approveAction() {
const action = state.pendingAction;
if (action === null || action.blocked) {
return;
}
state.cash -= action.cost;
state.inventory += action.inventoryAdded;
addLog(`Approved: ${action.title}. Spent ${dollars(action.cost)}.`);
}
function rejectAction() {
const action = state.pendingAction;
if (action === null) {
return;
}
addLog(`Rejected: ${action.title}. No money was spent.`);
}
ui.unsafe.addEventListener("click", runUnsafeDemo);
ui.start.addEventListener("click", startSafeDay);
ui.reset.addEventListener("click", resetSimulation);
ui.approve.addEventListener("click", approveAction);
ui.reject.addEventListener("click", rejectAction);
resetSimulation();
</script>
</body>
</html>
Your safe workflow can inspect proposals and stop unaffordable spending. Next up, you will add the sales loop that carries approved decisions through a complete business day.
Run the Safe Business Day
Your approval boundary now stops the unaffordable robot juicer before it touches the budget. The safe business day still pauses after a decision.
In this step, you will add a simulation loop that converts each choice into one sales round. It keeps proposing actions until revenue reaches $100.
In this step, get ready to:
- Convert each approval or rejection into a sales round.
- Choose each proposal from the current inventory or round.
- Prove the safe simulation reaches its goal from a clean reset.
Turn a rejection into a sales round
The finishRound() function turns a human decision into sales. It also checks whether the business day should continue.
- In index.html, find the existing rejectAction() function.
- Replace the existing rejectAction() function by pasting this code in its place:
function rejectAction() { const action = state.pendingAction; if (action === null) return; addLog(`Rejected: ${action.title}. No money was spent.`); finishRound(0); }
function finishRound(demandBoost) { const baseDemand = [4, 5, 5, 6, 7][state.round]; const customers = Math.min(state.inventory, baseDemand + demandBoost); const sales = customers * state.price; state.inventory -= customers; state.revenue += sales; state.cash += sales; state.round += 1; state.pendingAction = null; addLog(`Round ${state.round}: sold ${customers} cups for ${dollars(sales)}.`); if (state.revenue >= 100) { state.ended = true; state.status = `Goal reached with ${dollars(state.revenue)} in revenue.`; addLog("Business day complete: the $100 revenue goal was reached."); render(); return; } if (state.round >= 5) { state.ended = true; state.status = `Day ended with ${dollars(state.revenue)} in revenue.`; addLog("Business day complete: the revenue goal was not reached."); render(); return; } proposeAction(); }
How does the round loop work?
- The baseDemand list supplies the expected customer count for the current round.
- Math.min() prevents the stand from selling more cups than it has.
- The sales result updates every business metric.
- The first stopping rule ends the day when revenue reaches $100.
- The final call to proposeAction() starts the next decision cycle.
Before you test this change, consider whether rejecting the robot juicer should now advance the round.
- Save index.html.
- Switch back to the page in Safari.
- Reload the page.
- Click Start safe day.
- Click Reject.
Revenue shows $20. Cash shows $45.
Inventory shows 4 cups. Round shows 1/5.
That first completed round proves the decision now feeds back into the business state.
Does Reject leave the proposal open?
Confirm that rejectAction() ends with finishRound(0).
Check that finishRound() remains inside the page's script section.
Help me debug the rejected round.
Choose actions from the current state
A decision policy uses the latest state to select the next proposal. Low inventory triggers a restock while the third proposal introduces the sidewalk sign.
- In index.html, locate the existing chooseAction() function.
- Replace the entire chooseAction() function by pasting this code:
function chooseAction() { if (state.round === 0) return robotJuicer(); if (state.inventory < 6) return { title: "Restock lemons and cups", cost: 18, inventoryAdded: 12, demandBoost: 0, rationale: "Inventory is too low to serve the next group of customers." }; if (state.round === 2) return { title: "Print a bright sidewalk sign", cost: 10, inventoryAdded: 0, demandBoost: 2, rationale: "A visible sign can bring two more customers to the stand this round." }; return { title: "Offer a tasting tray", cost: 8, inventoryAdded: 0, demandBoost: 2, rationale: "Small samples can bring more customers without a large commitment." }; }
How does the policy choose?
- Round 0 always proposes the robot juicer.
- Inventory below 6 cups triggers a restock.
- Round 2 triggers the sidewalk sign.
- The final action provides a fallback proposal for every other state.
Before you test the policy, consider which proposal should follow a round that leaves only four cups.
- Save index.html.
- Switch back to Safari.
- Reload the page.
- Click Start safe day.
- Click Reject.
You'll see Restock lemons and cups as the next proposal. Its rationale explains that inventory is too low for the next group.
The agent is now choosing its next move from the result of the previous round.
Seeing a different proposal?
Check that the inventory rule appears before the round 2 rule inside chooseAction().
Confirm that the inventory comparison uses state.inventory < 6.
Help me fix the decision policy.
Connect approvals to the full loop
An approved action must apply its cost before the round begins. Its inventory or demand effect then becomes part of that round's result.
- In index.html, locate the existing approveAction() function.
- Replace the entire approveAction() function by pasting this code:
function approveAction() { const action = state.pendingAction; if (action === null || action.blocked) return; state.cash -= action.cost; state.inventory += action.inventoryAdded; addLog(`Approved: ${action.title}. Spent ${dollars(action.cost)}.`); finishRound(action.demandBoost); }
What does approval change?
- The guard clause prevents a missing or blocked proposal from running.
- The approved cost comes out of cash before sales begin.
- Any purchased inventory becomes available for the current round.
- The action's demand boost passes into finishRound().
Before you run the full day, consider whether the protected sequence can reach the revenue goal without producing a negative balance.
- Save index.html.
- Switch back to Safari.
- Reload the page.
- Click Start safe day.
- Click Reject for the blocked robot juicer.
- Click Approve for Restock lemons and cups.
- Click Approve for Print a bright sidewalk sign.
- Click Approve for the next Restock lemons and cups proposal.
Revenue shows $110. Cash shows $89.
Inventory shows 10 cups. Round shows 4/5.
The newest log entry confirms that the $100 revenue goal was reached. The entries below it trace each proposal through its decision and sales result.
That's the complete safe loop working. Every simulated expense passed through the approval boundary before it changed cash.
Does the run stop early?
Confirm that approveAction() calls finishRound(action.demandBoost) after updating cash and inventory.
Check that you rejected only the blocked robot juicer. Approve each later proposal in the displayed order.
Help me debug the safe business day.
✔️ Awesome, I've got everything!
Your saved index.html now contains the complete safe business simulation.
ⓧ I'd like to double check the full code
Your saved index.html should match this reference.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lemonade Agent Control Room</title>
<style>
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #fff8cc; color: #17223b; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: linear-gradient(145deg, #fff8cc, #fffdf2 55%, #dff7ea); }
button { border: 0; border-radius: 12px; padding: 0.8rem 1rem; font: inherit; font-weight: 800; cursor: pointer; background: #17223b; color: white; }
button.secondary { background: #ffffff; color: #17223b; border: 2px solid #17223b; }
button.danger { background: #a62929; }
button:disabled { cursor: not-allowed; opacity: 0.45; }
.wrapper { width: min(960px, 92vw); margin: 0 auto; padding: 2rem 0 3rem; }
.hero { background: #ffd84d; border: 3px solid #17223b; border-radius: 24px; padding: 1.5rem; box-shadow: 8px 8px 0 #17223b; }
.hero h1 { margin: 0 0 0.4rem; font-size: clamp(2rem, 5vw, 3.5rem); }
.hero p { margin: 0; max-width: 68ch; font-weight: 650; }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin: 1.5rem 0; }
.card, .panel { background: rgba(255, 255, 255, 0.92); border: 2px solid #17223b; border-radius: 18px; padding: 1rem; box-shadow: 4px 4px 0 #17223b; }
.card span { display: block; color: #52606d; font-size: 0.85rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
.card strong { display: block; margin-top: 0.3rem; font-size: 1.8rem; }
.controls, .approval-buttons { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1rem 0; }
.status { min-height: 1.5rem; font-weight: 800; }
.proposal { margin-top: 1.5rem; border-left: 10px solid #f0ad00; }
.proposal h2, .log-panel h2 { margin-top: 0; }
.guardrail { padding: 0.75rem; border-radius: 10px; background: #fff0f0; font-weight: 850; }
.log-panel { margin-top: 1.5rem; }
#activity-log { min-height: 9rem; margin: 0; padding: 1rem; overflow: auto; white-space: pre-wrap; background: #101827; color: #d8ffe5; border-radius: 12px; line-height: 1.5; }
</style>
</head>
<body>
<main class="wrapper">
<header class="hero"><h1>Lemonade Agent Control Room</h1><p>Goal: reach $100 in revenue without letting the simulated agent spend money it does not have.</p></header>
<section class="metrics" aria-label="Business metrics"><article class="card"><span>Revenue</span><strong id="revenue">$0</strong></article><article class="card"><span>Cash</span><strong id="cash">$25</strong></article><article class="card"><span>Inventory</span><strong id="inventory">8 cups</strong></article><article class="card"><span>Round</span><strong id="round">0/5</strong></article></section>
<section class="panel"><h2>Agent controls</h2><div class="controls"><button id="unsafe-button" class="danger">Run unsafe demo</button><button id="start-button">Start safe day</button><button id="reset-button" class="secondary">Reset</button></div><p id="status" class="status" aria-live="polite">Ready. Run the unsafe demo first.</p></section>
<section id="proposal-panel" class="panel proposal" hidden><h2>Pending agent proposal</h2><h3 id="proposal-title"></h3><p id="proposal-reason"></p><p id="proposal-details"></p><p id="guardrail" class="guardrail"></p><div class="approval-buttons"><button id="approve-button">Approve</button><button id="reject-button" class="secondary">Reject</button></div></section>
<section class="panel log-panel"><h2>Activity log</h2><pre id="activity-log" aria-live="polite">Ready for a simulation.</pre></section>
</main>
<script>
function robotJuicer() { return { title: "Order a deluxe robot juicer", cost: 60, inventoryAdded: 0, demandBoost: 8, rationale: "Automation could attract a crowd, but the machine costs more than the available cash." }; }
function freshState() { return { cash: 25, revenue: 0, inventory: 8, round: 0, price: 5, pendingAction: null, mode: "idle", ended: false, status: "Ready. Run the unsafe demo first." }; }
let state = freshState();
const ui = { revenue: document.querySelector("#revenue"), cash: document.querySelector("#cash"), inventory: document.querySelector("#inventory"), round: document.querySelector("#round"), status: document.querySelector("#status"), proposal: document.querySelector("#proposal-panel"), proposalTitle: document.querySelector("#proposal-title"), proposalReason: document.querySelector("#proposal-reason"), proposalDetails: document.querySelector("#proposal-details"), guardrail: document.querySelector("#guardrail"), log: document.querySelector("#activity-log"), unsafe: document.querySelector("#unsafe-button"), start: document.querySelector("#start-button"), reset: document.querySelector("#reset-button"), approve: document.querySelector("#approve-button"), reject: document.querySelector("#reject-button") };
function dollars(value) { return `$${value}`; }
function addLog(message) { ui.log.textContent = `${message}\n${ui.log.textContent}`; }
function render() { ui.revenue.textContent = dollars(state.revenue); ui.cash.textContent = dollars(state.cash); ui.inventory.textContent = `${state.inventory} cups`; ui.round.textContent = `${state.round}/5`; ui.status.textContent = state.status; const action = state.pendingAction; ui.proposal.hidden = action === null; if (action !== null) { ui.proposalTitle.textContent = action.title; ui.proposalReason.textContent = action.rationale; ui.proposalDetails.textContent = `Cost: ${dollars(action.cost)} | Adds ${action.inventoryAdded} cups | Demand boost: ${action.demandBoost}`; ui.guardrail.textContent = action.blocked ? "Blocked by budget guardrail. Reject this action to continue." : "Within budget. A human decision is required."; ui.approve.disabled = action.blocked; } else { ui.approve.disabled = true; } ui.start.disabled = state.mode === "safe" && !state.ended; }
function resetSimulation() { state = freshState(); ui.log.textContent = "Ready for a simulation."; render(); }
function runUnsafeDemo() { state = freshState(); state.mode = "unsafe"; const action = robotJuicer(); state.cash -= action.cost; state.status = "Unsafe agent spent first and checked the budget too late."; ui.log.textContent = ""; addLog(`Unchecked purchase: ${action.title} cost ${dollars(action.cost)}. Cash is now ${dollars(state.cash)}.`); render(); }
function chooseAction() { if (state.round === 0) return robotJuicer(); if (state.inventory < 6) return { title: "Restock lemons and cups", cost: 18, inventoryAdded: 12, demandBoost: 0, rationale: "Inventory is too low to serve the next group of customers." }; if (state.round === 2) return { title: "Print a bright sidewalk sign", cost: 10, inventoryAdded: 0, demandBoost: 2, rationale: "A visible sign can bring two more customers to the stand this round." }; return { title: "Offer a tasting tray", cost: 8, inventoryAdded: 0, demandBoost: 2, rationale: "Small samples can bring more customers without a large commitment." }; }
function proposeAction() { const action = chooseAction(); action.blocked = action.cost > state.cash; state.pendingAction = action; state.status = action.blocked ? "The guardrail stopped an unaffordable action before execution." : "The agent is waiting for your decision."; addLog(`Proposal: ${action.title} for ${dollars(action.cost)}.`); render(); }
function startSafeDay() { state = freshState(); state.mode = "safe"; state.status = "Safe mode started."; ui.log.textContent = ""; addLog("Safe day started with $25 cash and 8 cups."); proposeAction(); }
function approveAction() { const action = state.pendingAction; if (action === null || action.blocked) return; state.cash -= action.cost; state.inventory += action.inventoryAdded; addLog(`Approved: ${action.title}. Spent ${dollars(action.cost)}.`); finishRound(action.demandBoost); }
function rejectAction() { const action = state.pendingAction; if (action === null) return; addLog(`Rejected: ${action.title}. No money was spent.`); finishRound(0); }
function finishRound(demandBoost) { const baseDemand = [4, 5, 5, 6, 7][state.round]; const customers = Math.min(state.inventory, baseDemand + demandBoost); const sales = customers * state.price; state.inventory -= customers; state.revenue += sales; state.cash += sales; state.round += 1; state.pendingAction = null; addLog(`Round ${state.round}: sold ${customers} cups for ${dollars(sales)}.`); if (state.revenue >= 100) { state.ended = true; state.status = `Goal reached with ${dollars(state.revenue)} in revenue.`; addLog("Business day complete: the $100 revenue goal was reached."); render(); return; } if (state.round >= 5) { state.ended = true; state.status = `Day ended with ${dollars(state.revenue)} in revenue.`; addLog("Business day complete: the revenue goal was not reached."); render(); return; } proposeAction(); }
ui.unsafe.addEventListener("click", runUnsafeDemo); ui.start.addEventListener("click", startSafeDay); ui.reset.addEventListener("click", resetSimulation); ui.approve.addEventListener("click", approveAction); ui.reject.addEventListener("click", rejectAction); resetSimulation();
</script>
</body>
</html>
A reset proves that the simulation can run again from the same known state.
- Click Reset.
Revenue returns to $0. Cash returns to $25.
Inventory returns to 8 cups. Round returns to 0/5.
You now have a repeatable safe simulation that observes results before choosing its next move.
Secret mission
Make the Agent React to Weather
Your safe agent can protect its budget, but customer demand stays predictable. Add changing weather so the agent observes new conditions, proposes a rainy-day special, and still reaches the revenue goal.
Clean Up Your Resources
Clean Up Your Resources
Your control room runs entirely on your Mac, so keeping it has no ongoing cost. Choose whether to keep the saved project, leave it for later, or delete its local files.
Resources you used:
- The local lemonade-agent folder in Documents, including your saved index.html file and weather extension (no cost).
Keep everything running
Keep the project as it is if you want to rerun the weather simulation or inspect the code later.
- Leave the lemonade-agent folder in Documents.
- Open index.html in Safari whenever you want to run the control room again.
Pause - I'll come back to this later
A local HTML page has no server or background process to pause. Closing its browser tab ends the current viewing session while preserving your work.
- Close the Safari tab containing the Lemonade Agent Control Room.
- Leave the lemonade-agent folder in Documents so you can return to it later.
Delete - I don't want to use this again
Deleting the folder removes your only saved copy of the control room. Clearing it from the Trash makes that removal permanent.
- In Finder, open Documents to locate the lemonade-agent folder.
- Select the lemonade-agent folder.
- Move the selected folder to the Trash.
- Empty the Trash to remove index.html permanently.
- Return to Documents to confirm that lemonade-agent is no longer listed.
Nice Work!
Nice Work!
You did it! Your Lemonade Agent Control Room now exposes reckless spending before showing a safer path. It protects the budget through human decisions. It also adapts its sales strategy when the weather changes.
You've learned how to:
- Built a browser-based business simulator whose DOM updates keep revenue, cash, inventory, rounds, proposals, status messages, and the activity log visible.
- Modeled an agent harness that turns business state into visible proposals across repeatable simulation rounds.
- Protected simulated spending with a budget guardrail that blocks the unaffordable $60 proposal before human approval can change the cash balance.
- Secret Mission: Extended the environment with weather-sensitive demand that triggers a rainy-day proposal when rain reduces customer demand.
Ready to quiz yourself?