🎯 Introduction
A journal is an interactive HTML workbook that allows users to track their progress, record reflections, and provide personalized data for AI analysis. Journals are course-specific and save user responses automatically to the device.
Key Features:
- Auto-saves user responses every 2 seconds after they stop typing
- Loads previous responses when users return to the journal
- Stores data locally on the device for privacy
- Provides structured data for AI personalization
- Works completely within HTML - no Flutter coding required
All journal logic (form interactions, data collection, auto-save) is written in JavaScript within the HTML file. Flutter only handles saving and loading JSON files to the device. You don't need to know Flutter - just HTML, CSS, and JavaScript.
🔄 How the Journal System Works
The Complete Flow
AppData/JSONs/User_Workbook_Responses/[filename].json
Key Concepts
📂 File Naming & Storage
Filename Format: {CourseCode}{WorkbookNumber}_response.json
All journals use workbook number 1
Examples:
C99ENB1_response.json- Confidence course, English, workbook 1H14ENM1_response.json- Anxiety course, English, Male voice, workbook 1S22ENB1_response.json- Sleep course, English, workbook 1
Storage Location: AppData/JSONs/User_Workbook_Responses/ (inside device internal storage)
⚡ Filename Override System
Important: Even though your HTML hardcodes a filename, Flutter automatically overrides it based on the user's current course.
Flutter reads user_profile.json → Gets the current_selected_course field → Creates filename like H011_response.json
Why? This ensures each course has its own separate journal file, even if users own multiple courses using the same journal HTML.
🛠️ Creating Your Journal
Step 1: Set Up the HTML Structure
Start with a basic HTML template that includes:
<!-- Status banner for auto-save feedback --> <div id="auto-save-status" class="status-banner status-idle"> ✅ Responses are auto-saved </div> <!-- Your form sections --> <section> <h2>Daily Check-In</h2> <!-- Example: Emoji button selection --> <div class="form-group"> <label>How are you feeling today?</label> <div class="emoji-buttons"> <button class="emoji-btn" data-field="mood" data-value="excellent">😊</button> <button class="emoji-btn" data-field="mood" data-value="good">🙂</button> <button class="emoji-btn" data-field="mood" data-value="okay">😐</button> </div> </div> <!-- Example: Text input --> <div class="form-group"> <label for="reflection">Your reflection:</label> <textarea id="reflection" placeholder="Write your thoughts..."></textarea> </div> </section>
Step 2: Define JavaScript Constants
const FILE_NAME = "C99ENB1_response.json"; // Your course-specific filename const WORKBOOK_ID = "c99en_journal"; // Unique identifier for this journal type const AUTO_SAVE_DELAY = 2000; // 2 seconds let savedData = null; // Will store loaded data from Flutter let autoSaveTimeout; // Timer for debouncing auto-save
Step 3: Initialize on Page Load
window.addEventListener('DOMContentLoaded', function() { // Set up all your event listeners setupEventListeners(); // If Flutter bridge is ready, check for existing data if (window.FlutterChannel) { checkForExistingResponses(); } }); // This function is called by Flutter when the bridge is ready window.onFlutterChannelReady = function() { console.log('Flutter channel ready'); checkForExistingResponses(); };
Step 4: Set Up Event Listeners
function setupEventListeners() { // Emoji buttons (custom interaction) document.querySelectorAll('.emoji-btn').forEach(button => { button.addEventListener('click', function() { // Remove active class from siblings this.parentElement.querySelectorAll('.emoji-btn') .forEach(btn => btn.classList.remove('active')); // Add active to clicked button this.classList.add('active'); // Trigger auto-save triggerAutoSave(); }); }); // Standard form inputs (text, textarea, select) document.querySelectorAll('input, textarea, select').forEach(element => { element.addEventListener('input', triggerAutoSave); element.addEventListener('change', triggerAutoSave); }); // Checkboxes and radio buttons document.querySelectorAll('input[type="checkbox"], input[type="radio"]') .forEach(element => { element.addEventListener('change', triggerAutoSave); }); }
Step 5: Implement Auto-Save Logic
function triggerAutoSave() { // Clear existing timer clearTimeout(autoSaveTimeout); // Show "typing" status showStatus('typing'); // Start new timer (saves after 2 seconds of no changes) autoSaveTimeout = setTimeout(() => { showStatus('saving'); saveWorkbook(); }, AUTO_SAVE_DELAY); } function showStatus(status) { const statusEl = document.getElementById('auto-save-status'); statusEl.className = `status-banner status-${status}`; const messages = { idle: '✅ Responses are auto-saved', typing: '⌨️ Editing...', saving: '💾 Saving...', saved: '✅ Saved successfully', error: '❌ Save failed - will retry' }; statusEl.textContent = messages[status]; // Auto-hide after showing saved/error if (status === 'saved' || status === 'error') { setTimeout(() => showStatus('idle'), 3000); } }
Step 6: Collect Form Data
function collectWorkbookData() { return { workbook_id: WORKBOOK_ID, created_at: savedData?.created_at || new Date().toISOString(), updated_at: new Date().toISOString(), ai_prompt: "Analyze this user's journal responses and provide personalized insights.", data: { // Collect all your form fields here // Example for emoji buttons: mood: document.querySelector('.emoji-btn.active')?.dataset.value, // Example for text input: reflection: document.getElementById('reflection').value, // Example for checkboxes: triggers: Array.from( document.querySelectorAll('input[name="triggers"]:checked') ).map(cb => cb.value), // Example for date: entry_date: document.getElementById('date-input').value } }; }
Step 7: Save to Flutter
function saveWorkbook() { const workbookData = collectWorkbookData(); if (window.FlutterChannel) { window.FlutterChannel.postMessage(JSON.stringify({ action: 'saveWorkbookResponse', responseData: workbookData, fileName: FILE_NAME })); } else { // Fallback for testing in browser console.log('Would save:', workbookData); setTimeout(() => showStatus('saved'), 500); } }
Step 8: Handle Flutter Responses
// Flutter sends messages back to this function window.handleFlutterMessage = function(messageString) { const message = JSON.parse(messageString); const data = message.data; switch(message.type) { case 'saveResult': if (data.success) { showStatus('saved'); } else { showStatus('error'); } break; case 'responseExistsResult': if (data.exists) { // File exists, load it loadExistingResponses(); } break; case 'loadResult': if (data.success && data.data) { // Important: Nested structure - data.data.data savedData = data.data.data; populateFormWithSavedData(savedData); } break; } };
Step 9: Load & Check Functions
function checkForExistingResponses() { if (window.FlutterChannel) { window.FlutterChannel.postMessage(JSON.stringify({ action: 'checkResponseExists', fileName: FILE_NAME })); } } function loadExistingResponses() { if (window.FlutterChannel) { window.FlutterChannel.postMessage(JSON.stringify({ action: 'loadWorkbookResponse', fileName: FILE_NAME })); } }
Step 10: Populate Form with Loaded Data
function populateFormWithSavedData(data) { // Populate emoji buttons if (data.mood) { const moodBtn = document.querySelector( `.emoji-btn[data-value="${data.mood}"]` ); if (moodBtn) moodBtn.classList.add('active'); } // Populate text inputs if (data.reflection) { document.getElementById('reflection').value = data.reflection; } // Populate checkboxes if (data.triggers) { data.triggers.forEach(trigger => { const checkbox = document.querySelector( `input[name="triggers"][value="${trigger}"]` ); if (checkbox) checkbox.checked = true; }); } // Continue for all your form fields... }
📝 Form Field Reference
Here's how to collect data from different types of form elements:
| Field Type | Collection Code | Population Code |
|---|---|---|
| Text Input | document.getElementById('id').value |
element.value = data.field |
| Textarea | document.getElementById('id').value |
element.value = data.field |
| Select Dropdown | document.getElementById('id').value |
element.value = data.field |
| Radio Button | document.querySelector('input[name="x"]:checked')?.value |
document.querySelector('[value="x"]').checked = true |
| Checkboxes | Array.from(querySelectorAll('[name="x"]:checked')).map(cb => cb.value) |
document.querySelector('[value="x"]').checked = true |
| Number Input | parseInt(document.getElementById('id').value) |
element.value = data.field |
| Range Slider | parseInt(document.getElementById('id').value) |
element.value = data.field |
| Date Input | document.getElementById('id').value |
element.value = data.field |
| Custom Button | document.querySelector('.btn.active')?.dataset.value |
document.querySelector('[data-value="x"]').classList.add('active') |
📋 Required JSON Structure
Your collectWorkbookData() function must return this structure:
{
"workbook_id": "unique_identifier",
"created_at": "2024-11-22T10:30:00.000Z",
"updated_at": "2024-11-22T15:45:23.000Z",
"ai_prompt": "Instructions for AI on how to analyze this data",
"data": {
// Your custom form fields go here
"field_name": "value",
"array_field": ["item1", "item2"],
"nested_object": {
"property": "value"
}
}
}
Field Descriptions
| Field | Description | Required? |
|---|---|---|
workbook_id |
Unique identifier for this journal type (e.g., "c99en_journal") | ✅ Yes |
created_at |
ISO timestamp of first save (preserve from loaded data if exists) | ✅ Yes |
updated_at |
ISO timestamp of current save (always current time) | ✅ Yes |
ai_prompt |
Instructions for AI on how to analyze the journal data | ✅ Yes |
data |
Object containing all your custom form fields and values | ✅ Yes |
💡 Tips & Best Practices
Data Structure
- Use consistent naming conventions (snake_case recommended)
- Store arrays for multiple selections (checkboxes, multi-select)
- Use ISO 8601 format for dates (YYYY-MM-DD)
- Store timestamps in ISO 8601 format with timezone
Common Mistakes to Avoid
- Forgetting the nested data access: Remember it's
data.data.datawhen loading - Overwriting created_at: Always preserve it from loaded data
- Missing event listeners: Attach to all interactive elements
- Not handling null values: Use
?.valueand|| ''for safety - Hardcoding dates: Always use
new Date().toISOString()
🎓 Complete Example
Scenario: Simple Mood Tracker
Let's create a minimal journal that tracks daily mood and one reflection note.
1. HTML Structure
<div id="status" class="status-banner">✅ Auto-saved</div> <div class="mood-buttons"> <button class="mood-btn" data-value="happy">😊</button> <button class="mood-btn" data-value="neutral">😐</button> <button class="mood-btn" data-value="sad">😢</button> </div> <textarea id="note" placeholder="Write a note..."></textarea>
2. JavaScript (Complete)
// Constants const FILE_NAME = "MOOD01_response.json"; let savedData = null; let autoSaveTimeout; // Initialize window.addEventListener('DOMContentLoaded', init); window.onFlutterChannelReady = function() { checkForExistingResponses(); }; function init() { // Mood buttons document.querySelectorAll('.mood-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active') ); this.classList.add('active'); triggerAutoSave(); }); }); // Textarea document.getElementById('note').addEventListener('input', triggerAutoSave); // Check for existing data if (window.FlutterChannel) checkForExistingResponses(); } function triggerAutoSave() { clearTimeout(autoSaveTimeout); document.getElementById('status').textContent = '⌨️ Editing...'; autoSaveTimeout = setTimeout(saveWorkbook, 2000); } function saveWorkbook() { document.getElementById('status').textContent = '💾 Saving...'; const data = { workbook_id: "mood_tracker", created_at: savedData?.created_at || new Date().toISOString(), updated_at: new Date().toISOString(), ai_prompt: "Analyze mood patterns and provide insights.", data: { mood: document.querySelector('.mood-btn.active')?.dataset.value, note: document.getElementById('note').value, date: new Date().toISOString().split('T')[0] } }; if (window.FlutterChannel) { window.FlutterChannel.postMessage(JSON.stringify({ action: 'saveWorkbookResponse', responseData: data, fileName: FILE_NAME })); } } function checkForExistingResponses() { window.FlutterChannel.postMessage(JSON.stringify({ action: 'checkResponseExists', fileName: FILE_NAME })); } function loadExistingResponses() { window.FlutterChannel.postMessage(JSON.stringify({ action: 'loadWorkbookResponse', fileName: FILE_NAME })); } window.handleFlutterMessage = function(msg) { const message = JSON.parse(msg); if (message.type === 'saveResult') { document.getElementById('status').textContent = message.data.success ? '✅ Saved' : '❌ Error'; } if (message.type === 'responseExistsResult' && message.data.exists) { loadExistingResponses(); } if (message.type === 'loadResult' && message.data.success) { savedData = message.data.data.data; if (savedData.mood) { document.querySelector(`.mood-btn[data-value="${savedData.mood}"]`) ?.classList.add('active'); } if (savedData.note) { document.getElementById('note').value = savedData.note; } } };
That's it! This complete example shows all the essential pieces working together.
⚡ Quick Reference
Flutter Channel Actions
| Action | Purpose | Parameters |
|---|---|---|
checkResponseExists |
Check if a saved file exists | fileName |
loadWorkbookResponse |
Load saved data from file | fileName |
saveWorkbookResponse |
Save data to file | fileName, responseData |
Flutter Response Types
| Message Type | When Received | Data Structure |
|---|---|---|
responseExistsResult |
After checkResponseExists | {exists: boolean} |
loadResult |
After loadWorkbookResponse | {success: boolean, data: {...}} |
saveResult |
After saveWorkbookResponse | {success: boolean} |