Skip to content

Instantly share code, notes, and snippets.

@JacquesGariepy
Created June 19, 2025 15:24
Show Gist options
  • Select an option

  • Save JacquesGariepy/4c56f3980918101c7ed218b6eb523950 to your computer and use it in GitHub Desktop.

Select an option

Save JacquesGariepy/4c56f3980918101c7ed218b6eb523950 to your computer and use it in GitHub Desktop.
AI Checklist Generator with Gemini
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