import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { DndContext, closestCenter, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import html2pdf from 'html2pdf.js'; import { createProjectPlan, generateStepContent, boostPrompt, transcribeAudio } from './services/geminiService'; import { Settings, Key, X, Check, Trash2 } from 'lucide-react'; import { Note, Project, NoteType, GenerationStatus, Attachment, Category, StyleMemory } from './types'; import NoteCard from './components/NoteCard'; import FlowchartView from './src/components/FlowchartView'; import { VoiceRecorder } from './components/VoiceRecorder'; import { CalendarView } from './components/CalendarView'; import { LoadingSpinner } from './components/LoadingSpinner'; import { AITemplate } from './types'; import { auth, signInWithGoogle, logOut, db } from './firebase'; import { onAuthStateChanged, User } from 'firebase/auth'; import { doc, setDoc, getDoc, getDocs, onSnapshot, collection, query, where, writeBatch, deleteDoc, deleteField, or } from 'firebase/firestore'; import { sanitizeProject, sanitizeNote, sanitizeCategory } from './utils/sanitize'; const STORAGE_KEY = 'mindspark_data_v1'; interface StoredData { projects: Project[]; notes: Record; categories?: Category[]; } const SortableItem: React.FC<{ id: string, children: (dragHandleProps: any) => React.ReactNode }> = ({ id, children }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 50 : 'auto', opacity: isDragging ? 0.5 : 1, }; return (
{children({ ...attributes, ...listeners })}
); }; interface SortableNoteProps { childId: string; index: number; note: Note; onRetry: (id: string) => void; onUpdate: (id: string, content: string) => void; onDelete: (id: string) => void; onRegenerateImage: (id: string, feedback: string) => void; onAddAttachment: (id: string, attachment: Attachment) => void; onRemoveAttachment: (id: string, attachmentId: string) => void; onAddTag: (id: string, tag: string) => void; onRemoveTag: (id: string, tag: string) => void; getNoteTitle: (id: string) => string; onNavigateToNote: (id: string) => void; } const SortableNote = React.memo(({ childId, index, note, onRetry, onUpdate, onDelete, onRegenerateImage, onAddAttachment, onRemoveAttachment, onAddTag, onRemoveTag, getNoteTitle, onNavigateToNote }: SortableNoteProps) => { return ( {(dragHandleProps) => (
{index + 1}
)}
); }); const CATEGORY_TEMPLATES: Record = { "Trends": [ { icon: "๐Ÿ“", title: "Blog Post Draft", desc: "An SEO-friendly, structured article plan.", prompt: "Create a comprehensive Blog Post Draft. Include SEO-friendly headings, introduction, body, and conclusion sections." }, { icon: "๐Ÿ›’", title: "E-commerce Site Structure", desc: "Core pages and database architecture.", prompt: "Plan a simple E-commerce Website Structure. Which pages should there be, and what should the database structure look like?" }, { icon: "๐Ÿ“ฑ", title: "Mobile App Design", desc: "UI/UX steps and key screens.", prompt: "Plan a comprehensive Mobile App Design. Include user interface (UI), user experience (UX) steps, and key screens." } ], "Finance": [ { icon: "๐Ÿ’ฐ", title: "Personal Budget Plan", desc: "Income-expense tracking and savings strategies.", prompt: "Create a comprehensive personal budget and finance plan. Include income items, expense categories, and monthly savings goals." }, { icon: "๐Ÿ“ˆ", title: "Stock Investment Strategy", desc: "Stock analysis and portfolio diversification.", prompt: "Plan a stock investment strategy for beginners. Include fundamental analysis, technical analysis tools, and risk management steps." }, { icon: "๐Ÿช™", title: "Crypto Asset Guide", desc: "Blockchain basics and wallet security.", prompt: "Prepare a guide for entering the world of crypto assets. Include blockchain logic, secure wallet setup, and basic investment principles." }, { icon: "๐Ÿฆ", title: "Credit Management", desc: "Debt repayment and credit score improvement.", prompt: "Create a debt management and credit score improvement plan. Include restructuring existing debts and score-boosting methods." } ], "Software": [ { icon: "๐Ÿ”Œ", title: "RESTful API Design", desc: "Endpoint structure, documentation, and security.", prompt: "Create a roadmap for a modern RESTful API design. Include endpoint structures, authentication models, and error management steps." }, { icon: "๐Ÿ—„๏ธ", title: "Database Schema", desc: "Relational modeling and query optimization.", prompt: "Design a scalable SQL database schema. Explain table relationships (Join), indexing strategies, and normalization steps." }, { icon: "โ˜๏ธ", title: "Microservices Architecture", desc: "Service communication and deployment strategy.", prompt: "Plan a distributed microservices architecture. Include service discovery, API Gateway usage, and Docker/K8s deployment phases." }, { icon: "๐Ÿ”’", title: "Cyber Security Audit", desc: "Vulnerability analysis and penetration testing steps.", prompt: "Create a cybersecurity checklist for a web application. Add SQL injection, XSS protection, and security certificate check steps." } ], "Business": [ { icon: "๐Ÿ“Š", title: "Market Research", desc: "Competitor analysis and target audience identification.", prompt: "Plan a market research process for a new product. Include SWOT analysis, competitor review, and survey preparation steps." }, { icon: "๐Ÿค", title: "Investor Presentation (Pitch Deck)", desc: "Slide structure and presentation techniques.", prompt: "Plan the content for an impressive investor presentation (Pitch Deck). Detail problem, solution, business model, and financial projection slides." }, { icon: "๐Ÿ“ฃ", title: "Marketing Strategy", desc: "Social media and digital advertising plan.", prompt: "Create a low-budget digital marketing strategy. Include social media content calendar and Google Ads optimization steps." } ], "Creative": [ { icon: "๐Ÿ–‹๏ธ", title: "Screenwriting", desc: "Character arc, dialogues, and scene structure.", prompt: "Create a script outline for a sci-fi short film. Plan the initial conflict, turning points, and character development." }, { icon: "๐ŸŽจ", title: "Worldbuilding", desc: "Mythology, geography, and social structure.", prompt: "Prepare a 'Worldbuilding' guide for a fantasy universe. Construct history, map details, and the magic system." }, { icon: "๐ŸŽง", title: "Podcast Planner", desc: "Episode topics, equipment, and distribution.", prompt: "Plan a 10-episode podcast series. Include sound quality, guest finding process, and Spotify/YouTube distribution steps." } ], "Education": [ { icon: "๐ŸŒ", title: "Foreign Language Program", desc: "6-month conversation-focused study plan.", prompt: "Prepare a 6-month intensive program to learn Spanish. Include daily practice methods and mobile app suggestions." }, { icon: "๐Ÿ“–", title: "Speed Reading Techniques", desc: "Eye exercises and comprehension improvement.", prompt: "Create a 30-day study plan to gain speed reading and comprehension skills. Add eye muscle exercises and focus methods." }, { icon: "๐ŸŽ“", title: "Exam Prep", desc: "Subject distribution and mock test frequency.", prompt: "Make a 3-month marathon plan for exam preparation. Include mock test analysis and filling missing subject gaps." } ], "Lifestyle": [ { icon: "โœˆ๏ธ", title: "Travel Itinerary", desc: "City guide, transportation, and accommodation.", prompt: "Plan a 2-week 'Culture and Technology' itinerary for Japan. Include inter-city transportation, hidden gems, and budget estimation." }, { icon: "๐Ÿก", title: "Minimalist Living", desc: "Decluttering and space organization guide.", prompt: "Create a 30-day minimalist life challenge to completely simplify the home. Include daily categories of items to declutter and organization ideas." }, { icon: "๐ŸŒฑ", title: "Garden Design", desc: "Plant selection and maintenance calendar.", prompt: "Make a plan for growing vegetables on a balcony or in a small garden. Include seasonal planting chart and natural fertilization methods." } ], "Health": [ { icon: "๐Ÿฅ—", title: "Healthy Eating", desc: "Meal prep and calorie balance.", prompt: "Create a first 4-week transition plan for a sustainable healthy eating habit. Include grocery shopping list and sugar detox steps." }, { icon: "๐Ÿ‹๏ธ", title: "Home Workout", desc: "Equipment-free strength and flexibility program.", prompt: "Plan an equipment-free home workout program to be applied 4 days a week. Detail HIIT sessions and cool-down exercises." }, { icon: "๐Ÿง˜", title: "Mental Health (Mindfulness)", desc: "Daily meditation and sleep routine.", prompt: "Prepare a 21-day 'Mindfulness' guide to cope with modern life stress. Include daily practices and steps to improve sleep quality." } ], "Tech Extras": [ { icon: "๐Ÿ ", title: "Smart Home Infrastructure", desc: "Automation scenarios and hardware selection.", prompt: "Plan a comprehensive smart home automation. Include security cameras, lighting scenarios, and central control unit selection." }, { icon: "๐Ÿ›ธ", title: "Future Technologies", desc: "Space mining and quantum computers.", prompt: "Plan a research report analyzing technological trends of the next 20 years. Address artificial general intelligence, Mars colonization, and the energy revolution." }, { icon: "๐Ÿ’ป", title: "Hacker Culture and Honor", desc: "Ethical hacking and the art of clean code.", prompt: "Create a roadmap for entering the ethical hacking world. Include basic Linux knowledge, network security, and principles of being an honorable hacker." } ], "Hobbies": [ { icon: "๐Ÿ“ธ", title: "Photography Workshop", desc: "Composition, light, and editing.", prompt: "Make a 10-step training plan to improve mobile photography skills. Include light usage, framing rules, and Lightroom editing steps." }, { icon: "๐Ÿงต", title: "Handmade Production", desc: "Knitting, ceramics, or leather processing.", prompt: "Create a material list and initial project production plan to start a hobby from scratch (e.g., leather processing)." }, { icon: "๐Ÿณ", title: "Gastronomy Discovery", desc: "Molecular cuisine and mother sauces.", prompt: "Plan a guide to apply professional gastronomy techniques at home. Include making 5 base sauces and the art of presentation." } ] }; const ALL_TEMPLATES = Object.values(CATEGORY_TEMPLATES).flat(); const App: React.FC = () => { const [user, setUser] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [prompt, setPrompt] = useState(''); const [isPlanning, setIsPlanning] = useState(false); const [isBoosting, setIsBoosting] = useState(false); const [templatePage, setTemplatePage] = useState(0); const [activeTemplateCategory, setActiveTemplateCategory] = useState("Trends"); const [autoRotateIndex, setAutoRotateIndex] = useState(0); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [userGeminiKey, setUserGeminiKey] = useState(localStorage.getItem('user_gemini_api_key') || ''); const itemsPerPage = 6; const getVisibleTemplates = () => { if (activeTemplateCategory === "All") return ALL_TEMPLATES; return CATEGORY_TEMPLATES[activeTemplateCategory] || []; }; const visibleTemplates = getVisibleTemplates(); const totalPages = Math.ceil(visibleTemplates.length / itemsPerPage); const currentTemplates = visibleTemplates.slice(templatePage * itemsPerPage, (templatePage + 1) * itemsPerPage); // Auto-rotate logic useEffect(() => { const timer = setInterval(() => { setAutoRotateIndex(prev => (prev + 1) % 4); // Rotate between 0-3 for visual effect }, 8000); return () => clearInterval(timer); }, []); // When category changes, reset page useEffect(() => { setTemplatePage(0); }, [activeTemplateCategory]); const [projects, setProjects] = useState([]); const [notes, setNotes] = useState>({}); const [categories, setCategories] = useState([]); const [currentProjectId, setCurrentProjectId] = useState(null); const [projectToDelete, setProjectToDelete] = useState(null); const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { if (!user) return; const allCollaboratorIds = Array.from(new Set(projects.flatMap(p => p.collaborators || []))); const idsToFetch = allCollaboratorIds.filter(id => !collaboratorProfiles[id]); if (idsToFetch.length > 0) { // Fetch in batches or individually if small idsToFetch.forEach(async (id) => { const snap = await getDoc(doc(db, 'users', id)); if (snap.exists()) { setCollaboratorProfiles(prev => ({ ...prev, [id]: snap.data() })); } }); } }, [projects, user]); const [aiTemplates, setAiTemplates] = useState(() => { const saved = localStorage.getItem('ai_templates'); return saved ? JSON.parse(saved) : []; }); const [isCalendarView, setIsCalendarView] = useState(false); const [projectView, setProjectView] = useState<'content' | 'flowchart'>('content'); const [collaboratorProfiles, setCollaboratorProfiles] = useState>({}); const [showShareModal, setShowShareModal] = useState(false); const [shareEmail, setShareEmail] = useState(''); const [isSharing, setIsSharing] = useState(false); const [isTranscribing, setIsTranscribing] = useState(false); const [showTemplateManager, setShowTemplateManager] = useState(false); const [showOriginalPrompt, setShowOriginalPrompt] = useState(false); useEffect(() => { setShowOriginalPrompt(false); }, [currentProjectId]); const [newTemplate, setNewTemplate] = useState({ name: '', prompt: '', icon: '๐Ÿค–' }); const projectsRef = useRef(projects); const notesRef = useRef(notes); const currentProjectIdRef = useRef(currentProjectId); useEffect(() => { projectsRef.current = projects; notesRef.current = notes; currentProjectIdRef.current = currentProjectId; }, [projects, notes, currentProjectId]); // Auth Listener useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { setUser(currentUser); setIsAuthReady(true); if (currentUser) { // Ensure user document exists try { const userRef = doc(db, 'users', currentUser.uid); const userSnap = await getDoc(userRef); if (!userSnap.exists()) { await setDoc(userRef, { email: currentUser.email || 'no-email@example.com', createdAt: Date.now() }); } // Migrate local data to Firebase const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed: StoredData = JSON.parse(saved); const localProjects = parsed.projects || []; const localNotes = parsed.notes || {}; const localCategories = parsed.categories || []; // Find projects that were created while logged out (they won't have a userId property) const projectsToMigrate = localProjects.filter(p => !(p as any).userId); if (projectsToMigrate.length > 0) { const batch = writeBatch(db); projectsToMigrate.forEach(p => { batch.set(doc(db, 'projects', p.id), sanitizeProject(p, currentUser.uid), { merge: true }); }); Object.values(localNotes).forEach(n => { if (projectsToMigrate.some(p => p.id === n.projectId)) { batch.set(doc(db, 'notes', n.id), sanitizeNote(n, currentUser.uid), { merge: true }); } }); localCategories.forEach(c => { if (!(c as any).userId) { batch.set(doc(db, 'categories', c.id), sanitizeCategory(c, currentUser.uid), { merge: true }); } }); await batch.commit(); console.log("Migrated local data to Firebase for user:", currentUser.uid); } } } catch (error: any) { console.error("Error checking/creating user document or migrating data:", error); setAlertMessage(`An error occurred while setting up account data: ${error.message || error}`); } } else { // Clear local state on logout setProjects([]); setNotes({}); setCategories([]); setCurrentProjectId(null); // Load local data for logged-out state const saved = localStorage.getItem(STORAGE_KEY); if (saved) { try { const parsed: StoredData = JSON.parse(saved); const localProjects = parsed.projects || []; const localNotes = parsed.notes || {}; const localCategories = parsed.categories || []; // Only show projects that were created offline (no userId) const offlineProjects = localProjects.filter(p => !(p as any).userId); setProjects(offlineProjects); const offlineNotes: Record = {}; Object.values(localNotes).forEach(n => { if (offlineProjects.some(p => p.id === n.projectId)) { offlineNotes[n.id] = n; } }); setNotes(offlineNotes); const offlineCategories = localCategories.filter(c => !(c as any).userId); setCategories(offlineCategories); if (offlineProjects.length > 0) { setCurrentProjectId(offlineProjects[0].id); } } catch (e) { console.error("Failed to load local data on logout", e); } } } }); return () => unsubscribe(); }, []); const handleLogin = async () => { try { await signInWithGoogle(); } catch (error: any) { console.error("Login error:", error); setAlertMessage(`An error occurred during login: ${error.message || error}`); } }; const handleLogout = async () => { try { await logOut(); } catch (error) { setAlertMessage("An error occurred during logout."); } }; // Search and Filter State const [searchQuery, setSearchQuery] = useState(''); const [selectedCategoryId, setSelectedCategoryId] = useState(null); const [isCreatingCategory, setIsCreatingCategory] = useState(false); const [newCategoryName, setNewCategoryName] = useState(''); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [commandQuery, setCommandQuery] = useState(''); const [isExportingPDF, setIsExportingPDF] = useState(false); const [isExportingMarkdown, setIsExportingMarkdown] = useState(false); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); setIsCommandPaletteOpen(true); } if (e.key === 'Escape' && isCommandPaletteOpen) { setIsCommandPaletteOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isCommandPaletteOpen]); // Real-time sync from Firebase: Projects and Categories useEffect(() => { if (!user || !isAuthReady) return; // Listen to Projects const projectsQuery = query(collection(db, 'projects'), or(where('userId', '==', user.uid), where('collaborators', 'array-contains', user.uid))); const unsubscribeProjects = onSnapshot(projectsQuery, (snapshot) => { const loadedProjects = snapshot.docs.map(doc => doc.data() as Project); // Sort projects by createdAt descending to maintain order loadedProjects.sort((a, b) => b.createdAt - a.createdAt); setProjects(loadedProjects); // If no project is selected and we have projects, select the first one if (loadedProjects.length > 0 && !currentProjectIdRef.current) { setCurrentProjectId(loadedProjects[0].id); } }, (error) => { console.error("Error listening to projects:", error); }); // Listen to Categories const categoriesQuery = query(collection(db, 'categories'), where('userId', '==', user.uid)); const unsubscribeCategories = onSnapshot(categoriesQuery, (snapshot) => { const loadedCategories = snapshot.docs.map(doc => doc.data() as Category); setCategories(loadedCategories); }, (error) => { console.error("Error listening to categories:", error); }); return () => { unsubscribeProjects(); unsubscribeCategories(); }; }, [user, isAuthReady]); // Real-time sync from Firebase: Notes (depends on projects list for collaboration) useEffect(() => { if (!user || !isAuthReady) return; // Listen to Notes - Note syncing logic updated to allow shared notes // We listen to notes where we are the owner or that belong to one of our projects const projectIds = projects.map(p => p.id); let notesQuery; if (projectIds.length > 0) { // Handle potential query limit (30 items for 'in' operator) const projectIdsChunk = projectIds.slice(0, 30); notesQuery = query( collection(db, 'notes'), or( where('userId', '==', user.uid), where('projectId', 'in', projectIdsChunk) ) ); } else { notesQuery = query(collection(db, 'notes'), where('userId', '==', user.uid)); } const unsubscribeNotes = onSnapshot(notesQuery, (snapshot) => { const loadedNotes: Record = {}; snapshot.docs.forEach(doc => { loadedNotes[doc.id] = doc.data() as Note; }); setNotes(loadedNotes); }, (error) => { console.error("Error listening to notes:", error); }); return () => unsubscribeNotes(); }, [user, isAuthReady, projects]); // Save to LocalStorage on change (debounced) - Only for offline data useEffect(() => { if (user) return; // Don't spam localStorage if logged in (Firebase is primary) const timeoutId = setTimeout(() => { // Filter out online projects to avoid hitting 5MB limit const offlineProjects = projects.filter(p => !(p as any).userId); const offlineNotes: Record = {}; Object.values(notes).forEach(n => { if (offlineProjects.some(p => p.id === n.projectId)) { offlineNotes[n.id] = n; } }); const offlineCategories = categories.filter(c => !(c as any).userId); localStorage.setItem(STORAGE_KEY, JSON.stringify({ projects: offlineProjects, notes: offlineNotes, categories: offlineCategories })); }, 2000); // Increased debounce to 2 seconds return () => clearTimeout(timeoutId); }, [projects, notes, categories, user]); const notesEndRef = useRef(null); // Scroll logic useEffect(() => { if (isPlanning && notesEndRef.current) { notesEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [notes, isPlanning]); const handleBoostPrompt = async () => { if (!prompt.trim() || isBoosting) return; setIsBoosting(true); try { const boostedText = await boostPrompt(prompt); setPrompt(boostedText); } catch (error) { console.error("Prompt boost error:", error); setAlertMessage("An error occurred while improving the text."); } finally { setIsBoosting(false); } }; const handleCreateProject = async () => { if (!prompt.trim()) return; setIsPlanning(true); const projectId = crypto.randomUUID(); const rootNoteId = crypto.randomUUID(); try { // 0. Fetch Memories for context const memories: StyleMemory[] = projects .filter(p => p.summary) .slice(0, 5) .map(p => ({ id: p.id, userId: user?.uid || 'guest', projectName: p.title, styleKeywords: [], // we could extract these but summary is enough for now summary: p.summary!, timestamp: p.createdAt })); // 1. Plan the project structure const plan = await createProjectPlan(prompt, memories); const timestamp = Date.now(); const projectTitle = plan.title.substring(0, 100) || 'New Project'; // 2. Create Root Note (The Cover Page) const rootNote: Note = { id: rootNoteId, projectId, parentId: null, title: projectTitle, content: `**Project Summary:** ${plan.summary}\n\nThis notebook contains all the necessary steps to bring your project to life. You can progress by following the steps below.`.substring(0, 1000000), type: NoteType.ROOT, status: GenerationStatus.COMPLETED, children: [], timestamp, }; const newNotes: Record = { [rootNoteId]: rootNote }; const childIds: string[] = []; // 3. Create Placeholder Child Notes (The Menu) plan.steps.forEach((step) => { const stepId = crypto.randomUUID(); childIds.push(stepId); let nType = NoteType.TEXT; if (step.type === 'code') nType = NoteType.CODE; if (step.type === 'image') nType = NoteType.IMAGE; newNotes[stepId] = { id: stepId, projectId, parentId: rootNoteId, title: step.title.substring(0, 200) || 'New Step', content: step.description.substring(0, 1000000) || 'No description.', // Initially show description as preview type: nType, status: GenerationStatus.IDLE, // Waiting to be generated children: [], timestamp: timestamp + 1, agentRole: step.agentRole, assignedAgent: step.assignedAgent, }; }); newNotes[rootNoteId].children = childIds; setNotes((prev) => ({ ...prev, ...newNotes })); const newProject: Project = { id: projectId, title: projectTitle, summary: plan.summary, originalPrompt: prompt, rootNoteId, createdAt: timestamp, creatorId: user?.uid || 'guest', creatorEmail: user?.email || '', collaborators: [] }; setProjects((prev) => [newProject, ...prev]); setCurrentProjectId(projectId); setPrompt(''); setIsPlanning(false); if (user) { try { const batch = writeBatch(db); batch.set(doc(db, 'projects', projectId), sanitizeProject(newProject, user.uid), { merge: true }); Object.values(newNotes as Record).forEach(note => { batch.set(doc(db, 'notes', note.id), sanitizeNote(note, user.uid), { merge: true }); }); await batch.commit(); } catch (error) { console.error("Error creating project in Firebase:", error); } } // 4. Trigger Content Generation for Children sequentially await generateChildrenContent(plan.title, plan.steps, childIds, newNotes); } catch (error: any) { console.error(error); setAlertMessage(`Plan Error: ${error?.message || JSON.stringify(error)}`); setIsPlanning(false); } }; const generateChildrenContent = async ( projectTitle: string, steps: any[], nodeIds: string[], initialNotes: Record ) => { for (let i = 0; i < steps.length; i++) { const step = steps[i]; const noteId = nodeIds[i]; // Skip if already generated (useful if restarting logic) // Use notesRef to get the most up-to-date state if (notesRef.current[noteId]?.status === GenerationStatus.COMPLETED) continue; const noteType = initialNotes[noteId]?.type || NoteType.TEXT; await regenerateNote(noteId, projectTitle, step.title, step.description, noteType); } }; const regenerateNote = async ( noteId: string, projectTitle: string, stepTitle: string, stepDescription: string, type: NoteType ) => { setNotes((prev) => { if (!prev[noteId]) return prev; return { ...prev, [noteId]: { ...prev[noteId], status: GenerationStatus.GENERATING } }; }); if (user) { const currentNote = notesRef.current[noteId]; if (currentNote) { setDoc(doc(db, 'notes', noteId), sanitizeNote({ ...currentNote, status: GenerationStatus.GENERATING }, user.uid), { merge: true }).catch(console.error); } } const project = projectsRef.current.find(p => p.id === currentProjectIdRef.current); const memories: StyleMemory[] = projects .filter(p => p.summary) .slice(0, 5) .map(p => ({ id: p.id, userId: user?.uid || 'guest', projectName: p.title, styleKeywords: [], summary: p.summary!, timestamp: p.createdAt })); try { const content = await generateStepContent(projectTitle, stepTitle, stepDescription, type, memories); if (user) { const currentNote = notesRef.current[noteId]; if (currentNote) { setDoc(doc(db, 'notes', noteId), sanitizeNote({ ...currentNote, content, status: GenerationStatus.COMPLETED }, user.uid), { merge: true }).catch(console.error); } } setNotes((prev) => { if (!prev[noteId]) return prev; return { ...prev, [noteId]: { ...prev[noteId], content, status: GenerationStatus.COMPLETED } }; }); } catch (error) { if (user) { const currentNote = notesRef.current[noteId]; if (currentNote) { setDoc(doc(db, 'notes', noteId), sanitizeNote({ ...currentNote, status: GenerationStatus.ERROR }, user.uid), { merge: true }).catch(console.error); } } setNotes((prev) => { if (!prev[noteId]) return prev; return { ...prev, [noteId]: { ...prev[noteId], status: GenerationStatus.ERROR } }; }); } } const handleRetryNote = React.useCallback(async (noteId: string) => { const note = notesRef.current[noteId]; if (!note || !currentProjectIdRef.current) return; const project = projectsRef.current.find(p => p.id === currentProjectIdRef.current); if(!project) return; await regenerateNote(noteId, project.title, note.title, note.title, note.type); }, []); const handleExportProject = React.useCallback(async () => { if(!currentProjectId) return; setIsExportingMarkdown(true); try { const project = projects.find(p => p.id === currentProjectId); if(!project) return; const rootNote = notes[project.rootNoteId]; if(!rootNote) return; const category = categories.find(c => c.id === project.categoryId); let markdownContent = `---\ntitle: ${project.title}\ncreated: ${new Date(project.createdAt).toISOString()}\nauthor: ${project.creatorEmail || 'Mind Notebook User'}\ncategory: ${category ? category.name : 'General'}\ntags: [mind-notebook, project${category ? `, ${category.name.toLowerCase()}` : ''}]\n---\n\n# ${project.title}\n\n`; if (project.summary) { markdownContent += `> **Project Summary:** ${project.summary}\n\n`; markdownContent += `--- \n\n`; } const processNote = (noteId: string, level: number): string => { const note = notes[noteId]; if (!note || note.status !== GenerationStatus.COMPLETED) return ""; let content = ""; const prefix = "#".repeat(Math.min(level + 1, 6)); // Max 6 levels for Markdown headers content += `${prefix} ${note.title}\n\n`; if (note.type === NoteType.IMAGE) { content += `![${note.title}](Image data left as link to avoid large file size)\n\n`; } else { content += `${note.content}\n\n`; } if (note.tags && note.tags.length > 0) { content += `**Tags:** ${note.tags.map(t => `\`${t}\``).join(', ')}\n\n`; } if (note.attachments && note.attachments.length > 0) { content += `**Attachments:** ${note.attachments.map(a => `[${a.name}](${a.url.startsWith('data:') ? 'Base64 Data' : a.url})`).join(', ')}\n\n`; } content += `---\n\n`; if (note.children && note.children.length > 0) { note.children.forEach(childId => { content += processNote(childId, level + 1); }); } return content; }; // Start processing from the root note, but we already added the project title as H1 // So we start root note as H2 or just its content if it's the "Cover" markdownContent += processNote(project.rootNoteId, 1); const blob = new Blob([markdownContent], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.title.replace(/\s+/g, '_')}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error("Export failed:", error); setAlertMessage("An error occurred during export."); } finally { setIsExportingMarkdown(false); } }, [currentProjectId, projects, notes]); const handleTranscription = async (base64Audio: string) => { setIsTranscribing(true); try { const text = await transcribeAudio(base64Audio); if (text.trim()) { setPrompt(prev => prev ? prev + " " + text : text); } } catch (error) { console.error("Transcription error:", error); setAlertMessage("An error occurred during transcription."); } finally { setIsTranscribing(false); } }; const saveTemplate = () => { if (!newTemplate.name || !newTemplate.prompt) return; const template: AITemplate = { id: Date.now().toString(), ...newTemplate }; const updated = [...aiTemplates, template]; setAiTemplates(updated); localStorage.setItem('ai_templates', JSON.stringify(updated)); setNewTemplate({ name: '', prompt: '', icon: '๐Ÿค–' }); }; const deleteTemplate = (id: string) => { const updated = aiTemplates.filter(t => t.id !== id); setAiTemplates(updated); localStorage.setItem('ai_templates', JSON.stringify(updated)); }; const handleShareProject = async () => { if (!currentProjectId || !shareEmail.trim()) return; setIsSharing(true); try { const usersRef = collection(db, 'users'); const q = query(usersRef, where('email', '==', shareEmail.trim().toLowerCase())); const snapshot = await getDocs(q); if (snapshot.empty) { setAlertMessage('No user found with this email address.'); setIsSharing(false); return; } const targetUserDoc = snapshot.docs[0]; const targetUserId = targetUserDoc.id; const project = projects.find(p => p.id === currentProjectId); if (!project) return; if (project.creatorId !== user?.uid) { setAlertMessage('Only the project owner can share.'); setIsSharing(false); return; } const updatedCollaborators = Array.from(new Set([...(project.collaborators || []), targetUserId])); await setDoc(doc(db, 'projects', currentProjectId), { collaborators: updatedCollaborators }, { merge: true }); setAlertMessage(`${shareEmail} successfully added to the project!`); setShareEmail(''); setShowShareModal(false); } catch (error) { console.error("Sharing error:", error); setAlertMessage('An error occurred during sharing.'); } finally { setIsSharing(false); } }; const handleExportPDF = async () => { if(!currentProjectId) return; const project = projects.find(p => p.id === currentProjectId); if(!project) return; const element = document.getElementById('project-content'); if (!element) return; setIsExportingPDF(true); element.classList.add('pdf-export-mode'); const style = document.createElement('style'); style.innerHTML = ` .pdf-export-mode .hide-in-pdf { display: none !important; } .pdf-export-mode pre { white-space: pre-wrap !important; word-break: break-word !important; overflow-x: hidden !important; border-radius: 4px !important; } .pdf-export-mode .break-inside-avoid { page-break-inside: avoid !important; break-inside: avoid !important; margin-bottom: 24px !important; } .pdf-export-mode { padding: 20px !important; background-color: #0b1120 !important; } `; document.head.appendChild(style); const opt: any = { margin: 15, filename: `${project.title.replace(/\s+/g, '_')}.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, backgroundColor: '#0b1120', windowWidth: 1024 }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'] } }; try { await html2pdf().set(opt).from(element).save(); } catch (error) { console.error("PDF Export failed:", error); setAlertMessage("An error occurred during PDF export."); } finally { element.classList.remove('pdf-export-mode'); document.head.removeChild(style); setIsExportingPDF(false); } }; const handleExportHTML = React.useCallback(() => { if(!currentProjectId) return; const project = projects.find(p => p.id === currentProjectId); if(!project) return; const element = document.getElementById('project-content'); if (!element) return; // Clone the element to remove hide-in-pdf elements const clone = element.cloneNode(true) as HTMLElement; const hiddenElements = clone.querySelectorAll('.hide-in-pdf'); hiddenElements.forEach(el => el.remove()); // Get all styles let styles = ''; for (let i = 0; i < document.styleSheets.length; i++) { try { const sheet = document.styleSheets[i]; if (sheet.cssRules) { for (let j = 0; j < sheet.cssRules.length; j++) { styles += sheet.cssRules[j].cssText + '\n'; } } } catch (e) { console.warn("Could not read stylesheet", e); } } const htmlContent = ` ${project.title}
${clone.innerHTML}
`; const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.title.replace(/\s+/g, '_')}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, [currentProjectId, projects]); const handleRegenerateImage = React.useCallback(async (noteId: string, feedback: string) => { const note = notesRef.current[noteId]; if (!note || !currentProjectIdRef.current) return; const project = projectsRef.current.find(p => p.id === currentProjectIdRef.current); if(!project) return; setNotes((prev) => ({ ...prev, [noteId]: { ...prev[noteId], status: GenerationStatus.GENERATING } })); try { const prompt = feedback ? `${note.title} (Feedback: ${feedback})` : note.title; const content = await generateStepContent(project.title, note.title, prompt, NoteType.IMAGE); const newNotes = { ...notesRef.current, [noteId]: { ...notesRef.current[noteId], content, status: GenerationStatus.COMPLETED } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } } catch (error) { const newNotes = { ...notesRef.current, [noteId]: { ...notesRef.current[noteId], status: GenerationStatus.ERROR } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } } }, [user]); const handleAddAttachment = React.useCallback((noteId: string, attachment: Attachment) => { const note = notesRef.current[noteId]; if (!note) return; const newNotes = { ...notesRef.current, [noteId]: { ...note, attachments: [...(note.attachments || []), attachment] } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } }, [user]); const handleRemoveAttachment = React.useCallback((noteId: string, attachmentId: string) => { const note = notesRef.current[noteId]; if (!note) return; const newNotes = { ...notesRef.current, [noteId]: { ...note, attachments: (note.attachments || []).filter(a => a.id !== attachmentId) } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } }, [user]); const handleUpdateNote = React.useCallback((noteId: string, updates: Partial | string) => { const currentNote = notesRef.current[noteId]; if (!currentNote) return; const newNotes = { ...notesRef.current }; let newContent = typeof updates === 'string' ? updates : (updates.content ?? currentNote.content); // Parse for [[Note Title]] const linkRegex = /\[\[(.*?)\]\]/g; const matches = [...newContent.matchAll(linkRegex)].map(m => m[1]); const newLinkedIds = new Set(currentNote.linkedNoteIds || []); // Find matching notes in the same project Object.values(newNotes as Record).forEach(n => { if (n.projectId === currentNote.projectId && n.id !== noteId) { if (matches.includes(n.title)) { newLinkedIds.add(n.id); // Add backlink newNotes[n.id] = { ...n, linkedNoteIds: Array.from(new Set([...(n.linkedNoteIds || []), noteId])) }; } } }); const updatedFields = typeof updates === 'string' ? { content: updates } : updates; newNotes[noteId] = { ...currentNote, ...updatedFields, linkedNoteIds: Array.from(newLinkedIds) }; setNotes(newNotes); if (user) { try { const batch = writeBatch(db); batch.set(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }); // Update backlinks in Firebase Object.values(newNotes as Record).forEach(n => { if (n.projectId === currentNote.projectId && n.id !== noteId && matches.includes(n.title)) { batch.set(doc(db, 'notes', n.id), sanitizeNote(newNotes[n.id], user.uid), { merge: true }); } }); batch.commit().catch(console.error); } catch (error) { console.error("Error updating note in Firebase:", error); } } }, [user]); const handleAddTag = React.useCallback((noteId: string, tag: string) => { if (!tag || !tag.trim()) return; const note = notesRef.current[noteId]; if (!note) return; const newNotes = { ...notesRef.current, [noteId]: { ...note, tags: Array.from(new Set([...(note.tags || []), tag.trim().toLowerCase()])) } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } }, [user]); const handleRemoveTag = React.useCallback((noteId: string, tag: string) => { const note = notesRef.current[noteId]; if (!note) return; const newNotes = { ...notesRef.current, [noteId]: { ...note, tags: (note.tags || []).filter(t => t !== tag) } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', noteId), sanitizeNote(newNotes[noteId], user.uid), { merge: true }).catch(console.error); } }, [user]); const handleDeleteNote = React.useCallback((noteId: string) => { const note = notesRef.current[noteId]; if (!note) return; let parentToUpdate: Note | null = null; let linkedNotesToUpdate: Note[] = []; const newNotes = { ...notesRef.current }; if (note.parentId) { const parent = newNotes[note.parentId]; if (parent) { newNotes[note.parentId] = { ...parent, children: parent.children.filter(id => id !== noteId) }; parentToUpdate = newNotes[note.parentId]; } } else { // Fallback for older data without parentId const project = projectsRef.current.find(p => p.id === note.projectId); if (project) { const rootNote = newNotes[project.rootNoteId]; if (rootNote && rootNote.children.includes(noteId)) { newNotes[project.rootNoteId] = { ...rootNote, children: rootNote.children.filter(id => id !== noteId) }; parentToUpdate = newNotes[project.rootNoteId]; } } } // Clean up backlinks to prevent memory leaks and dangling references Object.keys(newNotes).forEach(id => { const n = newNotes[id]; if (n.linkedNoteIds && n.linkedNoteIds.includes(noteId)) { newNotes[id] = { ...n, linkedNoteIds: n.linkedNoteIds.filter(linkedId => linkedId !== noteId) }; linkedNotesToUpdate.push(newNotes[id]); } }); delete newNotes[noteId]; setNotes(newNotes); // Immediate local storage sync for guest users if (!user) { localStorage.setItem(STORAGE_KEY, JSON.stringify({ projects, notes: newNotes, categories })); } if (user) { try { const batch = writeBatch(db); batch.delete(doc(db, 'notes', noteId)); if (parentToUpdate) { batch.set(doc(db, 'notes', parentToUpdate.id), { ...parentToUpdate, userId: user.uid }, { merge: true }); } linkedNotesToUpdate.forEach(n => { batch.set(doc(db, 'notes', n.id), { ...n, userId: user.uid }, { merge: true }); }); batch.commit().catch(console.error); } catch (error) { console.error("Error deleting note from Firebase:", error); } } }, [user, projects, categories]); const handleNavigateToNote = React.useCallback((noteId: string) => { const element = document.getElementById(`note-${noteId}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); element.classList.add('ring-4', 'ring-indigo-500', 'transition-all', 'duration-500'); setTimeout(() => { element.classList.remove('ring-4', 'ring-indigo-500'); }, 2000); } }, []); const getNoteTitle = React.useCallback((noteId: string) => { return notesRef.current[noteId]?.title || ''; }, []); const handleAddNoteToProject = () => { if (!currentProjectId) return; const project = projects.find(p => p.id === currentProjectId); if (!project) return; const rootNote = notesRef.current[project.rootNoteId]; if (!rootNote) return; const newNoteId = crypto.randomUUID(); const newNote: Note = { id: newNoteId, projectId: currentProjectId, parentId: project.rootNoteId, title: 'New Note', content: 'Write your note here...', type: NoteType.TEXT, status: GenerationStatus.COMPLETED, children: [], timestamp: Date.now(), }; const newNotes = { ...notesRef.current, [project.rootNoteId]: { ...notesRef.current[project.rootNoteId], children: [...(notesRef.current[project.rootNoteId].children || []), newNoteId] }, [newNoteId]: newNote }; setNotes(newNotes); if (user) { try { const batch = writeBatch(db); batch.set(doc(db, 'notes', project.rootNoteId), { ...newNotes[project.rootNoteId], userId: user.uid }, { merge: true }); batch.set(doc(db, 'notes', newNoteId), { ...newNotes[newNoteId], userId: user.uid }, { merge: true }); batch.commit().catch(console.error); } catch (error) { console.error("Error adding note to project in Firebase:", error); } } }; const handleGenerateNewStep = async () => { if (!prompt.trim() || !currentProjectId) return; setIsPlanning(true); const project = projects.find(p => p.id === currentProjectId); if (!project) return; const rootNote = notes[project.rootNoteId]; if (!rootNote) return; try { const newNoteId = crypto.randomUUID(); const newNote: Note = { id: newNoteId, projectId: currentProjectId, parentId: project.rootNoteId, title: prompt.substring(0, 200) || 'New Step', content: '', type: NoteType.TEXT, status: GenerationStatus.GENERATING, children: [], timestamp: Date.now(), }; const newNotes = { ...notesRef.current, [project.rootNoteId]: { ...notesRef.current[project.rootNoteId], children: [...(notesRef.current[project.rootNoteId].children || []), newNoteId] }, [newNoteId]: newNote }; setNotes(newNotes); if (user) { try { const batch = writeBatch(db); batch.set(doc(db, 'notes', project.rootNoteId), sanitizeNote(newNotes[project.rootNoteId], user.uid), { merge: true }); batch.set(doc(db, 'notes', newNoteId), sanitizeNote(newNotes[newNoteId], user.uid), { merge: true }); await batch.commit(); } catch (error) { console.error("Error updating notes in Firebase:", error); } } const currentPrompt = prompt; setPrompt(''); setIsPlanning(false); const content = await generateStepContent(project.title, currentPrompt, currentPrompt, NoteType.TEXT); const finalNotes = { ...notesRef.current, [newNoteId]: { ...notesRef.current[newNoteId], content, status: GenerationStatus.COMPLETED } }; setNotes(finalNotes); if (user) { setDoc(doc(db, 'notes', newNoteId), sanitizeNote(finalNotes[newNoteId], user.uid), { merge: true }).catch(console.error); } } catch (error) { console.error(error); setAlertMessage("An error occurred while creating the step."); setIsPlanning(false); } }; const handleDragEnd = React.useCallback((event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id || !currentProjectId) return; const project = projects.find(p => p.id === currentProjectId); if (!project) return; const rootNote = notesRef.current[project.rootNoteId]; if (!rootNote) return; const oldIndex = rootNote.children.indexOf(active.id as string); const newIndex = rootNote.children.indexOf(over.id as string); const newChildrenOrder = arrayMove(rootNote.children, oldIndex, newIndex); const newNotes = { ...notesRef.current, [project.rootNoteId]: { ...notesRef.current[project.rootNoteId], children: newChildrenOrder } }; setNotes(newNotes); if (user) { setDoc(doc(db, 'notes', project.rootNoteId), sanitizeNote(newNotes[project.rootNoteId], user.uid), { merge: true }).catch(console.error); } }, [currentProjectId, projects, user]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleDeleteProject = React.useCallback((e: React.MouseEvent | React.PointerEvent, id: string) => { e.preventDefault(); e.stopPropagation(); setProjectToDelete(id); }, []); const confirmDeleteProject = React.useCallback(async () => { if (!projectToDelete) return; const updatedProjects = projects.filter(p => p.id !== projectToDelete); setProjects(updatedProjects); if(currentProjectId === projectToDelete) setCurrentProjectId(null); // Cleanup notes associated with the deleted project to prevent memory leaks const newNotes = { ...notesRef.current }; Object.keys(newNotes).forEach(noteId => { if (newNotes[noteId].projectId === projectToDelete) { delete newNotes[noteId]; } }); setNotes(newNotes); // Immediate local storage sync for guest users if (!user) { localStorage.setItem(STORAGE_KEY, JSON.stringify({ projects: updatedProjects, notes: newNotes, categories })); } // Delete from Firebase if (user) { try { const batch = writeBatch(db); batch.delete(doc(db, 'projects', projectToDelete)); Object.keys(notesRef.current).forEach(noteId => { if (notesRef.current[noteId].projectId === projectToDelete) { batch.delete(doc(db, 'notes', noteId)); } }); await batch.commit(); } catch (error) { console.error("Error deleting project from Firebase:", error); } } setProjectToDelete(null); }, [projectToDelete, currentProjectId, user, projects, categories]); const cancelDeleteProject = React.useCallback(() => { setProjectToDelete(null); }, []); const [showFeedbackModal, setShowFeedbackModal] = React.useState(false); const [feedbackText, setFeedbackText] = React.useState(''); const handleFeedbackSubmit = () => { if (!feedbackText.trim()) return; // Save to local storage const existingFeedback = JSON.parse(localStorage.getItem('mindspark_feedback') || '[]'); const newFeedback = { id: crypto.randomUUID(), text: feedbackText, date: new Date().toISOString() }; localStorage.setItem('mindspark_feedback', JSON.stringify([...existingFeedback, newFeedback])); console.log('Feedback submitted:', newFeedback); setFeedbackText(''); setShowFeedbackModal(false); setAlertMessage('Your feedback has been saved successfully. Thank you!'); }; const renderSearchResults = () => { const query = searchQuery.toLowerCase(); const matchingNotes = Object.values(notes as Record).filter(note => note.title.toLowerCase().includes(query) || note.content.toLowerCase().includes(query) || (note.tags && note.tags.some(tag => tag.toLowerCase().includes(query))) ); return (

Search Results: "{searchQuery}"

{matchingNotes.length === 0 ? (

No matching notes found.

) : (
{matchingNotes.map(note => { const project = projects.find(p => p.id === note.projectId); return (
{project?.title || 'Unknown Notebook'} {note.type === NoteType.ROOT ? '(Main Note)' : ''}
); })}
)}
); }; const renderProjectNotes = () => { if (!currentProjectId) return null; const project = projects.find(p => p.id === currentProjectId); if (!project) return null; const rootNote = notes[project.rootNoteId]; if (!rootNote) return null; return (
{/* Project Header & Controls */}
{/* View Tabs */}
{/* Project Category Selector */} {/* Collaboration Avatars */}
{user?.email?.[0].toUpperCase() || 'U'}
{project.collaborators?.map(cId => { const profile = collaboratorProfiles[cId]; return (
{profile?.photoURL ? ( ) : ( (profile?.email?.[0] || '?').toUpperCase() )}
); })}
{projectView === 'flowchart' ? ( ) : ( <> {/* Root Note (Cover) */}
{project.originalPrompt && (
{showOriginalPrompt && (

"{project.originalPrompt}"

)}
)}
{/* Children Notes (Steps) */}
{rootNote.children.map((childId, index) => { const note = notes[childId]; if(!note) return null; return ( ) })}
)}
); }; return (
{/* API Key Gateway Modal */} {!userGeminiKey && (

Welcome to MindSpark

To use this AI Workspace, you need your own Google Gemini API Key. Your key is stored securely in your browser's local storage and is never sent to our servers.

Get your free API key here →
{ if (e.key === 'Enter') { const val = e.currentTarget.value.trim(); if (val) { localStorage.setItem('user_gemini_api_key', val); setUserGeminiKey(val); window.location.reload(); } } }} />
)} {/* Mobile Sidebar Overlay */} {isMobileMenuOpen && (
setIsMobileMenuOpen(false)} /> )} {/* Sidebar */}
{ setCurrentProjectId(null); setPrompt(''); setSearchQuery(''); setIsMobileMenuOpen(false); }}>

