Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Fasting Tracker โ€“ 3-Month Calendar</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <style>
    html, body { height: 100%; font-family: 'Inter', sans-serif; }
    .day-box {
      transition: transform 0.1s;
      position: relative;
      width: 100%;
      height: 110px; /* Adjusted height for better content fit */
      overflow: hidden;
      border-radius: 0.375rem; /* rounded-md */
      border: 1px solid transparent;
    }
    .day-box:active { transform: scale(0.95); }
    /* Style for weekend days */
    .day-box.weekend::before {
      content: "";
      position: absolute; top: 0; left: 0; right: 0; bottom: 0;
      background-color: rgba(0, 0, 0, 0.08); /* Subtle overlay */
      border-radius: inherit;
      pointer-events: none; z-index: 0;
    }
    .day-box > * { position: relative; z-index: 1; } /* Ensure content is above overlay */
    .day-number {
      display: block;
      font-weight: 600; /* semibold */
      font-size: 10px; /* Smaller day number */
      margin-bottom: 2px;
      color: #E5E7EB; /* gray-200 */
    }
    .notes-preview-container {
      font-size: 0.7rem; /* Slightly smaller icons */
      line-height: 1;
      margin-top: 1px;
      max-height: 3.5em; /* Limit height */
      overflow: hidden;
      display: flex;
      flex-wrap: wrap;
      gap: 2px;
    }
    .stats-container {
      position: absolute;
      bottom: 4px;
      left: 4px;
      right: 4px;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 1px;
      font-size: 12px; /* Small text for stats */
      line-height: 1.1;
      color: rgba(255, 255, 255, 0.85); /* Slightly transparent white */
    }
    .stat-steps { font-size:16px; font-weight: bold; } /* Make steps stand out */
    /* Ensure chat messages wrap correctly */
    #chatContainer > div {
      word-wrap: break-word;
      overflow-wrap: break-word;
      white-space: pre-wrap;
    }
    /* Styling for the AI thinking indicator */
    .thinking-indicator {
      background-color: #4a5568; /* gray-700 */
      padding: 0.5rem;
      border-radius: 0.375rem; /* rounded-md */
      margin-bottom: 0.5rem;
      align-self: flex-start;
      animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: .5; }
    }
    /* Fade out animation for save confirmation */
    .fade-out {
      animation: fadeOut 1.5s ease-out forwards;
    }
    @keyframes fadeOut {
      from { opacity: 1; }
      to { opacity: 0; }
    }
    /* Ensure calendar grid takes available space */
    #calendarContainer > div {
      display: flex;
      flex-direction: column;
      min-height: 0; /* Important for flex-grow in child */
    }
    #calendarContainer .grid {
      flex-grow: 1; /* Allow grid to fill space */
    }
    /* Custom scrollbar styling */
    ::-webkit-scrollbar { width: 8px; height: 8px;}
    ::-webkit-scrollbar-track { background: #2d3748; border-radius: 10px;} /* gray-800 */
    ::-webkit-scrollbar-thumb { background: #4a5568; border-radius: 10px;} /* gray-700 */
    ::-webkit-scrollbar-thumb:hover { background: #718096;} /* gray-600 */
  </style>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-900 text-white flex">
  <div class="flex h-screen w-full">

    <div id="sidebar" class="w-[250px] border-r border-gray-700 p-4 flex flex-col justify-between bg-gray-800 shadow-lg overflow-y-auto">
      <div>
        <h2 class="text-xl font-bold mb-4 text-gray-100">Day Details</h2>
        <div id="sidebarContent" class="text-sm text-gray-300"><p>Select a day from the calendar.</p></div>
        <div id="globalSaveContainer" class="mt-4"></div>
      </div>
      <div class="text-xs mt-auto pt-4 border-t border-gray-700">
        <div class="flex justify-between items-center mb-3">
             <a href="#" id="resetLink" class="text-red-400 hover:text-red-300 hover:underline">Reset All Data</a>
             <a href="#" id="settingsLink" class="text-blue-400 hover:text-blue-300 hover:underline">Settings</a>
        </div>
        <div class="flex justify-between items-center mb-3">
             <a href="#" id="exportLink" class="text-green-400 hover:text-green-300 hover:underline">Export Data</a>
             <a href="#" id="importLink" class="text-yellow-400 hover:text-yellow-300 hover:underline">Import Data</a>
        </div>
        <div id="exportArea" class="mt-2 hidden">
          <textarea id="exportTextarea" class="w-full bg-gray-700 border border-gray-600 rounded p-2 text-xs text-gray-200" rows="5" readonly placeholder="JSON data will appear here..."></textarea>
        </div>
        <div id="importArea" class="mt-2 hidden">
          <textarea id="importTextarea" class="w-full bg-gray-700 border border-gray-600 rounded p-2 text-xs text-gray-200" rows="5" placeholder="Paste JSON data here"></textarea>
          <button id="importButton" class="w-full mt-2 px-3 py-1 bg-yellow-600 rounded hover:bg-yellow-700 text-white text-xs font-semibold">Import Data Now</button>
        </div>
        <div id="settingsArea" class="mt-2 hidden bg-gray-700 p-3 rounded border border-gray-600">
          <label class="block mb-1 text-xs font-medium text-gray-300">OpenAI API Key:</label>
          <input id="apiKeyInput" type="password" placeholder="Enter OpenAI API Key" class="w-full bg-gray-600 border border-gray-500 rounded p-1 mb-2 text-xs text-white" />
          <button id="saveApiKeyBtn" class="w-full px-3 py-1 bg-blue-600 rounded hover:bg-blue-700 text-xs text-white font-semibold mb-3">Save API Key</button>
          <label class="block mb-1 text-xs font-medium text-gray-300">AI System Prompt:</label>
          <textarea id="systemPromptInput" placeholder="Enter system prompt for AI (optional)" class="w-full bg-gray-600 border border-gray-500 rounded p-1 mb-2 text-xs text-white" rows="3"></textarea>
          <button id="saveSystemPromptBtn" class="w-full px-3 py-1 bg-blue-600 rounded hover:bg-blue-700 text-xs text-white font-semibold">Save Prompt</button>
        </div>
      </div>
    </div>

    <div class="flex-1 p-4 flex flex-col h-full overflow-hidden">
       <div class="mb-4">
         <p class="text-xs text-center text-gray-400 mb-1">Fasting Progress (Visible Months)</p>
         <div class="w-full bg-gray-700 rounded-full h-2.5 overflow-hidden shadow-inner">
           <div id="percentageBar" class="h-full bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full transition-all duration-500 ease-out" style="width:0%;"></div>
         </div>
       </div>

       <div class="mb-4 flex items-center justify-between">
         <button id="prevMonth" class="px-4 py-1.5 bg-gray-700 rounded-md hover:bg-gray-600 text-xs font-semibold shadow">Prev</button>
         <div class="flex space-x-8 text-center">
           <h1 id="monthTitle0" class="text-lg font-bold text-gray-100 w-48 truncate"></h1>
           <h1 id="monthTitle1" class="text-lg font-bold text-gray-100 w-48 truncate"></h1>
           <h1 id="monthTitle2" class="text-lg font-bold text-gray-100 w-48 truncate"></h1>
         </div>
         <button id="nextMonth" class="px-4 py-1.5 bg-gray-700 rounded-md hover:bg-gray-600 text-xs font-semibold shadow">Next</button>
       </div>

       <div id="calendarContainer" class="flex-1 overflow-y-auto flex flex-col space-y-6 pb-4">
         </div>
    </div>

    <div id="chatSidebar" class="w-[400px] border-l border-gray-700 p-4 flex flex-col bg-gray-800 shadow-lg h-full">
        <div class="flex justify-between items-center mb-3 pb-3 border-b border-gray-700">
         <h2 class="text-xl font-bold text-gray-100">AI Assistant</h2>
         <div class="flex space-x-2">
             <button id="newChatButton" title="Start New Chat" class="px-2 py-1 bg-blue-600 rounded-md hover:bg-blue-700 text-xs font-bold text-white shadow">+</button>
             <button id="deleteChatButton" title="Delete Current Chat" class="px-2 py-1 bg-red-600 rounded-md hover:bg-red-700 text-xs font-bold text-white shadow disabled:opacity-50 disabled:cursor-not-allowed" disabled>Delete</button>
         </div>
        </div>
        <div id="chatPagination" class="flex justify-between mb-2">
         <button id="prevChat" class="px-3 py-1 bg-gray-700 rounded-md hover:bg-gray-600 text-xs font-semibold shadow disabled:opacity-50 disabled:cursor-not-allowed" disabled>Prev Chat</button>
         <span id="chatSessionIndicator" class="text-xs text-gray-400 self-center">Chat 1 / 1</span>
         <button id="nextChat" class="px-3 py-1 bg-gray-700 rounded-md hover:bg-gray-600 text-xs font-semibold shadow disabled:opacity-50 disabled:cursor-not-allowed" disabled>Next Chat</button>
        </div>
        <div id="chatContainer" class="flex-1 overflow-y-auto mb-3 bg-gray-900 p-3 rounded-lg shadow-inner flex flex-col space-y-2" style="font-size: 0.875rem;">
         </div>
        <textarea id="chatInput" placeholder="Ask AI about your fasting data..." class="w-full bg-gray-700 border border-gray-600 rounded-lg p-2 text-sm text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none" rows="3"></textarea>
    </div>

  </div>

  <script>
    // --- Global State ---
    let today = new Date();
    let currentMonth = today.getMonth();
    let currentYear = today.getFullYear();
    let selected = { monthIndex: null, dayIndex: null }; // Tracks selected day { view index, day index }
    let fastingData = {}; // Main data store: { "YYYY-MM": [dayData1, dayData2, ...] }
    let openaiApiKey = ""; // User's OpenAI API key
    let systemPrompt = ""; // Custom system prompt for the AI
    let chatSessions = []; // Array of chat sessions: [{ id: timestamp, messages: [{ sender, text }] }]
    let currentChatIndex = 0; // Index of the currently viewed chat session
    let dataCheckInterval = null; // Interval timer for checking localStorage changes (for multi-tab sync)
    let lastKnownStorageState = { fastingData: null, chatSessions: null, settings: null }; // For multi-tab sync check

    // --- DOM Elements ---
    const sidebarContent = document.getElementById("sidebarContent");
    const globalSaveContainer = document.getElementById("globalSaveContainer");
    const percentageBar = document.getElementById("percentageBar");
    const calendarContainer = document.getElementById("calendarContainer");
    const monthTitles = [
      document.getElementById("monthTitle0"),
      document.getElementById("monthTitle1"),
      document.getElementById("monthTitle2")
    ];
    const prevMonthBtn = document.getElementById("prevMonth");
    const nextMonthBtn = document.getElementById("nextMonth");
    const resetLink = document.getElementById("resetLink");
    const exportLink = document.getElementById("exportLink");
    const importLink = document.getElementById("importLink");
    const settingsLink = document.getElementById("settingsLink");
    const exportArea = document.getElementById("exportArea");
    const exportTextarea = document.getElementById("exportTextarea");
    const importArea = document.getElementById("importArea");
    const importTextarea = document.getElementById("importTextarea");
    const importButton = document.getElementById("importButton");
    const settingsArea = document.getElementById("settingsArea");
    const apiKeyInput = document.getElementById("apiKeyInput");
    const saveApiKeyBtn = document.getElementById("saveApiKeyBtn");
    const systemPromptInput = document.getElementById("systemPromptInput");
    const saveSystemPromptBtn = document.getElementById("saveSystemPromptBtn");
    const chatContainer = document.getElementById("chatContainer");
    const chatInput = document.getElementById("chatInput");
    const newChatButton = document.getElementById("newChatButton");
    const deleteChatButton = document.getElementById("deleteChatButton");
    const prevChatBtn = document.getElementById("prevChat");
    const nextChatBtn = document.getElementById("nextChat");
    const chatSessionIndicator = document.getElementById("chatSessionIndicator");

    // --- Data Handling ---

    /**
     * Loads data (fasting records, settings, chat history) from localStorage.
     * Initializes data structures if they don't exist or are invalid.
     */
    function loadData() {
      try {
        fastingData = JSON.parse(localStorage.getItem("fastingData")) || {};
        // Basic validation: ensure it's an object, not an array or primitive
        if (typeof fastingData !== 'object' || Array.isArray(fastingData) || fastingData === null) {
          fastingData = {};
        }
      } catch {
        fastingData = {}; // Reset if parsing fails
        console.error("Failed to parse fastingData from localStorage.");
      }

      openaiApiKey = localStorage.getItem("openaiApiKey") || "";
      // Default system prompt includes the current date/time for context
      const dynamicDate = new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric',hour:'numeric',minute:'numeric',hour12:false});
      // ** Added explicit instruction about fastingHours and summaries **
      systemPrompt = localStorage.getItem("systemPrompt") || `You are a helpful assistant analyzing fasting data. Be concise and encouraging. Today is ${dynamicDate}. The 'fastingHours' field represents the duration of the fast in hours for that specific day (use this for calculations). Summaries for the relevant period are provided first, followed by daily details.`;

      try {
        chatSessions = JSON.parse(localStorage.getItem("chatSessions")) || [];
        if (!Array.isArray(chatSessions)) chatSessions = []; // Ensure it's an array
      } catch {
        chatSessions = []; // Reset if parsing fails
        console.error("Failed to parse chatSessions from localStorage.");
      }

      // Ensure there's at least one chat session
      if (!chatSessions.length || chatSessions.some(s => !s || !Array.isArray(s.messages))) {
         chatSessions = [{ id: Date.now(), messages: [] }];
      }
      currentChatIndex = chatSessions.length - 1; // Start with the latest chat

      // Populate settings fields if they exist
      if (apiKeyInput) apiKeyInput.value = openaiApiKey;
      if (systemPromptInput) systemPromptInput.value = systemPrompt; // Load potentially custom prompt

      updateLastKnownStorageState(); // Store initial state for multi-tab sync check
    }

    /**
     * Saves the current `fastingData` object to localStorage.
     */
    function saveData() {
      try {
        const dataString = JSON.stringify(fastingData);
        localStorage.setItem("fastingData", dataString);
        lastKnownStorageState.fastingData = dataString; // Update known state
      } catch(err) {
        console.error("Error saving fasting data:", err);
        alert("Error saving fasting data. Check console for details.");
      }
    }

    /**
     * Saves the current AI settings (API key, system prompt) to localStorage.
     */
    function saveSettings() {
      localStorage.setItem("openaiApiKey", openaiApiKey);
      localStorage.setItem("systemPrompt", systemPrompt);
      lastKnownStorageState.settings = JSON.stringify({ openaiApiKey, systemPrompt }); // Update known state
    }

    /**
     * Saves the current `chatSessions` array to localStorage.
     */
    function saveChats() {
      try {
        const chatsString = JSON.stringify(chatSessions);
        localStorage.setItem("chatSessions", chatsString);
        lastKnownStorageState.chatSessions = chatsString; // Update known state
      } catch(err) {
        console.error("Error saving chat data:", err);
        alert("Error saving chat data. Check console for details.");
      }
    }

    /**
     * Initializes or validates the data structure for a given month in `fastingData`.
     * Ensures the month exists and has the correct number of days, filling missing days/properties.
     * @param {number} y - Year
     * @param {number} m - Month (0-indexed)
     * @returns {string} The key for the month data (e.g., "2023-09")
     */
    function initMonthData(y, m) {
        const key = `${y}-${String(m + 1).padStart(2, '0')}`;
        const daysInMonth = new Date(y, m + 1, 0).getDate();
        let monthDataNeedsSave = false;

        // Default structure for a single day's data
        const defaultDay = () => ({
            fasting: false,       // boolean: Was the user fasting?
            fastingHours: "",     // string: Number of hours fasted (allows flexible input, e.g., "16.5")
            notes: [],            // array of {icon: string, text: string}: User notes for the day
            weight: "",           // string: Weight measurement
            keton: "",            // string: Keton measurement
            steps: ""             // string: Step count
        });

        // Ensure the month key exists and is an array
        if (!fastingData[key] || !Array.isArray(fastingData[key])) {
            fastingData[key] = Array.from({ length: daysInMonth }, defaultDay);
            monthDataNeedsSave = true;
        } else {
            const existingData = fastingData[key];
            // Adjust array length if month length is incorrect (e.g., due to leap year changes)
            if (existingData.length !== daysInMonth) {
                fastingData[key] = Array.from({ length: daysInMonth }, (_, i) => existingData[i] || defaultDay());
                monthDataNeedsSave = true;
            }

            // Iterate through each day to validate its structure and types
            fastingData[key].forEach((day, idx) => {
                let dayChanged = false;
                const current = day || {}; // Handle potentially null/undefined days
                const base = defaultDay();

                // Ensure all default properties exist
                for (const prop in base) {
                    if (typeof current[prop] === 'undefined') {
                        current[prop] = base[prop];
                        dayChanged = true;
                    }
                }

                // Validate 'notes' array and its contents
                if (!Array.isArray(current.notes)) {
                    current.notes = [];
                    dayChanged = true;
                } else {
                    // Ensure each note is an object with icon/text strings, filter out invalid/empty notes
                    current.notes = current.notes.map(note => {
                        const correctedNote = { icon: '', text: '' };
                        let noteChanged = false;
                        if (typeof note === 'string') { // Handle legacy string notes
                            correctedNote.text = note; noteChanged = true;
                        } else if (note && typeof note === 'object') {
                            correctedNote.icon = typeof note.icon === 'string' ? note.icon : '';
                            correctedNote.text = typeof note.text === 'string' ? note.text : '';
                            if (note.icon !== correctedNote.icon || note.text !== correctedNote.text) noteChanged = true;
                        } else { // Invalid note format
                            noteChanged = true;
                        }
                        if (noteChanged) dayChanged = true;
                        return correctedNote;
                    }).filter(n => n.text); // Remove notes without text
                }

                // Validate numeric/string fields (ensure they are strings)
                ['weight', 'keton', 'steps', 'fastingHours'].forEach(prop => {
                    // Ensure the property exists and handle potential null values from older data formats
                    if (typeof current[prop] === 'undefined' || current[prop] === null) {
                         current[prop] = ""; dayChanged = true;
                    } else if (typeof current[prop] !== 'string' && typeof current[prop] !== 'number') {
                         current[prop] = ""; dayChanged = true;
                    } else if (typeof current[prop] === 'number') { // Convert numbers to strings
                        current[prop] = String(current[prop]); dayChanged = true;
                    }
                });


                // Validate 'fasting' boolean
                if (typeof current.fasting !== 'boolean') {
                    current.fasting = false; dayChanged = true;
                }

                // If any corrections were made, update the data and mark for saving
                if (dayChanged) {
                    fastingData[key][idx] = current;
                    monthDataNeedsSave = true;
                }
            });
        }

        // Save if any changes were made during initialization/validation
        if (monthDataNeedsSave) saveData();
        return key; // Return the month key
    }


    // --- Rendering ---

    /**
     * Updates the overall progress bar based on fasting days in the visible months.
     */
    function updateStats() {
      const months = getVisibleMonths();
      let totalDays = 0, totalFasting = 0;
      months.forEach(mo => {
        const year = mo.y + mo.yOff, month = mo.m;
        const key = initMonthData(year, month); // Ensure data is initialized
        const arr = fastingData[key];
        totalDays += arr.length;
        totalFasting += arr.filter(d => d && d.fasting).length; // Count days marked as fasting
      });
      const pct = totalDays > 0 ? (totalFasting / totalDays) * 100 : 0;
      percentageBar.style.width = pct.toFixed(0) + "%"; // Update bar width
    }

    /**
     * Renders the 3-month calendar view.
     */
    function renderCalendar() {
      updateStats(); // Update progress bar first
      calendarContainer.innerHTML = ""; // Clear previous calendar

      getVisibleMonths().forEach((mo, viewIdx) => {
        const year = mo.y + mo.yOff, month = mo.m;
        // Set month title
        monthTitles[viewIdx].textContent = new Date(year, month, 1)
          .toLocaleDateString('en-US', { month: 'long', year: 'numeric' });

        const key = initMonthData(year, month); // Ensure data exists and is valid
        const arr = fastingData[key];

        // Create container for this month's grid
        const calendarDiv = document.createElement("div");
        calendarDiv.className = "flex flex-col w-full"; // Container for grid

        const grid = document.createElement("div");
        grid.className = "grid gap-1"; // Grid layout for days

        // Determine grid columns (aim for ~2 rows, min 14 cols)
        const cols = Math.max(14, Math.ceil(arr.length / 2));
        grid.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;

        // Create a box for each day
        arr.forEach((dayData, dayIdx) => {
          if (!dayData) return; // Skip if data is somehow missing

          const date = new Date(year, month, dayIdx + 1);
          const dow = date.getDay(); // Day of the week (0=Sun, 6=Sat)

          const box = document.createElement("div");
          box.className = "day-box cursor-pointer p-1.5 shadow-sm"; // Base styling

          // Calculate fasting percentage for background gradient
          // Ensure fastingHours is treated as a number for calculation
          const hours = Number(dayData.fastingHours || 0) || (dayData.fasting ? 24 : 0);
          const pct = Math.min(Math.max((hours / 24) * 100, 0), 100); // Clamp between 0-100

          // Apply background gradient and border based on fasting hours
          if (pct > 0) {
            box.style.background = `linear-gradient(to right, #2563EB ${pct}%, #374151 ${pct}%)`; // Blue gradient fill
            box.style.borderColor = "#3B82F6"; // blue-500 border
          } else {
            box.style.backgroundColor = "#374151"; // gray-700 background
            box.style.borderColor = "#4B5563"; // gray-600 border
          }

          // Add weekend indicator style
          if ([0, 6].includes(dow)) box.classList.add("weekend");

          // Highlight selected day
          if (selected.monthIndex === viewIdx && selected.dayIndex === dayIdx) {
            box.style.borderColor = "#FACC15"; // yellow-400
            box.style.borderWidth = "2px"; // Make border thicker
            box.style.boxShadow = "0 0 0 2px #FACC15"; // Add outer glow
          }

          // Day number
          const num = document.createElement("span");
          num.className = "day-number";
          num.textContent = dayIdx + 1;
          box.appendChild(num);

          // Notes preview (icons only)
          const notesPrev = document.createElement("div");
          notesPrev.className = "notes-preview-container";
          (dayData.notes || []).forEach(n => {
            const ic = document.createElement("span");
            ic.textContent = n.icon || "๐Ÿ“"; // Default icon if none provided
            if (!n.icon) ic.classList.add("opacity-60"); // Dim default icon
            notesPrev.appendChild(ic);
          });
          box.appendChild(notesPrev);

          // Stats preview (Weight, Keton, Fasting Hours, Steps)
          const statsDiv = document.createElement("div");
          statsDiv.className = "stats-container";
          let addedStat = false;
          if (dayData.weight) {
            const w = document.createElement("span"); w.textContent = `โš–๏ธ ${dayData.weight}`; statsDiv.appendChild(w); addedStat = true;
          }
          if (dayData.keton) {
            const k = document.createElement("span"); k.textContent = `๐Ÿงช ${dayData.keton}`; statsDiv.appendChild(k); addedStat = true;
          }
          if (hours > 0) { // Only show hours if > 0
            const fh = document.createElement("span"); fh.textContent = `โฑ๏ธ ${hours}h`; statsDiv.appendChild(fh); addedStat = true;
          }
          if (dayData.steps) {
            const s = document.createElement("span"); s.textContent = `๐Ÿ‘Ÿ ${dayData.steps}`; s.className = "stat-steps"; statsDiv.appendChild(s); addedStat = true;
          }
          if (addedStat) box.appendChild(statsDiv);

          // Click handler to select the day
          box.addEventListener("click", () => {
            selected = { monthIndex: viewIdx, dayIndex: dayIdx };
            localStorage.setItem("selectedDay", JSON.stringify(selected)); // Persist selection
            renderCalendar(); // Re-render to show selection highlight
            renderSidebar(); // Update sidebar with selected day's details
          });

          // Store data attributes for potential future use
          box.dataset.monthKey = key;
          box.dataset.dayIndex = dayIdx;

          grid.appendChild(box); // Add day box to the grid
        });

        calendarDiv.appendChild(grid); // Add grid to month container
        calendarContainer.appendChild(calendarDiv); // Add month container to main calendar area
      });
    }

    /**
     * Renders the content of the left sidebar based on the currently selected day.
     */
    function renderSidebar() {
      sidebarContent.innerHTML = ""; // Clear previous content
      globalSaveContainer.innerHTML = ""; // Clear previous save button

      const { monthIndex, dayIndex } = selected;

      // If no day is selected, show placeholder message
      if (monthIndex === null || dayIndex === null) {
        sidebarContent.innerHTML = "<p class='text-gray-400 italic'>Select a day from the calendar to see details.</p>";
        return;
      }

      const months = getVisibleMonths();
      // Validate selected month index
      if (monthIndex < 0 || monthIndex >= months.length) {
        selected = { monthIndex: null, dayIndex: null }; // Reset selection
        localStorage.removeItem("selectedDay");
        sidebarContent.innerHTML = "<p class='text-red-400'>Error: Invalid selected month index.</p>";
        renderCalendar(); // Re-render calendar to remove highlight
        return;
      }

      const mo = months[monthIndex];
      const year = mo.y + mo.yOff, month = mo.m;
      const key = initMonthData(year, month); // Ensure data exists

      // Validate selected day index
      if (!fastingData[key] || dayIndex < 0 || dayIndex >= fastingData[key].length) {
        selected = { monthIndex: null, dayIndex: null }; // Reset selection
        localStorage.removeItem("selectedDay");
        sidebarContent.innerHTML = "<p class='text-red-400'>Error: Could not load data for the selected day.</p>";
        renderCalendar(); // Re-render calendar to remove highlight
        return;
      }

      const dayData = fastingData[key][dayIndex];
      const date = new Date(year, month, dayIndex + 1);
      // Ensure hoursVal is treated as a number for the slider/display
      const hoursVal = Number(dayData.fastingHours || 0) || (dayData.fasting ? 24 : 0);

      // Build sidebar HTML content
      let html = `
        <h3 class="text-lg font-semibold mb-1 text-gray-100">Details for Day ${dayIndex + 1}</h3>
        <p class="text-xs text-gray-400 mb-3">${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>

        <div class="mb-4 flex items-center justify-between bg-gray-700 p-2 rounded-md shadow-sm">
          <span class="text-sm font-medium text-gray-200">Fasting: <span class="font-bold ${dayData.fasting ? 'text-blue-400' : 'text-gray-400'}">${dayData.fasting ? 'Yes' : 'No'}</span></span>
          <button id="toggleFastingBtn" class="px-3 py-1 bg-blue-600 rounded hover:bg-blue-700 text-xs text-white font-semibold shadow">Toggle</button>
        </div>

        <div class="mb-4">
          <label class="block mb-0.5 text-xs font-medium text-gray-400">Fasting Hours:</label>
          <input id="fastingHoursInput" type="range" min="0" max="24" step="0.5" value="${hoursVal}" class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"/>
          <span id="fastingHoursValue" class="text-sm text-gray-200">${hoursVal}h</span>
        </div>

        <div class="mb-4 space-y-2">
          <h4 class="text-md font-semibold mb-1 text-gray-200 border-b border-gray-600 pb-1">Daily Stats:</h4>
          <div>
            <label class="block mb-0.5 text-xs font-medium text-gray-400">Weight:</label>
            <input id="weightInput" type="text" inputmode="decimal" value="${dayData.weight || ''}" placeholder="e.g., 70.5 kg" class="w-full bg-gray-600 border border-gray-500 rounded p-1 text-xs text-white shadow-inner focus:ring-1 focus:ring-blue-500 focus:border-blue-500"/>
          </div>
          <div>
            <label class="block mb-0.5 text-xs font-medium text-gray-400">Keton Value:</label>
            <input id="ketonInput" type="text" inputmode="decimal" value="${dayData.keton || ''}" placeholder="e.g., 1.2 mmol/L" class="w-full bg-gray-600 border border-gray-500 rounded p-1 text-xs text-white shadow-inner focus:ring-1 focus:ring-blue-500 focus:border-blue-500"/>
          </div>
          <div>
            <label class="block mb-0.5 text-xs font-medium text-gray-400">Step Count:</label>
            <input id="stepsInput" type="number" inputmode="numeric" value="${dayData.steps || ''}" placeholder="e.g., 10000" class="w-full bg-gray-600 border border-gray-500 rounded p-1 text-xs text-white shadow-inner focus:ring-1 focus:ring-blue-500 focus:border-blue-500"/>
          </div>
        </div>

        <div class="mb-4">
          <h4 class="text-md font-semibold mb-2 text-gray-200 border-b border-gray-600 pb-1">Notes:</h4>
          <div id="notesListContainer" class="space-y-1.5 mb-2 max-h-48 overflow-y-auto pr-1"></div>
          <button id="toggleNoteFormBtn" class="w-full px-3 py-1.5 bg-green-600 rounded hover:bg-green-700 text-xs text-white font-semibold shadow mb-2">Add Note +</button>
          <div id="noteForm" class="space-y-1 bg-gray-700 p-2 rounded-md hidden shadow-sm">
            <input id="newNoteIcon" type="text" placeholder="Icon (emoji, optional)" maxlength="2" class="w-full bg-gray-600 border border-gray-500 rounded p-1 text-xs text-white shadow-inner"/>
            <textarea id="newNoteText" placeholder="Enter your note..." class="w-full bg-gray-600 border border-gray-500 rounded p-1 text-xs text-white shadow-inner resize-none" rows="2"></textarea>
             <div class="flex justify-end space-x-2 mt-1">
                <button id="saveNewNoteBtn" class="px-3 py-1 bg-green-600 rounded hover:bg-green-700 text-xs text-white font-semibold">Save Note</button>
                <button id="cancelNewNoteBtn" class="px-3 py-1 bg-gray-600 rounded hover:bg-gray-500 text-xs text-gray-300">Cancel</button>
            </div>
          </div>
        </div>`;
      sidebarContent.innerHTML = html;

      // --- Add Event Listeners for Sidebar Elements ---

      // Live update for fasting hours slider display
      document.getElementById("fastingHoursInput")?.addEventListener("input", e => {
        document.getElementById("fastingHoursValue").textContent = `${e.target.value}h`;
      });

      // Render existing notes
      const notesListContainer = document.getElementById("notesListContainer");
      if (!dayData.notes || !dayData.notes.length) {
        notesListContainer.innerHTML = `<p class="text-xs text-gray-500 italic px-1">No notes for this day.</p>`;
      } else {
        dayData.notes.forEach((note, idx) => {
          const noteDiv = document.createElement("div");
          noteDiv.className = "flex items-start justify-between bg-gray-700 p-1.5 rounded shadow-sm";
          // Display note content and edit/delete buttons
          noteDiv.innerHTML = `
            <div class="flex items-start space-x-2 flex-grow min-w-0 note-display-container">
              ${note.icon ? `<span class="mt-0.5 flex-shrink-0 w-5 text-center">${note.icon}</span>` : `<span class="opacity-0 w-5 flex-shrink-0"></span>`}
              <span class="note-text text-xs text-gray-200 flex-1 break-words pt-0.5" data-idx="${idx}">${note.text}</span>
            </div>
            <div class="flex items-center space-x-1.5 text-xs flex-shrink-0 ml-2 note-actions">
              <button class="text-blue-400 hover:text-blue-300 edit-note p-0.5" data-idx="${idx}" title="Edit Note">&#9998;</button> <button class="text-red-400 hover:text-red-300 delete-note p-0.5" data-idx="${idx}" title="Delete Note">&times;</button> </div>
            <div class="flex-1 space-y-1 note-edit-container hidden">
                 <div class="flex items-center space-x-1">
                    <input type="text" value="${note.icon}" maxlength="2" class="note-edit-icon w-10 bg-gray-500 border border-gray-400 rounded p-0.5 text-xs text-white" data-idx="${idx}">
                    <input type="text" value="${note.text}" class="note-edit-input flex-1 bg-gray-500 border border-gray-400 rounded p-0.5 text-xs text-white" data-idx="${idx}">
                </div>
                <div class="flex justify-end space-x-2 mt-1">
                    <button class="save-edit-btn text-green-400 hover:text-green-300 text-xs font-semibold" data-idx="${idx}">Save</button>
                    <button class="cancel-edit-btn text-gray-400 hover:text-gray-300 text-xs" data-idx="${idx}">Cancel</button>
                </div>
            </div>
          `;
          notesListContainer.appendChild(noteDiv);
        });
      }

      // Toggle fasting button listener
      document.getElementById("toggleFastingBtn")?.addEventListener("click", () => {
        const currentDayData = fastingData[key][dayIndex];
        // Toggle the fasting state
        currentDayData.fasting = !currentDayData.fasting;
        // If now fasting, set hours to 24 unless already set; if not fasting, clear hours (optional)
        if (currentDayData.fasting) {
            // Only set to 24 if hours are currently 0 or empty
            if (!currentDayData.fastingHours || Number(currentDayData.fastingHours) === 0) {
                 currentDayData.fastingHours = "24";
            }
        } else {
             currentDayData.fastingHours = "0"; // Reset hours when toggling off
        }
        // Save data and update UI
        saveData(); updateStats(); renderCalendar(); renderSidebar();
      });

      // Toggle Note Form button listener
      document.getElementById("toggleNoteFormBtn")?.addEventListener("click", () => {
        const noteForm = document.getElementById("noteForm");
        const isHidden = noteForm.classList.toggle("hidden");
        const toggleBtn = document.getElementById("toggleNoteFormBtn");
        toggleBtn.textContent = isHidden ? "Add Note +" : "Cancel";
        toggleBtn.classList.toggle("bg-green-600", isHidden);
        toggleBtn.classList.toggle("hover:bg-green-700", isHidden);
        toggleBtn.classList.toggle("bg-gray-600", !isHidden);
        toggleBtn.classList.toggle("hover:bg-gray-500", !isHidden);
        if (!isHidden) { // If form is shown, clear inputs and focus
            document.getElementById("newNoteIcon").value = "";
            document.getElementById("newNoteText").value = "";
            document.getElementById("newNoteText").focus();
        }
      });

       // Save New Note button listener (inside the form)
       document.getElementById("saveNewNoteBtn")?.addEventListener("click", () => {
            const textInput = document.getElementById("newNoteText");
            const iconInput = document.getElementById("newNoteIcon");
            const text = textInput.value.trim();
            const icon = iconInput.value.trim();

            if (text) {
                fastingData[key][dayIndex].notes.push({ icon: icon, text: text });
                saveData();
                renderCalendar(); // Update calendar preview
                renderSidebar(); // Re-render sidebar to show new note and hide form
            } else {
                alert("Note text cannot be empty.");
                textInput.focus();
            }
        });

        // Cancel New Note button listener
        document.getElementById("cancelNewNoteBtn")?.addEventListener("click", () => {
            const noteForm = document.getElementById("noteForm");
            noteForm.classList.add("hidden");
            const toggleBtn = document.getElementById("toggleNoteFormBtn");
            toggleBtn.textContent = "Add Note +";
            toggleBtn.className = "w-full px-3 py-1.5 bg-green-600 rounded hover:bg-green-700 text-xs text-white font-semibold shadow mb-2";
        });


      // Event delegation for notes list (Edit/Delete/Save Edit/Cancel Edit)
      notesListContainer?.addEventListener("click", e => {
        const target = e.target;
        const currentDayData = fastingData[key][dayIndex];
        const noteDiv = target.closest(".flex.items-start.justify-between"); // Get the main container for the note item

        if (!noteDiv) return; // Click wasn't on a relevant element

        const displayContainer = noteDiv.querySelector(".note-display-container");
        const editContainer = noteDiv.querySelector(".note-edit-container");
        const actionsContainer = noteDiv.querySelector(".note-actions");
        // Ensure dataset.idx exists before parsing
        const indexStr = target.dataset.idx;
        if (indexStr === undefined) return; // Clicked element doesn't have index
        const index = parseInt(indexStr);

        // Handle Delete
        if (target.classList.contains("delete-note")) {
          if (confirm("Delete this note?")) {
            currentDayData.notes.splice(index, 1);
            saveData(); renderCalendar(); renderSidebar();
          }
        }
        // Handle Edit Button Click
        else if (target.classList.contains("edit-note")) {
          displayContainer.classList.add("hidden");
          actionsContainer.classList.add("hidden");
          editContainer.classList.remove("hidden");
          editContainer.querySelector(".note-edit-input").focus(); // Focus the text input
        }
        // Handle Save Edit Button Click
        else if (target.classList.contains("save-edit-btn")) {
          const textInput = editContainer.querySelector(".note-edit-input");
          const iconInput = editContainer.querySelector(".note-edit-icon");
          const newText = textInput.value.trim();
          const newIcon = iconInput.value.trim();

          if (newText) {
            currentDayData.notes[index] = { icon: newIcon, text: newText };
            saveData(); renderCalendar(); renderSidebar(); // Re-render to show updated note
          } else {
            alert("Note text cannot be empty.");
            textInput.focus();
          }
        }
        // Handle Cancel Edit Button Click
        else if (target.classList.contains("cancel-edit-btn")) {
          editContainer.classList.add("hidden");
          displayContainer.classList.remove("hidden");
          actionsContainer.classList.remove("hidden");
          // No data save needed, just revert UI
        }
      });

      // Global Save Button (saves all fields for the selected day)
      const saveBtn = document.createElement("button");
      saveBtn.className = "w-full px-3 py-2 bg-gradient-to-r from-green-500 to-emerald-600 rounded-md hover:from-green-600 hover:to-emerald-700 text-sm text-white font-bold shadow-md transition duration-150 ease-in-out";
      saveBtn.textContent = "Save All Changes for This Day";
      saveBtn.addEventListener("click", () => {
        const weightVal = document.getElementById("weightInput")?.value || "";
        const ketonVal = document.getElementById("ketonInput")?.value || "";
        const stepsVal = document.getElementById("stepsInput")?.value || "";
        const hoursVal = document.getElementById("fastingHoursInput")?.value || "0";
        const currentDayData = fastingData[key][dayIndex];

        // Update data object
        currentDayData.weight = weightVal;
        currentDayData.keton = ketonVal;
        currentDayData.steps = stepsVal;
        // Ensure fastingHours is saved as a string
        currentDayData.fastingHours = String(hoursVal);
        // Also update fasting boolean based on hours (if hours > 0, set fasting to true)
        currentDayData.fasting = Number(hoursVal) > 0;

        // Check if there's a new note pending in the form (handle case where user types then hits global save)
        const newNoteTextElem = document.getElementById("newNoteText");
        const newNoteIconElem = document.getElementById("newNoteIcon");
        const noteForm = document.getElementById("noteForm");
        if (newNoteTextElem && newNoteIconElem && noteForm && !noteForm.classList.contains("hidden") && newNoteTextElem.value.trim()) {
            currentDayData.notes.push({ icon: newNoteIconElem.value.trim(), text: newNoteTextElem.value.trim() });
            // Clear and hide the form after saving
            newNoteTextElem.value = "";
            newNoteIconElem.value = "";
            noteForm.classList.add("hidden");
            const toggleBtn = document.getElementById("toggleNoteFormBtn");
            if (toggleBtn) {
                toggleBtn.textContent = "Add Note +";
                toggleBtn.className = "w-full px-3 py-1.5 bg-green-600 rounded hover:bg-green-700 text-xs text-white font-semibold shadow mb-2";
            }
        }


        saveData(); // Save all changes
        updateStats(); // Update progress bar
        renderCalendar(); // Update calendar view
        localStorage.setItem("selectedDay", JSON.stringify(selected)); // Re-save selection just in case
        renderSidebar(); // Re-render sidebar to reflect saved state

        // Show save confirmation feedback
        const conf = document.createElement("span");
        conf.textContent = " Saved!";
        conf.className = "text-green-300 text-xs ml-2 fade-out";
        saveBtn.appendChild(conf);
        setTimeout(() => conf.remove(), 1500); // Remove confirmation after 1.5s
      });
      globalSaveContainer.appendChild(saveBtn);
    }

    /**
     * Renders the chat interface for the current chat session.
     */
    function renderChat() {
      if (!chatContainer) return; // Ensure chat elements exist

      // Validate currentChatIndex and ensure chatSessions has at least one session
      if (currentChatIndex < 0 || currentChatIndex >= chatSessions.length) {
        currentChatIndex = Math.max(0, chatSessions.length - 1);
        if (!chatSessions.length) {
          chatSessions.push({ id: Date.now(), messages: [] });
          currentChatIndex = 0;
          saveChats(); // Save the newly created session
        }
      }

      chatContainer.innerHTML = ""; // Clear previous messages
      const session = chatSessions[currentChatIndex];

      // Display messages or placeholder if empty
      if (!session || !session.messages.length) {
        chatContainer.innerHTML = `<p class="text-center text-gray-500 text-sm italic mt-4">Chat history is empty. Ask the AI something!</p>`;
      } else {
        session.messages.forEach(m => {
          const div = document.createElement("div");
          div.classList.add("mb-2", "p-2.5", "rounded-lg", "max-w-[85%]", "shadow", "text-sm");
          div.style.overflowWrap = 'break-word'; // Ensure long words wrap

          if (m.sender === "ai") {
            div.classList.add("bg-gray-600", "self-start", "text-gray-100");
            try {
              // Use marked library to render Markdown from AI
              marked.setOptions({ breaks: true, gfm: true, sanitize: false }); // Configure marked
              div.innerHTML = marked.parse(m.text || ''); // Render markdown
            } catch (e) {
              console.error("Markdown parsing error:", e);
              div.textContent = m.text || ''; // Fallback to plain text
            }
          } else { // User message
            div.classList.add("bg-blue-600", "self-end", "text-white");
            div.textContent = m.text || ''; // Display plain text
          }
          chatContainer.appendChild(div);
        });
      }

      // Scroll to the bottom of the chat container
      chatContainer.scrollTop = chatContainer.scrollHeight;

      // Update chat pagination indicator and button states
      chatSessionIndicator.textContent = `Chat ${currentChatIndex + 1} / ${chatSessions.length}`;
      prevChatBtn.disabled = currentChatIndex === 0;
      nextChatBtn.disabled = currentChatIndex === chatSessions.length - 1;
      deleteChatButton.disabled = chatSessions.length <= 1; // Can't delete the last chat
    }


    // --- Helpers ---

    /**
     * Calculates the year and month for the three currently visible calendar views.
     * @returns {Array<{y: number, m: number, yOff: number}>} Array of month objects
     */
    function getVisibleMonths() {
      const m1 = currentMonth; // Current month
      const y1Off = 0;        // Year offset for current month

      const m2 = (currentMonth + 1) % 12; // Next month
      const y2Off = currentMonth + 1 > 11 ? 1 : 0; // Year offset for next month

      const m3 = (currentMonth + 2) % 12; // Month after next
      const y3Off = currentMonth + 2 > 11 ? 1 : 0; // Year offset for month after next

      return [
        { y: currentYear, m: m1, yOff: y1Off },
        { y: currentYear, m: m2, yOff: y2Off },
        { y: currentYear, m: m3, yOff: y3Off }
      ];
    }

    /**
     * Updates the `lastKnownStorageState` object with current localStorage values.
     * Used for multi-tab synchronization check.
     */
    function updateLastKnownStorageState() {
      lastKnownStorageState.fastingData = localStorage.getItem("fastingData") || "{}";
      lastKnownStorageState.chatSessions = localStorage.getItem("chatSessions") || "[]";
      lastKnownStorageState.settings = JSON.stringify({
        openaiApiKey: localStorage.getItem("openaiApiKey") || "",
        systemPrompt: localStorage.getItem("systemPrompt") || ""
      });
    }

    /**
     * Checks if localStorage data has changed (e.g., in another tab) and updates the UI accordingly.
     */
    function checkStorageChanges() {
      const currentFastingData = localStorage.getItem("fastingData") || "{}";
      const currentChatSessions = localStorage.getItem("chatSessions") || "[]";
      const currentSettings = JSON.stringify({
        openaiApiKey: localStorage.getItem("openaiApiKey") || "",
        systemPrompt: localStorage.getItem("systemPrompt") || ""
      });

      let needsRerender = false;
      let needsChatRerender = false;
      let needsSettingsUpdate = false;

      // Check Fasting Data
      if (currentFastingData !== lastKnownStorageState.fastingData) {
        try {
          const newData = JSON.parse(currentFastingData);
          if (typeof newData === 'object' && !Array.isArray(newData) && newData !== null) {
             fastingData = newData;
             lastKnownStorageState.fastingData = currentFastingData;
             // Re-initialize visible months to ensure data integrity after external change
             getVisibleMonths().forEach(mo => initMonthData(mo.y + mo.yOff, mo.m));
             needsRerender = true;
             console.log("Fasting data updated from another tab.");
          }
        } catch (e) { console.error("Error parsing external fasting data update:", e); }
      }

      // Check Chat Sessions
      if (currentChatSessions !== lastKnownStorageState.chatSessions) {
        try {
          const newSessions = JSON.parse(currentChatSessions);
          if (Array.isArray(newSessions)) {
            chatSessions = newSessions;
            lastKnownStorageState.chatSessions = currentChatSessions;
            // Adjust currentChatIndex if it's now out of bounds
            currentChatIndex = Math.max(0, Math.min(currentChatIndex, chatSessions.length - 1));
            if (!chatSessions.length) { // Ensure at least one session exists
              chatSessions.push({ id: Date.now(), messages: [] });
              currentChatIndex = 0;
              saveChats(); // Save the new empty session immediately
            }
            needsChatRerender = true;
            console.log("Chat sessions updated from another tab.");
          }
        } catch (e) { console.error("Error parsing external chat session update:", e); }
      }

      // Check Settings
      if (currentSettings !== lastKnownStorageState.settings) {
        try {
          const newSettingsObj = JSON.parse(currentSettings);
          openaiApiKey = newSettingsObj.openaiApiKey || "";
          systemPrompt = newSettingsObj.systemPrompt || ""; // Update local systemPrompt variable
          lastKnownStorageState.settings = currentSettings;
          needsSettingsUpdate = true;
          console.log("Settings updated from another tab.");
        } catch (e) { console.error("Error parsing external settings update:", e); }
      }

      // Apply updates to the UI
      if (needsRerender) { renderCalendar(); renderSidebar(); }
      if (needsChatRerender) renderChat();
      if (needsSettingsUpdate) {
        // Update settings fields only if the settings panel is currently visible
        if (apiKeyInput && !settingsArea.classList.contains('hidden')) apiKeyInput.value = openaiApiKey;
        // Update system prompt input field regardless of visibility, as it affects AI calls
        if (systemPromptInput) systemPromptInput.value = systemPrompt;
      }
    }


    // --- Initialization & Event Listeners ---

    /**
     * Initial setup when the DOM is fully loaded.
     */
    document.addEventListener('DOMContentLoaded', () => {
      loadData(); // Load all data from localStorage

      // Ensure data for initially visible months is initialized/validated
      getVisibleMonths().forEach(mo => initMonthData(mo.y + mo.yOff, mo.m));

      // Restore previously selected day, if valid
      try {
        const savedSelection = JSON.parse(localStorage.getItem("selectedDay"));
        if (savedSelection && typeof savedSelection.monthIndex === 'number' && typeof savedSelection.dayIndex === 'number') {
          // Validate the saved selection against current view and data
          if (savedSelection.monthIndex >= 0 && savedSelection.monthIndex < 3) {
            const mmo = getVisibleMonths()[savedSelection.monthIndex];
            const key = `${mmo.y + mmo.yOff}-${String(mmo.m + 1).padStart(2, '0')}`;
            if (fastingData[key] && savedSelection.dayIndex >= 0 && savedSelection.dayIndex < fastingData[key].length) {
              selected = savedSelection; // Restore valid selection
            } else {
              localStorage.removeItem("selectedDay"); // Remove invalid selection
            }
          } else {
             localStorage.removeItem("selectedDay"); // Remove invalid selection
          }
        }
      } catch (e) {
        console.error("Error parsing selectedDay from localStorage:", e);
        localStorage.removeItem("selectedDay");
      }

      renderCalendar(); // Initial render of the calendar
      renderSidebar(); // Initial render of the sidebar (shows selection or placeholder)
      renderChat(); // Initial render of the chat interface

      // Start interval timer to check for changes in other tabs
      if (dataCheckInterval) clearInterval(dataCheckInterval); // Clear existing interval if any
      dataCheckInterval = setInterval(checkStorageChanges, 1500); // Check every 1.5 seconds
    });

    // --- Event Listeners for Controls ---

    // Previous Month Button
    prevMonthBtn?.addEventListener("click", () => {
      currentMonth--;
      if (currentMonth < 0) { currentMonth = 11; currentYear--; }
      selected = { monthIndex: null, dayIndex: null }; // Clear selection when changing view
      localStorage.removeItem("selectedDay");
      renderCalendar(); renderSidebar();
    });

    // Next Month Button
    nextMonthBtn?.addEventListener("click", () => {
      currentMonth++;
      if (currentMonth > 11) { currentMonth = 0; currentYear++; }
      selected = { monthIndex: null, dayIndex: null }; // Clear selection
      localStorage.removeItem("selectedDay");
      renderCalendar(); renderSidebar();
    });

    // Reset All Data Link
    resetLink?.addEventListener("click", e => {
      e.preventDefault();
      if (confirm("Are you sure? This will erase ALL fasting data, chat history, and settings.")) {
        // Clear data variables
        fastingData = {};
        chatSessions = [{ id: Date.now(), messages: [] }]; // Reset to one empty chat
        currentChatIndex = 0;
        selected = { monthIndex: null, dayIndex: null };
        openaiApiKey = "";
        // Reset system prompt to default
        const dynamicDate = new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric',hour:'numeric',minute:'numeric',hour12:false});
        systemPrompt = `You are a helpful assistant analyzing fasting data. Be concise and encouraging. Today is ${dynamicDate}. The 'fastingHours' field represents the duration of the fast in hours for that specific day (use this for calculations). Summaries for the relevant period are provided first, followed by daily details.`;


        // Clear localStorage
        localStorage.removeItem("fastingData");
        localStorage.removeItem("chatSessions");
        localStorage.removeItem("selectedDay");
        localStorage.removeItem("openaiApiKey");
        localStorage.removeItem("systemPrompt");

        // Re-initialize current view and update UI
        getVisibleMonths().forEach(mo => initMonthData(mo.y + mo.yOff, mo.m)); // Initialize empty months
        saveChats(); // Save the single empty chat session
        renderCalendar(); renderSidebar(); renderChat();
        updateLastKnownStorageState(); // Update state for multi-tab sync
        if(settingsArea && !settingsArea.classList.contains('hidden')) { // Update settings view if open
            apiKeyInput.value = "";
            systemPromptInput.value = systemPrompt; // Show default prompt
        } else if (systemPromptInput) {
            systemPromptInput.value = systemPrompt; // Ensure input reflects reset even if hidden
        }
        alert("Tracker data, chats, and settings have been reset.");
      }
    });

    // Export Data Link
    exportLink?.addEventListener("click", e => {
      e.preventDefault();
      try {
        // Export only fastingData
        exportTextarea.value = JSON.stringify(fastingData, null, 2); // Pretty print JSON
        exportArea.classList.remove("hidden");
        // Hide other panels
        importArea.classList.add("hidden");
        settingsArea.classList.add("hidden");
        exportTextarea.select(); // Select text for easy copying
      } catch (err) {
        console.error("Error generating export data:", err);
        alert("Error generating export data: " + err.message);
      }
    });

    // Import Data Link
    importLink?.addEventListener("click", e => {
      e.preventDefault();
      importArea.classList.remove("hidden");
      // Hide other panels
      exportArea.classList.add("hidden");
      settingsArea.classList.add("hidden");
      importTextarea.focus();
    });

    // Settings Link
    settingsLink?.addEventListener("click", e => {
      e.preventDefault();
      // Populate fields with current settings
      apiKeyInput.value = openaiApiKey;
      systemPromptInput.value = systemPrompt; // Load current prompt into textarea
      settingsArea.classList.remove("hidden");
      // Hide other panels
      exportArea.classList.add("hidden");
      importArea.classList.add("hidden");
    });

    // Import Data Button
    importButton?.addEventListener("click", () => {
      const jsonData = importTextarea.value;
      if (!jsonData) { alert("No data pasted to import."); return; }
      try {
        const importedObj = JSON.parse(jsonData);
        // Basic validation: must be a non-null, non-array object
        if (importedObj && typeof importedObj === 'object' && !Array.isArray(importedObj)) {
          if (confirm("Importing will OVERWRITE your current fasting data. Chat history and settings will NOT be affected. Continue?")) {
            fastingData = importedObj; // Overwrite existing data

            // Re-validate / initialize all months present in the imported data
            Object.keys(fastingData).forEach(key => {
              const parts = key.split('-');
              if (parts.length === 2) {
                const year = parseInt(parts[0]);
                const month = parseInt(parts[1]) - 1; // Month is 0-indexed
                if (!isNaN(year) && !isNaN(month) && month >= 0 && month <= 11) {
                  initMonthData(year, month); // Validate/initialize structure
                } else {
                    console.warn(`Invalid key format found during import: ${key}. Skipping.`);
                    delete fastingData[key]; // Remove invalid keys
                }
              } else {
                 console.warn(`Invalid key format found during import: ${key}. Skipping.`);
                 delete fastingData[key]; // Remove invalid keys
              }
            });

            // Also initialize currently visible months in case they weren't in the import
            getVisibleMonths().forEach(mo => initMonthData(mo.y + mo.yOff, mo.m));

            saveData(); // Save the imported data
            selected = { monthIndex: null, dayIndex: null }; // Reset selection
            localStorage.removeItem("selectedDay");
            renderCalendar(); renderSidebar(); // Update UI
            updateLastKnownStorageState(); // Update sync state
            alert("Import successful! Fasting data has been replaced.");
            importArea.classList.add("hidden"); // Hide import panel
            importTextarea.value = ""; // Clear textarea
          }
        } else {
          alert("Invalid import format. Data must be a JSON object (e.g., {\"YYYY-MM\": [...]}).");
        }
      } catch (err) {
        console.error("Import error:", err);
        alert("JSON parse error during import:\n" + err.message);
      }
    });

    // Save API Key Button
    saveApiKeyBtn?.addEventListener("click", () => {
      openaiApiKey = apiKeyInput.value.trim();
      saveSettings();
      alert("API Key saved!");
      settingsArea.classList.add("hidden"); // Hide settings panel
    });

    // Save System Prompt Button
    saveSystemPromptBtn?.addEventListener("click", () => {
      // Use default if input is empty, otherwise use trimmed input
      const dynamicDate = new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric',hour:'numeric',minute:'numeric',hour12:false});
      const defaultPrompt = `You are a helpful assistant analyzing fasting data. Be concise and encouraging. Today is ${dynamicDate}. The 'fastingHours' field represents the duration of the fast in hours for that specific day (use this for calculations). Summaries for the relevant period are provided first, followed by daily details.`;
      systemPrompt = systemPromptInput.value.trim() || defaultPrompt;
      systemPromptInput.value = systemPrompt; // Update input field in case it was defaulted
      saveSettings();
      alert("System Prompt saved!");
      settingsArea.classList.add("hidden"); // Hide settings panel
    });

    // Chat Input Listener (Send on Enter, newline on Shift+Enter)
    chatInput?.addEventListener("keydown", e => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault(); // Prevent default newline insertion
        const message = chatInput.value.trim();
        if (message) {
          chatInput.value = ""; // Clear input field
          sendMessage(message); // Send message to AI
        }
      }
    });

    // New Chat Button
    newChatButton?.addEventListener("click", () => {
      chatSessions.push({ id: Date.now(), messages: [] }); // Add new empty session
      currentChatIndex = chatSessions.length - 1; // Switch to the new session
      saveChats();
      renderChat(); // Update chat UI
    });

    // Delete Chat Button
    deleteChatButton?.addEventListener("click", () => {
      if (chatSessions.length <= 1) {
        alert("Cannot delete the last chat session.");
        return;
      }
      if (confirm(`Are you sure you want to delete Chat ${currentChatIndex + 1}? This cannot be undone.`)) {
        chatSessions.splice(currentChatIndex, 1); // Remove current session
        // Adjust index if the last session was deleted
        if (currentChatIndex >= chatSessions.length) {
          currentChatIndex = chatSessions.length - 1;
        }
        saveChats();
        renderChat(); // Update chat UI
        updateLastKnownStorageState(); // Update sync state
      }
    });

    // Previous Chat Button
    prevChatBtn?.addEventListener("click", () => {
      if (currentChatIndex > 0) {
        currentChatIndex--;
        renderChat();
      }
    });

    // Next Chat Button
    nextChatBtn?.addEventListener("click", () => {
      if (currentChatIndex < chatSessions.length - 1) {
        currentChatIndex++;
        renderChat();
      }
    });

    /**
     * Sends a user message to the OpenAI API and displays the response.
     * @param {string} message - The user's message text.
     */
    async function sendMessage(message) {
      if (!openaiApiKey) {
        alert("Please set your OpenAI API Key in the Settings panel first.");
        return;
      }

      // Add user message to the current chat session
      chatSessions[currentChatIndex].messages.push({ sender: "user", text: message });
      renderChat(); // Update UI immediately
      saveChats(); // Save the updated chat history

      // Display thinking indicator
      const thinkingDiv = document.createElement("div");
      thinkingDiv.className = "thinking-indicator text-sm text-gray-300 italic";
      thinkingDiv.textContent = "AI is thinking...";
      chatContainer.appendChild(thinkingDiv);
      chatContainer.scrollTop = chatContainer.scrollHeight; // Scroll to show indicator

      // --- Prepare Context for AI ---
      // 1. Get current date/time for the system prompt
      const dynamicDate = new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric',hour:'numeric',minute:'numeric',hour12:false});
      // Retrieve the potentially user-customized prompt, ensure date is current, and add instructions
      const basePromptInstruction = `The 'fastingHours' field represents the duration of the fast in hours for that specific day (use this for calculations). Summaries for the relevant period are provided first, followed by daily details.`;
      const currentSystemPrompt = localStorage.getItem("systemPrompt") || `You are a helpful assistant analyzing fasting data. Be concise and encouraging. Today is ${dynamicDate}.`;
      const finalSystemPrompt = currentSystemPrompt
                                  .replace(/\bToday is .*?\./, `Today is ${dynamicDate}.`) // Update date
                                  + `\n\n${basePromptInstruction}`; // Add instructions

      // 2. Prepare relevant fasting data (last ~6 months, explicit dates) AND calculate summaries
      let dataContext = "";
      let summarySection = "--- DATA SUMMARY (Last ~6 Months) ---\n";
      let totalFastingHours = 0;
      let totalSteps = 0;
      let daysWithDataCount = 0;
      let fastingDaysCount = 0;

      try {
          const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth()-6);
          const relevantDataForAI = {}; // Use an object keyed by YYYY-MM-DD

          // Iterate through saved data, sorted by month key
          Object.keys(fastingData).sort().forEach(key => {
              const [yy, mm] = key.split('-').map(Number);
              const keyDate = new Date(yy, mm - 1, 1); // Date object for the start of the month

              // Check if the month is within the last 6 months
              if (keyDate >= sixMonthsAgo) {
                  const monthData = fastingData[key];
                  if (Array.isArray(monthData)) {
                      monthData.forEach((dayData, dayIndex) => {
                          // Only include days that actually have *some* data entered
                          const hasData = dayData && (
                              dayData.fasting ||
                              (dayData.fastingHours && Number(dayData.fastingHours || 0) > 0) ||
                              (dayData.notes && dayData.notes.length > 0) ||
                              dayData.weight ||
                              dayData.keton ||
                              dayData.steps
                          );

                          if (hasData) {
                              daysWithDataCount++;
                              const day = dayIndex + 1;
                              const fullDateStr = `${yy}-${String(mm).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
                              relevantDataForAI[fullDateStr] = dayData; // Map date string to day's data

                              // Accumulate summaries
                              const hours = Number(dayData.fastingHours || 0);
                              totalFastingHours += hours;
                              if (dayData.fasting || hours > 0) {
                                  fastingDaysCount++;
                              }
                              totalSteps += Number(dayData.steps || 0);
                          }
                      });
                  }
              }
          });

          // Add calculated summaries to the summary section
          summarySection += `Total Days with Data: ${daysWithDataCount}\n`;
          summarySection += `Total Fasting Days: ${fastingDaysCount}\n`;
          summarySection += `Total Fasting Hours: ${totalFastingHours.toFixed(1)}\n`;
          summarySection += `Total Steps: ${totalSteps}\n`;
          summarySection += `--- END SUMMARY ---`;


          // Combine summary and detailed data
          dataContext = `${summarySection}\n\n--- RELEVANT FASTING DATA DETAILS (Last ~6 Months, entries with data only) ---\n${JSON.stringify(relevantDataForAI, null, 2)}\n--- END FASTING DATA DETAILS ---`;

      } catch(err) {
          console.error("Error preparing data context for AI:", err);
          dataContext = "--- Error preparing fasting data for context ---";
      }


      // 3. Construct message history for the API call
      const systemMsg = { role: "system", content: finalSystemPrompt + "\n\n" + dataContext };
      // Include recent messages (max ~10 previous + current user message)
      const recentMessages = chatSessions[currentChatIndex].messages.slice(-11, -1); // Get up to 10 previous messages
      const conversationHistory = [
        systemMsg,
        ...recentMessages.map(m => ({ role: m.sender === "user" ? "user" : "assistant", content: m.text })),
        { role: "user", content: message } // Add the latest user message
      ];

      // --- Call OpenAI API ---
      try {
        const response = await fetch("https://api.openai.com/v1/chat/completions", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + openaiApiKey // Use the stored API key
          },
          body: JSON.stringify({
            model: "gpt-4o-mini", // Specify the model
            messages: conversationHistory,
            temperature: 0.7 // Adjust creativity/randomness
          })
        });

        thinkingDiv.remove(); // Remove thinking indicator

        if (!response.ok) {
          // Handle API errors gracefully
          const errorData = await response.json().catch(() => ({ error: { message: "Failed to parse error response from API." } }));
          throw new Error(`API Error (${response.status}): ${errorData.error?.message || response.statusText}`);
        }

        const data = await response.json();
        const aiText = data.choices?.[0]?.message?.content?.trim();

        // Add AI response to chat history
        chatSessions[currentChatIndex].messages.push({ sender: "ai", text: aiText || "Sorry, I received an empty response from the AI." });
        renderChat(); // Update UI with AI response
        saveChats(); // Save the updated chat history

      } catch (err) {
        console.error("Error calling OpenAI API:", err);
        thinkingDiv.remove(); // Remove thinking indicator on error
        // Add error message to chat history
        chatSessions[currentChatIndex].messages.push({ sender: "ai", text: `Sorry, an error occurred while contacting the AI: ${err.message}` });
        renderChat(); // Update UI with error message
        saveChats(); // Save chat history including the error
      }
    }

  </script>
</body>
</html>

              
            
!

CSS

              
                
              
            
!

JS

              
                
              
            
!
999px

Console