@dataclouder/ngx-lessons 0.0.30 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/fesm2022/dataclouder-ngx-lessons.mjs +1170 -449
  2. package/fesm2022/dataclouder-ngx-lessons.mjs.map +1 -1
  3. package/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.d.ts +2 -2
  4. package/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.d.ts +11 -0
  5. package/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.d.ts +39 -38
  6. package/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.d.ts +22 -0
  7. package/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.d.ts +32 -37
  8. package/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.d.ts +2 -4
  9. package/lib/components/lesson-mini-components/components/ComponentBuilder.d.ts +7 -2
  10. package/lib/components/lesson-mini-components/components/lessons.clases.d.ts +17 -42
  11. package/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.d.ts +13 -0
  12. package/lib/components/lesson-mini-components/components/speaker/speaker.component.d.ts +12 -0
  13. package/lib/services/lesson-ai.service.d.ts +18 -0
  14. package/lib/services/lesson-notion.service.d.ts +35 -0
  15. package/lib/services/lesson-utils.service.d.ts +34 -0
  16. package/package.json +3 -2
  17. package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.html +40 -35
  18. package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.scss +15 -2
  19. package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.ts +3 -3
  20. package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.css +1 -0
  21. package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.html +46 -0
  22. package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.ts +52 -0
  23. package/src/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.html +54 -92
  24. package/src/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.ts +268 -230
  25. package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.css +1 -0
  26. package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.html +72 -0
  27. package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.ts +60 -0
  28. package/src/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.html +23 -27
  29. package/src/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.ts +247 -186
  30. package/src/lib/components/dc-lessons/lesson-form/lesson-form.component.ts +2 -2
  31. package/src/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.html +3 -3
  32. package/src/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.ts +5 -5
  33. package/src/lib/components/lesson-mini-components/components/ComponentBuilder.ts +23 -15
  34. package/src/lib/components/lesson-mini-components/components/lessons.clases.ts +32 -66
  35. package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.html +62 -58
  36. package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.ts +2 -2
  37. package/src/lib/components/lesson-mini-components/components/selector/selector.component.html +1 -2
  38. package/src/lib/components/lesson-mini-components/components/selector/selector.component.ts +2 -2
  39. package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.html +5 -27
  40. package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.ts +38 -25
  41. package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.html +9 -7
  42. package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.ts +30 -26
  43. package/src/lib/components/lesson-mini-components/components/translationSwitcher/translationSwitcher.component.ts +2 -2
  44. package/src/lib/components/lesson-mini-components/components/translationSwitcher/translationSwitcherBuilder/translationSwitcherBuilder.component.ts +2 -2
  45. package/src/lib/services/lesson-ai.service.ts +103 -0
  46. package/src/lib/services/lesson-notion.service.ts +161 -0
  47. package/src/lib/services/lesson-utils.service.ts +181 -0
  48. package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.spec.ts +0 -25
  49. package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.spec.ts +0 -25
  50. package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.spec.ts +0 -25