MindSpark

AI Workspace

{user ? ( ) : ( )}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full bg-[#161B26] text-slate-200 text-xs pl-11 pr-4 py-3.5 rounded-2xl border border-white/5 focus:outline-none focus:border-indigo-500/30 transition-all font-medium" />
โŒ˜ K
{/* Categories */}

Categories

{isCreatingCategory && (

New Category

setNewCategoryName(e.target.value)} placeholder="Collection name..." className="w-full bg-[#0B0F19] text-slate-200 text-xs p-3 rounded-xl border border-white/10 focus:border-indigo-500/50 focus:outline-none transition-all" onKeyDown={async (e) => { if (e.key === 'Enter' && newCategoryName.trim()) { const newCategory = { id: crypto.randomUUID(), name: newCategoryName.trim(), color: (document.getElementById('cat-color') as HTMLInputElement)?.value || '#6366f1' }; setCategories(prev => [...prev, newCategory]); if (user) { try { await setDoc(doc(db, 'categories', newCategory.id), sanitizeCategory(newCategory, user.uid), { merge: true }); } catch (error) { console.error("Error creating category in Firebase:", error); } } setNewCategoryName(''); setIsCreatingCategory(false); } else if (e.key === 'Escape') { setIsCreatingCategory(false); setNewCategoryName(''); } }} autoFocus />
)}
{categories.map(cat => (
))}
{/* Notebooks */}

