Files
plume/hooks.ts

431 lines
16 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';
// --- 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<UserProfile | null>(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<BookProject[]>([]);
const [currentProjectId, setCurrentProjectId] = useState<string | null>(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<BookProject>) => {
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: "<p>Contenu...</p>",
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<Chapter>) => {
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<Entity, 'id'>) => {
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<Entity>) => {
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<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 };
}