import { useState, useCallback, useEffect } from 'react'; import { UserProfile, BookProject, Chapter, ChatMessage, PlanType, Entity, Idea, WorkflowData } from './types'; import { INITIAL_CHAPTER } from './constants'; import { generateStoryContent } from './services/geminiService'; import { authService, dataService } from './services/api'; // --- UTILS --- const safeJSON = (data: any, fallback: any = {}) => { if (typeof data === 'object' && data !== null) return data; try { return data ? JSON.parse(data) : fallback; } catch (e) { console.error("JSON Parse Error:", e); return fallback; } }; // --- AUTH HOOK --- export function useAuth() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const checkSession = useCallback(async (injectedUser: any = null) => { setLoading(true); console.log("[useAuth] Starting session check..."); try { // 1. Get Session from Auth API const sessionUser = injectedUser || await authService.getSession(); if (sessionUser) { console.log("[useAuth] Session valid:", sessionUser.id); // 2. Get Profile from Data API let profile = await dataService.getProfile(sessionUser.id); // 3. Create Profile if missing if (!profile) { console.log("[useAuth] Profile missing, creating default..."); const createResult = await dataService.createItem('profiles', { user_id: sessionUser.id, email: sessionUser.email, full_name: sessionUser.name || 'Author', ai_actions_limit: 10, projects_limit: 1, subscription_plan: 'free', theme: 'light' }); // Optimistic profile for immediate UI render profile = { user_id: sessionUser.id, email: sessionUser.email, full_name: sessionUser.name, ai_actions_limit: 10, projects_limit: 1, subscription_plan: 'free' }; } // 4. Map DB Profile to Frontend Type const userProfile: UserProfile = { id: sessionUser.id, email: sessionUser.email, name: profile.full_name || sessionUser.name || 'Writer', avatar: profile.avatar_url || `https://i.pravatar.cc/150?u=${sessionUser.email}`, bio: profile.bio || "", subscription: { plan: (profile.subscription_plan as PlanType) || 'free', startDate: 0, status: 'active' }, usage: { aiActionsCurrent: profile.ai_actions_current || 0, aiActionsLimit: profile.ai_actions_limit || 10, projectsLimit: profile.projects_limit || 1 }, preferences: { theme: profile.theme || 'light', dailyWordGoal: profile.daily_word_goal || 500, language: profile.language || 'fr' }, stats: { totalWordsWritten: profile.total_words_written || 0, writingStreak: profile.writing_streak || 0, lastWriteDate: profile.last_write_date ? new Date(profile.last_write_date).getTime() : 0 } }; setUser(userProfile); console.log("[useAuth] User fully loaded."); } else { console.log("[useAuth] No active session."); setUser(null); } } catch (err) { console.error("[useAuth] Error:", err); setUser(null); } finally { setLoading(false); } }, []); useEffect(() => { checkSession(); }, [checkSession]); const login = async (data: any) => { setLoading(true); const result = await authService.signIn(data.email, data.password); if (result && !result.error) { // Optimistic session check with returned user data const userObj = result.user || (result.id ? result : null); await checkSession(userObj); } else { setLoading(false); } return result; }; const signup = async (data: any) => { setLoading(true); const result = await authService.signUp(data.email, data.password, data.name); if (result && !result.error) { const userObj = result.user || result; await checkSession(userObj); } else { setLoading(false); } return result; }; const logout = async () => { setLoading(true); await authService.signOut(); setUser(null); setLoading(false); }; const incrementUsage = async () => { // In a real app with RLS, this should be done via a secure backend function // or by the AI service itself. // For now we optimistically update UI. if (user) { setUser(prev => prev ? { ...prev, usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 } } : null); } }; return { user, login, signup, logout, incrementUsage, loading }; } // --- PROJECTS HOOK --- export function useProjects(user: UserProfile | null) { const [projects, setProjects] = useState([]); const [currentProjectId, setCurrentProjectId] = useState(null); const [loading, setLoading] = useState(false); // Initial Fetch const fetchProjects = useCallback(async () => { if (!user) return; setLoading(true); try { const dbProjects = await dataService.getProjects(user.id); const mappedProjects = dbProjects.map((p: any) => ({ id: p.id.toString(), title: p.title, author: p.author || user.name, lastModified: Date.now(), // No proper modify date in schema yet settings: { genre: p.genre, subGenre: p.sub_genre, targetAudience: p.target_audience, tone: p.tone, pov: p.pov, tense: p.tense, synopsis: p.synopsis, themes: p.themes }, styleGuide: p.style_guide, // Empty init, full load happens on selection chapters: [], entities: [], ideas: [], workflow: { nodes: [], connections: [] } })); setProjects(mappedProjects); } catch (e) { console.error("[useProjects] Fetch error:", e); } finally { setLoading(false); } }, [user]); useEffect(() => { fetchProjects(); }, [fetchProjects]); // Deep Fetch Triggered on Project Selection const fetchProjectDetails = async (projectId: string) => { const id = parseInt(projectId); try { const [chapters, entities, ideas, workflows] = await Promise.all([ dataService.getRelatedData('chapters', id), dataService.getRelatedData('entities', id), dataService.getRelatedData('ideas', id), dataService.getRelatedData('workflows', id) ]); setProjects(prev => prev.map(p => p.id === projectId ? { ...p, chapters: chapters.map((c: any) => ({ ...c, id: c.id.toString() })), entities: entities.map((e: any) => ({ ...e, id: e.id.toString(), // JSON Columns parsing customValues: safeJSON(e.custom_values), attributes: safeJSON(e.attributes), storyContext: e.story_context })), ideas: ideas.map((i: any) => ({ ...i, id: i.id.toString() })), workflow: workflows[0] ? { nodes: safeJSON(workflows[0].nodes, []), connections: safeJSON(workflows[0].connections, []) } : { nodes: [], connections: [] } } : p)); } catch (e) { console.error("[useProjects] Details error:", e); } }; // --- CRUD OPERATIONS --- const createProject = async () => { if (!user) return null; const result = await dataService.createProject({ user_id: user.id, title: "Nouveau Roman", author: user.name, // Default values genre: "Fiction", pov: "First Person", tense: "Past" }); if (result.status === 'success') { await fetchProjects(); // Refresh list return result.id.toString(); } return null; }; const updateProject = async (updates: Partial) => { if (!currentProjectId) return; const id = parseInt(currentProjectId); // Map Frontend updates to DB Columns const ncbData: any = {}; if (updates.title) ncbData.title = updates.title; if (updates.author) ncbData.author = updates.author; if (updates.settings) { ncbData.genre = updates.settings.genre; ncbData.sub_genre = updates.settings.subGenre; ncbData.synopsis = updates.settings.synopsis; ncbData.tone = updates.settings.tone; ncbData.pov = updates.settings.pov; ncbData.tense = updates.settings.tense; ncbData.target_audience = updates.settings.targetAudience; } if (updates.styleGuide) ncbData.style_guide = updates.styleGuide; if (Object.keys(ncbData).length > 0) { await dataService.updateItem('projects', id, ncbData); } // Optimistic Update setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, ...updates } as BookProject : p)); }; const addChapter = async () => { console.log("[Hooks] addChapter called. Current Project ID:", currentProjectId); if (!currentProjectId) { console.error("[Hooks] addChapter failed: No currentProjectId"); alert("Erreur: Impossible d'ajouter un chapitre car aucun projet n'est actif."); return null; } const projectId = parseInt(currentProjectId); console.log("[Hooks] Creating chapter for project:", projectId); try { const result = await dataService.createItem('chapters', { project_id: projectId, title: "Nouveau Chapitre", content: "