Notebooks

{projects.length}
{projects.length === 0 ? (

No projects yet

) : ( projects .filter(p => !selectedCategoryId || p.categoryId === selectedCategoryId) .filter(p => { const titleMatch = p.title.toLowerCase().includes(searchQuery.toLowerCase()); const dateMatch = new Date(p.createdAt).toLocaleDateString('en-US', { day: 'numeric', month: 'short' }).toUpperCase().includes(searchQuery.toUpperCase()); return titleMatch || dateMatch; }) .map(p => (
)) )}
System Online
{/* Main Content */}
{/* Mobile Header */}

MindSpark

{projects.length > 0 && ( )}
{/* Scrollable Area */}
{/* Subtle background gradient */}
{searchQuery ? ( renderSearchResults() ) : !currentProjectId && !isPlanning ? ( <>
โœจ

Free Your
Mind

Take the first step to turn your ideas into reality. Let AI plan every detail for you.

{[ { title: 'Game Scenario', icon: '๐ŸŽฎ', prompt: 'Create original story and character outlines for an RPG game.' }, { title: 'Business Plan', icon: '๐Ÿš€', prompt: 'Prepare a 12-month business plan for a sustainable fashion startup.' }, { title: 'System Architecture', icon: '๐Ÿ’ป', prompt: 'Design the system architecture and database schema for a real-time chat app.' } ].map((item, idx) => ( ))}
{["HEALTH", "TECHNOLOGY", "HOBBIES"].map(catName => { const cat = categories.find(c => c.name.toUpperCase() === catName) || { id: catName, name: catName }; const isSelected = activeTemplateCategory === cat.name; return ( ); })}

DISCOVER TRENDS

{currentTemplates.map((template, idx) => ( setPrompt(template.prompt)} className={`group p-8 rounded-[2rem] transition-all duration-500 text-left relative overflow-hidden h-[300px] flex flex-col ${idx === 1 ? 'bg-indigo-500/10 border-2 border-indigo-500/30' : 'bg-white/[0.02] border border-white/5 hover:bg-white/[0.04]'}`} >
{template.icon === '๐Ÿ“' && ( )} {template.icon === '๐Ÿ›’' && ( )} {template.icon === '๐Ÿ“ฑ' && ( )} {template.icon !== '๐Ÿ“' && template.icon !== '๐Ÿ›’' && template.icon !== '๐Ÿ“ฑ' && template.icon}

{template.title}

{template.desc}

))}
) : isCalendarView ? (
{ setSearchQuery(dateStr); setIsMobileMenuOpen(true); // Open sidebar on mobile to see filtered results }} />
) : ( renderProjectNotes() )} {isPlanning && (
Experts are assigning tasks...
)}
{/* Input Area */}