385 lines
13 KiB
TypeScript
385 lines
13 KiB
TypeScript
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.bak2';
|
|
|
|
export function useAuth() {
|
|
const [user, setUser] = useState<UserProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const checkSession = useCallback(async (injectedUser: any = null) => {
|
|
setLoading(true); // On affiche l'écran de chargement
|
|
console.log("[useAuth]checkSession ==> Démarrage de la connexion complète...");
|
|
|
|
try {
|
|
// 1. Vérification de la session Auth
|
|
const sessionUser = injectedUser || await authService.getSession();
|
|
|
|
if (sessionUser) {
|
|
console.log("[useAuth] 1/2 Session validée. Récupération du profil...");
|
|
|
|
// 2. Récupération du profil (BLOQUANT avec await)
|
|
// On attend les 2 secondes ici pour avoir une transition propre
|
|
let profile = await dataService.getProfile(sessionUser.id);
|
|
|
|
// 3. Si le profil n'existe pas, on le crée avant de continuer
|
|
if (!profile) {
|
|
console.log("[useAuth] Profil absent, création en cours...");
|
|
const createResult = await dataService.createItem('profiles', {
|
|
user_id: sessionUser.id,
|
|
email: sessionUser.email,
|
|
full_name: sessionUser.name || 'Nouvel Écrivain'
|
|
// ... autres champs par défaut
|
|
});
|
|
|
|
// On simule l'objet profil pour éviter un second appel API
|
|
profile = { user_id: sessionUser.id, email: sessionUser.email, full_name: sessionUser.name };
|
|
}
|
|
|
|
// 4. On remplit l'état final
|
|
setUser({
|
|
id: sessionUser.id,
|
|
email: sessionUser.email,
|
|
name: profile.full_name || sessionUser.name,
|
|
avatar: profile.avatar_url || `https://i.pravatar.cc/150?u=${sessionUser.email}`,
|
|
subscription: { plan: profile.subscription_plan || '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: 500, language: 'fr' },
|
|
stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 },
|
|
bio: profile.bio || ""
|
|
});
|
|
|
|
console.log("[useAuth] 2/2 Profil récupéré. Accès au Dashboard.");
|
|
|
|
|
|
} else {
|
|
console.log("[useAuth] 2/2 Profil non récupéré. Redirection vers la page de connexion.");
|
|
setUser(null);
|
|
}
|
|
} catch (err) {
|
|
console.error("[useAuth] Erreur lors de la connexion:", err);
|
|
setUser(null);
|
|
} finally {
|
|
// C'est seulement ici que l'écran de chargement disparaît
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { checkSession(); }, [checkSession]);
|
|
|
|
const login = async (data: any) => {
|
|
// UI Feedback immédiat si nécessaire
|
|
setLoading(true);
|
|
|
|
// Appel API
|
|
const result = await authService.signIn(data.email, data.password);
|
|
console.log("[useAuth] Résultat de la connexion:", result);
|
|
if (result && !result.error) {
|
|
// Utilisation directe de la réponse API pour connecter l'utilisateur
|
|
// Cela évite d'attendre le round-trip réseau de getSession
|
|
const userObj = result.user || (result.id ? result : null);
|
|
|
|
if (userObj) {
|
|
console.log("[useAuth] Login réussi, injection immédiate.");
|
|
await checkSession(userObj);
|
|
// Note: checkSession est maintenant optimisé pour ne pas re-bloquer
|
|
} else {
|
|
await checkSession();
|
|
}
|
|
} else {
|
|
// En cas d'erreur, on arrête le chargement pour afficher le message
|
|
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.id ? result : null);
|
|
// Injection immédiate comme pour le login
|
|
await checkSession(userObj);
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const logout = async () => {
|
|
setLoading(true);
|
|
await authService.signOut();
|
|
setUser(null);
|
|
setLoading(false);
|
|
};
|
|
|
|
const incrementUsage = async () => {
|
|
if (user) {
|
|
setUser(prev => prev ? {
|
|
...prev,
|
|
usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 }
|
|
} : null);
|
|
}
|
|
};
|
|
|
|
return { user, login, signup, logout, incrementUsage, loading };
|
|
}
|
|
|
|
export function useProjects(user: UserProfile | null) {
|
|
const [projects, setProjects] = useState<BookProject[]>([]);
|
|
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// DEBUG: Check if user is present
|
|
useEffect(() => {
|
|
console.log("[useProjects DEBUG] Hook initialized/updated with user:", user);
|
|
}, [user]);
|
|
|
|
const fetchProjects = useCallback(async () => {
|
|
if (!user) {
|
|
console.log("[useProjects DEBUG] fetchProjects skipped: No user");
|
|
return;
|
|
}
|
|
console.log("[useProjects DEBUG] fetchProjects starting for user:", user.id);
|
|
setLoading(true);
|
|
try {
|
|
const data = await dataService.getProjects(user.id);
|
|
console.log("[useProjects DEBUG] Projects fetched:", data);
|
|
setProjects(data.map((p: any) => ({
|
|
id: p.id.toString(),
|
|
title: p.title,
|
|
author: p.author,
|
|
lastModified: Date.now(),
|
|
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,
|
|
chapters: [],
|
|
entities: [],
|
|
ideas: [],
|
|
workflow: { nodes: [], connections: [] }
|
|
})));
|
|
} catch (e) {
|
|
console.error("[useProjects] Erreur chargement projets:", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [user]);
|
|
|
|
useEffect(() => { fetchProjects(); }, [fetchProjects]);
|
|
|
|
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(),
|
|
customValues: e.custom_values ? JSON.parse(e.custom_values) : {},
|
|
attributes: e.attributes ? (typeof e.attributes === 'string' ? JSON.parse(e.attributes) : e.attributes) : undefined
|
|
})),
|
|
ideas: ideas.map((i: any) => ({ ...i, id: i.id.toString() })),
|
|
workflow: workflows[0] ? {
|
|
nodes: JSON.parse(workflows[0].nodes || '[]'),
|
|
connections: JSON.parse(workflows[0].connections || '[]')
|
|
} : { nodes: [], connections: [] }
|
|
} : p));
|
|
} catch (e) {
|
|
console.error("[useProjects] Erreur détails projet:", e);
|
|
}
|
|
};
|
|
|
|
const createProject = async () => {
|
|
if (!user) return null;
|
|
const result = await dataService.createProject({
|
|
user_id: user.id,
|
|
title: "Nouveau Roman",
|
|
author: user.name
|
|
});
|
|
if (result.status === 'success') {
|
|
await fetchProjects();
|
|
return result.id.toString();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const addChapter = async () => {
|
|
if (!currentProjectId) return null;
|
|
const projectId = parseInt(currentProjectId);
|
|
const result = await dataService.createItem('chapters', {
|
|
project_id: projectId,
|
|
title: "Nouveau Chapitre",
|
|
content: "<p>Écrivez ici votre prochain chapitre...</p>",
|
|
summary: ""
|
|
});
|
|
if (result.status === 'success' && result.id) {
|
|
await fetchProjectDetails(currentProjectId);
|
|
return result.id.toString();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const createEntity = async (entity: Omit<Entity, 'id'>) => {
|
|
if (!currentProjectId) return null;
|
|
const projectId = parseInt(currentProjectId);
|
|
|
|
const dbPayload = {
|
|
project_id: projectId,
|
|
type: entity.type,
|
|
name: entity.name,
|
|
description: entity.description,
|
|
details: entity.details,
|
|
story_context: entity.storyContext,
|
|
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();
|
|
const newEntity = { ...entity, id: newId };
|
|
|
|
setProjects(prev => prev.map(p => p.id === currentProjectId ? {
|
|
...p,
|
|
entities: [...p.entities, newEntity]
|
|
} : p));
|
|
return newId;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const updateEntity = async (entityId: string, updates: Partial<Entity>) => {
|
|
if (!currentProjectId) return;
|
|
const pId = parseInt(currentProjectId);
|
|
const eId = parseInt(entityId);
|
|
|
|
const dbPayload: any = {};
|
|
if (updates.name !== undefined) dbPayload.name = updates.name;
|
|
if (updates.description !== undefined) dbPayload.description = updates.description;
|
|
if (updates.details !== undefined) dbPayload.details = updates.details;
|
|
if (updates.storyContext !== undefined) dbPayload.story_context = updates.storyContext;
|
|
if (updates.attributes !== undefined) dbPayload.attributes = JSON.stringify(updates.attributes);
|
|
if (updates.customValues !== undefined) dbPayload.custom_values = JSON.stringify(updates.customValues);
|
|
|
|
if (Object.keys(dbPayload).length > 0) {
|
|
await dataService.updateItem('entities', eId, 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;
|
|
const eId = parseInt(entityId);
|
|
|
|
await dataService.deleteItem('entities', eId);
|
|
|
|
setProjects(prev => prev.map(p => p.id === currentProjectId ? {
|
|
...p,
|
|
entities: p.entities.filter(e => e.id !== entityId)
|
|
} : p));
|
|
};
|
|
|
|
const updateProject = async (updates: Partial<BookProject>) => {
|
|
if (!currentProjectId) return;
|
|
const id = parseInt(currentProjectId);
|
|
|
|
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;
|
|
}
|
|
// Note: workflow, ideas, templates are not yet persisted via updateProject fully if complex types
|
|
// Using separate updaters is better.
|
|
|
|
if (Object.keys(ncbData).length > 0) {
|
|
await dataService.updateItem('projects', id, ncbData);
|
|
}
|
|
|
|
setProjects(prev => prev.map(p => p.id === currentProjectId ? { ...p, ...updates } as BookProject : p));
|
|
};
|
|
|
|
const updateChapter = async (chapterId: string, updates: Partial<Chapter>) => {
|
|
if (!currentProjectId) return;
|
|
const pId = parseInt(currentProjectId);
|
|
const cId = parseInt(chapterId);
|
|
|
|
// Call API
|
|
await dataService.updateItem('chapters', cId, updates);
|
|
|
|
// Update Local State
|
|
setProjects(prev => prev.map(p => p.id === currentProjectId ? {
|
|
...p,
|
|
chapters: p.chapters.map(c => c.id === chapterId ? { ...c, ...updates } : c)
|
|
} : p));
|
|
};
|
|
|
|
return {
|
|
projects, currentProjectId, setCurrentProjectId: (id: string | null) => {
|
|
setCurrentProjectId(id);
|
|
if (id) fetchProjectDetails(id);
|
|
},
|
|
createProject, updateProject, updateChapter, addChapter,
|
|
createEntity, updateEntity, deleteEntity,
|
|
loading
|
|
};
|
|
}
|
|
|
|
export function useChat() {
|
|
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
|
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 };
|
|
} |