diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache index 29b41e5..def137c 100644 --- a/.firebase/hosting.YnVpbGQ.cache +++ b/.firebase/hosting.YnVpbGQ.cache @@ -1,14 +1,14 @@ -robots.txt,1752820012049,bfe106a3fb878dc83461c86818bf74fc1bdc7f28538ba613cd3e775516ce8b49 -prodhub.svg,1754019485965,c2c8cf818dc05dc535464f2334c4c6049749c2a541c7b8d4ff246e334c89c729 -manifest.json,1752820011412,ee04fb47e525c67d8424ffe9b4d8a8a24e434504478afca4e0ca602146836d4c -logo512.png,1752820011921,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d -logo192.png,1752820011825,79e2b749561016bc8af300ea19f48347ceed3cb1a54f48ae456172eca45e08f0 -favicorn.svg,1754024730579,e80dddc27b301efdf9e44c2df1952423ea4b443d9ef1fe22f4bbc3797a0f7a73 -favicon.ico,1752820010545,27edce7be5922cf0bef7d4136f69b5bfbdd5bf8c13c7b026f71187d41a00aa7d -index.html,1754028003818,57e6789a3d329598a44645286795982fa8b73e8015c81a7fdc29bbf1aa8689a8 -asset-manifest.json,1754028003818,a08001f483f26f26d0956c310d6bf864cf2d16aff7f6e6b724572a794e98401e -static/js/main.704858e1.js.LICENSE.txt,1754028003834,016d7efb946774a48216c296cff20c2b87a782bac74ad8d5a1808a48879486df -static/css/main.6ba6e54a.css.map,1754028003834,a7381dc3fa64bc0f389db7aab40bc58e27d99abb5081ac9475c6e0333743e40e -static/css/main.6ba6e54a.css,1754028003834,e623b62e114b105d31402c3a35740ed4c6105f633ac219018b4f35214af3448f -static/js/main.704858e1.js,1754028003834,557517fda1c252521658c8a60cc03d4b8320e37752b174c5fb520b1da3e29c28 -static/js/main.704858e1.js.map,1754028003834,defd1375fe3794f16bd6317f41672b1950d32b233ae79d51aeaa93863c1d816c +favicorn.svg,1755761307597,e80dddc27b301efdf9e44c2df1952423ea4b443d9ef1fe22f4bbc3797a0f7a73 +asset-manifest.json,1769145784811,7003c3de58911ea083c6b4c996323b85197ee04ff75892bc1d4c0f641015a522 +robots.txt,1755761307597,b2090cf9761ef60aa06e4fab97679bd43dfa5e5df073701ead5879d7c68f1ec5 +manifest.json,1755761307597,4368aeaf848ae2e048765562c289452f33ad2a175c4b1951ea8bdf2ada0d5b10 +static/js/main.ed1316f0.js.LICENSE.txt,1769145784829,016d7efb946774a48216c296cff20c2b87a782bac74ad8d5a1808a48879486df +index.html,1769145784811,8bc49c33b64813d87cf40385c7a8716995325053ca57701670095b2d5687e12b +static/css/main.4409151e.css.map,1769145784827,7dd7f69674e3cf47346aebcac1862d1851d6f1e9fb45159a938e2918f8ee8cfc +logo192.png,1755761307597,79e2b749561016bc8af300ea19f48347ceed3cb1a54f48ae456172eca45e08f0 +static/css/main.4409151e.css,1769145784827,5d4ab4f8c7df3fafa8c4bcbf5364d12f6628b371e2cccd12c5d5e03252317ccc +favicon.ico,1755761307597,27edce7be5922cf0bef7d4136f69b5bfbdd5bf8c13c7b026f71187d41a00aa7d +logo512.png,1755761307597,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d +prodhub.svg,1755761307597,c9ccf834f48ca21eaeda0f5566073e9f6757b1e14aaf82591d5afd1486ac39ba +static/js/main.ed1316f0.js,1769145784829,9b44dbd94b84b0732dbff34210e2d2e9a9ab671ba9f29182eed7949187977881 +static/js/main.ed1316f0.js.map,1769145784830,438f6987590f4ffea01fb79415dd8fe340f043f1ae9aa7cbc0c8ac4975d8779c diff --git a/.firebaserc b/.firebaserc index a59a7dc..3fe2292 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,5 +1,8 @@ { "projects": { - "default": "my-productivity-hub-5a3ba" - } -} + "default": "my-productivity-hub-5a3ba", + "staging": "my-productivity-hub-5a3ba" + }, + "targets": {}, + "etags": {} +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 98c1b0c..164e859 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,7 @@ import { initializeApp } from 'firebase/app'; import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut, createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'; import { getFirestore, collection, doc, addDoc, updateDoc, deleteDoc, onSnapshot, query, where, getDocs, writeBatch } from 'firebase/firestore'; import { Book, Calendar, CheckSquare, ChevronDown, ChevronRight, Clock, Edit2, Flame, Info, LogOut, Plus, Repeat, Save, Sparkles, Tag, Trash2, TrendingUp, X } from 'lucide-react'; +import { callGeminiWithRetry } from './services/geminiService'; // --- Firebase Configuration --- const firebaseConfig = { @@ -478,10 +479,7 @@ function Dashboard({ projects, tasks, goals, onViewChange, syncedEvents }) { const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }] }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed: ${response.statusText}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { setDailyPlan(result.candidates[0].content.parts[0].text); @@ -677,10 +675,7 @@ function ProjectDetail({ project, allTasks, syncedEvents }) { const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { tasks: { type: "ARRAY", items: { type: "OBJECT", properties: { title: { type: "STRING" }, priority: { type: "STRING", enum: ["High", "Medium", "Low"] } }, required: ["title", "priority"] } } }, required: ["tasks"] } } }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed with status ${response.status}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { const generated = JSON.parse(result.candidates[0].content.parts[0].text); if (generated.tasks && generated.tasks.length > 0) { @@ -1389,10 +1384,7 @@ function GoalPlannerModal({ isOpen, onClose, userId }) { }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed: ${response.statusText}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { const plan = JSON.parse(result.candidates[0].content.parts[0].text); diff --git a/src/App2.js b/src/App2.js index 4af3b89..8532da4 100644 --- a/src/App2.js +++ b/src/App2.js @@ -3,6 +3,7 @@ import { initializeApp } from 'firebase/app'; import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut, createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'; import { getFirestore, collection, doc, addDoc, updateDoc, deleteDoc, onSnapshot, query, where, getDocs, writeBatch } from 'firebase/firestore'; import { Book, Calendar, CheckSquare, ChevronDown, ChevronRight, Clock, Edit2, Flame, Info, LogOut, Plus, Repeat, Save, Sparkles, Tag, Trash2, TrendingUp, X } from 'lucide-react'; +import { callGeminiWithRetry } from './services/geminiService'; // --- Firebase Configuration --- // It's recommended to store these in environment variables @@ -468,10 +469,7 @@ function Dashboard({ projects, tasks, onViewChange, syncedEvents }) { const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }] }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed: ${response.statusText}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { setDailyPlan(result.candidates[0].content.parts[0].text); @@ -622,10 +620,7 @@ function ProjectDetail({ project, allTasks, syncedEvents }) { const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { tasks: { type: "ARRAY", items: { type: "OBJECT", properties: { title: { type: "STRING" }, priority: { type: "STRING", enum: ["High", "Medium", "Low"] } }, required: ["title", "priority"] } } }, required: ["tasks"] } } }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed with status ${response.status}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { const generated = JSON.parse(result.candidates[0].content.parts[0].text); if (generated.tasks && generated.tasks.length > 0) { @@ -1334,10 +1329,7 @@ function GoalPlannerModal({ isOpen, onClose, userId }) { }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed: ${response.statusText}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { const plan = JSON.parse(result.candidates[0].content.parts[0].text); diff --git a/src/App_v1.js b/src/App_v1.js index accd062..e82d4fb 100644 --- a/src/App_v1.js +++ b/src/App_v1.js @@ -3,6 +3,7 @@ import { initializeApp } from 'firebase/app'; import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut, createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'; import { getFirestore, collection, doc, addDoc, updateDoc, deleteDoc, onSnapshot, query, where, getDocs, writeBatch } from 'firebase/firestore'; import { Book, Calendar, CheckSquare, Clock, Edit2, Flame, Info, LogOut, Plus, Repeat, Save, Sparkles, Tag, Trash2, TrendingUp, X } from 'lucide-react'; +import { callGeminiWithRetry } from './services/geminiService'; // --- Firebase Configuration --- const firebaseConfig = { @@ -438,10 +439,7 @@ function ProjectDetail({ project, allTasks, syncedEvents }) { const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { tasks: { type: "ARRAY", items: { type: "OBJECT", properties: { title: { type: "STRING" }, priority: { type: "STRING", enum: ["High", "Medium", "Low"] } }, required: ["title", "priority"] } } }, required: ["tasks"] } } }; try { - const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; - const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API request failed with status ${response.status}`); - const result = await response.json(); + const result = await callGeminiWithRetry(payload, apiKey); if (result.candidates && result.candidates[0].content.parts[0].text) { const generated = JSON.parse(result.candidates[0].content.parts[0].text); if (generated.tasks && generated.tasks.length > 0) { diff --git a/src/services/geminiService.js b/src/services/geminiService.js new file mode 100644 index 0000000..f639712 --- /dev/null +++ b/src/services/geminiService.js @@ -0,0 +1,46 @@ +const GEMINI_MODEL = 'gemini-2.5-flash'; + +export const callGeminiWithRetry = async (payload, apiKey, maxRetries = 3) => { + let retries = 0; + while (retries < maxRetries) { + try { + const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${apiKey}`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + return await response.json(); + } + + if (response.status === 429) { + retries++; + if (retries >= maxRetries) { + throw new Error("Rate limit exceeded. Please try again later."); + } + const waitTime = Math.pow(2, retries) * 1000 + Math.random() * 1000; + console.warn(`Gemini API 429: Retrying in ${Math.round(waitTime)}ms...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; + } + + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error?.message || `API request failed with status ${response.status}`); + } catch (error) { + if (error.message.includes("Rate limit exceeded") || (retries < maxRetries && error.name === 'TypeError')) { + // If it's a network error (TypeError) or we already handled 429, we might want to try again unless it's the last retry + if (error.message.includes("Rate limit exceeded")) throw error; + + retries++; + const waitTime = Math.pow(2, retries) * 1000; + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; + } + throw error; + } + } +};