Skip to content

Commit b63dfd8

Browse files
Matteo-itdsammaruga
authored andcommitted
Add Bedtime story teller example (arduino#15)
1 parent 1e034ad commit b63dfd8

File tree

5 files changed

+91
-272
lines changed

5 files changed

+91
-272
lines changed
Lines changed: 17 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,42 @@
1-
# Bedtime Story Teller
2-
3-
The **Bedtime Story Teller** example demonstrates how to build a generative AI application using the Arduino UNO Q. It uses a Large Language Model (LLM) to create personalized bedtime stories based on user-selected parameters like age, theme, and characters, streaming the result in real-time to a web interface.
4-
5-
![Bedtime Story Teller Example](assets/docs_assets/thumbnail.png)
1+
# Bedtime Story Teller Example
62

73
## Description
8-
9-
This App transforms the UNO Q into an AI storytelling assistant. It uses the `cloud_llm` Brick to connect to a cloud-based AI model and the `web_ui` Brick to provide a rich configuration interface.
10-
11-
The workflow allows you to craft a story by selecting specific parameters—such as the child's age, story theme, tone, and specific characters—or to let the App **generate a story randomly** for a quick surprise. The backend constructs a detailed prompt, sends it to the AI model, and streams the generated story back to the browser text-token by text-token.
4+
This example demonstrates how to build a bedtime story teller application using Arduino UNO Q.
5+
The application shows how to use a cloud-based large language model (LLM) to generate a bedtime story based on user input.
126

137
## Bricks Used
148

15-
The bedtime story teller example uses the following Bricks:
9+
The code detector example uses the following bricks:
1610

17-
- `cloud_llm`: Brick to interact with cloud-based Large Language Models (LLMs) like Google Gemini, OpenAI GPT, or Anthropic Claude.
18-
- `web_ui`: Brick to create the web interface for parameter input and story display.
11+
- `cloud_llm`: brick to interact with a cloud-based large language model (LLM) for generating story content.
12+
- `web_ui`: brick to create a web interface to get user input and display the generated story.
1913

2014
## Hardware and Software Requirements
2115

2216
### Hardware
2317

2418
- Arduino UNO Q (x1)
25-
- USB-C® cable (for power and programming) (x1)
19+
- USB camera (x1)
20+
- USB-C® to USB-A Cable (x1)
21+
- Personal computer with internet access
2622

2723
### Software
2824

29-
- Arduino App Lab
25+
- Apps Lab IDE
3026

31-
**Note:** This example requires an active internet connection to reach the AI provider's API. You will also need a valid **API Key** for the service used (e.g., Google AI Studio API Key).
27+
Note: You can run this example using your Arduino UNO Q as a Single Board Computer (SBC) using a [USB-C hub](https://store.arduino.cc/products/usb-c-to-hdmi-multiport-adapter-with-ethernet-and-usb-hub) with a mouse, keyboard and display attached.
3228

3329
## How to Use the Example
3430

35-
This example requires a valid API Key from an LLM provider (Google Gemini, OpenAI GPT, or Anthropic Claude) and an internet connection.
36-
37-
### Configure & Launch App
38-
39-
1. **Duplicate the Example**
40-
Since built-in examples are read-only, you must duplicate this App to edit the configuration. Click the arrow next to the App name and select **Duplicate** or click the **Copy and edit app** button on the top right corner of the App page.
41-
![Duplicate example](assets/docs_assets/duplicate-app.png)
42-
43-
2. **Open Brick Configuration**
44-
On the App page, locate the **Bricks** section on the left. Click on the **Cloud LLM** Brick, then click the **Brick Configuration** button on the right side of the screen.
45-
![Open Brick Configuration](assets/docs_assets/brick-config.png)
46-
47-
3. **Add API Key**
48-
In the configuration panel, enter your API Key into the corresponding field. This securely saves your credentials for the App to use. You can generate an API key from your preferred provider:
49-
* **Google Gemini:** [Get API Key](https://aistudio.google.com/app/apikey)
50-
* **OpenAI GPT:** [Get API Key](https://platform.openai.com/api-keys)
51-
* **Anthropic Claude:** [Get API Key](https://console.anthropic.com/settings/keys)
52-
53-
![Enter your API KEY](assets/docs_assets/brick-credentials.png)
54-
55-
4. **Run the App**
56-
Launch the App by clicking the **Run** button in the top right corner. Wait for the App to start.
57-
![Launch the App](assets/docs_assets/launch-app.png)
58-
59-
5. **Access the Web Interface**
60-
Open the App in your browser at `<UNO-Q-IP-ADDRESS>:7000`.
61-
62-
### Interacting with the App
63-
64-
1. **Choose Your Path**
65-
You have two options to create a story:
66-
* **Option A: Manual Configuration** (Follow step 2)
67-
* **Option B: Random Generation** (Skip to step 3)
68-
69-
2. **Set Parameters (Manual)**
70-
Use the interactive interface to configure the story details. The interface unlocks sections sequentially:
71-
- **Age:** Select the target audience (3-5, 6-8, 9-12, 13-16 years, or Adult).
72-
- **Theme:** Choose a genre (Fantasy/Adventure, Fairy Tale, Mystery/Horror, Science/Universe, Animals, or Comedy).
73-
- **Story Type (Optional):** Fine-tune the narrative:
74-
- *Tone:* e.g., Calm and sweet, Epic and adventurous, Tense and grotesque.
75-
- *Ending:* e.g., Happy, With a moral, Open and mysterious.
76-
- *Structure:* Classic, Chapter-based, or Episodic.
77-
- *Duration:* Short (5 min), Medium (10-15 min), or Long (20+ min).
78-
- **Characters:** You must add **at least one character** (max 5). Define their Name, Description, and Role (Protagonist, Antagonist, Positive/Negative Helper, or Other).
79-
- **Generate:** Once ready, click the **Generate story** button.
80-
81-
3. **Generate Randomly**
82-
If you prefer a surprise, click the **Generate Randomly** button on the right side of the screen. The App will automatically select random options for age, theme, tone, and structure to create a unique story instantly.
83-
84-
4. **Interact**
85-
The story streams in real-time. Once complete, you can:
86-
- **Copy** the text to your clipboard.
87-
- Click **New story** to reset the interface and start over.
31+
1. Run the app
32+
2. Open the App on your browser
8833

8934
## How it Works
9035

91-
Once the App is running, it performs the following operations:
36+
Here is a brief explanation of the full-stack application:
9237

93-
- **User Input Collection**: The `web_ui` Brick serves an HTML page where users select story attributes via interactive "chips" and forms.
94-
- **Prompt Engineering**: When the user requests a story, the Python backend receives a JSON object containing all parameters. It dynamically constructs a natural language prompt optimized for the LLM (e.g., "As a parent... I need a story about [Theme]...").
95-
- **AI Inference**: The `cloud_llm` Brick sends this prompt to the configured cloud provider using the API Key set in the Brick Configuration.
96-
- **Stream Processing**: Instead of waiting for the full text, the backend receives the response in chunks (tokens) and forwards them immediately to the frontend via WebSockets, ensuring the user sees progress instantly.
38+
### 🔧 Backend (main.py)
9739

98-
## Understanding the Code
40+
### 💻 Frontend (index.html + app.js)
9941

100-
### 🔧 Backend (`main.py`)
101-
102-
The Python script handles the logic of connecting to the AI and managing the data flow. Note that the API Key is not hardcoded; it is retrieved automatically from the Brick configuration.
103-
104-
- **Initialization**: The `CloudLLM` is set up with a system prompt that enforces HTML formatting for the output. The `CloudModel` constants map to specific efficient model versions:
105-
* `CloudModel.GOOGLE_GEMINI``gemini-2.5-flash`
106-
* `CloudModel.OPENAI_GPT``gpt-4o-mini`
107-
* `CloudModel.ANTHROPIC_CLAUDE``claude-3-7-sonnet-latest`
108-
109-
```python
110-
# The API Key is loaded automatically from the Brick Configuration
111-
llm = CloudLLM(
112-
model=CloudModel.GOOGLE_GEMINI,
113-
system_prompt="You are a bedtime story teller. Your response must be the story itself, formatted directly in HTML..."
114-
)
115-
llm.with_memory()
116-
```
117-
118-
- **Prompt Construction**: The `generate_story` function translates the structured data from the UI into a descriptive text prompt for the AI.
119-
120-
```python
121-
def generate_story(_, data):
122-
# Extract parameters
123-
age = data.get('age', 'any')
124-
theme = data.get('theme', 'any')
125-
126-
# Build natural language prompt
127-
prompt_for_display = f"As a parent who loves to read bedtime stories to my <strong>{age}</strong> year old child..."
128-
129-
# ... logic to append characters and settings ...
130-
131-
# Stream response back to UI
132-
prompt_for_llm = re.sub('<[^>]*>', '', prompt_for_display) # Clean tags for LLM
133-
for resp in llm.chat_stream(prompt_for_llm):
134-
ui.send_message("response", resp)
135-
136-
ui.send_message("stream_end", {})
137-
```
138-
139-
### 🔧 Frontend (`app.js`)
140-
141-
The JavaScript manages the complex UI interactions, random generation logic, and WebSocket communication.
142-
143-
- **Random Generation**: If the user chooses "Generate Randomly", the frontend programmatically selects random chips from the available options and submits the request.
144-
145-
```javascript
146-
document.getElementById('generate-randomly-button').addEventListener('click', () => {
147-
// Select random elements from the UI lists
148-
const ageChips = document.querySelectorAll('.parameter-container:nth-child(1) .chip');
149-
const randomAgeChip = getRandomElement(ageChips);
150-
// ... repeat for theme, tone, etc ...
151-
152-
const storyData = {
153-
age: randomAgeChip ? randomAgeChip.textContent.trim() : 'any',
154-
// ...
155-
characters: [], // Random stories use generic characters
156-
};
157-
158-
generateStory(storyData);
159-
});
160-
```
161-
162-
- **Socket Listeners**: The frontend listens for chunks of text and appends them to the display buffer, creating the streaming effect.
163-
164-
```javascript
165-
socket.on('response', (data) => {
166-
document.getElementById('story-container').style.display = 'flex';
167-
storyBuffer += data; // Accumulate text
168-
});
169-
170-
socket.on('stream_end', () => {
171-
const storyResponse = document.getElementById('story-response');
172-
storyResponse.innerHTML = storyBuffer; // Final render
173-
document.getElementById('loading-spinner').style.display = 'none';
174-
});
175-
```
42+
## Understanding the Code

examples/bedtime-story-teller/assets/app.js

Lines changed: 34 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,83 +7,55 @@ const socket = io(`http://${window.location.host}`);
77
let generateStoryButtonOriginalHTML = ''; // To store the original content of the generate story button
88
let storyBuffer = '';
99

10-
// Error container elements
11-
const errorContainer = document.getElementById('error-container');
1210

13-
function showError(message) {
14-
errorContainer.textContent = message;
15-
errorContainer.style.display = 'block';
16-
}
1711

18-
function hideError() {
19-
errorContainer.style.display = 'none';
20-
errorContainer.textContent = '';
21-
}
12+
function initSocketIO() {
13+
socket.on('prompt', (data) => {
14+
const promptContainer = document.getElementById('prompt-container');
15+
const promptDisplay = document.getElementById('prompt-display');
16+
promptDisplay.innerHTML = data;
17+
promptContainer.style.display = 'block';
18+
});
2219

20+
socket.on('response', (data) => {
2321

24-
function handlePrompt(data) {
25-
const promptContainer = document.getElementById('prompt-container');
26-
const promptDisplay = document.getElementById('prompt-display');
27-
promptDisplay.innerHTML = data;
28-
promptContainer.style.display = 'block';
29-
}
22+
document.getElementById('story-container').style.display = 'flex';
3023

31-
function handleResponse(data) {
32-
document.getElementById('story-container').style.display = 'flex';
33-
storyBuffer += data;
34-
}
24+
storyBuffer += data;
3525

36-
function handleStreamEnd() {
37-
hideError(); // Hide any errors on successful stream end
26+
});
3827

39-
const storyResponse = document.getElementById('story-response');
40-
storyResponse.innerHTML = storyBuffer;
28+
4129

42-
document.getElementById('loading-spinner').style.display = 'none';
43-
const clearStoryButton = document.getElementById('clear-story-button');
44-
clearStoryButton.style.display = 'block';
45-
clearStoryButton.disabled = false;
30+
socket.on('stream_end', () => {
4631

47-
const generateStoryButton = document.querySelector('.generate-story-button');
48-
if (generateStoryButton) {
49-
generateStoryButton.disabled = false;
50-
generateStoryButton.innerHTML = generateStoryButtonOriginalHTML; // Restore original content
51-
}
52-
}
32+
const storyResponse = document.getElementById('story-response');
5333

54-
function handleStoryError(data) {
55-
// Hide the loading spinner
56-
document.getElementById('loading-spinner').style.display = 'none';
34+
storyResponse.innerHTML = storyBuffer;
5735

58-
// Restore the generate story button
59-
const generateStoryButton = document.querySelector('.generate-story-button');
60-
if (generateStoryButton) {
61-
generateStoryButton.disabled = false;
62-
generateStoryButton.innerHTML = generateStoryButtonOriginalHTML;
63-
}
36+
6437

65-
// Display the error message in the dedicated error container
66-
showError(`An error occurred while generating the story: ${data.error}`);
38+
document.getElementById('loading-spinner').style.display = 'none';
6739

68-
// Also show the "New story" button to allow the user to restart
69-
const clearStoryButton = document.getElementById('clear-story-button');
70-
clearStoryButton.style.display = 'block';
71-
clearStoryButton.disabled = false;
72-
}
40+
const clearStoryButton = document.getElementById('clear-story-button');
7341

74-
function initSocketIO() {
75-
socket.on('prompt', handlePrompt);
76-
socket.on('response', handleResponse);
77-
socket.on('stream_end', handleStreamEnd);
78-
socket.on('story_error', handleStoryError);
42+
clearStoryButton.style.display = 'block';
7943

80-
socket.on('connect', () => {
81-
hideError(); // Clear any previous errors on successful connection
82-
});
44+
clearStoryButton.disabled = false;
8345

84-
socket.on('disconnect', () => {
85-
showError("Connection to backend lost. Please refresh the page or check the backend server.");
86-
});
46+
47+
48+
const generateStoryButton = document.querySelector('.generate-story-button');
49+
50+
if (generateStoryButton) {
51+
52+
generateStoryButton.disabled = false;
53+
54+
generateStoryButton.innerHTML = generateStoryButtonOriginalHTML; // Restore original content
55+
56+
}
57+
58+
});
8759
}
8860

8961
function unlockAndOpenNext(currentContainer) {
@@ -257,7 +229,6 @@ function gatherDataAndGenerateStory() {
257229
}
258230

259231
function generateStory(data) {
260-
hideError(); // Hide any errors when starting a new generation
261232
document.querySelector('.story-output-placeholder').style.display = 'none';
262233
const responseArea = document.getElementById('story-response-area');
263234
responseArea.style.display = 'flex';
@@ -280,7 +251,6 @@ function generateStory(data) {
280251
}
281252

282253
function resetStoryView() {
283-
hideError(); // Hide any errors when resetting view
284254
document.querySelector('.story-output-placeholder').style.display = 'flex';
285255
const responseArea = document.getElementById('story-response-area');
286256
responseArea.style.display = 'none';
@@ -499,7 +469,6 @@ document.addEventListener('DOMContentLoaded', () => {
499469
});
500470

501471
document.getElementById('generate-randomly-button').addEventListener('click', () => {
502-
hideError(); // Hide any errors when starting a new generation
503472
// Age
504473
const ageChips = document.querySelectorAll('.parameter-container:nth-child(1) .chip');
505474
const randomAgeChip = getRandomElement(ageChips);
@@ -550,4 +519,4 @@ document.addEventListener('DOMContentLoaded', () => {
550519

551520
generateStory(storyData);
552521
});
553-
});
522+
});

examples/bedtime-story-teller/assets/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ <h3 class="parameter-title">Age</h3>
4545
<span class="chip">6-8 years</span>
4646
<span class="chip">9-12 years</span>
4747
<span class="chip">13-16 years</span>
48-
48+
<span class="chip">Adult</span>
4949
</div>
5050
</div>
5151
</div>
@@ -201,7 +201,6 @@ <h3 class="response-title">Story</h3>
201201
</div>
202202

203203
</div>
204-
<div id="error-container" class="error-message" style="display: none;"></div>
205204
</div>
206205
</div>
207206

examples/bedtime-story-teller/assets/style.css

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -687,14 +687,6 @@ body {
687687
100% { transform: rotate(360deg); }
688688
}
689689

690-
.error-message {
691-
background-color: #f8d7da;
692-
color: #721c24;
693-
padding: 10px;
694-
margin-top: 20px;
695-
border-radius: 5px;
696-
text-align: center;
697-
}
698690

699691

700692
/*
@@ -799,10 +791,5 @@ body {
799791
}
800792

801793
#story-response::-webkit-scrollbar-track {
802-
background-color: #ECF1F1;
794+
background-color: transparent;
803795
}
804-
805-
#story-response::-webkit-scrollbar-thumb {
806-
background-color: #C9D2D2;
807-
border-radius: 4px;
808-
}

0 commit comments

Comments
 (0)