@@ -0,0 +1,103 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { IAgentCard, IMiniAgentCard, TextEngines, ConversationType } from '@dataclouder/ngx-agent-cards';
3
+ import { ILesson, LangCodeDescription, LESSONS_TOKEN, LessonsAbstractService } from '../components/lesson-mini-components/components/lessons.clases';
4
+
5
+ // Re-define or import constants if they are not exported from the component file
6
+ const DEFAULT_LESSON_AGENT_CARD: IAgentCard = {
7
+ conversationSettings: {
8
+ conversationType: ConversationType.General,
9
+ textEngine: TextEngines.SimpleText,
10
+ autoStart: true,
11
+ },
12
+ characterCard: {
13
+ data: {
14
+ name: 'Lesson Master',
15
+ description: 'You are an AI developed by Polilan Company, you are a lesson master, you are going to help the user to learn english',
16
+ tags: ['lesson', 'master', 'ai'],
17
+ post_history_instructions:
18
+ 'Your reply should be always short, 1 or 2 paragraphs at most, and to the point, and you should ask friendly questions all the time',
19
+ },
20
+ },
21
+ model: { provider: 'google' },
22
+ };
23
+
24
+ function getDefaultLessonEvaluatorAgentCard(lessonText: string): IMiniAgentCard {
25
+ return {
26
+ expectedResponseType: `interface EvalResult {
27
+ score: number; // Score of the user's response 0 to 3
28
+ feedback: string; // Feedback of the user's understanding of the conversation
29
+ }`,
30
+ messages: [],
31
+ model: { id: 'gpt-4o-mini', provider: 'openai' },
32
+ sources: [lessonText],
33
+ task: `User is reading a taking a lesson, now their are having a conversation,
34
+ you have to evaluate the current conversation, and give a feedback of the user understanding of the lesson,
35
+ this is the lesson: ${lessonText}`,
36
+ };
37
+ }
38
+
39
+ @Injectable({
40
+ providedIn: 'root', // Or provide appropriately if it's library-specific
41
+ })
42
+ export class LessonAIService {
43
+ private readonly lessonService = inject<LessonsAbstractService>(LESSONS_TOKEN);
44
+ // TODO: Inject the application-level UserService
45
+ // private readonly userService = inject(UserService);
46
+
47
+ constructor() {}
48
+
49
+ /**
50
+ * Generates the necessary agent cards for a lesson chat session.
51
+ * @param lesson The lesson data.
52
+ * @returns An object containing the master agent card and the evaluator agent card.
53
+ */
54
+ async generateAgentCards(lesson: ILesson): Promise<{ masterAgent: IAgentCard; evaluatorAgent: IMiniAgentCard } | null> {
55
+ // TODO: Implement the logic moved from DCLessonRendererComponent.startAI here
56
+ // 1. Get user data using the injected UserService
57
+ // 2. Extract lesson text using lessonService
58
+ // 3. Build prompts (scenario, userInformationPrompt)
59
+ // 4. Configure and return the agent cards
60
+
61
+ alert('AI User data fetching needs refactoring into this service.');
62
+ // Placeholder for user data - replace with actual service call
63
+ const user = {
64
+ personalData: { firstname: 'Test', lastname: 'User' },
65
+ settings: { targetLanguage: 'en', baseLanguage: 'es' },
66
+ languageProgress: { en: { level: '1' } },
67
+ } as any; // Replace 'any' with your actual User type/interface
68
+
69
+ if (!user) {
70
+ console.error('User data not available to generate agent cards.');
71
+ // Handle error appropriately - maybe return null or throw?
72
+ return null;
73
+ }
74
+
75
+ const lessonText = this.lessonService.extractTextFromHtml(lesson.textCoded);
76
+
77
+ const scenario = `The user is reading lessons through this app interface. They will now talk with you, and you need to evaluate their understanding of the lesson.
78
+ Ask friendly questions throughout the conversation and help them learn English. Here is the lesson text the user just read:
79
+ ${lessonText}
80
+ In your next reply, start by greeting the user, asking something about the lesson, and then continue the conversation.`;
81
+
82
+ const targetLevel = parseInt(user.languageProgress[user.settings.targetLanguage]?.level ?? '1');
83
+ const langTargetDesc = LangCodeDescription[user.settings.targetLanguage] ?? user.settings.targetLanguage;
84
+ const langBaseDesc = LangCodeDescription[user.settings.baseLanguage] ?? user.settings.baseLanguage;
85
+
86
+ let userInformationPrompt = `
87
+ User information: user name is ${user.personalData.firstname} ${user.personalData.lastname}, their native language is ${langBaseDesc},
88
+ and right now is learning ${langTargetDesc}, their current level is ${targetLevel} out of 5.`;
89
+
90
+ if (targetLevel <= 2) {
91
+ userInformationPrompt += `\nUser is a beginner in ${langTargetDesc}, always reply mainly in ${langBaseDesc}, but during the conversation use simple words and phrases in ${langTargetDesc} to help them learn.`;
92
+ }
93
+
94
+ // Create a deep copy of the default card to avoid modifying the constant
95
+ const masterAgent: IAgentCard = JSON.parse(JSON.stringify(DEFAULT_LESSON_AGENT_CARD));
96
+ masterAgent.characterCard.data.scenario = scenario;
97
+ masterAgent.characterCard.data.post_history_instructions += `\n${userInformationPrompt}`;
98
+
99
+ const evaluatorAgent = getDefaultLessonEvaluatorAgentCard(lessonText);
100
+
101
+ return { masterAgent, evaluatorAgent };
102
+ }
103
+ }
@@ -0,0 +1,161 @@
1
+ import { Injectable, inject, signal } from '@angular/core';
2
+ import { ILesson, LESSONS_TOKEN } from '../components/lesson-mini-components/components/lessons.clases';
3
+ import { NOTION_SERVICE_TOKEN, NotionExportType } from '../models/notion.models';
4
+ import { TOAST_ALERTS_TOKEN } from '@dataclouder/ngx-core';
5
+
6
+ @Injectable() // Consider providing in root or specific module/component
7
+ export class LessonNotionService {
8
+ #notionService = inject(NOTION_SERVICE_TOKEN);
9
+ #lessonService = inject(LESSONS_TOKEN);
10
+ #toastService = inject(TOAST_ALERTS_TOKEN);
11
+
12
+ // Keep track of loading state specific to Notion operations
13
+ isLoading = signal(false);
14
+
15
+ /**
16
+ * Extracts the Notion Page ID from a URL.
17
+ * @param url The Notion page URL.
18
+ * @returns The extracted page ID or null if invalid.
19
+ */
20
+ private extractNotionPageId(url: string): string | null {
21
+ const notionIdRegex = /[a-f0-9]{32}(?=\?|$)/;
22
+ const match = url.match(notionIdRegex);
23
+ const notionId = match ? match[0] : null;
24
+ if (!notionId) {
25
+ this.#toastService.error({
26
+ title: 'URL inválido',
27
+ subtitle: 'Por favor ingresa una URL válida de Notion.',
28
+ });
29
+ return null;
30
+ }
31
+ return notionId;
32
+ }
33
+
34
+ /**
35
+ * Links an existing lesson with a Notion page ID by updating the lesson's extras.
36
+ * @param lesson The current lesson data.
37
+ * @param notionPageId The Notion page ID to link.
38
+ * @returns The updated lesson data from the backend or undefined on failure.
39
+ */
40
+ public async linkLessonWithNotion(lesson: ILesson, notionPageId: string): Promise<ILesson | undefined> {
41
+ if (!lesson || !lesson.id) {
42
+ // Ensure lesson exists and has an ID
43
+ this.#toastService.warn({ title: 'No se puede enlazar', subtitle: 'La lección debe existir y tener un ID.' });
44
+ return undefined;
45
+ }
46
+
47
+ const updatedLesson = {
48
+ ...lesson,
49
+ extras: {
50
+ ...(lesson.extras || {}),
51
+ notionPageId: notionPageId,
52
+ },
53
+ };
54
+
55
+ this.isLoading.set(true);
56
+ try {
57
+ const savedLesson = await this.#lessonService.postLesson(updatedLesson);
58
+ this.#toastService.success({ title: 'Listo', subtitle: 'Se enlazó la lección con Notion.' });
59
+ return savedLesson;
60
+ } catch (error) {
61
+ // Remove explicit type, rely on tsconfig setting
62
+ console.error('Error linking with Notion:', error);
63
+ this.#toastService.error({ title: 'Error al enlazar', subtitle: 'Ocurrió un error inesperado.' });
64
+ return undefined;
65
+ } finally {
66
+ this.isLoading.set(false);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Handles the process of importing lesson content from Notion.
72
+ * It prompts the user for a URL if necessary, extracts the ID, fetches content,
73
+ * and potentially links the lesson if it's an existing one.
74
+ * @param currentLesson The current lesson data (can be a new or existing lesson).
75
+ * @param lessonId The current lesson ID (null if it's a new lesson).
76
+ * @returns The fetched HTML content from Notion, or null if the process fails.
77
+ */
78
+ public async importAndLinkLessonFromNotion(currentLesson: ILesson | undefined, lessonId: string | null): Promise<string | null> {
79
+ if (!currentLesson) return null;
80
+
81
+ let notionPageId: string | null = null;
82
+
83
+ if (currentLesson.extras?.notionPageId) {
84
+ const useExisting = confirm(`Ya tenemos el id ${currentLesson.extras.notionPageId} ¿Quieres usar este id para importar?`);
85
+ if (useExisting) {
86
+ notionPageId = currentLesson.extras.notionPageId;
87
+ } else {
88
+ const inputUrl = prompt('Ingresa la NUEVA URL de Notion para importar (este ID NO se guardará automáticamente si la lección ya existe)');
89
+ if (!inputUrl) return null; // User cancelled
90
+ notionPageId = this.extractNotionPageId(inputUrl);
91
+ }
92
+ } else {
93
+ const inputUrl = prompt('Ingresa el URL de Notion para importar la lección (se enlazará si la lección ya existe)');
94
+ if (!inputUrl) return null; // User cancelled
95
+ notionPageId = this.extractNotionPageId(inputUrl);
96
+ // Link automatically only if we got a valid Notion ID AND the lesson already exists (has an ID)
97
+ if (notionPageId && lessonId) {
98
+ const linkedLesson = await this.linkLessonWithNotion(currentLesson, notionPageId);
99
+ if (!linkedLesson) {
100
+ // Linking failed, maybe stop the import? Or proceed without linking?
101
+ // For now, let's stop.
102
+ this.#toastService.error({ title: 'Error de Enlace', subtitle: 'No se pudo enlazar con Notion antes de importar.' });
103
+ return null;
104
+ }
105
+ // If linking succeeded, the lesson object might have changed, but we proceed with the import using the notionPageId.
106
+ }
107
+ }
108
+
109
+ if (!notionPageId) {
110
+ this.#toastService.warn({ title: 'Sin ID de Notion', subtitle: 'No se proporcionó un ID de Notion válido para importar.' });
111
+ return null;
112
+ }
113
+
114
+ this.isLoading.set(true);
115
+ try {
116
+ this.#toastService.info({ title: 'Importando lección...', subtitle: 'Espera unos segundos' });
117
+ const md = await this.#notionService.getPageInSpecificFormat(notionPageId, NotionExportType.HTML);
118
+ console.log('Imported MD/HTML:', md);
119
+ this.#toastService.success({ title: 'Contenido Importado', subtitle: 'Contenido de Notion obtenido.' });
120
+ return md.content; // Return the fetched content
121
+ } catch (error) {
122
+ // Remove explicit type, rely on tsconfig setting
123
+ console.error('Error importing from Notion:', error);
124
+ this.#toastService.error({ title: 'Error de importación', subtitle: 'Ocurrió un error inesperado.' });
125
+ return null; // Return null on failure
126
+ } finally {
127
+ this.isLoading.set(false);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Fetches content from the linked Notion page for AI improvement (placeholder).
133
+ * @param lesson The current lesson data.
134
+ */
135
+ public async improveLessonWithNotionAI(lesson: ILesson | undefined): Promise<void> {
136
+ if (!lesson) return;
137
+ const notionId = lesson.extras?.notionPageId;
138
+
139
+ if (!notionId) {
140
+ this.#toastService.warn({ title: 'Sin ID de Notion', subtitle: 'Enlaza la lección con Notion primero.' });
141
+ return;
142
+ }
143
+
144
+ this.isLoading.set(true);
145
+ try {
146
+ this.#toastService.info({ title: 'Mejorando con IA...', subtitle: 'Obteniendo contenido de Notion.' });
147
+ const md = await this.#notionService.getPageInSpecificFormat(notionId, NotionExportType.HTML);
148
+ console.log('Content to improve:', md);
149
+ // TODO: Add actual AI improvement logic here
150
+ // e.g., call another service: await this.aiImprovementService.improve(md.content);
151
+ // Then potentially update the lesson via lessonService or return data
152
+ this.#toastService.success({ title: 'Contenido Obtenido', subtitle: 'Listo para mejorar con IA (lógica no implementada).' });
153
+ } catch (error) {
154
+ // Remove explicit type, rely on tsconfig setting
155
+ console.error('Error improving with AI:', error);
156
+ this.#toastService.error({ title: 'Error de IA', subtitle: 'Ocurrió un error inesperado.' });
157
+ } finally {
158
+ this.isLoading.set(false);
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,181 @@
1
+ import { Injectable, inject, WritableSignal } from '@angular/core';
2
+ import { ILesson, LessonImage, LESSONS_TOKEN, DynamicContentComponent } from '../components/lesson-mini-components/components/lessons.clases'; // Added DynamicContentComponent
3
+ import { TOAST_ALERTS_TOKEN } from '@dataclouder/ngx-core';
4
+
5
+ @Injectable({
6
+ providedIn: 'root', // Provide globally or in a specific module if preferred
7
+ })
8
+ export class LessonUtilsService {
9
+ #lessonService = inject(LESSONS_TOKEN);
10
+ #toastService = inject(TOAST_ALERTS_TOKEN);
11
+
12
+ constructor() {}
13
+
14
+ /**
15
+ * Validates if audios need generation for the given lesson.
16
+ * Currently logs a placeholder message.
17
+ * @param lesson The lesson object (or signal) to validate.
18
+ */
19
+ public validateAudios(lesson: ILesson | undefined): void {
20
+ // Access lesson data directly
21
+ if (!lesson?.components) {
22
+ return;
23
+ }
24
+ // Placeholder logic - adapt as needed from original component
25
+ console.log('Validating audios for lesson:', lesson.id);
26
+ // Original logic commented out - requires SpeakerCompConfiguration type
27
+ // const needsGeneration = lesson.components.some((component: any) => // Use 'any' or define SpeakerCompConfiguration
28
+ // component.component === 'speaker' && component.settings?.voice && !component.audio
29
+ // );
30
+ // if (needsGeneration) {
31
+ // this.#toastService.warn({ title: 'Audios por generar', subtitle: 'Se encontraron audios pendientes' });
32
+ // // Consider calling generation service here or returning a flag
33
+ // // this.#lessonService.generateAudiosForLesson(lesson.id).then(res => { ... });
34
+ // }
35
+ }
36
+
37
+ /**
38
+ * Updates the lesson signal with a new cover image.
39
+ * @param lessonSignal The signal holding the lesson data.
40
+ * @param imageUploaded The image data object from the upload event.
41
+ */
42
+ public uploadCover(lessonSignal: WritableSignal<ILesson | undefined>, imageUploaded: any): void {
43
+ // Use 'any' for now, refine if event structure is known
44
+ const newImage: LessonImage = {
45
+ type: 'cover',
46
+ url: imageUploaded?.url,
47
+ path: imageUploaded?.path,
48
+ fullPath: imageUploaded?.fullPath,
49
+ resolutions: imageUploaded?.resolutions ?? [],
50
+ resolution: imageUploaded?.resolution,
51
+ bucket: imageUploaded?.bucket,
52
+ // Add any other required fields from LessonImage or ImgStorageData
53
+ };
54
+
55
+ lessonSignal.update((currentLesson) => {
56
+ if (!currentLesson) return undefined;
57
+
58
+ let updatedImages: LessonImage[];
59
+ const existingMedia = currentLesson.media;
60
+
61
+ if (!existingMedia || !existingMedia.images) {
62
+ updatedImages = [newImage];
63
+ } else {
64
+ const filteredImages = existingMedia.images.filter((img: any) => img.type !== 'cover');
65
+ const mappedExistingImages: LessonImage[] = filteredImages.map(
66
+ (img: any): LessonImage => ({
67
+ type: img.type,
68
+ url: img.url,
69
+ path: img.path,
70
+ fullPath: img.fullPath,
71
+ resolutions: img.resolutions ?? [],
72
+ resolution: img.resolution,
73
+ bucket: img.bucket,
74
+ }),
75
+ );
76
+ updatedImages = [...mappedExistingImages, newImage];
77
+ }
78
+
79
+ return {
80
+ ...currentLesson,
81
+ media: {
82
+ ...(existingMedia || {}),
83
+ images: updatedImages,
84
+ },
85
+ };
86
+ });
87
+ // Note: Saving the lesson should be triggered from the component after calling this.
88
+ }
89
+
90
+ /**
91
+ * Generates lesson content using AI. Assumes the lesson is already saved.
92
+ * @param lessonId The ID of the lesson to generate content for.
93
+ * @returns The updated lesson object after AI generation, or null on failure.
94
+ */
95
+ public async generateByAI(lessonId: string): Promise<ILesson | null> {
96
+ if (!lessonId) {
97
+ this.#toastService.warn({ title: 'ID Requerido', subtitle: 'Se necesita un ID de lección para usar IA.' });
98
+ return null;
99
+ }
100
+
101
+ // No need to save here, component should ensure it's saved before calling.
102
+ try {
103
+ await this.#lessonService.postGenerateByAI(lessonId);
104
+ // Re-fetch the lesson data to get AI updates
105
+ const updatedLesson = await this.#lessonService.getLesson(lessonId);
106
+ if (updatedLesson) {
107
+ this.#toastService.success({ title: 'IA completada', subtitle: 'Lección actualizada con IA.' });
108
+ return updatedLesson;
109
+ } else {
110
+ throw new Error('Failed to fetch lesson after AI generation');
111
+ }
112
+ } catch (error: any) {
113
+ // Type the error object
114
+ console.error('Error during AI generation in service:', error);
115
+ this.#toastService.error({ title: 'Error de IA', subtitle: 'No se pudo generar la lección con IA.' });
116
+ return null; // Return null in catch block
117
+ }
118
+ // Loading state should be managed by the component calling this service.
119
+ }
120
+
121
+ /**
122
+ * Cleans orphaned components from the lesson's dynamicComponents.
123
+ * An orphaned component is one present in dynamicComponents but its ID is not found in the textCoded HTML.
124
+ * @param lesson The lesson object to clean.
125
+ * @returns A new lesson object with orphaned components removed from dynamicComponents.
126
+ */
127
+ public cleanOrphanedComponents(lesson: ILesson): ILesson {
128
+ if (!lesson || !lesson.textCoded || !lesson.dynamicComponents) {
129
+ // Return the original lesson if essential parts are missing
130
+ return lesson;
131
+ }
132
+
133
+ const textCoded = lesson.textCoded;
134
+ const existingComponents = lesson.dynamicComponents;
135
+ const existingComponentIds = new Set(Object.keys(existingComponents));
136
+
137
+ // Regex to find "id":"<component_id>" within the textCoded string
138
+ const idRegex = /"id":"([^"]+)"/g;
139
+ const foundIdsInText = new Set<string>();
140
+ let match;
141
+
142
+ while ((match = idRegex.exec(textCoded)) !== null) {
143
+ const potentialId = match[1];
144
+ // Check if the found ID actually exists in our dynamic components map
145
+ // This ensures we only count IDs that correspond to known components.
146
+ if (existingComponentIds.has(potentialId)) {
147
+ foundIdsInText.add(potentialId);
148
+ }
149
+ }
150
+
151
+ const orphanedIds: string[] = [];
152
+ const cleanedDynamicComponents: Record<string, DynamicContentComponent> = {};
153
+
154
+ // Iterate through existing component IDs
155
+ for (const componentId of existingComponentIds) {
156
+ if (foundIdsInText.has(componentId)) {
157
+ // Keep the component if its ID was found in the text
158
+ cleanedDynamicComponents[componentId] = existingComponents[componentId];
159
+ } else {
160
+ // Mark as orphaned if not found
161
+ orphanedIds.push(componentId);
162
+ }
163
+ }
164
+
165
+ // Log a warning if any orphaned components were found
166
+ if (orphanedIds.length > 0) {
167
+ console.warn(`[LessonUtilsService] Orphaned components detected and will be removed from lesson data (IDs not found in textCoded):`, orphanedIds);
168
+ this.#toastService.warn({
169
+ title: 'Componentes Huérfanos Detectados',
170
+ subtitle: `Se removerán ${orphanedIds.length} componentes no usados del editor.`,
171
+ // life: 5000, // Removed 'life' property as it might not be supported by ToastData
172
+ });
173
+ }
174
+
175
+ // Return a new lesson object with the cleaned components
176
+ return {
177
+ ...lesson,
178
+ dynamicComponents: cleanedDynamicComponents,
179
+ };
180
+ }
181
+ }
@@ -1,25 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { SelectorBuilderComponent } from './selector-builder.component';
4
-
5
- describe('SelectorBuilderComponent', () => {
6
- let component: SelectorBuilderComponent;
7
- let fixture: ComponentFixture<SelectorBuilderComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [SelectorBuilderComponent]
12
- })
13
- .compileComponents();
14
- });
15
-
16
- beforeEach(() => {
17
- fixture = TestBed.createComponent(SelectorBuilderComponent);
18
- component = fixture.componentInstance;
19
- fixture.detectChanges();
20
- });
21
-
22
- it('should create', () => {
23
- expect(component).toBeTruthy();
24
- });
25
- });
@@ -1,25 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { SpeakerBuilderComponent } from './speaker-builder.component';
4
-
5
- describe('SpeakerBuilderComponent', () => {
6
- let component: SpeakerBuilderComponent;
7
- let fixture: ComponentFixture<SpeakerBuilderComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [SpeakerBuilderComponent]
12
- })
13
- .compileComponents();
14
- });
15
-
16
- beforeEach(() => {
17
- fixture = TestBed.createComponent(SpeakerBuilderComponent);
18
- component = fixture.componentInstance;
19
- fixture.detectChanges();
20
- });
21
-
22
- it('should create', () => {
23
- expect(component).toBeTruthy();
24
- });
25
- });
@@ -1,25 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { SpeakerComponent } from './speaker.component';
4
-
5
- describe('SpeakerComponent', () => {
6
- let component: SpeakerComponent;
7
- let fixture: ComponentFixture<SpeakerComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [SpeakerComponent]
12
- })
13
- .compileComponents();
14
- });
15
-
16
- beforeEach(() => {
17
- fixture = TestBed.createComponent(SpeakerComponent);
18
- component = fixture.componentInstance;
19
- fixture.detectChanges();
20
- });
21
-
22
- it('should create', () => {
23
- expect(component).toBeTruthy();
24
- });
25
- });