Files
natreex 7f34421212 Add error handling, enhance syncing, and refactor deletion logic
- Introduce new error messages for syncing and book deletion in `en.json`.
- Update `DeleteBook` to support local-only deletion and synced book management.
- Refine offline/online behavior with `deleteLocalToo` checkbox and update related state handling.
- Extend repository and IPC methods to handle optional IDs for updates.
- Add `SyncQueueContext` for queueing offline changes and improving synchronization workflows.
- Enhance refined text generation logic in `DraftCompanion` and `GhostWriter` components.
- Replace PUT with PATCH for world updates to align with API expectations.
- Streamline `AlertBox` by integrating dynamic translation keys for deletion prompts.
2026-01-10 15:50:03 -05:00

392 lines
17 KiB
TypeScript

import ChapterRepo, {
ActChapterQuery,
ChapterQueryResult,
ChapterContentQueryResult,
LastChapterResult,
CompanionContentQueryResult,
ChapterStoryQueryResult,
ContentQueryResult
} from "../repositories/chapter.repository.js";
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
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, System.timeStampInSeconds(), 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', existingChapterId?: string): string {
const hashedTitle: string = System.hashElement(title);
const userKey: string = getUserEncryptionKey(userId);
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey);
if (!existingChapterId && 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 = existingChapterId || 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', existingChapterInfoId?: string): string {
const chapterInfoId: string = existingChapterInfoId || 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, System.timeStampInSeconds(), lang);
}
static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, 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, System.timeStampInSeconds(), 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 {
interface TipTapNode {
type?: string;
text?: string;
content?: TipTapNode[];
attrs?: Record<string, unknown>;
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
}
const escapeHtml = (text: string): string => {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
const renderMarks = (text: string, marks?: Array<{ type: string; attrs?: Record<string, unknown> }>): string => {
if (!marks || marks.length === 0) return escapeHtml(text);
let result = escapeHtml(text);
marks.forEach((mark) => {
switch (mark.type) {
case 'bold':
result = `<strong>${result}</strong>`;
break;
case 'italic':
result = `<em>${result}</em>`;
break;
case 'underline':
result = `<u>${result}</u>`;
break;
case 'strike':
result = `<s>${result}</s>`;
break;
case 'code':
result = `<code>${result}</code>`;
break;
case 'link':
const href = mark.attrs?.href || '#';
result = `<a href="${escapeHtml(String(href))}">${result}</a>`;
break;
}
});
return result;
};
const renderNode = (node: TipTapNode): string => {
if (!node) return '';
if (node.type === 'text') {
const textContent = node.text || '\u00A0';
return renderMarks(textContent, node.marks);
}
const children = node.content?.map(renderNode).join('') || '';
const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
switch (node.type) {
case 'doc':
return children;
case 'paragraph':
return `<p${textAlign}>${children || '\u00A0'}</p>`;
case 'heading':
const level = node.attrs?.level || 1;
return `<h${level}${textAlign}>${children}</h${level}>`;
case 'bulletList':
return `<ul>${children}</ul>`;
case 'orderedList':
return `<ol>${children}</ol>`;
case 'listItem':
return `<li>${children}</li>`;
case 'blockquote':
return `<blockquote>${children}</blockquote>`;
case 'codeBlock':
return `<pre><code>${children}</code></pre>`;
case 'hardBreak':
return '<br />';
case 'horizontalRule':
return '<hr />';
default:
return children;
}
};
const contentNode = tipTapContent as unknown as TipTapNode;
return renderNode(contentNode);
}
}