From baa45ac10603a0212578007144e97fe1f7dc4537 Mon Sep 17 00:00:00 2001 From: natreex Date: Mon, 17 Nov 2025 20:14:22 -0500 Subject: [PATCH] Add `Content`, `Model`, and `Story` models with text processing and AI model configuration utilities - Implement `Content` model for converting Tiptap raw data into HTML and plain text. - Add `Model` for storing and managing AI model configurations with pricing and metadata. - Introduce `Story` model to handle verbal styles and linguistic properties for diverse narrative structures. - Update `book.repository.ts` to refine `updateBookBasicInformation` and `insertNewPlotPoint` methods, removing unused parameters and optimizing queries. --- electron/database/models/Chapter.ts | 327 +++++++ electron/database/models/Character.ts | 295 ++++++ electron/database/models/Content.ts | 160 ++++ electron/database/models/EpubStyle.ts | 11 + electron/database/models/Location.ts | 252 +++++ electron/database/models/Model.ts | 253 +++++ electron/database/models/Story.ts | 712 ++++++++++++++ electron/database/models/User.ts | 885 ++++++++++++++++++ .../database/repositories/book.repository.ts | 10 +- .../repositories/chapter.repository.ts | 8 +- 10 files changed, 2904 insertions(+), 9 deletions(-) create mode 100644 electron/database/models/Chapter.ts create mode 100644 electron/database/models/Character.ts create mode 100755 electron/database/models/Content.ts create mode 100755 electron/database/models/EpubStyle.ts create mode 100644 electron/database/models/Location.ts create mode 100644 electron/database/models/Model.ts create mode 100755 electron/database/models/Story.ts create mode 100644 electron/database/models/User.ts diff --git a/electron/database/models/Chapter.ts b/electron/database/models/Chapter.ts new file mode 100644 index 0000000..c3bce67 --- /dev/null +++ b/electron/database/models/Chapter.ts @@ -0,0 +1,327 @@ +import StarterKit from '@tiptap/starter-kit' +import TextAlign from '@tiptap/extension-text-align' +import ChapterRepo, { + ActChapterQuery, + ChapterQueryResult, + ChapterContentQueryResult, + LastChapterResult, + CompanionContentQueryResult, + ChapterStoryQueryResult, + ContentQueryResult +} from "@/electron/database/repositories/chapter.repository"; +import System from "../System"; +import {getUserEncryptionKey} from "../keyManager"; +import { generateHTML } from "@tiptap/react"; + +export interface ChapterContent { + version: number; + content: string; + wordsCount: number; +} + +export interface ChapterContentData extends ChapterContent { + title: string; + chapterOrder: number; +} + +export interface ChapterProps { + chapterId: string; + title: string; + chapterOrder: number; + chapterContent?: ChapterContent +} + +export interface ActChapter { + chapterInfoId: number; + chapterId: string; + title: string; + chapterOrder: number; + actId: number; + incidentId: string | null; + plotPointId: string | null; + summary: string; + goal: string; +} + +export interface CompanionContent { + version: number; + content: string; + wordsCount: number; +} + +export interface ActStory { + actId: number; + summary: string; + chapterSummary: string; + chapterGoal: string; + incidents: IncidentStory[]; + plotPoints: PlotPointStory[]; +} + +export interface IncidentStory { + incidentTitle: string; + incidentSummary: string; + chapterSummary: string; + chapterGoal: string; +} + +export interface PlotPointStory { + plotTitle: string; + plotSummary: string; + chapterSummary: string; + chapterGoal: string; +} + +export default class Chapter { + public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] { + const chapters: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang); + let returnChapters: ChapterProps[] = []; + const userKey: string = getUserEncryptionKey(userId); + for (const chapter of chapters) { + const title: string = System.decryptDataWithUserKey(chapter.title, userKey); + returnChapters.push({ + chapterId: chapter.chapter_id, + title: title, + chapterOrder: chapter.chapter_order + }); + } + return returnChapters; + } + + public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] { + const query: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang); + let chapters: ActChapter[] = []; + let tempChapter: { id: string, title: string }[] = [] + const userKey: string = getUserEncryptionKey(userId); + if (query.length > 0) { + for (const chapter of query) { + let decryptTitle: string = ''; + const newTitleId: number = tempChapter.findIndex((temp: { id: string, title: string }) => temp.id === chapter.chapter_id); + if (newTitleId > -1) { + decryptTitle = tempChapter[newTitleId]?.title ?? '' + } else { + decryptTitle = System.decryptDataWithUserKey(chapter.title, userKey); + tempChapter.push({id: chapter.chapter_id, title: decryptTitle}); + } + chapters.push({ + chapterId: chapter.chapter_id, + title: decryptTitle, + actId: chapter.act_id, + chapterInfoId: chapter.chapter_info_id, + chapterOrder: chapter.chapter_order, + goal: chapter.goal ? System.decryptDataWithUserKey(chapter.goal, userKey) : '', + summary: chapter.summary ? System.decryptDataWithUserKey(chapter.summary, userKey) : '', + incidentId: chapter.incident_id, + plotPointId: chapter.plot_point_id + }) + } + return chapters; + } else { + return [] + } + } + + public static getWholeChapter(userId: string, chapterId: string, version: number, bookId?: string, lang: 'fr' | 'en' = 'fr'): ChapterProps { + const chapter: ChapterContentQueryResult = ChapterRepo.fetchWholeChapter(userId, chapterId, version, lang); + const userKey: string = getUserEncryptionKey(userId); + + if (bookId) { + ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang); + } + return { + chapterId: chapter.chapter_id, + title: System.decryptDataWithUserKey(chapter.title, userKey), + chapterOrder: chapter.chapter_order, + chapterContent: { + content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', + version: version, + wordsCount: chapter.words_count + } + }; + } + + public static saveChapterContent(userId: string, chapterId: string, version: number, content: JSON, wordsCount: number, currentTime: number, lang: 'fr' | 'en' = 'fr'): boolean { + const userKey: string = getUserEncryptionKey(userId); + const encryptContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userKey); + /*if (version === 2){ + const QS = new AI(); + const prompt:string = System.htmlToText(Chapter.tipTapToHtml(content)); + const response:string = await QS.request(prompt,'summary-chapter'); + console.log(response); + }*/ + return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, lang); + } + + public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null { + const lastChapter: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang); + if (lastChapter) { + return Chapter.getWholeChapter(userId, lastChapter.chapter_id, lastChapter.version, bookId, lang); + } + const chapter: ChapterContentQueryResult[] = ChapterRepo.fetchLastChapterContent(userId, bookId, lang); + if (chapter.length === 0) { + return null + } + const chapterData: ChapterContentQueryResult = chapter[0]; + const userKey: string = getUserEncryptionKey(userId); + return { + chapterId: chapterData.chapter_id, + title: chapterData.title ? System.decryptDataWithUserKey(chapterData.title, userKey) : '', + chapterOrder: chapterData.chapter_order, + chapterContent: { + content: chapterData.content ? System.decryptDataWithUserKey(chapterData.content, userKey) : '', + version: chapterData.version, + wordsCount: chapterData.words_count + } + }; + } + + public static addChapter(userId: string, bookId: string, title: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): string { + const hashedTitle: string = System.hashElement(title); + const userKey: string = getUserEncryptionKey(userId); + const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + + if (ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) { + throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`); + } + const chapterId: string = System.createUniqueId(); + return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang); + } + + public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return ChapterRepo.deleteChapter(userId, chapterId, lang); + } + + public static addChapterInformation(userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr'): string { + const chapterInfoId: string = System.createUniqueId(); + return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang); + } + + public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { + const hashedTitle: string = System.hashElement(title); + const userKey: string = getUserEncryptionKey(userId); + const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, lang); + } + + static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, meta: string, lang: 'fr' | 'en' = 'fr') { + const userKey: string = getUserEncryptionKey(userId); + for (const chapter of chapters) { + const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : ''; + const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : ''; + const chapterId: string = chapter.chapterId; + ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, meta, lang); + } + } + + static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent { + const versionNum: number = version - 1; + const chapterResponse: CompanionContentQueryResult[] = ChapterRepo.fetchCompanionContent(userId, chapterId, versionNum, lang); + if (chapterResponse.length === 0) { + return { + version: version, + content: '', + wordsCount: 0 + }; + } + const chapter: CompanionContentQueryResult = chapterResponse[0]; + const userKey: string = getUserEncryptionKey(userId); + return { + version: chapter.version, + content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', + wordsCount: chapter.words_count + }; + } + + static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] { + const stories: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang); + const actStories: Record = {}; + const userKey: string = getUserEncryptionKey(userId); + + for (const story of stories) { + const actId: number = story.act_id; + + if (!actStories[actId]) { + actStories[actId] = { + actId: actId, + summary: story.summary ? System.decryptDataWithUserKey(story.summary, userKey) : '', + chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', + chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '', + incidents: [], + plotPoints: [] + }; + } + + if (story.incident_id) { + const incidentTitle = story.incident_title ? System.decryptDataWithUserKey(story.incident_title, userKey) : ''; + const incidentSummary = story.incident_summary ? System.decryptDataWithUserKey(story.incident_summary, userKey) : ''; + + const incidentExists = actStories[actId].incidents.some( + (incident) => incident.incidentTitle === incidentTitle && incident.incidentSummary === incidentSummary + ); + + if (!incidentExists) { + actStories[actId].incidents.push({ + incidentTitle: incidentTitle, + incidentSummary: incidentSummary, + chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', + chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' + }); + } + } + + if (story.plot_point_id) { + const plotTitle = story.plot_title ? System.decryptDataWithUserKey(story.plot_title, userKey) : ''; + const plotSummary = story.plot_summary ? System.decryptDataWithUserKey(story.plot_summary, userKey) : ''; + + const plotPointExists = actStories[actId].plotPoints.some( + (plotPoint) => plotPoint.plotTitle === plotTitle && plotPoint.plotSummary === plotSummary + ); + + if (!plotPointExists) { + actStories[actId].plotPoints.push({ + plotTitle: plotTitle, + plotSummary: plotSummary, + chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', + chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' + }); + } + } + } + + return Object.values(actStories); + } + + + static getChapterContentByVersion(userId: string, chapterid: string, version: number, lang: 'fr' | 'en' = 'fr'): string { + const chapter: ContentQueryResult = ChapterRepo.fetchChapterContentByVersion(userId, chapterid, version, lang); + const userKey: string = getUserEncryptionKey(userId); + return chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : ''; + } + + static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr') { + return ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang); + } + + static tipTapToHtml(tipTapContent: JSON): string { + const fixNode = (node: Record): Record => { + if (!node) return node; + + if (node.type === 'text' && (!node.text || node.text === '')) { + node.text = '\u00A0'; + } + + if (Array.isArray(node.content) && node.content.length) { + node.content = node.content.map(fixNode); + } + + return node; + }; + + return generateHTML(fixNode(tipTapContent as unknown as Record), [ + StarterKit, + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + ]); + } +} diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts new file mode 100644 index 0000000..42245ed --- /dev/null +++ b/electron/database/models/Character.ts @@ -0,0 +1,295 @@ +import User from "./User"; +import System, {UserKey} from "./System"; +import CharacterRepo, {AttributeResult, CharacterResult, CompleteCharacterResult} from "../repositories/character.repo"; + +export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring'; + +export interface CharacterPropsPost { + id: number | null; + name: string; + lastName: string; + category: CharacterCategory; + title: string; + image: string; + physical: { name: string }[]; + psychological: { name: string }[]; + relations: { name: string }[]; + skills: { name: string }[]; + weaknesses: { name: string }[]; + strengths: { name: string }[]; + goals: { name: string }[]; + motivations: { name: string }[]; + role: string; + biography?: string; + history?: string; +} + + +export interface CharacterProps { + id: string; + name: string; + lastName: string; + title: string; + category: string; + image: string; + role: string; + biography: string; + history: string; +} + +export interface CompleteCharacterProps { + id?: string; + name: string; + lastName: string; + title: string; + category: string; + image?: string; + role: string; + biography: string; + history: string; + [key: string]: Attribute[] | string | undefined; +} + +export interface Attribute { + id: string; + name: string; +} + + +export interface CharacterAttribute { + type: string; + values: Attribute[]; +} + +export default class Character{ + public static async getCharacterList(userId: string, bookId: string): Promise { + const user = new User(userId); + const keys:UserKey[] = await System.getAllUserKeysAndVersions(userId); + const characters: CharacterResult[] = await CharacterRepo.fetchCharacters(userId, bookId); + if (!characters) return []; + if (characters.length === 0) return []; + const characterList:CharacterProps[] = []; + for (const character of characters) { + const userKey:string = await user.getUserKey(character.char_meta,true,keys); + characterList.push({ + id:character.character_id, + name:character.first_name ? System.decryptDataWithUserKey(character.first_name,userKey) : '', + lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', + title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', + category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '', + image: character.image ? System.decryptDataWithUserKey(character.image, userKey) : '', + role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '', + biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '', + history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '', + }) + } + return characterList; + } + + public static async addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string): Promise { + const user = new User(userId); + const meta:string = System.encryptDateKey(userId); + const userKey:string = await user.getUserKey(meta,true); + const characterId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); + const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); + const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); + const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); + const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); + const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); + const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); + const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); + await CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, meta); + const attributes: string[] = Object.keys(character); + for (const key of attributes) { + if (Array.isArray(character[key as keyof CharacterPropsPost])) { + const array = character[key as keyof CharacterPropsPost] as { name: string }[]; + if (array.length > 0) { + for (const item of array) { + const type: string = key; + const name: string = item.name; + await this.addNewAttribute(characterId, userId, type, name); + } + } + } + } + return characterId; + } + + static async updateCharacter(userId: string, character: CharacterPropsPost): Promise { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); + const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); + const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); + const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); + const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); + const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); + const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); + const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); + return await CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, meta); + } + + static async addNewAttribute(characterId: string, userId: string, type: string, name: string): Promise { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + const attributeId: string = System.createUniqueId(); + const encryptedType: string = System.encryptDataWithUserKey(type, userKey); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + return await CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, meta); + } + + static async deleteAttribute(userId: string, attributeId: string) { + return await CharacterRepo.deleteAttribute(userId, attributeId); + } + + static async getAttributes(characterId: string, userId: string): Promise { + const user: User = new User(userId); + const keys: UserKey[] = await System.getAllUserKeysAndVersions(userId); + const attributes: AttributeResult[] = await CharacterRepo.fetchAttributes(characterId, userId); + if (!attributes?.length) return []; + + const groupedMap: Map = new Map(); + + for (const attribute of attributes) { + const userKey: string = await user.getUserKey(attribute.attr_meta, true, keys); + const type: string = System.decryptDataWithUserKey(attribute.attribute_name, userKey); + const value: string = attribute.attribute_value ? System.decryptDataWithUserKey(attribute.attribute_value, userKey) : ''; + + if (!groupedMap.has(type)) { + groupedMap.set(type, []); + } + + groupedMap.get(type)!.push({ + id: attribute.attr_id, + name: value + }); + } + + return Array.from<[string, Attribute[]], CharacterAttribute>( + groupedMap, + ([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values}) + ); + } + + static async getCompleteCharacterList(userId: string, bookId: string, characters: string[]): Promise { + const characterList: CompleteCharacterResult[] = await CharacterRepo.fetchCompleteCharacters(userId, bookId, characters); + + if (!characterList || characterList.length === 0) { + return []; + } + + const user = new User(userId); + const keys: UserKey[] = await System.getAllUserKeysAndVersions(userId); + const completeCharactersMap = new Map(); + for (const character of characterList) { + if (!character.character_id) { + continue; + } + + if (!completeCharactersMap.has(character.character_id)) { + const userKey: string = await user.getUserKey(character.char_meta, true, keys); + if (!userKey) { + continue; + } + const personnageObj: CompleteCharacterProps = { + id: '', + name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', + lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', + title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', + category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '', + role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '', + biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '', + history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '', + physical: [], + psychological: [], + relations: [], + skills: [], + weaknesses: [], + strengths: [], + goals: [], + motivations: [] + }; + completeCharactersMap.set(character.character_id, personnageObj); + } + + const personnage: CompleteCharacterProps | undefined = completeCharactersMap.get(character.character_id); + + if (!character.attr_meta) { + continue; + } + + const rowKey: string = await user.getUserKey(character.attr_meta, true, keys); + + if (!personnage || !rowKey) { + continue; + } + const decryptedName: string = System.decryptDataWithUserKey(character.attribute_name, rowKey); + const decryptedValue: string = character.attribute_value ? System.decryptDataWithUserKey(character.attribute_value, rowKey) : ''; + + if (Array.isArray(personnage[decryptedName])) { + personnage[decryptedName].push({ + id: '', + name: decryptedValue + }); + } + } + return Array.from(completeCharactersMap.values()); + } + + static characterVCard(characters: CompleteCharacterProps[]): string { + const charactersMap = new Map(); + let charactersDescription: string = ''; + + characters.forEach((character: CompleteCharacterProps): void => { + const characterKey: string = character.name || character.id || 'unknown'; + + if (!charactersMap.has(characterKey)) { + charactersMap.set(characterKey, { + name: character.name, + lastName: character.lastName, + category: character.category, + title: character.title, + role: character.role, + biography: character.biography, + history: character.history + }); + } + + const characterData: CompleteCharacterProps = charactersMap.get(characterKey)!; + + Object.keys(character).forEach((fieldName: string): void => { + if (Array.isArray(character[fieldName])) { + if (!characterData[fieldName]) characterData[fieldName] = []; + (characterData[fieldName] as Attribute[]).push(...(character[fieldName] as Attribute[])); + } + }); + }); + + charactersDescription = Array.from(charactersMap.values()).map((character: CompleteCharacterProps): string => { + const descriptionFields: string[] = []; + const fullName: string = [character.name, character.lastName].filter(Boolean).join(' '); + if (fullName) descriptionFields.push(`Nom : ${fullName}`); + + (['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => { + if (character[propertyKey]) { + descriptionFields.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`); + } + }); + + Object.keys(character).forEach((propertyKey: string): void => { + const propertyValue: string | Attribute[] | undefined = character[propertyKey]; + if (Array.isArray(propertyValue) && propertyValue.length > 0) { + const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1); + const formattedValues: string = propertyValue.map((item: Attribute) => item.name).join(', '); + descriptionFields.push(`${capitalizedPropertyKey} : ${formattedValues}`); + } + }); + + return descriptionFields.join('\n'); + }).join('\n\n'); + return charactersDescription; + } +} diff --git a/electron/database/models/Content.ts b/electron/database/models/Content.ts new file mode 100755 index 0000000..9d226c2 --- /dev/null +++ b/electron/database/models/Content.ts @@ -0,0 +1,160 @@ +export interface TiptapNode { + type: string; + content?: TiptapNode[]; + text?: string; + attrs?: { + [key: string]: any; + }; +} + +export default class Content { + static convertTipTapRawToText(content: string): string { + const text: string = this.convertTiptapToHTMLFromString(content); + return this.htmlToText(text); + } + + static htmlToText(html: string) { + return html + .replace(//gi, '\n') // Gérer les
d'abord + .replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') // Balises bloc + .replace(/<\/?[^>]+(>|$)/g, '') // Supprimer toutes les balises restantes + .replace(/(\n\s*){2,}/g, '\n\n') // Préserver les paragraphes + .replace(/^\s+|\s+$|(?<=\s)\s+/g, '') // Nettoyer les espaces + .trim(); + } + + static convertTiptapToHTMLFromString(jsonString: string): string { + // Convert the JSON string to an object + let jsonObject: TiptapNode; + try { + jsonObject = JSON.parse(jsonString); + } catch (error) { + console.error('Invalid JSON string:', error); + return ''; + } + + // Use the existing conversion function + return this.convertTiptapToHTML(jsonObject); + } + + static convertTiptapToHTML(node: TiptapNode): string { + let html = ''; + + switch (node.type) { + case 'doc': + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + break; + + case 'paragraph': + html += '

'; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += '

'; + break; + + case 'text': + let textContent = node.text || ''; + + // Apply attributes like bold, italic, etc. + if (node.attrs) { + if (node.attrs.bold) { + textContent = `${textContent}`; + } + if (node.attrs.italic) { + textContent = `${textContent}`; + } + if (node.attrs.underline) { + textContent = `${textContent}`; + } + if (node.attrs.strike) { + textContent = `${textContent}`; + } + if (node.attrs.link) { + textContent = `${textContent}`; + } + } + + html += textContent; + break; + + case 'heading': + const level = node.attrs?.level || 1; + html += ``; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += ``; + break; + + case 'bulletList': + html += '
    '; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += '
'; + break; + + case 'orderedList': + html += '
    '; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += '
'; + break; + + case 'listItem': + html += '
  • '; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += '
  • '; + break; + + case 'blockquote': + html += '
    '; + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + html += '
    '; + break; + + case 'codeBlock': + html += '
    ';
    +                if (node.content) {
    +                    node.content.forEach(childNode => {
    +                        html += this.convertTiptapToHTML(childNode);
    +                    });
    +                }
    +                html += '
    '; + break; + + default: + console.warn(`Unhandled node type: ${node.type}`); + if (node.content) { + node.content.forEach(childNode => { + html += this.convertTiptapToHTML(childNode); + }); + } + break; + } + + return html; + } +} diff --git a/electron/database/models/EpubStyle.ts b/electron/database/models/EpubStyle.ts new file mode 100755 index 0000000..cf79982 --- /dev/null +++ b/electron/database/models/EpubStyle.ts @@ -0,0 +1,11 @@ +export const mainStyle = `h1 { + font-size: 24px !important; + font-weight: bold !important; + text-indent: 24px !important; +} +p { + text-indent: 30px !important; + margin-top: 0.7em !important; + margin-bottom: 0.7em !important; + text-align: justify !important; +}` diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts new file mode 100644 index 0000000..b0552a2 --- /dev/null +++ b/electron/database/models/Location.ts @@ -0,0 +1,252 @@ +import User from "./User"; +import System, {UserKey} from "./System"; +import LocationRepo, { + LocationByTagResult, + LocationElementQueryResult, + LocationQueryResult +} from "../repositories/location.repo"; + +export interface SubElement { + id: string; + name: string; + description: string; +} + +export interface Element { + id: string; + name: string; + description: string; + subElements: SubElement[]; +} + +export interface LocationProps { + id: string; + name: string; + elements: Element[]; +} + +export default class Location { + /** + * Récupère toutes les locations pour un utilisateur et un livre donnés. + * @param {string} userId - L'ID de l'utilisateur. + * @param {string} bookId - L'ID du livre. + * @returns {Promise} - Une promesse qui résout un tableau de propriétés de location. + * @throws {Error} - Lance une erreur si une exception se produit lors de la récupération des locations. + */ + static async getAllLocations(userId: string, bookId: string): Promise { + const locations: LocationQueryResult[] = await LocationRepo.getLocation(userId, bookId); + if (!locations || locations.length === 0) return []; + const user = new User(userId); + const userKeys: UserKey[] = await System.getAllUserKeysAndVersions(userId); + + const locationArray: LocationProps[] = []; + + for (const record of locations) { + let location = locationArray.find(loc => loc.id === record.loc_id); + + if (!location) { + const key: string = await user.getUserKey(record.loc_meta, true, userKeys); + const decryptedName: string = System.decryptDataWithUserKey(record.loc_name, key)/* ton code de décryptage ici avec record.loc_name */; + location = { + id: record.loc_id, + name: decryptedName, + elements: [] + }; + locationArray.push(location); + } + + if (record.element_id) { + let element = location.elements.find(elem => elem.id === record.element_id); + if (!element) { + const key: string = await user.getUserKey(record.element_meta, true, userKeys); + const decryptedName: string = System.decryptDataWithUserKey(record.element_name, key)/* ton code de décryptage ici avec record.element_name */; + const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, key) : ''/* ton code de décryptage ici avec record.element_description */; + + element = { + id: record.element_id, + name: decryptedName, + description: decryptedDesc, + subElements: [] + }; + location.elements.push(element); + } + + if (record.sub_element_id) { + const subElementExists = element.subElements.some(sub => sub.id === record.sub_element_id); + + if (!subElementExists) { + const key: string = await user.getUserKey(record.sub_elem_meta, true, userKeys); + const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, key)/* ton code de décryptage ici avec record.sub_elem_name */; + const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, key) : ''/* ton code de décryptage ici avec record.sub_elem_description */; + + + element.subElements.push({ + id: record.sub_element_id, + name: decryptedName, + description: decryptedDesc + }); + } + } + } + } + return locationArray; + } + + static async addLocationSection(userId: string, locationName: string, bookId: string): Promise { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + const originalName: string = System.hashElement(locationName); + const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey); + const locationId: string = System.createUniqueId(); + return await LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, originalName, meta); + } + + static async addLocationElement(userId: string, locationId: string, elementName: string) { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + const originalName: string = System.hashElement(elementName); + const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey); + const elementId: string = System.createUniqueId(); + return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, originalName, meta) + } + + static async addLocationSubElement(userId: string, elementId: string, subElementName: string) { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + const originalName: string = System.hashElement(subElementName); + const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey); + const subElementId: string = System.createUniqueId(); + return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, originalName, meta) + } + + static async updateLocationSection(userId: string, locations: LocationProps[]) { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta, true); + + for (const location of locations) { + const originalName: string = System.hashElement(location.name); + const encryptedName: string = System.encryptDataWithUserKey(location.name, userKey); + await LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, meta) + for (const element of location.elements) { + const originalName: string = System.hashElement(element.name); + const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey); + const encryptDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; + await LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, meta) + for (const subElement of element.subElements) { + const originalName: string = System.hashElement(subElement.name); + const encryptedName: string = System.encryptDataWithUserKey(subElement.name, userKey); + const encryptDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : ''; + await LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription, meta) + } + } + } + return { + valid: true, + message: 'Les sections ont été mis à jour.' + } + } + + static async deleteLocationSection(userId: string, locationId: string) { + return LocationRepo.deleteLocationSection(userId, locationId); + } + + static async deleteLocationElement(userId: string, elementId: string) { + return LocationRepo.deleteLocationElement(userId, elementId); + } + + static async deleteLocationSubElement(userId: string, subElementId: string) { + return LocationRepo.deleteLocationSubElement(userId, subElementId); + } + + static async getLocationTags(userId: string, bookId: string): Promise { + const data: LocationElementQueryResult[] = await LocationRepo.fetchLocationTags(userId, bookId); + if (!data || data.length === 0) return []; + const user = new User(userId); + const userKeys: UserKey[] = await System.getAllUserKeysAndVersions(userId); + + const elementCounts = new Map(); + data.forEach((record: LocationElementQueryResult): void => { + elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1); + }); + + const subElements: SubElement[] = []; + const processedIds = new Set(); + + for (const record of data) { + const elementCount: number = elementCounts.get(record.element_id) || 0; + + if (elementCount > 1 && record.sub_element_id) { + if (processedIds.has(record.sub_element_id)) continue; + + const key: string = await user.getUserKey(record.sub_elem_meta, true, userKeys); + subElements.push({ + id: record.sub_element_id, + name: System.decryptDataWithUserKey(record.sub_elem_name, key), + description: record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, key) : '' + }); + processedIds.add(record.sub_element_id); + } else if (elementCount === 1 && !record.sub_element_id) { + if (processedIds.has(record.element_id)) continue; + + const key: string = await user.getUserKey(record.element_meta, true, userKeys); + subElements.push({ + id: record.element_id, + name: System.decryptDataWithUserKey(record.element_name, key), + description: record.element_description ? System.decryptDataWithUserKey(record.element_description, key) : '' + }); + processedIds.add(record.element_id); + } + } + return subElements; + } + + static async getLocationsByTags(userId: string, locations: string[]): Promise { + const locationsTags: LocationByTagResult[] = await LocationRepo.fetchLocationsByTags(userId, locations); + if (!locationsTags || locationsTags.length === 0) return []; + const user = new User(userId); + const userKeys: UserKey[] = await System.getAllUserKeysAndVersions(userId); + const locationTags: Element[] = []; + for (const record of locationsTags) { + let element: Element | undefined = locationTags.find((elem: Element): boolean => elem.name === record.element_name); + if (!element) { + const key: string = await user.getUserKey(record.element_meta, true, userKeys); + const decryptedName: string = System.decryptDataWithUserKey(record.element_name, key); + const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, key) : ''; + element = { + id: record.element_id, + name: decryptedName, + description: decryptedDesc, + subElements: [] + }; + locationTags.push(element); + } + if (record.sub_elem_name) { + const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name); + if (!subElementExists) { + const key: string = await user.getUserKey(record.sub_elem_meta, true, userKeys); + const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, key); + const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, key) : ''; + element.subElements.push({ + id: '', + name: decryptedName, + description: decryptedDesc + }); + } + } + } + return locationTags; + } + + static async locationsDescription(locations: Element[]): Promise { + return locations.map((location: Element): string => { + const fields: string[] = []; + if (location.name) fields.push(`Nom : ${location.name}`); + if (location.description) fields.push(`Description : ${location.description}`); + return fields.join('\n'); + }).join('\n\n'); + } +} diff --git a/electron/database/models/Model.ts b/electron/database/models/Model.ts new file mode 100644 index 0000000..c3fa219 --- /dev/null +++ b/electron/database/models/Model.ts @@ -0,0 +1,253 @@ +export type GPTModel = "gpt-4o-mini" | "gpt-4o-turbo" | "gpt-3.5-turbo" | "gpt-4o" | "gpt-4.1" | "gpt-4.1-nano"; +export type AnthropicModel = + "claude-3-7-sonnet-20250219" + | "claude-sonnet-4-20250514" + | "claude-sonnet-4-5-20250929" + | "claude-3-5-haiku-20241022" + | "claude-3-5-sonnet-20241022" + | "claude-3-5-sonnet-20240620" + | "claude-3-opus-20240229"; +export type GeminiModel = + | "gemini-2.0-flash-001" + | "gemini-2.0-flash-lite-001" + | "gemini-2.5-flash" + | "gemini-2.5-flash-lite" + | "gemini-2.5-pro"; + +export interface AIModelConfig { + model_id: string; + model_name: string; + brand: string; + price_token_in: number; + per_quantity_in: number; + price_token_out: number; + per_quantity_out: number; +} + +export const AIModels: AIModelConfig[] = [ + { + "model_id": "claude-3-5-haiku-20241022", + "model_name": "Claude Haiku 3.5", + "brand": "Anthropic", + "price_token_in": 0.8, + "per_quantity_in": 1000000, + "price_token_out": 4, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-3-5-sonnet-20241022", + "model_name": "Claude Sonnet 3.5", + "brand": "Anthropic", + "price_token_in": 3, + "per_quantity_in": 1000000, + "price_token_out": 15, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-3-7-sonnet-20250219", + "model_name": "Claude Sonnet 3.7", + "brand": "Anthropic", + "price_token_in": 3, + "per_quantity_in": 1000000, + "price_token_out": 15, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-3-haiku-20240307", + "model_name": "Claude Haiku 3", + "brand": "Anthropic", + "price_token_in": 0.25, + "per_quantity_in": 1000000, + "price_token_out": 1.25, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-3-opus-20240229", + "model_name": "Claude Opus 3", + "brand": "Anthropic", + "price_token_in": 15, + "per_quantity_in": 1000000, + "price_token_out": 75, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-opus-4-20250514", + "model_name": "Claude Opus 4", + "brand": "Anthropic", + "price_token_in": 15, + "per_quantity_in": 1000000, + "price_token_out": 75, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-sonnet-4-20250514", + "model_name": "Claude Sonnet 4", + "brand": "Anthropic", + "price_token_in": 3, + "per_quantity_in": 1000000, + "price_token_out": 15, + "per_quantity_out": 1000000 + }, + { + "model_id": "claude-sonnet-4-5-20250929", + "model_name": "Claude Sonnet 4.5", + "brand": "Anthropic", + "price_token_in": 3, + "per_quantity_in": 1000000, + "price_token_out": 15, + "per_quantity_out": 1000000 + }, + { + "model_id": "gemini-2.0-flash-001", + "model_name": "Gemini 2.0 Flash", + "brand": "Google", + "price_token_in": 0.1, + "per_quantity_in": 1000000, + "price_token_out": 0.4, + "per_quantity_out": 1000000 + }, + { + "model_id": "gemini-2.0-flash-lite-001", + "model_name": "Gemini 2.0 Flash-Lite", + "brand": "Google", + "price_token_in": 0.075, + "per_quantity_in": 1000000, + "price_token_out": 0.3, + "per_quantity_out": 1000000 + }, + { + "model_id": "gemini-2.5-flash", + "model_name": "Gemini 2.5 Flash", + "brand": "Google", + "price_token_in": 0.3, + "per_quantity_in": 1000000, + "price_token_out": 2.5, + "per_quantity_out": 1000000 + }, + { + "model_id": "gemini-2.5-flash-lite", + "model_name": "Gemini 2.5 Flash-Lite", + "brand": "Google", + "price_token_in": 0.1, + "per_quantity_in": 1000000, + "price_token_out": 0.4, + "per_quantity_out": 1000000 + }, + { + "model_id": "gemini-2.5-pro", + "model_name": "Gemini 2.5 Pro", + "brand": "Google", + "price_token_in": 1.25, + "per_quantity_in": 1000000, + "price_token_out": 10, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-3.5-turbo", + "model_name": "GPT-3.5 Turbo", + "brand": "OpenAI", + "price_token_in": 0.5, + "per_quantity_in": 1000000, + "price_token_out": 1.5, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4", + "model_name": "GPT-4", + "brand": "OpenAI", + "price_token_in": 30, + "per_quantity_in": 1000000, + "price_token_out": 60, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4-turbo", + "model_name": "GPT-4 Turbo", + "brand": "OpenAI", + "price_token_in": 10, + "per_quantity_in": 1000000, + "price_token_out": 30, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4.1", + "model_name": "GPT-4.1", + "brand": "OpenAI", + "price_token_in": 2, + "per_quantity_in": 1000000, + "price_token_out": 8, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4.1-mini", + "model_name": "GPT-4.1 Mini", + "brand": "OpenAI", + "price_token_in": 0.4, + "per_quantity_in": 1000000, + "price_token_out": 0.6, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4.1-nano", + "model_name": "GPT-4.1 Nano", + "brand": "OpenAI", + "price_token_in": 0.1, + "per_quantity_in": 1000000, + "price_token_out": 0.4, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4o", + "model_name": "GPT-4o", + "brand": "OpenAI", + "price_token_in": 5, + "per_quantity_in": 1000000, + "price_token_out": 20, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4o-2024-11-20", + "model_name": "GPT-4o (2024-11-20)", + "brand": "OpenAI", + "price_token_in": 5, + "per_quantity_in": 1000000, + "price_token_out": 15, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-4o-mini", + "model_name": "GPT-4o Mini", + "brand": "OpenAI", + "price_token_in": 0.6, + "per_quantity_in": 1000000, + "price_token_out": 2.4, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-5", + "model_name": "GPT 5", + "brand": "OpenAI", + "price_token_in": 1.25, + "per_quantity_in": 1000000, + "price_token_out": 10, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-5-mini", + "model_name": "GPT 5 Mini", + "brand": "OpenAI", + "price_token_in": 0.25, + "per_quantity_in": 1000000, + "price_token_out": 2, + "per_quantity_out": 1000000 + }, + { + "model_id": "gpt-5-nano", + "model_name": "GPT 5 Nano", + "brand": "OpenAI", + "price_token_in": 0.05, + "per_quantity_in": 1000000, + "price_token_out": 0.4, + "per_quantity_out": 1000000 + } +] \ No newline at end of file diff --git a/electron/database/models/Story.ts b/electron/database/models/Story.ts new file mode 100755 index 0000000..22d93d8 --- /dev/null +++ b/electron/database/models/Story.ts @@ -0,0 +1,712 @@ +export interface VerbalTimeProps{ + actions: string; + descriptions: string; + dialogues: string; + thoughts: string; + summary: string; +} + +export interface DialogueProps{ + description: string; + example: string; +} + +export default class Story { + constructor() {} + + static getVerbesStyle(verbalTimeValue: number, level: number, lang: "fr" | "en"): VerbalTimeProps { + switch (verbalTimeValue) { + case 1: // Passé Simple / Simple Past + return { + actions: level === 1 + ? (lang === "fr" ? 'Passé composé' : 'Past perfect') + : level === 2 + ? (lang === "fr" ? 'Passé simple' : 'Simple past') + : (lang === "fr" ? 'Passé simple + passé antérieur' : 'Simple past + past perfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + plus-que-parfait' : 'Imperfect + pluperfect'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé composé' : 'Present + present perfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Plus-que-parfait + subjonctif passé' : 'Pluperfect + past subjunctive'), + summary: lang === "fr" ? '→ Narrations épurées, style classique' : '→ Clean narratives, classical style', + }; + + case 2: // Passé Immédiat / Immediate Past + return { + actions: level === 1 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Passé composé + présent' : 'Present perfect + present'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + participe passé' : 'Imperfect + past participle'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé composé' : 'Present + present perfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé composé' : 'Present + present perfect'), + summary: lang === "fr" ? '→ Témoignages, récits autobiographiques' : '→ Testimonies, autobiographical narratives', + }; + + case 3: // Passé Profond / Deep Past + return { + actions: level === 1 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Passé antérieur' : 'Past anterior'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Plus-que-parfait + conditionnel passé' : 'Pluperfect + past conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Plus-que-parfait + subjonctif passé' : 'Pluperfect + past subjunctive'), + summary: lang === "fr" ? '→ Flashbacks littéraires, tragédies' : '→ Literary flashbacks, tragedies', + }; + + case 4: // Présent Brut / Raw Present + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + participe présent' : 'Present + present participle'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + adjectifs' : 'Present + adjectives'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + impératif' : 'Present + imperative'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + infinitif' : 'Present + infinitive'), + summary: lang === "fr" ? '→ Urgence, immersion totale' : '→ Urgency, total immersion', + }; + + case 5: // Présent Réflexif / Reflective Present + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + gérondif' : 'Present + gerund'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + adjectifs' : 'Present + adjectives') + : (lang === "fr" ? 'Présent + métaphores' : 'Present + metaphors'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + style indirect libre' : 'Present + free indirect style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + subjonctif présent' : 'Present + present subjunctive'), + summary: lang === "fr" ? '→ Méditations philosophiques' : '→ Philosophical meditations', + }; + + case 6: // Futur Projeté / Projected Future + return { + actions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + dialogues: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur simple + conditionnel' : 'Simple future + conditional'), + thoughts: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + summary: lang === "fr" ? '→ Prophéties, plans stratégiques' : '→ Prophecies, strategic plans', + }; + + case 7: // Futur Catastrophe / Catastrophic Future + return { + actions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur + conditionnel' : 'Future perfect + conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur simple + impératif' : 'Simple future + imperative'), + thoughts: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Conditionnel présent' : 'Present conditional'), + summary: lang === "fr" ? '→ Dystopies, récits post-apocalyptiques' : '→ Dystopias, post-apocalyptic narratives', + }; + + case 8: // Imparfait Onirique / Dreamlike Imperfect + return { + actions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + conditionnel présent' : 'Imperfect + present conditional'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + conditionnel présent' : 'Imperfect + present conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + conditionnel présent' : 'Imperfect + present conditional'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + subjonctif présent' : 'Imperfect + present subjunctive'), + summary: lang === "fr" ? '→ Rêves, souvenirs déformés' : '→ Dreams, distorted memories', + }; + + case 9: // Conditionnel Hypothétique / Hypothetical Conditional + return { + actions: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + descriptions: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel présent + subjonctif' : 'Present conditional + subjunctive'), + thoughts: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + summary: lang === "fr" ? '→ Uchronies, réalités alternatives' : '→ Alternate histories, alternative realities', + }; + + case 10: // Subjonctif Angoissé / Anxious Subjunctive + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + subjonctif présent' : 'Present + present subjunctive'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + subjonctif présent' : 'Present + present subjunctive'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + interjections' : 'Present + interjections') + : (lang === "fr" ? 'Présent + subjonctif présent' : 'Present + present subjunctive'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Subjonctif présent' : 'Present subjunctive'), + summary: lang === "fr" ? '→ Drames psychologiques, dilemmes' : '→ Psychological dramas, dilemmas', + }; + + case 11: // Mélancolie Composée / Compound Melancholy + return { + actions: level === 1 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Passé composé + plus-que-parfait' : 'Present perfect + pluperfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + conditionnel passé' : 'Imperfect + past conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Plus-que-parfait + conditionnel passé' : 'Pluperfect + past conditional'), + summary: lang === "fr" ? '→ Regrets, introspection nostalgique' : '→ Regrets, nostalgic introspection', + }; + + case 12: // Urgence Narrative / Narrative Urgency + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + participe présent' : 'Present + present participle'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + adjectifs courts' : 'Present + short adjectives'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + impératif' : 'Present + imperative') + : (lang === "fr" ? 'Impératif + présent' : 'Imperative + present'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + infinitif' : 'Present + infinitive'), + summary: lang === "fr" ? '→ Crise en cours, compte à rebours' : '→ Ongoing crisis, countdown', + }; + + case 13: // Présent Émotionnel / Emotional Present + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + participe présent' : 'Present + present participle'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + adjectifs' : 'Present + adjectives') + : (lang === "fr" ? 'Présent + adjectifs expressifs' : 'Present + expressive adjectives'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + interjections' : 'Present + interjections') + : (lang === "fr" ? 'Présent + style expressif' : 'Present + expressive style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + exclamations' : 'Present + exclamations'), + summary: lang === "fr" ? '→ Émotions intenses, introspections vives' : '→ Intense emotions, vivid introspections', + }; + + case 14: // Présent Introspectif / Introspective Present + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + gérondif' : 'Present + gerund'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent + adjectifs' : 'Present + adjectives') + : (lang === "fr" ? 'Présent + métaphores' : 'Present + metaphors'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Style indirect libre' : 'Free indirect style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + subjonctif présent' : 'Present + present subjunctive'), + summary: lang === "fr" ? '→ Réflexions profondes, analyse des émotions' : '→ Deep reflections, emotional analysis', + }; + + case 15: // Présent Historique / Historical Present + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé simple' : 'Present + simple past'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + imparfait' : 'Present + imperfect'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé composé' : 'Present + present perfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + plus-que-parfait' : 'Present + pluperfect'), + summary: lang === "fr" ? '→ Histoires historiques avec intensité immédiate' : '→ Historical stories with immediate intensity', + }; + + case 16: // Passé Réflexif / Reflective Past + return { + actions: level === 1 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Passé composé + plus-que-parfait' : 'Present perfect + pluperfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + conditionnel passé' : 'Imperfect + past conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : (lang === "fr" ? 'Style indirect libre' : 'Free indirect style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + subjonctif passé' : 'Imperfect + past subjunctive'), + summary: lang === "fr" ? '→ Récits introspectifs, auto-analyse' : '→ Introspective narratives, self-analysis', + }; + + case 17: // Futur Prophétique / Prophetic Future + return { + actions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + dialogues: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur simple + présent gnomique' : 'Simple future + gnomic present'), + thoughts: level === 1 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : level === 2 + ? (lang === "fr" ? 'Futur simple' : 'Simple future') + : (lang === "fr" ? 'Futur antérieur' : 'Future perfect'), + summary: lang === "fr" ? '→ Prophéties, visions apocalyptiques' : '→ Prophecies, apocalyptic visions', + }; + + case 18: // Conditionnel Visionnaire / Visionary Conditional + return { + actions: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + descriptions: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel présent + subjonctif' : 'Present conditional + subjunctive'), + thoughts: level === 1 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : level === 2 + ? (lang === "fr" ? 'Conditionnel présent' : 'Present conditional') + : (lang === "fr" ? 'Conditionnel passé' : 'Past conditional'), + summary: lang === "fr" ? '→ Mondes parallèles, uchronies' : '→ Parallel worlds, alternate histories', + }; + + case 19: // Imparfait Poétique / Poetic Imperfect + return { + actions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + participe présent' : 'Imperfect + present participle'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait + adjectifs' : 'Imperfect + adjectives') + : (lang === "fr" ? 'Imparfait + métaphores' : 'Imperfect + metaphors'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Style poétique libre' : 'Free poetic style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + subjonctif présent' : 'Imperfect + present subjunctive'), + summary: lang === "fr" ? '→ Lyrisme, poésie narrative' : '→ Lyricism, narrative poetry', + }; + + case 20: // Second Person Narrative / Narration à la deuxième personne + return { + actions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + imparfait' : 'Present + imperfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + conditionnel présent' : 'Present + present conditional'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + style direct' : 'Present + direct style'), + thoughts: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + conditionnel présent' : 'Present + present conditional'), + summary: lang === "fr" ? '→ Immersion totale, récits interactifs' : '→ Total immersion, interactive narratives', + }; + + default: + return { + actions: level === 1 + ? (lang === "fr" ? 'Passé composé' : 'Present perfect') + : level === 2 + ? (lang === "fr" ? 'Passé simple' : 'Simple past') + : (lang === "fr" ? 'Passé simple + passé antérieur' : 'Simple past + past perfect'), + descriptions: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : (lang === "fr" ? 'Imparfait + plus-que-parfait' : 'Imperfect + pluperfect'), + dialogues: level === 1 + ? (lang === "fr" ? 'Présent' : 'Present') + : level === 2 + ? (lang === "fr" ? 'Présent' : 'Present') + : (lang === "fr" ? 'Présent + passé composé' : 'Present + present perfect'), + thoughts: level === 1 + ? (lang === "fr" ? 'Imparfait' : 'Imperfect') + : level === 2 + ? (lang === "fr" ? 'Plus-que-parfait' : 'Pluperfect') + : (lang === "fr" ? 'Plus-que-parfait + subjonctif passé' : 'Pluperfect + past subjunctive'), + summary: lang === "fr" ? '→ Narrations épurées, style classique' : '→ Clean narratives, classical style', + }; + } + } + + static getDialogueType(value: number, level: number, lang: "fr" | "en"): string { + if (lang === "fr") { + // Version française existante + if (level === 1 /* Débutant - Secondaire 5 */) { + switch (value) { + case 1: + return 'Dialogue direct - Paroles exactes (ex: "Je t\'aime !")'; + case 2: + return 'Dialogue indirect - Résumé par le narrateur (ex: Il dit qu\'il m\'aime)'; + default: + return 'Dialogue direct'; + } + } else if (level === 2 /* Intermédiaire - Collégial */) { + switch (value) { + case 1: + return 'Dialogue direct'; + case 2: + return 'Dialogue indirect'; + case 3: + return 'Dialogue mixte (ex: "Je t\'aime" dit-il, puis explique ses sentiments)'; + default: + return 'Dialogue direct'; + } + } else if (level === 3 /* Avancé - Universitaire/Littéraire */) { + switch (value) { + case 1: + return 'Dialogue direct'; + case 2: + return 'Dialogue indirect'; + case 3: + return 'Dialogue mixte'; + case 4: + return 'Monologue intérieur (ex: *Je ne peux pas le perdre...*)'; + default: + return 'Dialogue direct'; + } + } + } else if (lang === "en") { + // Version anglaise canadienne + if (level === 1 /* Beginner - Grade 12 level */) { + switch (value) { + case 1: + return 'Direct dialogue - Exact words (e.g., "I love you!")'; + case 2: + return 'Indirect dialogue - Summarized by narrator (e.g., He said that he loved me)'; + default: + return 'Direct dialogue'; + } + } else if (level === 2 /* Intermediate - College level */) { + switch (value) { + case 1: + return 'Direct dialogue'; + case 2: + return 'Indirect dialogue'; + case 3: + return 'Mixed dialogue (e.g., "I love you," he said, then explained his feelings)'; + default: + return 'Direct dialogue'; + } + } else if (level === 3 /* Advanced - University/Literary level */) { + switch (value) { + case 1: + return 'Direct dialogue'; + case 2: + return 'Indirect dialogue'; + case 3: + return 'Mixed dialogue'; + case 4: + return 'Interior monologue (e.g., *I cannot lose him...*)'; + default: + return 'Direct dialogue'; + } + } + } + + return lang === "fr" ? 'Dialogue direct' : 'Direct dialogue'; + } + static getLanguage(value: number): string { + switch (value) { + case 1: // Français Canada + return 'Français Canada'; + case 2: // Français France + return 'Français France'; + case 3: // Français Québécois + return 'Français Québécois'; + case 4: // Anglais + return 'English Canada'; + default: + return 'Français Canada'; + } + } + + static getFormat(value: number): string { + switch (value) { + case 1: // Français Canada (espace fine « texte ») + case 3: // Français Québécois + return '«dialogue» - avec guillemet - [Espace fine insécable]'; + case 2: // Français France + return '« dialogue » - avec guillemet - [Espace insécable standard]'; + case 4: // Anglais + return `"dialogue" - or with a dash - [No space]`; + default: + return 'Format : « dialogue » - avec guillemet - [Espace fine insécable]'; + } + } + + static getNarrativePerson(value: number, level: number, lang: "fr" | "en"): string { + if (level === 1) { + switch (value) { + case 1: + return lang === "fr" ? 'Première personne (Je acteur)' : 'First Person (I as actor)'; + case 3: + return lang === "fr" ? 'Troisième omnisciente - Narration globale' : 'Third Person Omniscient - Global narration'; + default: + return 'Première personne'; + } + } else if (level === 2) { + switch (value) { + case 1: + return lang === "fr" ? 'Première personne (Je acteur)' : 'First Person (I as actor)'; + case 2: + return lang === "fr" ? 'Première personne (Je témoin) - Observateur' : 'First Person (I as witness) - Observer'; + case 3: + return lang === "fr" ? 'Troisième omnisciente' : 'Third Person Omniscient'; + case 4: + return lang === "fr" ? 'Troisième limitée - Focus sur un personnage' : 'Third Person Limited - Focus on a character'; + default: + return 'Première personne'; + } + } else if (level === 3) { + switch (value) { + case 1: + return lang === "fr" ? 'Première personne (Je acteur)' : 'First Person (I as actor)'; + case 2: + return lang === "fr" ? 'Première personne (Je témoin) - Observateur' : 'First Person (I as witness) - Observer'; + case 3: + return lang === "fr" ? 'Troisième omnisciente' : 'Third Person Omniscient'; + case 4: + return lang === "fr" ? 'Troisième limitée - Focus sur un personnage' : 'Third Person Limited - Focus on a character'; + case 5: + return lang === "fr" ? 'Deuxième personne (Tu) - Immersion forte' : 'Second Person (You) - Strong immersion'; + case 6: + return lang === "fr" ? 'Nous collectif - Voix chorale' : 'We Collective - Choral voice'; + default: + return lang === "fr" ? 'Troisième omnisciente' : 'Third Person Omniscient'; + } + } + return 'Première personne'; + } + + static getStoryState(value: number): string { + switch (value) { + case 0: + return 'Continue à partir de la mise en contexte avec cela :'; + case 1: + return 'Débutons le chapitre avec :'; + case 2: + return 'Continue à partir de la mise en contexte et ferme le chapitre avec :'; + case 3: + return `Commençons le l'histoire avec :`; + case 4: + return 'Terminons l\'histoire avec :'; + default: + return 'Continue à partir de la mise en contexte avec cela :'; + } + } +} diff --git a/electron/database/models/User.ts b/electron/database/models/User.ts new file mode 100644 index 0000000..8563a3a --- /dev/null +++ b/electron/database/models/User.ts @@ -0,0 +1,885 @@ +import UserRepo, { + BasicUserCredentials, + GuideTourResult, + PasswordResponse, + TOTPQuery, + UserAccountQuery, + UserAiUsageResult, + UserAPIKeyResult, + UserInfosQueryResponse, + UserQueryResponse, + UserSubscription +} from "../repositories/user.repo"; +import {FetchQueryResponse} from "../../config/SharedInterface"; +import System, {EncryptedKey, EncryptedUserKey, UserKey} from "./System"; +import {FastifyInstance} from "fastify"; +import EmailTemplate from "./EmailTemplate"; +import path from "path"; +import fs from "fs"; +import {Secret, TOTP} from "otpauth"; +import Book, {BookProps} from "./Book"; +import {JWT} from "@fastify/jwt"; +import {AIBrand} from "./AI"; +import {ConflictError, NotFoundError} from "../error"; +import {getLanguage} from "../context"; +import Subscription, { SubscriptionInfo } from "./Subscription"; + +export type UserLanguage = 'fr' | 'en'; + +export interface LoginData{ + valid:boolean; + token?:string; + id?:string; + message?:string; +} + +export interface UserAPIKey { + brand: AIBrand, + key: string; +} + +interface UserAccount{ + firstName:string; + lastName:string; + username:string + authorName:string; + email:string; +} + +export interface TOTPData{ + totpCode:string; +} + +export interface GuideTour { + [key: string]: boolean; +} + +export interface UserSubscriptionProps { + userId: string; + subType: string; + subTier: number; + startDate: string; + endDate: string; + status: number; +} + +interface UserSubscriptionInfo { + subType: string; + subTier: number; + status: boolean; +} + +export default class User{ + + private readonly id:string; + private firstName: string; + private lastName: string; + private username: string; + private email: string; + private platform: string; + private accountVerified: boolean; + private authorName: string; + private writingLang: number; + private writingLevel: number; + private ritePoints: number; + private groupId: number; + private balancedCredits: number; + private termsAccepted: boolean; + + constructor(id:string){ + this.id = id; + this.firstName = ''; + this.lastName = ''; + this.username = ''; + this.email = ''; + this.platform = ''; + this.accountVerified = false; + this.authorName = ''; + this.writingLang = 0; + this.writingLevel = 1; + this.ritePoints = 0; + this.groupId = 0; + this.balancedCredits = 0; + this.termsAccepted = false; + } + + public async getUserInfos(): Promise { + const data: UserInfosQueryResponse = await UserRepo.fetchUserInfos(this.id); + const userKey: string = await this.getUserKey(data.user_meta); + this.firstName = System.decryptDataWithUserKey(data.first_name, userKey); + this.lastName = System.decryptDataWithUserKey(data.last_name, userKey); + this.username = System.decryptDataWithUserKey(data.username, userKey); + this.email = System.decryptDataWithUserKey(data.email, userKey); + this.platform = data.plateform; + this.accountVerified = data.account_verified === 1; + this.authorName = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : ''; + this.writingLang = data.writing_lang ? data.writing_lang : 1; + this.writingLevel = data.writing_level ? data.writing_level : 1; + this.groupId = data.user_group ? data.user_group : 0; + this.ritePoints = data.rite_points ? data.rite_points : 0; + this.balancedCredits = data.credits_balance ? data.credits_balance : 0; + this.termsAccepted = data.term_accepted === 1; + } + + public static async returnUserInfos(userId: string, plateforme: string) { + const user: User = new User(userId); + await user.getUserInfos(); + const books: BookProps[] = await Book.getBooks(userId); + const guideTourResult: GuideTourResult[] = await UserRepo.fetchGuideTour(userId, plateforme); + const guideTour: GuideTour[] = []; + const requestSubscription: UserSubscriptionProps[] = await User.getSubscription(userId); + const userSubscriptions:SubscriptionInfo = await Subscription.getSubscriptionInfo(userId); + const requestKeys: UserAPIKey[] = await User.getAPIKey(userId); + let moneySpentThisMonth: number = 0; + if (requestKeys.length > 0) { + moneySpentThisMonth = await User.moneySpentThisMonth(userId) + } + const subscriptions: UserSubscriptionInfo[] = []; + for (const userSubscriptionProp of requestSubscription) { + subscriptions.push({ + subType: userSubscriptionProp.subType, + subTier: userSubscriptionProp.subTier, + status: userSubscriptionProp.status === 1 + }); + } + + if (guideTourResult) { + guideTourResult.forEach((tour: GuideTourResult): void => { + guideTour.push({ + [tour.step_tour]: true + }); + }); + } + return { + id: user.getId(), + name: user.getFirstName(), + lastName: user.getLastName(), + username: user.getUsername(), + email: user.getEmail(), + accountVerified: user.isAccountVerified(), + authorName: user.getAuthorName(), + writingLang: user.getWritingLang(), + writingLevel: user.getWritingLevel(), + ritePoints: user.getRitePoints(), + groupId: user.getGroupId(), + termsAccepted: user.isTermsAccepted(), + guideTour: guideTour, + subscription: subscriptions, + aiUsage: moneySpentThisMonth, + creditsBalance: userSubscriptions.totalCredits, + apiKeys: { + gemini: !!requestKeys.find((key: UserAPIKey): boolean => { + return key.brand === 'gemini' + }), + openai: !!requestKeys.find((key: UserAPIKey): boolean => { + return key.brand === 'openai' + }), + anthropic: !!requestKeys.find((key: UserAPIKey): boolean => { + return key.brand === 'anthropic' + }), + }, + books: books.map((book: BookProps) => { + return { + bookId: book.id, + title: book.title, + subTitle: book.subTitle, + }; + }) + } + } + + public static async isTwoFactorEnabled(userId: string): Promise { + return UserRepo.isTwoFactorEnabled(userId); + } + + public static async changePassword(userId: string, password: string, newPassword: string, email: string): Promise { + if (await User.validUser(email, password)) { + const newPasswordHash: string = await System.hashPassword(newPassword); + return await UserRepo.updatePassword(userId, System.hashElement(email), newPasswordHash); + } else { + throw new Error('Your password is incorrect'); + } + } + + public static async deleteAccount(userId: string, email: string): Promise { + return UserRepo.deleteAccount(userId, System.hashElement(email)); + } + + public static async resetPassword(newPassword: string, email: string, verifyCode: string): Promise { + const encryptPassword: string = await System.hashPassword(newPassword); + const user: PasswordResponse = await UserRepo.verifyUserAuth(System.hashElement(email), verifyCode) + const userId: string = user.user_id; + return await UserRepo.updatePassword(userId, System.hashElement(email), encryptPassword); + } + + public static async verifyCode(verifyCode: string, email: string): Promise { + return await UserRepo.fetchVerifyCode(System.hashElement(email), verifyCode); + } + + static async subscribeToBeta(email: string): Promise { + let checkEmail: string[] = email.split('@'); + checkEmail = checkEmail[1].split('.'); + if (checkEmail[0] === 'gmail' || checkEmail[0] === 'hotmail' || checkEmail[0] === 'outlook') { + if (await UserRepo.checkBetaSubscriber(email)) { + throw new Error(`L'adresse courriel est déjà enregistré.`); + } + const insertEmail: boolean = await UserRepo.insertBetaSubscriber(email); + if (insertEmail) { + System.sendEmailTo(email, 'Inscription à la bêta.', '', EmailTemplate('newsletter', {})); + return insertEmail; + } else { + throw new Error('Une erreur est survenue lors de l\'inscription à la bêta.'); + } + } else { + throw new Error('Votre adresse email n\'est pas valide.'); + } + } + + public static async addUser(firstName: string, lastName: string, username: string, email: string, notEncryptPassword: string, userId: string | null = null, provider: string = 'credential', socialId: string | null = null, tempAdd: boolean = false): Promise { + const originEmail:string = System.hashElement(email); + const originUsername:string = System.hashElement(username); + await this.checkUser(originUsername, originEmail); + const lang: "fr" | "en" = getLanguage() + if (tempAdd) { + const password:string = notEncryptPassword ? await System.hashPassword(notEncryptPassword) : ''; + const newUserId: string = System.createUniqueId(); + await UserRepo.insertTempUser(newUserId, firstName, lastName, username, originUsername, email, originEmail, password); + await User.sendVerifyCode(username, email); + return newUserId; + } else { + if (!userId) { + throw new Error(lang === 'fr' ? 'L\'identifiant utilisateur est requis.' : 'User ID is required.'); + } + const encryptData: EncryptedUserKey = await System.generateAndEncryptUserKey(userId); + const userKey: string = encryptData.userKey; + const encryptFirstName: string = System.encryptDataWithUserKey(firstName, userKey); + const encryptLastName: string = System.encryptDataWithUserKey(lastName, userKey); + const encryptUsername: string = System.encryptDataWithUserKey(username, userKey); + const encryptEmail: string = System.encryptDataWithUserKey(email, userKey); + const originalEmail: string = System.hashElement(email); + const originalUsername: string = System.hashElement(username); + const meta: string = System.encryptDateKey(userId); + const insertedUserId: string = await UserRepo.insertUser(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail, notEncryptPassword, meta, socialId, provider); + await System.storeUserKey(userId, encryptData.encryptedKey); + await UserRepo.deleteTempUser(userId); + return insertedUserId; + } + } + + public static async updateUserInfos(userKey: string, userId: string, firstName: string, lastName: string, username: string, email: string, authorName?: string): Promise { + const encryptFirstName:string = System.encryptDataWithUserKey(firstName,userKey); + const encryptLastName:string = System.encryptDataWithUserKey(lastName,userKey); + const encryptUsername:string = System.encryptDataWithUserKey(username,userKey); + const encryptEmail:string = System.encryptDataWithUserKey(email,userKey); + const originalEmail:string = System.hashElement(email); + const originalUsername:string = System.hashElement(username); + const userMeta:string = System.encryptDateKey(userId); + let encryptAuthorName:string = ''; + let originalAuthorName: string = ''; + if (authorName){ + encryptAuthorName = System.encryptDataWithUserKey(authorName,userKey); + originalAuthorName = System.hashElement(authorName); + } + return UserRepo.updateUserInfos(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail, userMeta, originalAuthorName, encryptAuthorName); + } + public async getUserKey(metaKey:string,isNeedToBeDecrypt:boolean=true,keys?:UserKey[]):Promise{ + let meta:string = ''; + if (isNeedToBeDecrypt){ + meta = System.decryptDateKey(this.id,metaKey); + } else { + meta = metaKey; + } + if (keys){ + return System.getClosestPreviousKey(keys,meta); + } else { + const userKey:FetchQueryResponse = await System.getUserKey(this.id,meta); + if (userKey.data){ + return userKey.data?.encrypted_key; + } else { + return ''; + } + } + } + + public static async checkUserName(username:string):Promise{ + return UserRepo.fetchUserName(username); + } + + public static async checkEmail(email:string):Promise{ + return UserRepo.fetchEmail(email); + } + + static getLevelDetail(level: number, lang: "fr" | "en"): string { + if (lang === "fr") { + switch (level) { + case 1: // Débutant français + return `**Niveau : Débutant** +**Objectif** : Texte écrit par un élève de secondaire qui apprend encore à écrire en français. +**Lexique** : +- **Vocabulaire limité** : Répétition des mêmes mots simples, manque de synonymes. +- **Exemples typiques** : "il y a", "c'est", "très", "beaucoup", "faire", "aller", "dire". +- **Caractéristiques** : Anglicismes possibles, répétitions non voulues. +**Structure** : +- **Phrases courtes par nécessité** : 8-15 mots (le français tolère plus de mots). +- **Constructions simples** : Sujet-Verbe-Complément, quelques "qui/que" basiques. +- **Coordinations basiques** : "et", "mais", "alors", "après". +- **Erreurs typiques** : "Il y a des arbres qui sont grands" au lieu de "Les arbres sont grands". +**Style littéraire** : +- **Descriptions factuelle** : "La forêt était sombre et silencieuse." +- **Pas de figures de style** : Tentatives maladroites si présentes. +- **Français scolaire** : Formulations apprises par cœur. +**Exemples comparatifs** : +✅ "Marc a couru dans la forêt. Il avait très peur des bruits qu'il entendait." +❌ "Marc s'élança dans les profondeurs sylvestres, son cœur battant la chamade..." +`; + + case 2: // Intermédiaire français + return `**Niveau : Intermédiaire** +**Objectif** : Texte écrit par un étudiant de cégep/université qui expérimente le style français. +**Lexique** : +- **Vocabulaire enrichi** : Recherche de synonymes, mots plus soutenus. +- **Exemples** : "s'élancer", "contempler", "mystérieux", "murmurer", "lueur". +- **Tentatives stylistiques** : Parfois réussies, parfois précieuses. +**Structure** : +- **Phrases moyennes à longues** : 12-25 mots, avec subordinations. +- **Constructions élaborées** : Relatives multiples, quelques inversions. +- **Style français émergent** : Tentatives de périodes, de nuances. +- **Connecteurs plus variés** : "cependant", "néanmoins", "tandis que". +**Style littéraire** : +- **Premières métaphores** : Simples mais françaises ("tel un fantôme"). +- **Descriptions étoffées** : Adjectifs et compléments circonstanciels. +- **Registre soutenu recherché** : Parfois forcé. +**Exemples comparatifs** : +✅ "Marc s'élança dans la forêt obscure, tandis que chaque bruissement mystérieux faisait naître en lui une crainte sourde." +❌ "Marc se précipita avec véhémence dans les méandres ténébreux de la sylve ancestrale..." +`; + + case 3: // Avancé français + return `**Niveau : Avancé** +**Objectif** : Texte écrit par un écrivain professionnel maîtrisant la rhétorique française. +**Lexique** : +- **Richesse lexicale maîtrisée** : Précision du mot juste, nuances subtiles. +- **Registres variés** : Du familier au soutenu selon l'effet recherché. +- **Synonymie élégante** : Évitement des répétitions par art, non par contrainte. +**Structure** : +- **Périodes françaises** : Phrases longues et rythmées (20-40 mots possibles). +- **Architecture complexe** : Subordinations enchâssées, incises, inversions maîtrisées. +- **Rythme classique** : Alternance entre brièveté saisissante et amplitude oratoire. +- **Ponctuation expressive** : Points-virgules, deux-points, parenthèses. +**Style littéraire** : +- **Figures maîtrisées** : Métaphores filées, antithèses, chiasmes. +- **Tradition française** : Élégance, clarté, harmonie des sonorités. +- **Sous-entendus et allusions** : Subtilité dans l'évocation. +**Exemples comparatifs** : +✅ "Marc courut." (Simplicité voulue) +✅ "Dans l'entrelacement des ombres que la lune, filtrant à travers la ramure, dessinait sur le sol moussu, Marc discernait les échos de sa propre terreur ; terreur ancestrale, née de ce dialogue éternel entre l'homme et la nuit." +`; + default: + return ``; + } + } else { // English Canadian + switch (level) { + case 1: // Beginner English + return `**Level: Beginner** +**Target**: Text written by a high school student still learning English composition. +**Vocabulary**: +- **Limited word choice**: Repetition of basic words, simple alternatives. +- **Typical examples**: "said", "went", "got", "thing", "stuff", "really", "very". +- **Characteristics**: Overuse of "and then", simple connectors. +**Structure**: +- **Short, choppy sentences**: 5-10 words (English naturally shorter). +- **Basic constructions**: Subject-Verb-Object, simple coordination. +- **Limited variety**: Mostly declarative sentences. +- **Common errors**: Run-on sentences with "and", fragments. +**Literary style**: +- **Plain description**: "The forest was dark. It was scary." +- **No literary devices**: Attempts at metaphors usually failed. +- **Direct emotion**: "He was afraid" (English directness). +**Comparative examples**: +✅ "Mark ran into the forest. He was scared. The trees looked big and dark." +❌ "Mark ventured forth into the sylvan depths, his heart pounding with trepidation..." +`; + + case 2: // Intermediate English + return `**Level: Intermediate** +**Target**: Text written by a college student developing English writing skills. +**Vocabulary**: +- **Expanded vocabulary**: Some sophisticated words, better word choice. +- **Examples**: "ventured", "glimpsed", "mysterious", "whispered", "shadowy". +- **Characteristics**: Occasional overreach, some pretentious word choices. +**Structure**: +- **Varied sentence length**: 8-18 words, some complex sentences. +- **Better flow**: Proper use of conjunctions, some subordination. +- **Paragraph development**: Topic sentences, basic transitions. +- **English rhythm**: Shorter than French but with variation. +**Literary style**: +- **Simple imagery**: Basic metaphors and similes that work. +- **Show don't tell**: Attempts at indirect description. +- **English conciseness**: Getting to the point while adding style. +**Comparative examples**: +✅ "Mark plunged into the dark forest. Every sound made him jump, like whispers from invisible watchers." +❌ "Mark precipitously advanced into the tenebrous woodland whilst phantasmagorical emanations..." +`; + + case 3: // Advanced English + return `**Level: Advanced** +**Target**: Text written by a professional English-speaking author. +**Vocabulary**: +- **Precise word choice**: Economy of language, powerful verbs. +- **Understated elegance**: Sophisticated but not showy. +- **Anglo-Saxon vs. Latin**: Strategic use of both registers. +**Structure**: +- **Masterful variety**: From punchy fragments to flowing periods. +- **English rhythm**: Natural speech patterns, emphasis through structure. +- **Controlled complexity**: Never convoluted, always clear. +- **Strategic brevity**: Power in conciseness. +**Literary style**: +- **Subtle imagery**: Metaphors that illuminate, don't decorate. +- **Understated power**: English preference for implication over elaboration. +- **Voice and tone**: Distinctive style through restraint and precision. +- **Anglo tradition**: Clarity, wit, emotional resonance through simplicity. +**Comparative examples**: +✅ "Mark ran. The forest swallowed him." (English power in brevity) +✅ "The shadows moved between the trees like living things, and Mark felt something ancient watching from the darkness—something that had been waiting." (Controlled complexity) +`; + default: + return ``; + } + } + } + + static getAudienceDetail(level: number, lang: "fr" | "en"): string { + if (lang === "fr") { + switch (level) { + case 1: // Débutant + return `**Niveau : Débutant** + **Objectif** : Texte accessible à un collégien (18-22 ans). + **Lexique** : + - **Priorité absolue** : Mots du quotidien, fréquents dans la langue parlée. + - **Exemples autorisés** : "marcher", "voir", "dire", "beau", "peur", "lumière". + - **Mots à éviter** : "luminescence", "éthéré", "volutes", "chuchotements", "centenaires". + **Structure** : + - **Longueur moyenne maximale par phrase** : 10 mots. + - **Interdit** : Phrases composées, subordonnées, ou parenthèses. + - **Style des descriptions** : Directes et visuelles (ex: "La forêt était sombre" → pas de "La forêt respirait la nuit"). + **Style littéraire** : + - **Interdits** : Métaphores, similes, ou figures de style. + - **Priorité** : Clarté et simplicité absolue. + **Exemples comparatifs** : + ❌ "La brume enveloppait les arbres avec une grâce éthérée" → ✅ "La brume couvrait les arbres." + ❌ "Elle courait, les jambes tremblantes, vers la lumière" → ✅ "Elle courait vers une lumière." + `; + + case 2: // Intermédiaire + return ` + **Niveau : Intermédiaire** + **Objectif** : Texte pour un étudiant universitaire (22-30 ans). + **Lexique** : + - **Priorité** : Vocabulaire varié mais compréhensible (ex: "murmurer", "luminescent", "mystère", "sentinelles"). + - **Mots à éviter** : Termes littéraires complexes (ex: "volutes", "centenaires", "éthéré"). + **Structure** : + - **Longueur moyenne maximale par phrase** : 18 mots. + - **Autorisé** : Phrases composées simples (ex: "Elle courait, mais la forêt était dense"). + - **Descriptions** : Métaphores basiques (ex: "La forêt respirait la nuit"). + **Style littéraire** : + - **Autorisé** : Similes simples (ex: "comme un fantôme"). + - **Interdit** : Figures de style complexes (antithèses, anaphores). + **Exemples comparatifs** : + ❌ "Les arbres, semblables à des sentinelles de pierre, veillaient" → ✅ "Les arbres ressemblaient à des gardiens." + ❌ "La lueur dansante évoquait un espoir éphémère" → ✅ "La lumière clignotait comme un espoir." + `; + + case 3: // Avancé + return ` + **Niveau : Avancé** + **Objectif** : Texte littéraire d'un professionnel littéraire, ayant un diplôme universitaire. + **Lexique** : + - **Priorité** : Vocabulaire riche et littéraire (ex: "volutes", "centenaires", "éthéré", "luminescence"). + - **Mots à éviter** : Termes simples jugés "banals" (ex: "marcher" → préférer "arpenter", "errer"). + **Structure** : + - **Longueur maximale par phrase** : aucune limite. + - **Autorisé** : Phrases complexes et enchaînées (ex: "Parmi les ombres dansantes, elle discerna une lueur vacillante, symbole de l'espoir qui s'évanouissait"). + - **Descriptions** : Métaphores poétiques et figures de style avancées. + **Style littéraire** : + - **Priorité** : Atmosphère immersive et langage élégant. + - **Autorisé** : Antithèses, anaphores, et métaphores multi-niveaux. + **Exemples comparatifs** : + ✅ "Les volutes de brume, tels des spectres en quête d'écho, enveloppaient les troncs centenaires dans un ballet spectral." + ✅ "Luminescence vacillante, espoir éphémère : les deux dansaient une dernière valse avant l'obscurité." + `; + default: + return ``; + } + } else { // English Canadian + switch (level) { + case 1: // Beginner (Grade 11/Secondary 5 level) + return ` + **Level: Beginner** + **Target**: Text accessible to a Grade 11 student (Secondary 5 equivalent). + **Vocabulary**: + - **Absolute priority**: Common everyday words, frequent in spoken language. + - **Allowed examples**: "walk", "see", "say", "nice", "scared", "light". + - **Words to avoid**: "luminescence", "ethereal", "tendrils", "whispers", "ancient". + **Structure**: + - **Maximum average sentence length**: 10 words. + - **Forbidden**: Compound sentences, subordinate clauses, or parentheses. + - **Description style**: Direct and visual (e.g., "The forest was dark" → not "The forest breathed with darkness"). + **Literary style**: + - **Forbidden**: Metaphors, similes, or figures of speech. + - **Priority**: Absolute clarity and simplicity. + **Comparative examples**: + ❌ "The mist enveloped the trees with ethereal grace" → ✅ "The mist covered the trees." + ❌ "She ran, her legs trembling, toward the light" → ✅ "She ran toward a light." + `; + + case 2: // Intermediate (CEGEP/College level) + return ` + **Level: Intermediate** + **Target**: Text for a CEGEP/College student. + **Vocabulary**: + - **Priority**: Varied but understandable vocabulary (e.g., "murmur", "luminous", "mystery", "sentinels"). + - **Words to avoid**: Complex literary terms (e.g., "tendrils", "ancient", "ethereal"). + **Structure**: + - **Maximum average sentence length**: 18 words. + - **Allowed**: Simple compound sentences (e.g., "She ran, but the forest was thick"). + - **Descriptions**: Basic metaphors (e.g., "The forest breathed in the night"). + **Literary style**: + - **Allowed**: Simple similes (e.g., "like a ghost"). + - **Forbidden**: Complex figures of speech (antithesis, anaphora). + **Comparative examples**: + ❌ "The trees, like stone sentinels, stood watch" → ✅ "The trees looked like guardians." + ❌ "The dancing light evoked fleeting hope" → ✅ "The light flickered like hope." + `; + + case 3: // Advanced (University/Professional writer level) + return ` + **Level: Advanced** + **Target**: Literary text from a professional writer or bestselling author with university education. + **Vocabulary**: + - **Priority**: Rich and literary vocabulary (e.g., "tendrils", "ancient", "ethereal", "luminescence"). + - **Words to avoid**: Simple terms deemed "mundane" (e.g., "walk" → prefer "traverse", "wander"). + **Structure**: + - **Maximum sentence length**: No limit. + - **Allowed**: Complex and chained sentences (e.g., "Among the dancing shadows, she discerned a flickering light, a symbol of hope that was fading away"). + - **Descriptions**: Poetic metaphors and advanced figures of speech. + **Literary style**: + - **Priority**: Immersive atmosphere and elegant language. + - **Allowed**: Antithesis, anaphora, and multi-layered metaphors. + **Comparative examples**: + ✅ "Tendrils of mist, like spectres seeking echo, enveloped the ancient trunks in a ghostly ballet." + ✅ "Flickering luminescence, fleeting hope: both danced a final waltz before darkness fell." + `; + default: + return ``; + } + } + } + + public static async checkUser(username: string, email: string): Promise { + const validUsername:boolean = await User.checkUserName(username); + const validEmail:boolean = await User.checkEmail(email); + const lang: "fr" | "en" = getLanguage() + if (validUsername){ + throw new ConflictError(lang === 'fr' ? "Le nom d'utilisateur est déjà utilisé." : 'Username is already taken.'); + } + if (validEmail){ + throw new ConflictError(lang === 'fr' ? "L'adresse courriel est déjà utilisée." : 'Email address is already in use.'); + } + } + + public static async login(email: string, password: string, jwtInstance: FastifyInstance): Promise { + const lang: "fr" | "en" = getLanguage() + if (await User.validUser(email, password)) { + const user: BasicUserCredentials = await UserRepo.fetchUserByCredentials(System.hashElement(email)); + + return jwtInstance.jwt.sign({userId: user.user_id}) + } else { + throw new NotFoundError(lang === 'fr' ? "Nom d'utilisateur ou mot de passe invalide." : "Invalid username or password."); + } + } + + public static async setUserAccountInformation(userId: string, authorName: string, username: string): Promise { + const user:User = new User(userId); + await user.getUserInfos(); + const meta:string = System.encryptDateKey(userId); + const userKey:string = await user.getUserKey(meta); + return User.updateUserInfos(userKey, userId, user.firstName, user.lastName, username, user.getEmail(), authorName); + } + + public static async validUser(email:string,password:string):Promise{ + const validUser: PasswordResponse | null = await UserRepo.verifyUserAuth(System.hashElement(email)); + if (!validUser) { + return false; + } + const stockedPassword: string = validUser.password; + return System.verifyPassword(stockedPassword, password); + } + + public static async socialLogin(firstName: string, lastName: string, email: string | null, socialId: string, provider: string, jwtInstance: JWT): Promise { + const validUserId: string | null = await UserRepo.fetchBySocialId(socialId); + let userId: string = ''; + let newEmail: string = ''; + if (email === null) { + newEmail = socialId + '@' + provider + '.com'; + } else { + newEmail = email; + } + if (!validUserId) { + const emailValidationUserId: string | null = await UserRepo.fetchByEmail(System.hashElement(newEmail)); + if (!emailValidationUserId) { + const randomNum: number = Math.floor(Math.random() * 10000); + const fourDigitNum: string = randomNum.toString().padStart(4, '0'); + const username: string = newEmail.split('@')[0] + fourDigitNum; + userId = System.createUniqueId(); + await this.addUser(firstName, lastName, username, newEmail, "", userId, provider, socialId); + } else { + userId = emailValidationUserId; + await UserRepo.updateSocialId(userId, socialId, provider); + } + } else { + userId = validUserId; + } + + return jwtInstance.sign({userId: userId}) + } + + public static async confirmAccount(verifyCode: string, email: string): Promise { + await UserRepo.fetchVerifyCode(System.hashElement(email), verifyCode); + const account: UserQueryResponse = await UserRepo.fetchTempAccount(email, verifyCode); + const userId: string = account.user_id; + const firstName: string = account.first_name; + const lastName: string = account.last_name; + const username: string = account.username; + const emailAddr: string = account.email; + const password: string = account.password; + const insertUser: string = await this.addUser(firstName, lastName, username, emailAddr, password, userId); + const projectDirectory: string = path.join(process.cwd(), 'uploads', userId.toString(), 'cover'); + + if (!insertUser) { + throw new Error('Une erreur est survenue lors de la confirmation du compte.'); + } + await fs.promises.mkdir(projectDirectory, {recursive: true}) + .catch(error => console.error(`Erreur lors de la création du dossier: ${error}`)); + return true; + } + + public static async sendVerifyCode(username: string, email: string): Promise { + const verifyCode: string = System.createVerifyCode(); + const validUpdate: boolean = await UserRepo.updateVerifyCode(username, username === "" ? System.hashElement(email) : email, verifyCode); + if (validUpdate) { + const options = {code: verifyCode} + System.sendEmailTo(email, 'Votre code de vérification', '', EmailTemplate('verify-code', options)); + } else { + throw new Error('Une erreur est survenue lors de l\'envoi du code de vérification'); + } + } + public static validateTOTPCode(code: string, token: string): boolean { + try { + const totp = new TOTP({ + secret: Secret.fromBase32(code) + }); + + const isValid = totp.validate({ + token, + window: 1 + }); + return isValid !== null; + } catch (error) { + return false; + } + } + + public static async checkTOTPExist(userId:string,email:string):Promise{ + return UserRepo.checkTOTPExist(userId,email); + } + + public static async handleTOTP(userId: string, email: string, verifyCode: string): Promise { + const user = new User(userId); + const meta:string = System.encryptDateKey(userId); + const userKey:string = await user.getUserKey(meta); + const encryptedCode:string = System.encryptDataWithUserKey(verifyCode,userKey); + const encryptedEmail:string = System.hashElement(email); + return UserRepo.insertTOTP(encryptedCode,encryptedEmail,userId,meta,true); + } + + public static async activateTOTP(userId: string, email: string, token: string): Promise { + const data: TOTPQuery = await UserRepo.fetchTempTOTP(userId, System.hashElement(email)); + const user = new User(userId); + const meta: string = data.totp_meta; + const userKey: string = await user.getUserKey(meta); + const code: string = System.decryptDataWithUserKey(data.totp_code, userKey); + if (User.validateTOTPCode(code, token)) { + return UserRepo.insertTOTP(data.totp_code, data.email, userId, meta, false); + } else { + throw new Error('Your token is incorrect'); + } + } + + public static async validateTOTP(userId: string, email: string, token: string): Promise { + const data: TOTPQuery = await UserRepo.fetchTOTP(userId, System.hashElement(email)); + const user = new User(userId); + const meta: string = data.totp_meta; + const userKey: string = await user.getUserKey(meta); + const code: string = System.decryptDataWithUserKey(data.totp_code, userKey); + if (User.validateTOTPCode(code, token)) { + return true; + } else { + throw new Error('Your token is incorrect'); + } + } + + public static async getUserAccountInformation(userId: string): Promise { + const user:User = new User(userId); + const data: UserAccountQuery = await UserRepo.fetchAccountInformation(userId); + const userKey: string = await user.getUserKey(data.user_meta); + const userName: string = data.first_name ? System.decryptDataWithUserKey(data.first_name, userKey) : ''; + const lastName: string = data.last_name ? System.decryptDataWithUserKey(data.last_name, userKey) : ''; + const username: string = data.username ? System.decryptDataWithUserKey(data.username, userKey) : ''; + const authorName: string = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : ''; + const email: string = data.email ? System.decryptDataWithUserKey(data.email, userKey) : ''; + return { + firstName: userName, + lastName: lastName, + username: username, + authorName: authorName, + email: email + }; + } + + public static async getSubscription(userId: string): Promise { + const result: UserSubscription[] = await UserRepo.fetchSubscription(userId); + const userSubscription: UserSubscriptionProps[] = []; + for (const sub of result) { + userSubscription.push({ + userId: sub.user_id, + subType: sub.sub_type, + subTier: sub.sub_tier, + startDate: sub.start_date, + endDate: sub.end_date, + status: sub.status + }) + } + return userSubscription; + } + + public async convertTimeToPoint(time:number):Promise{ + const points:number = Math.ceil(time/30); + await UserRepo.updatePoints(points,this.id); + } + + static async setUserPreferences(userId: string, writingLang: number, writingLevel: number): Promise { + if (writingLang === 0 || writingLevel === 0) { + throw new Error('Veuillez choisir une langue et un niveau de rédaction valides.'); + } + if (await UserRepo.isUserPreferencesExist(userId)) { + return UserRepo.updateUserPreference(userId, writingLang, writingLevel) + } else { + const preferenceId: string = System.createUniqueId(); + return await UserRepo.insertUserPreference(preferenceId, userId, writingLang, writingLevel); + } + } + + static async acceptTerms(userId: string, version: string): Promise { + const log: boolean = await UserRepo.logAcceptTerms(userId, version); + if (log) { + return await UserRepo.updateTermsAccepted(userId); + } else { + return false; + } + } + + public getId(): string { + return this.id; + } + + public getFirstName(): string { + return this.firstName; + } + + public getLastName(): string { + return this.lastName; + } + + public getUsername(): string { + return this.username; + } + + public getEmail(): string { + return this.email; + } + + public isAccountVerified(): boolean { + return this.accountVerified; + } + + public getWritingLang(): number { + return this.writingLang; + } + + public getWritingLevel(): number { + return this.writingLevel; + } + + public getRitePoints(): number { + return this.ritePoints; + } + + public isTermsAccepted(): boolean { + return this.termsAccepted; + } + + public getGroupId(): number { + return this.groupId; + } + + public getBalancedCredits(): number { + return this.balancedCredits; + } + + public getAuthorName(): string { + return this.authorName; + } + + static async updateAIUserKey(userId: string, anthropicKey: string | null, openaiKey: string | null, geminiKey: string | null): Promise { + const user = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta); + const groupedKey: { brand: 'anthropic' | 'openai' | 'gemini', key: string }[] = []; + const anthropicKeyEncrypted: string = anthropicKey ? System.encryptDataWithUserKey(anthropicKey, userKey) : ''; + groupedKey.push({brand: 'anthropic', key: anthropicKeyEncrypted}); + const openaiKeyEncrypted: string = openaiKey ? System.encryptDataWithUserKey(openaiKey, userKey) : ''; + groupedKey.push({brand: 'openai', key: openaiKeyEncrypted}); + const geminiKeyEncrypted: string = geminiKey ? System.encryptDataWithUserKey(geminiKey, userKey) : ''; + groupedKey.push({brand: 'gemini', key: geminiKeyEncrypted}); + for (const key of groupedKey) { + const updateAI: boolean = await UserRepo.updateAIUserKey(userId, key.brand, key.key); + if (!updateAI) { + return false; + } + } + return true; + } + + static async getAPIKey(userId: string): Promise { + const user: User = new User(userId); + const meta: string = System.encryptDateKey(userId); + const userKey: string = await user.getUserKey(meta); + const apiKeys: UserAPIKeyResult[] = await UserRepo.fetchAPIKey(userId); + const decryptKeys: UserAPIKey[] = []; + for (const apiKey of apiKeys) { + if (apiKey.key && apiKey.key.length > 0) { + decryptKeys.push({ + brand: apiKey.brand, + key: System.decryptDataWithUserKey(apiKey.key, userKey) + }) + } + } + return decryptKeys; + } + + private static async moneySpentThisMonth(userId: string): Promise { + const {last, first} = System.getMonthBounds(); + const usage: UserAiUsageResult = await UserRepo.fetchUserAiUsage(userId, first, last); + return usage.total_price; + } +} diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 2c37a5d..c262efd 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -291,11 +291,11 @@ export default class BookRepo { } } - static updateBookBasicInformation(userId: string, title: string, hashedTitle: string, subTitle: string, hashedSubTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, bookMeta: string, lang: 'fr' | 'en'): boolean { + static updateBookBasicInformation(userId: string, title: string, hashedTitle: string, subTitle: string, hashedSubTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=?, book_meta=? WHERE author_id=? AND book_id=?', - [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, bookMeta, userId, bookId]); + const result: RunResult = db.run('UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=? WHERE author_id=? AND book_id=?', + [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, userId, bookId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -381,14 +381,14 @@ export default class BookRepo { } } - static insertNewPlotPoint(plotPointId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, incidentId: string, metaPlot: string, lang: 'fr' | 'en'): string { + static insertNewPlotPoint(plotPointId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, incidentId: string, lang: 'fr' | 'en'): string { try { const db: Database = System.getDb(); const existingResult = db.get('SELECT plot_point_id FROM book_plot_points WHERE author_id=? AND book_id=? AND hashed_title=?', [userId, bookId, hashedName]); if (existingResult !== null) { throw new Error(lang === 'fr' ? `Ce point de l'intrigue existe déjà.` : `This plot point already exists.`); } - const insertResult: RunResult = db.run('INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id,meta_plot) VALUES (?,?,?,?,?,?,?)', [plotPointId, encryptedName, hashedName, userId, bookId, incidentId, metaPlot]); + const insertResult: RunResult = db.run('INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id) VALUES (?,?,?,?,?,?)', [plotPointId, encryptedName, hashedName, userId, bookId, incidentId]); if (insertResult.changes > 0) { return plotPointId; } else { diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts index 268078c..3799c76 100644 --- a/electron/database/repositories/chapter.repository.ts +++ b/electron/database/repositories/chapter.repository.ts @@ -84,11 +84,11 @@ export default class ChapterRepo{ } } - public static insertChapter(chapterId: string, userId: string, bookId: string, title: string, hashedTitle: string, wordsCount: number, chapterOrder: number, meta: string, lang: 'fr' | 'en' = 'fr'): string { + public static insertChapter(chapterId: string, userId: string, bookId: string, title: string, hashedTitle: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): string { let result: RunResult; try { const db: Database = System.getDb(); - result = db.run('INSERT INTO book_chapters (chapter_id,author_id, book_id, title, hashed_title, words_count, chapter_order, meta_chapter) VALUES (?,?,?,?,?,?,?,?)', [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder, meta]); + result = db.run('INSERT INTO book_chapters (chapter_id,author_id, book_id, title, hashed_title, words_count, chapter_order) VALUES (?,?,?,?,?,?,?)', [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder]); } catch (e: unknown) { if (e instanceof Error) { console.error(`DB Error: ${e.message}`); @@ -224,10 +224,10 @@ export default class ChapterRepo{ } } - public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, meta: string, lang: 'fr' | 'en' = 'fr'): boolean { + public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, meta_chapter=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, meta, userId, chapterId]); + const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, meta_chapter=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, userId, chapterId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) {