login ok
This commit is contained in:
656
hooks.ts
656
hooks.ts
@@ -1,417 +1,397 @@
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { UserProfile, BookProject, Chapter, ChatMessage, PlanType, Entity, Idea, WorkflowData } from './types';
|
||||
import { INITIAL_CHAPTER } from './constants';
|
||||
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 { 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;
|
||||
}
|
||||
};
|
||||
import {
|
||||
DEFAULT_BOOK_TITLE,
|
||||
DEFAULT_AUTHOR,
|
||||
INITIAL_CHAPTER
|
||||
} from './constants';
|
||||
|
||||
// --- AUTH HOOK ---
|
||||
|
||||
export function useAuth() {
|
||||
export const 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'
|
||||
// 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 }
|
||||
});
|
||||
|
||||
// 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'
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Session check failed', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
checkSession();
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
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("[useAuth] Error:", err);
|
||||
setUser(null);
|
||||
console.error('Login failed', err);
|
||||
throw err;
|
||||
} 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) => {
|
||||
const signup = async (email: string, pass: string, name: string) => {
|
||||
setLoading(true);
|
||||
const result = await authService.signUp(data.email, data.password, data.name);
|
||||
try {
|
||||
await api.auth.signUp(email, pass, name);
|
||||
|
||||
if (result && !result.error) {
|
||||
const userObj = result.user || result;
|
||||
await checkSession(userObj);
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
setLoading(true);
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
try {
|
||||
await api.auth.signOut();
|
||||
setUser(null);
|
||||
} catch (err) {
|
||||
console.error('Logout failed', err);
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
const incrementUsage = () => {
|
||||
if (user) {
|
||||
setUser(prev => prev ? {
|
||||
...prev,
|
||||
usage: { ...prev.usage, aiActionsCurrent: prev.usage.aiActionsCurrent + 1 }
|
||||
} : null);
|
||||
// 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 function useProjects(user: UserProfile | null) {
|
||||
export const 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);
|
||||
// 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]);
|
||||
|
||||
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
|
||||
// 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: [],
|
||||
workflow: { nodes: [], connections: [] }
|
||||
}));
|
||||
settings: JSON.parse(newProjectData.settings)
|
||||
};
|
||||
|
||||
setProjects(mappedProjects);
|
||||
} catch (e) {
|
||||
console.error("[useProjects] Fetch error:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
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);
|
||||
}
|
||||
}, [user]);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchProjects(); }, [fetchProjects]);
|
||||
const updateProject = async (id: string, data: Partial<BookProject>) => {
|
||||
// Optimistic update
|
||||
setProjects(prev => prev.map(p => p.id === id ? { ...p, ...data } : p));
|
||||
|
||||
// Deep Fetch Triggered on Project Selection
|
||||
const fetchProjectDetails = async (projectId: string) => {
|
||||
const id = parseInt(projectId);
|
||||
// DB Update
|
||||
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)
|
||||
]);
|
||||
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);
|
||||
|
||||
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);
|
||||
await api.data.update('projects', id, payload);
|
||||
} catch (err) {
|
||||
console.error("Failed to update project", err);
|
||||
// Revert?
|
||||
}
|
||||
};
|
||||
|
||||
// --- 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);
|
||||
|
||||
const addChapter = async (projectId: string, chapterData: Partial<Chapter>) => {
|
||||
try {
|
||||
const result = await dataService.createItem('chapters', {
|
||||
const chapterPayload = {
|
||||
project_id: projectId,
|
||||
title: "Nouveau Chapitre",
|
||||
content: "<p>Contenu...</p>",
|
||||
summary: ""
|
||||
});
|
||||
console.log("[Hooks] createItem result:", result);
|
||||
title: chapterData.title || 'New Chapter',
|
||||
content: chapterData.content || '',
|
||||
summary: chapterData.summary || '',
|
||||
order_index: 0
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
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 ? {
|
||||
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
|
||||
// Optimistic
|
||||
setProjects(prev => prev.map(p => {
|
||||
if (p.id !== projectId) return p;
|
||||
return {
|
||||
...p,
|
||||
entities: [...p.entities, { ...entity, id: newId }]
|
||||
} : p));
|
||||
return newId;
|
||||
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);
|
||||
}
|
||||
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);
|
||||
const createEntity = async (projectId: string, type: EntityType) => {
|
||||
try {
|
||||
const entityPayload = {
|
||||
project_id: projectId,
|
||||
type: type,
|
||||
name: `Nouveau ${type}`,
|
||||
description: '',
|
||||
details: '',
|
||||
attributes: '{}'
|
||||
};
|
||||
|
||||
await dataService.updateItem('entities', parseInt(entityId), dbPayload);
|
||||
const newEntity = await api.data.create<any>('entities', entityPayload);
|
||||
|
||||
setProjects(prev => prev.map(p => p.id === currentProjectId ? {
|
||||
...p,
|
||||
entities: p.entities.map(e => e.id === entityId ? { ...e, ...updates } : e)
|
||||
} : p));
|
||||
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 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));
|
||||
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: (id: string | null) => {
|
||||
setCurrentProjectId(id);
|
||||
if (id) fetchProjectDetails(id);
|
||||
},
|
||||
createProject, updateProject, updateChapter, addChapter,
|
||||
createEntity, updateEntity, deleteEntity,
|
||||
loading
|
||||
setCurrentProjectId,
|
||||
createProject,
|
||||
updateProject,
|
||||
addChapter,
|
||||
updateChapter,
|
||||
createEntity,
|
||||
updateEntity,
|
||||
deleteEntity
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- CHAT HOOK ---
|
||||
|
||||
export function useChat() {
|
||||
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, chapterId: string, msg: string, user: UserProfile, onUsed: () => void) => {
|
||||
const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: msg };
|
||||
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, chapterId, msg, user, onUsed);
|
||||
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',
|
||||
@@ -419,12 +399,16 @@ export function useChat() {
|
||||
responseType: response.type
|
||||
};
|
||||
setChatHistory(prev => [...prev, aiMsg]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} 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 };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user