Contenu...

", summary: "" }); console.log("[Hooks] createItem result:", result); if (result.status === 'success') { await fetchProjectDetails(currentProjectId); return result.id.toString(); } else { console.error("[Hooks] createItem failed status:", result); } } catch (e) { console.error("[Hooks] createItem exception:", e); } return null; }; const updateChapter = async (chapterId: string, updates: Partial) => { if (!currentProjectId) return; await dataService.updateItem('chapters', parseInt(chapterId), updates); setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...updates } : c) } : p)); }; const createEntity = async (entity: Omit) => { if (!currentProjectId) return null; const dbPayload = { project_id: parseInt(currentProjectId), type: entity.type, name: entity.name, description: entity.description, details: entity.details, story_context: entity.storyContext, // Handled as strings in JSON columns attributes: JSON.stringify(entity.attributes || {}), custom_values: JSON.stringify(entity.customValues || {}) }; const result = await dataService.createItem('entities', dbPayload); if (result.status === 'success') { const newId = result.id.toString(); // Update local setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, entities: [...p.entities, { ...entity, id: newId }] } : p)); return newId; } return null; }; const updateEntity = async (entityId: string, updates: Partial) => { if (!currentProjectId) return; const dbPayload: any = {}; // Map specific fields if (updates.name) dbPayload.name = updates.name; if (updates.description) dbPayload.description = updates.description; if (updates.details) dbPayload.details = updates.details; if (updates.storyContext) dbPayload.story_context = updates.storyContext; if (updates.attributes) dbPayload.attributes = JSON.stringify(updates.attributes); if (updates.customValues) dbPayload.custom_values = JSON.stringify(updates.customValues); await dataService.updateItem('entities', parseInt(entityId), dbPayload); setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, entities: p.entities.map(e => e.id === entityId ? { ...e, ...updates } : e) } : p)); }; const deleteEntity = async (entityId: string) => { if (!currentProjectId) return; await dataService.deleteItem('entities', parseInt(entityId)); setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, entities: p.entities.filter(e => e.id !== entityId) } : p)); }; return { projects, currentProjectId, setCurrentProjectId: (id: string | null) => { setCurrentProjectId(id); if (id) fetchProjectDetails(id); }, createProject, updateProject, updateChapter, addChapter, createEntity, updateEntity, deleteEntity, loading }; } // --- CHAT HOOK --- export function useChat() { const [chatHistory, setChatHistory] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const sendMessage = async (project: BookProject, chapterId: string, msg: string, user: UserProfile, onUsed: () => void) => { const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: msg }; setChatHistory(prev => [...prev, userMsg]); setIsGenerating(true); try { const response = await generateStoryContent(project, chapterId, msg, user, onUsed); const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'model', text: response.text, responseType: response.type }; setChatHistory(prev => [...prev, aiMsg]); } catch (error) { console.error(error); } finally { setIsGenerating(false); } }; return { chatHistory, isGenerating, sendMessage }; }