415 lines
14 KiB
TypeScript
415 lines
14 KiB
TypeScript
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<UserProfile | null>(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<BookProject[]>([]);
|
|
const [currentProjectId, setCurrentProjectId] = useState<string | null>(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<any>('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<any>('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<BookProject>) => {
|
|
// 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<Chapter>) => {
|
|
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<any>('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
|
|
}]
|
|
};
|
|
}));
|
|
} catch (err) {
|
|
console.error("Failed to add chapter", err);
|
|
}
|
|
};
|
|
|
|
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
|
|
// 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) => {
|
|
try {
|
|
const entityPayload = {
|
|
project_id: projectId,
|
|
type: type,
|
|
name: `Nouveau ${type}`,
|
|
description: '',
|
|
details: '',
|
|
attributes: '{}'
|
|
};
|
|
|
|
const newEntity = await api.data.create<any>('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)
|
|
}]
|
|
};
|
|
}));
|
|
} catch (err) {
|
|
console.error("Failed to create entity", err);
|
|
}
|
|
};
|
|
|
|
const updateEntity = async (projectId: string, entityId: string, data: Partial<Entity>) => {
|
|
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<ChatMessage[]>([]);
|
|
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 };
|
|
};
|