if you dont know what is fasting what are the great health benefits just watch this youtube video and thank me later.
copy paste your openai key to the its place search openai it will popup
change the prompt as you want like give your name age weight..etc search prompt it will popup
Be aware everything saved on browser localstorage. Becase all saved on your browser you can export and import your data to create backups or before clearing your browser cache. As long as you dont clear browsre cache data will stay long time just fine.

See the Pen Untitled by sinanisler (@sinanisler) on CodePen.
<!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: 125px; /* 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">✎</button> <button class="text-red-400 hover:text-red-300 delete-note p-0.5" data-idx="${idx}" title="Delete Note">×</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>