import { useState, useEffect, useCallback } from 'react'; import { BookProject, Chapter, Entity, Idea, UserProfile, ChatMessage, EntityType } from './types'; import api from './services/api'; import { generateStoryContent } from './services/geminiService'; import { DEFAULT_BOOK_TITLE, DEFAULT_AUTHOR, INITIAL_CHAPTER } from './constants'; // --- AUTH HOOK --- export const useAuth = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // Check session on mount useEffect(() => { const checkSession = async () => { try { const session = await api.auth.getSession(); if (session && session.user) { // Normalize user data from session setUser({ id: session.user.id, email: session.user.email, name: session.user.name || 'User', subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } }); } } catch (err) { console.error('Session check failed', err); } finally { setLoading(false); } }; checkSession(); }, []); const login = async (email: string, pass: string) => { setLoading(true); try { await api.auth.signIn(email, pass); // Re-fetch session to get full user details const session = await api.auth.getSession(); if (session?.user) { setUser({ id: session.user.id, email: session.user.email, name: session.user.name || 'User', subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } }); } } catch (err) { console.error('Login failed', err); throw err; } finally { setLoading(false); } }; const signup = async (email: string, pass: string, name: string) => { setLoading(true); try { await api.auth.signUp(email, pass, name); // 1. Try to get session immediately (some backends auto-login) let session = await api.auth.getSession(); // 2. If no session, force login if (!session?.user) { await api.auth.signIn(email, pass); session = await api.auth.getSession(); } if (session?.user) { setUser({ id: session.user.id, email: session.user.email, name: session.user.name || name, subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 } }); } } catch (err) { console.error('Signup failed', err); throw err; } finally { setLoading(false); } }; const logout = async () => { try { await api.auth.signOut(); setUser(null); } catch (err) { console.error('Logout failed', err); } }; const incrementUsage = () => { if (user) { // Optimistic update setUser({ ...user, usage: { ...user.usage, aiActionsCurrent: user.usage.aiActionsCurrent + 1 } }); // TODO: Persist usage to backend } }; return { user, login, signup, logout, incrementUsage, loading }; }; // --- PROJECTS HOOK --- export const useProjects = (user: UserProfile | null) => { const [projects, setProjects] = useState([]); const [currentProjectId, setCurrentProjectId] = useState(null); const [loading, setLoading] = useState(false); // Load Projects useEffect(() => { if (!user) { setProjects([]); return; } const loadProjects = async () => { setLoading(true); try { const data = await api.data.list('projects'); // Map DB response to BookProject type const mapped: BookProject[] = data.map(p => ({ id: p.id, title: p.title, author: p.author, lastModified: p._created_at || Date.now(), // Fallback chapters: [], // Loaded on demand usually, but needed for type entities: [], ideas: [], settings: p.settings ? JSON.parse(p.settings) : undefined })); setProjects(mapped); } catch (err) { console.error('Failed to load projects', err); } finally { setLoading(false); } }; loadProjects(); }, [user]); // Load details when project is selected useEffect(() => { if (!currentProjectId) return; const loadProjectDetails = async () => { try { // This fetches everything. In a real app we might optimize. const fullProject = await api.data.getFullProject(currentProjectId); setProjects(prev => prev.map(p => p.id === currentProjectId ? fullProject : p)); } catch (err) { console.error("Failed to load project details", err); } }; loadProjectDetails(); }, [currentProjectId]); const createProject = async () => { if (!user) return; const newProjectData = { title: DEFAULT_BOOK_TITLE, author: user.name || DEFAULT_AUTHOR, settings: JSON.stringify({ genre: 'Fantasy', targetAudience: 'Adult', tone: 'Epic' }) // Defaults }; try { const created = await api.data.create('projects', newProjectData); const newProject: BookProject = { id: created.id.toString(), // Ensure string if needed, DB returns int title: newProjectData.title, author: newProjectData.author, lastModified: Date.now(), chapters: [], entities: [], ideas: [], settings: JSON.parse(newProjectData.settings) }; setProjects(prev => [...prev, newProject]); // Create initial chapter await addChapter(created.id.toString(), INITIAL_CHAPTER); return created.id.toString(); } catch (err) { console.error('Failed to create project', err); } }; const updateProject = async (id: string, data: Partial) => { // Optimistic update setProjects(prev => prev.map(p => p.id === id ? { ...p, ...data } : p)); // DB Update try { const payload: any = {}; if (data.title) payload.title = data.title; if (data.author) payload.author = data.author; if (data.settings) payload.settings = JSON.stringify(data.settings); await api.data.update('projects', id, payload); } catch (err) { console.error("Failed to update project", err); // Revert? } }; const addChapter = async (projectId: string, chapterData: Partial) => { try { const chapterPayload = { project_id: projectId, title: chapterData.title || 'New Chapter', content: chapterData.content || '', summary: chapterData.summary || '', order_index: 0 }; const newChap = await api.data.create('chapters', chapterPayload); setProjects(prev => prev.map(p => { if (p.id !== projectId) return p; return { ...p, chapters: [...p.chapters, { id: newChap.id.toString(), title: chapterPayload.title, content: chapterPayload.content, summary: chapterPayload.summary }] }; })); return newChap.id.toString(); } catch (err) { console.error("Failed to add chapter", err); return null; } }; const updateChapter = async (projectId: string, chapterId: string, data: Partial) => { // Optimistic setProjects(prev => prev.map(p => { if (p.id !== projectId) return p; return { ...p, chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...data } : c) }; })); try { await api.data.update('chapters', chapterId, data); } catch (err) { console.error("Failed to update chapter", err); } }; const createEntity = async (projectId: string, type: EntityType, initialData?: Partial) => { try { const entityPayload = { project_id: projectId, type: type, name: initialData?.name || `Nouveau ${type}`, description: initialData?.description || '', details: initialData?.details || '', attributes: initialData?.attributes ? JSON.stringify(initialData.attributes) : '{}', // Handle customValues if they exist in initialData, though not in original payload }; const newEntity = await api.data.create('entities', entityPayload); setProjects(prev => prev.map(p => { if (p.id !== projectId) return p; return { ...p, entities: [...p.entities, { id: newEntity.id.toString(), type: entityPayload.type, name: entityPayload.name, description: entityPayload.description, details: entityPayload.details, attributes: JSON.parse(entityPayload.attributes), customValues: initialData?.customValues || {} }] }; })); return newEntity.id.toString(); } catch (err) { console.error("Failed to create entity", err); throw err; // Re-throw to let caller know } }; const updateEntity = async (projectId: string, entityId: string, data: Partial) => { setProjects(prev => prev.map(p => { if (p.id !== projectId) return p; return { ...p, entities: p.entities.map(e => e.id === entityId ? { ...e, ...data } : e) }; })); try { const payload: any = { ...data }; if (data.attributes) payload.attributes = JSON.stringify(data.attributes); // Clean up fields that might not match DB columns exactly if needed await api.data.update('entities', entityId, payload); } catch (err) { console.error("Failed to update entity", err); } }; const deleteEntity = async (projectId: string, entityId: string) => { setProjects(prev => prev.map(p => { if (p.id !== projectId) return p; return { ...p, entities: p.entities.filter(e => e.id !== entityId) }; })); try { await api.data.delete('entities', entityId); } catch (err) { console.error("Failed to delete entity", err); } }; return { projects, currentProjectId, setCurrentProjectId, createProject, updateProject, addChapter, updateChapter, createEntity, updateEntity, deleteEntity }; }; // --- CHAT HOOK --- export const useChat = () => { // Mock implementation for now, or connect to an AI service const [chatHistory, setChatHistory] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const sendMessage = async ( project: BookProject, context: string, text: string, user: UserProfile, incrementUsage: () => void ) => { const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: text }; setChatHistory(prev => [...prev, userMsg]); setIsGenerating(true); try { const response = await generateStoryContent( project, // If context is 'global', pass empty string as chapterId, or handle appropriately context === 'global' ? '' : context, text, user, incrementUsage ); const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'model', text: response.text, responseType: response.type }; setChatHistory(prev => [...prev, aiMsg]); } catch (err) { setChatHistory(prev => [...prev, { id: Date.now().toString(), role: 'model', text: "Désolé, une erreur est survenue lors de la génération." }]); } finally { setIsGenerating(false); } }; return { chatHistory, isGenerating, sendMessage }; };