📓 Journal Creation Guide

A Complete Guide for Content Creators

🎯 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
⚡ Important:

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

1 Journal Opens: The HTML file loads in the app's WebView
2 Communication Bridge Established: Flutter injects a JavaScript bridge called "FlutterChannel" that allows HTML and Flutter to communicate
3 Check for Existing Data: JavaScript asks Flutter if a saved response file exists for this journal
4A If No Saved Data: Journal displays empty form, user fills it out from scratch
OR
4B If Saved Data Exists: JavaScript requests the data, Flutter sends it back, and JavaScript populates all form fields with saved responses
5 User Interaction: User clicks buttons, types in fields, selects options - any change triggers the auto-save timer
6 Auto-Save Trigger: After 2 seconds of no changes, JavaScript collects all form data into a structured JSON object
7 Send to Flutter: JavaScript sends the JSON data through FlutterChannel with the filename
8 Flutter Saves: Flutter writes the JSON to the device at: AppData/JSONs/User_Workbook_Responses/[filename].json
9 Confirmation: Flutter sends success/failure message back to JavaScript, which updates the status banner

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 1
  • H14ENM1_response.json - Anxiety course, English, Male voice, workbook 1
  • S22ENB1_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.data when loading
  • Overwriting created_at: Always preserve it from loaded data
  • Missing event listeners: Attach to all interactive elements
  • Not handling null values: Use ?.value and || '' 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}