'use client'; import {useContext, useEffect, useState} from 'react'; import {BookContext} from "@/context/BookContext"; import {ChapterProps} from "@/lib/models/Chapter"; import {ChapterContext} from '@/context/ChapterContext'; import {EditorContext} from '@/context/EditorContext' import {Editor, useEditor} from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import TextAlign from "@tiptap/extension-text-align"; import {AlertContext, AlertProvider} from "@/context/AlertContext"; import System from "@/lib/models/System"; import {SessionContext} from '@/context/SessionContext'; import {SessionProps} from "@/lib/models/Session"; import User, {UserProps} from "@/lib/models/User"; import {BookProps} from "@/lib/models/Book"; import ScribeTopBar from "@/components/ScribeTopBar"; import ScribeControllerBar from "@/components/ScribeControllerBar"; import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar"; import ScribeEditor from "@/components/editor/ScribeEditor"; import ComposerRightBar from "@/components/rightbar/ComposerRightBar"; import ScribeFooterBar from "@/components/ScribeFooterBar"; import GuideTour, {GuideStep} from "@/components/GuideTour"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faBookMedical, faFeather} from "@fortawesome/free-solid-svg-icons"; import TermsOfUse from "@/components/TermsOfUse"; import frMessages from '@/lib/locales/fr.json'; import enMessages from '@/lib/locales/en.json'; import {NextIntlClientProvider, useTranslations} from "next-intl"; import {LangContext} from "@/context/LangContext"; import {AIUsageContext} from "@/context/AIUsageContext"; import OfflineProvider from "@/context/OfflineProvider"; import OfflineContext from "@/context/OfflineContext"; import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook"; import {BooksSyncContext} from "@/context/BooksSyncContext"; const messagesMap = { fr: frMessages, en: enMessages }; function ScribeContent() { const t = useTranslations(); const {lang: locale} = useContext(LangContext); const {errorMessage} = useContext(AlertContext); const {initializeDatabase, setOfflineMode, isCurrentlyOffline, offlineMode} = useContext(OfflineContext); const editor: Editor | null = useEditor({ extensions: [ StarterKit, Underline, TextAlign.configure({ types: ['heading', 'paragraph'], }), ], injectCSS: false, immediatelyRender: false, }); const [session, setSession] = useState({user: null, accessToken: '', isConnected: false}); const [currentChapter, setCurrentChapter] = useState(undefined); const [currentBook, setCurrentBook] = useState(null); const [serverSyncedBooks, setServerSyncedBooks] = useState([]); const [localSyncedBooks, setLocalSyncedBooks] = useState([]); const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState([]); const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState([]); const [serverOnlyBooks, setServerOnlyBooks] = useState([]); const [localOnlyBooks, setLocalOnlyBooks] = useState([]); const [currentCredits, setCurrentCredits] = useState(160); const [amountSpent, setAmountSpent] = useState(session.user?.aiUsage || 0); const [isLoading, setIsLoading] = useState(true); const [isTermsAccepted, setIsTermsAccepted] = useState(false); const [homeStepsGuide, setHomeStepsGuide] = useState(false); const [showPinSetup, setShowPinSetup] = useState(false); const [showPinVerify, setShowPinVerify] = useState(false); const homeSteps: GuideStep[] = [ { id: 0, x: 50, y: 50, title: t("homePage.guide.welcome", {name: session.user?.name || ''}), content: (

{t("homePage.guide.step0.description1")}


{t("homePage.guide.step0.description2")}

), }, { id: 1, position: 'right', targetSelector: `[data-guide="left-panel-container"]`, title: t("homePage.guide.step1.title"), content: (

: {t("homePage.guide.step1.addBook")}


: {t("homePage.guide.step1.generateStory")}

), }, { id: 2, title: t("homePage.guide.step2.title"), position: 'bottom', targetSelector: `[data-guide="search-bar"]`, content: (

{t("homePage.guide.step2.description")}

), }, { id: 3, title: t("homePage.guide.step3.title"), targetSelector: `[data-guide="user-dropdown"]`, position: 'auto', content: (

{t("homePage.guide.step3.description")}

), }, { id: 4, title: t("homePage.guide.step4.title"), content: (

{t("homePage.guide.step4.description1")}


{t("homePage.guide.step4.description2")}

), }, ]; useEffect((): void => { checkAuthentification().then() }, []); useEffect((): void => { if (session.isConnected) { getBooks().then() setIsTermsAccepted(session.user?.termsAccepted ?? false); setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic')); setIsLoading(false); } }, [session]); useEffect((): void => { if (currentBook) { getLastChapter().then(); } }, [currentBook]); useEffect((): void => { const diffsFromServer: BookSyncCompare[] = []; const diffsToServer: BookSyncCompare[] = []; serverSyncedBooks.forEach((serverBook: SyncedBook): void => { const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id); if (!localBook) { return; } const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook); if (diff) { diffsFromServer.push(diff); } }); localSyncedBooks.forEach((localBook: SyncedBook): void => { const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id); if (!serverBook) { return; } const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook); if (diff) { diffsToServer.push(diff); } }); setBookSyncDiffsFromServer(diffsFromServer); setBookSyncDiffsToServer(diffsToServer); setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => !localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id))) setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook):boolean => !serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id))) }, [localSyncedBooks, serverSyncedBooks]); async function getBooks(): Promise { try { let localBooksResponse: SyncedBook[] = []; let serverBooksResponse: SyncedBook[] = []; if (!isCurrentlyOffline()){ // Mode online: récupérer les livres du serveur ET de la DB locale if (offlineMode.isDatabaseInitialized) { localBooksResponse = await window.electron.invoke('db:books:synced'); } serverBooksResponse = await System.authGetQueryToServer('books/synced', session.accessToken, locale); } else { // Mode offline: récupérer uniquement depuis la DB locale if (offlineMode.isDatabaseInitialized) { localBooksResponse = await window.electron.invoke('db:books:synced'); } } setServerSyncedBooks(serverBooksResponse); setLocalSyncedBooks(localBooksResponse); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("homePage.errors.fetchBooksError")); } } } useEffect(():void => { async function checkPinSetup() { if (session.isConnected && window.electron) { try { const offlineStatus = await window.electron.offlineModeGet(); if (!offlineStatus.hasPin) { setTimeout(():void => { console.log('[Page] Showing PIN setup dialog'); setShowPinSetup(true); }, 2000); } } catch (e:unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage('Unknown error occurred while checking offline mode') } } } } checkPinSetup().then(); }, [session.isConnected]); async function handlePinVerifySuccess(userId: string): Promise { try { if (window.electron) { const storedToken: string | null = await window.electron.getToken(); const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId); if (encryptionKey) { await window.electron.dbInitialize(userId, encryptionKey); setOfflineMode(prev => ({...prev, isDatabaseInitialized: true})); const localUser:UserProps = await window.electron.invoke('db:user:info'); if (localUser && localUser.id) { setSession({ isConnected: true, user: localUser, accessToken: storedToken || '', }); setShowPinVerify(false); setCurrentCredits(localUser.creditsBalance || 0); setAmountSpent(localUser.aiUsage || 0); } else { errorMessage(t("homePage.errors.localDataError")); } } else { errorMessage(t("homePage.errors.encryptionKeyError")); } } } catch (error) { console.error('[OfflinePin] Error initializing offline mode:', error); errorMessage(t("homePage.errors.offlineModeError")); } } async function handleHomeTour(): Promise { try { if (!isCurrentlyOffline()) { const response: boolean = await System.authPostToServer('logs/tour', { plateforme: 'desktop', tour: 'home-basic' }, session.accessToken, locale ); if (response) { setSession(User.setNewGuideTour(session, 'home-basic')); setHomeStepsGuide(false); } } else { // Mode offline: stocker dans localStorage const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]'); if (!completedGuides.includes('home-basic')) { completedGuides.push('home-basic'); localStorage.setItem('completedGuides', JSON.stringify(completedGuides)); } setSession(User.setNewGuideTour(session, 'home-basic')); setHomeStepsGuide(false); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("homePage.errors.termsError")); } } } async function checkAuthentification(): Promise { let token: string | null = null; if (typeof window !== 'undefined' && window.electron) { try { token = await window.electron.getToken(); } catch (e) { console.error('Error getting token from electron:', e); } } if (token) { try { const user: UserProps = await System.authGetQueryToServer('user/infos', token, locale); if (!user) { errorMessage(t("homePage.errors.userNotFound")); if (window.electron) { await window.electron.removeToken(); window.electron.logout(); } return; } if (window.electron && user.id) { try { const initResult = await window.electron.initUser(user.id); if (!initResult.success) { errorMessage(initResult.error || t("homePage.errors.offlineInitError")); return; } try { const offlineStatus = await window.electron.offlineModeGet(); if (!offlineStatus.hasPin) { setTimeout(():void => { setShowPinSetup(true); }, 2000); } } catch (error) { console.error('[Page] Error checking offline mode:', error); } } catch (error) { console.error('[Page] Error initializing user:', error); } } setSession({ isConnected: true, user: user, accessToken: token, }); setCurrentCredits(user.creditsBalance) setAmountSpent(user.aiUsage) if (window.electron && user.id) { try { const dbInitialized:boolean = await initializeDatabase(user.id); if (dbInitialized) { try { await window.electron.invoke('db:user:sync', { userId: user.id, firstName: user.name, lastName: user.lastName, username: user.username, email: user.email }); console.log('User synced to local DB'); } catch (syncError) { console.error('Failed to sync user to local DB:', syncError); } } } catch (error) { console.error('Failed to initialize database:', error); } } } catch (e: unknown) { if (window.electron) { try { const offlineStatus = await window.electron.offlineModeGet(); if (offlineStatus.hasPin && offlineStatus.lastUserId) { setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false})); setShowPinVerify(true); setIsLoading(false); return; } else { if (window.electron) { await window.electron.removeToken(); window.electron.logout(); } } } catch (offlineError) { console.error('[Auth] Error checking offline mode:', offlineError); } } if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("homePage.errors.authenticationError")); } } } else { if (window.electron) { try { const offlineStatus = await window.electron.offlineModeGet(); if (offlineStatus.hasPin && offlineStatus.lastUserId) { setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false})); setShowPinVerify(true); setIsLoading(false); return; } } catch (error) { console.error('[Auth] Error checking offline mode:', error); } window.electron.logout(); } } } async function handleTermsAcceptance(): Promise { try { const response: boolean = await System.authPostToServer(`user/terms/accept`, { version: '2025-07-1' }, session.accessToken, locale); if (response) { setIsTermsAccepted(true); setHomeStepsGuide(true); const newSession: SessionProps = { ...session, user: { ...session?.user as UserProps, termsAccepted: true } } setSession(newSession); } else { errorMessage(t("homePage.errors.termsAcceptError")); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("homePage.errors.termsAcceptError")); } } } async function getLastChapter(): Promise { if (session?.accessToken) { try { let response: ChapterProps | null if (isCurrentlyOffline()){ if (!offlineMode.isDatabaseInitialized) { setCurrentChapter(undefined); return; } response = await window.electron.invoke('db:chapter:last', currentBook?.bookId) } else { response = await System.authGetQueryToServer(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId}); } if (response) { setCurrentChapter(response) } else { setCurrentChapter(undefined); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("homePage.errors.lastChapterError")); } } } } if (isLoading) { return (
ERitors Logo

{t("homePage.loading")}

) } return (
{ homeStepsGuide && !isCurrentlyOffline() && setHomeStepsGuide(false)}/> } { !isTermsAccepted && !isCurrentlyOffline() && } { showPinSetup && window.electron && ( setShowPinSetup(false)} onSuccess={() => { setShowPinSetup(false); console.log('[Page] PIN configured successfully'); }} /> ) } { showPinVerify && window.electron && ( { //window.electron.logout(); }} /> ) }
); } export default function Scribe() { const [locale, setLocale] = useState<'fr' | 'en'>('fr'); useEffect((): void => { const lang: "fr" | "en" | null = System.getCookie('lang') as "fr" | "en" | null; if (lang) { setLocale(lang); } }, []); const messages = messagesMap[locale]; return ( ); }