Files
plume/hooks.ts
2026-02-16 22:02:45 +01:00

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 };
};