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.
This commit is contained in:
natreex
2025-11-17 20:14:22 -05:00
parent 4cd4f68d1a
commit baa45ac106
10 changed files with 2904 additions and 9 deletions

View File

@@ -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<number, ActStory> = {};
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<string, unknown>): Record<string, unknown> => {
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<string, unknown>), [
StarterKit,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
]);
}
}

View File

@@ -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<CharacterProps[]> {
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<string> {
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<boolean> {
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<string> {
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<CharacterAttribute[]> {
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<string, Attribute[]> = new Map<string, Attribute[]>();
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<CompleteCharacterProps[]> {
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<string, CompleteCharacterProps>();
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<string, CompleteCharacterProps>();
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;
}
}

View File

@@ -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(/<br\s*\/?>/gi, '\n') // Gérer les <br> 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 += '<p>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</p>';
break;
case 'text':
let textContent = node.text || '';
// Apply attributes like bold, italic, etc.
if (node.attrs) {
if (node.attrs.bold) {
textContent = `<strong>${textContent}</strong>`;
}
if (node.attrs.italic) {
textContent = `<em>${textContent}</em>`;
}
if (node.attrs.underline) {
textContent = `<u>${textContent}</u>`;
}
if (node.attrs.strike) {
textContent = `<s>${textContent}</s>`;
}
if (node.attrs.link) {
textContent = `<a href="${node.attrs.link.href}">${textContent}</a>`;
}
}
html += textContent;
break;
case 'heading':
const level = node.attrs?.level || 1;
html += `<h${level}>`;
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += `</h${level}>`;
break;
case 'bulletList':
html += '<ul>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</ul>';
break;
case 'orderedList':
html += '<ol>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</ol>';
break;
case 'listItem':
html += '<li>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</li>';
break;
case 'blockquote':
html += '<blockquote>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</blockquote>';
break;
case 'codeBlock':
html += '<pre><code>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</code></pre>';
break;
default:
console.warn(`Unhandled node type: ${node.type}`);
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
break;
}
return html;
}
}

View File

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

View File

@@ -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<LocationProps[]>} - 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<LocationProps[]> {
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<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(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<SubElement[]> {
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<string, number>();
data.forEach((record: LocationElementQueryResult): void => {
elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
});
const subElements: SubElement[] = [];
const processedIds = new Set<string>();
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<Element[]> {
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<string> {
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');
}
}

View File

@@ -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
}
]

712
electron/database/models/Story.ts Executable file
View File

@@ -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 :';
}
}
}

View File

@@ -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<void> {
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<boolean> {
return UserRepo.isTwoFactorEnabled(userId);
}
public static async changePassword(userId: string, password: string, newPassword: string, email: string): Promise<boolean> {
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<boolean> {
return UserRepo.deleteAccount(userId, System.hashElement(email));
}
public static async resetPassword(newPassword: string, email: string, verifyCode: string): Promise<boolean> {
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<string> {
return await UserRepo.fetchVerifyCode(System.hashElement(email), verifyCode);
}
static async subscribeToBeta(email: string): Promise<boolean> {
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<string> {
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<boolean> {
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<string>{
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<EncryptedKey> = await System.getUserKey(this.id,meta);
if (userKey.data){
return userKey.data?.encrypted_key;
} else {
return '';
}
}
}
public static async checkUserName(username:string):Promise<boolean>{
return UserRepo.fetchUserName(username);
}
public static async checkEmail(email:string):Promise<boolean>{
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<void> {
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<string> {
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<boolean> {
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<boolean>{
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<string> {
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<boolean> {
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<void> {
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<boolean>{
return UserRepo.checkTOTPExist(userId,email);
}
public static async handleTOTP(userId: string, email: string, verifyCode: string): Promise<boolean> {
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<boolean> {
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<boolean> {
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<UserAccount> {
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<UserSubscriptionProps[]> {
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<void>{
const points:number = Math.ceil(time/30);
await UserRepo.updatePoints(points,this.id);
}
static async setUserPreferences(userId: string, writingLang: number, writingLevel: number): Promise<boolean | string> {
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<boolean> {
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<boolean> {
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<UserAPIKey[]> {
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<number> {
const {last, first} = System.getMonthBounds();
const usage: UserAiUsageResult = await UserRepo.fetchUserAiUsage(userId, first, last);
return usage.total_price;
}
}