Created
June 19, 2025 15:24
-
-
Save JacquesGariepy/4c56f3980918101c7ed218b6eb523950 to your computer and use it in GitHub Desktop.
AI Checklist Generator with Gemini
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useEffect } from 'react'; | |
| // Error Boundary Component to catch runtime errors in children | |
| class ErrorBoundary extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { hasError: false, error: null, errorInfo: null }; | |
| } | |
| static getDerivedStateFromError(error) { | |
| // Update state so the next render will show the fallback UI. | |
| return { hasError: true }; | |
| } | |
| componentDidCatch(error, errorInfo) { | |
| // You can also log the error to an error reporting service | |
| this.setState({ | |
| error: error, | |
| errorInfo: errorInfo, | |
| }); | |
| console.error("ErrorBoundary caught an error:", error, errorInfo); | |
| } | |
| render() { | |
| if (this.state.hasError) { | |
| // You can render any custom fallback UI | |
| return ( | |
| <div className="p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg"> | |
| <h2 className="font-bold text-lg mb-2">Something went wrong.</h2> | |
| <details className="whitespace-pre-wrap"> | |
| {this.state.error && this.state.error.toString()} | |
| <br /> | |
| {this.state.errorInfo && this.state.errorInfo.componentStack} | |
| </details> | |
| </div> | |
| ); | |
| } | |
| return this.props.children; | |
| } | |
| } | |
| // Checklist Component to render the list of items | |
| function Checklist({ items }) { | |
| // **THE FIX IS HERE (PART 1)** | |
| // Defensive check: Ensure `items` is an array before calling .map(). | |
| // If it's not an array, it will render a message instead of crashing. | |
| if (!Array.isArray(items)) { | |
| // If items is a string (e.g., a status message), display it. | |
| if (typeof items === 'string') { | |
| return <p className="text-gray-500 text-center p-8">{items}</p>; | |
| } | |
| // Fallback for any other non-array type | |
| return <p className="text-gray-500 text-center p-8">Enter a topic and click "Generate" to create a checklist.</p>; | |
| } | |
| if (items.length === 0) { | |
| return <p className="text-gray-500 text-center p-8">Your checklist will appear here.</p>; | |
| } | |
| return ( | |
| <ul className="list-none space-y-3"> | |
| {items.map((item, index) => ( | |
| <li key={index} className="bg-gray-50 p-4 rounded-lg flex items-center shadow-sm hover:shadow-md transition-shadow duration-200"> | |
| <input | |
| type="checkbox" | |
| id={`item-${index}`} | |
| className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded cursor-pointer shrink-0" | |
| /> | |
| <label htmlFor={`item-${index}`} className="ml-3 block text-gray-900 select-none cursor-pointer"> | |
| {item} | |
| </label> | |
| </li> | |
| ))} | |
| </ul> | |
| ); | |
| } | |
| // Main App Component | |
| function App() { | |
| const [topic, setTopic] = useState('a productive morning routine'); | |
| // Initialize state with a non-array value to show the fix works on initial render. | |
| const [generatedItems, setGeneratedItems] = useState("Enter a topic and click \"Generate\" to start."); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState(null); | |
| const handleGenerate = async () => { | |
| setIsLoading(true); | |
| setError(null); | |
| setGeneratedItems("Generating your checklist... ✨"); // Show a loading message (string) | |
| const prompt = `Create a checklist of actionable tasks for the following topic: "${topic}". The tasks should be clear and concise.`; | |
| const schema = { | |
| type: "OBJECT", | |
| properties: { | |
| "tasks": { | |
| "type": "ARRAY", | |
| "items": { "type": "STRING" } | |
| } | |
| }, | |
| required: ["tasks"] | |
| }; | |
| const payload = { | |
| contents: [{ role: "user", parts: [{ text: prompt }] }], | |
| generationConfig: { | |
| responseMimeType: "application/json", | |
| responseSchema: schema | |
| } | |
| }; | |
| const apiKey = ""; // API key is not needed for gemini-2.0-flash | |
| const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; | |
| try { | |
| const response = await fetch(apiUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) { | |
| const errorBody = await response.text(); | |
| throw new Error(`API request failed with status ${response.status}: ${errorBody}`); | |
| } | |
| const result = await response.json(); | |
| if (result.candidates && result.candidates.length > 0 && | |
| result.candidates[0].content && result.candidates[0].content.parts && | |
| result.candidates[0].content.parts.length > 0) { | |
| const jsonText = result.candidates[0].content.parts[0].text; | |
| // **THE FIX IS HERE (PART 2)** | |
| // The API returns a JSON string. We must parse it to get the object, | |
| // then access the 'tasks' array. | |
| try { | |
| const parsedData = JSON.parse(jsonText); | |
| if (parsedData && Array.isArray(parsedData.tasks)) { | |
| setGeneratedItems(parsedData.tasks); | |
| } else { | |
| throw new Error("Invalid data structure in API response. Expected a 'tasks' array."); | |
| } | |
| } catch (e) { | |
| console.error("Error parsing JSON response:", e, "Raw text:", jsonText); | |
| setError("Failed to parse the checklist from the response. Please try again."); | |
| setGeneratedItems([]); // Reset to an empty array on parsing error | |
| } | |
| } else { | |
| throw new Error("No content received from API. The response may be empty or blocked."); | |
| } | |
| } catch (err) { | |
| console.error("Error generating checklist:", err); | |
| setError(err.message || "An unknown error occurred. Please check the console for details."); | |
| setGeneratedItems([]); // Reset to an empty array on fetch error | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| return ( | |
| <ErrorBoundary> | |
| <div className="bg-gray-100 min-h-screen font-sans flex items-center justify-center p-4"> | |
| <div className="w-full h-full flex justify-center items-center"> | |
| <main className="bg-white rounded-2xl shadow-xl p-6 md:p-10 w-full max-w-2xl"> | |
| <div className="text-center mb-8"> | |
| <h1 className="text-3xl md:text-4xl font-bold text-gray-800">AI Checklist Generator</h1> | |
| <p className="text-gray-500 mt-2">Let AI help you break down any task into a simple checklist.</p> | |
| </div> | |
| <div className="flex flex-col sm:flex-row gap-3 mb-6"> | |
| <input | |
| type="text" | |
| value={topic} | |
| onChange={(e) => setTopic(e.target.value)} | |
| placeholder="e.g., planning a vacation" | |
| className="flex-grow p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none transition-shadow" | |
| disabled={isLoading} | |
| /> | |
| <button | |
| onClick={handleGenerate} | |
| disabled={isLoading || !topic} | |
| className="bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed transition-colors shadow-md hover:shadow-lg" | |
| > | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center"> | |
| <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| Generating... | |
| </div> | |
| ) : 'Generate Checklist'} | |
| </button> | |
| </div> | |
| {error && ( | |
| <div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded-r-lg" role="alert"> | |
| <p className="font-bold">Error</p> | |
| <p>{error}</p> | |
| </div> | |
| )} | |
| <div className="bg-white rounded-lg p-6 min-h-[200px] border border-gray-200"> | |
| <Checklist items={generatedItems} /> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| </ErrorBoundary> | |
| ); | |
| } | |
| export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment