@dataclouder/ngx-lessons 0.0.29 → 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 +1205 -514
  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 +6 -8
  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 +7 -17
  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 +16 -18
  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 +28 -46
  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
@@ -1,34 +1,43 @@
1
- import { ChangeDetectorRef, Component, ComponentRef, ElementRef, Inject, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
2
- import { ActivatedRoute, Router } from '@angular/router';
1
+ import { Component, ComponentRef, ElementRef, computed, effect, inject, signal, ViewChild, ViewContainerRef } from '@angular/core';
2
+ import { ActivatedRoute, Router, ParamMap } from '@angular/router';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
3
4
  import { FormsModule } from '@angular/forms';
4
- import { Observable } from 'rxjs';
5
+ import { map } from 'rxjs';
5
6
 
6
7
  import BalloonEditor from '@ckeditor/ckeditor5-build-balloon-block';
7
8
  import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
8
9
  // PrimeNG
9
- import { DialogService } from 'primeng/dynamicdialog';
10
- import { SpeedDialModule } from 'primeng/speeddial';
10
+ // Removed DialogService, DynamicDialogRef, SpeedDialModule, MenuItem
11
11
  import { ButtonModule } from 'primeng/button';
12
12
  import { InputTextModule } from 'primeng/inputtext';
13
13
  import { SplitterModule } from 'primeng/splitter';
14
14
  import { TooltipModule } from 'primeng/tooltip';
15
15
  // Dataclouder
16
- import { AspectType, CropImageSettings, CropperComponentModal, ResolutionType, StorageImageSettings } from '@dataclouder/ngx-cloud-storage';
16
+ import { AspectType, CropperComponentModal, ResolutionType, StorageImageSettings } from '@dataclouder/ngx-cloud-storage';
17
17
  import { TOAST_ALERTS_TOKEN, ToastAlertsAbstractService } from '@dataclouder/ngx-core';
18
18
 
19
19
  import { DCLessonRendererComponent } from '../dc-lesson-renderer/dc-lesson-renderer.component';
20
+ // Added DynamicContentComponent to the import below
20
21
  import {
21
22
  LessonComponentEnum,
22
23
  LessonImage,
23
24
  ILesson,
24
25
  LessonComponentInterface,
25
26
  LESSONS_TOKEN,
26
- LessonsAbstractService,
27
+ DynamicContentComponent,
28
+ LessonComponentConfiguration,
27
29
  } from '../../lesson-mini-components/components/lessons.clases';
28
30
  import { LessonComponentBuilders } from '../../lesson-mini-components/components/lessons.clases';
29
31
  import { FlagLanguagePipe, LangDescTranslationPipe } from '../../../models/lessons.pipes';
30
- import { NOTION_SERVICE_TOKEN, NotionAbstractService, NotionExportType } from '../../../models/notion.models';
31
- import { MenuItem } from 'primeng/api';
32
+ // Notion types might still be needed if passed/returned, but token injection removed
33
+ import { NotionExportType } from '../../../models/notion.models';
34
+ // Removed MenuItem import
35
+ import { LessonNotionService } from '../../../services/lesson-notion.service';
36
+ import { LessonUtilsService } from '../../../services/lesson-utils.service'; // Import the new utils service
37
+ import { DCLessonComponentAdderComponent } from '../dc-lesson-component-adder/dc-lesson-component-adder.component'; // Import the component adder
38
+ import { DCLessonMetadataEditorComponent } from '../dc-lesson-metadata-editor/dc-lesson-metadata-editor.component'; // Import the metadata editor
39
+ import { ComponentBuildData } from '../../lesson-mini-components/components/ComponentBuilder';
40
+ import { JsonPipe } from '@angular/common';
32
41
 
33
42
  const GradientCss = 'linear-gradient(to bottom,var(--primary-color), rgba(213, 238, 239, 0.31))';
34
43
 
@@ -38,281 +47,310 @@ const GradientCss = 'linear-gradient(to bottom,var(--primary-color), rgba(213, 2
38
47
  styleUrls: ['./dc-lesson-editor.component.css'],
39
48
  standalone: true,
40
49
  imports: [
41
- FormsModule,
50
+ ButtonModule,
42
51
  CKEditorModule,
43
52
  CropperComponentModal,
44
- ButtonModule,
53
+ DCLessonRendererComponent,
54
+ FormsModule,
45
55
  InputTextModule,
46
56
  SplitterModule,
47
- DCLessonRendererComponent,
48
- LangDescTranslationPipe,
49
- FlagLanguagePipe,
50
57
  TooltipModule,
51
- SpeedDialModule,
58
+ // Removed SpeedDialModule
59
+ DCLessonComponentAdderComponent, // Add the component adder here
60
+ DCLessonMetadataEditorComponent, // Add the metadata editor here
61
+ JsonPipe,
52
62
  ],
63
+ providers: [LessonNotionService], // Provide the service here
53
64
  })
54
- export class DCLessonEditorComponent implements OnInit {
55
- // public get activatedRoute(): ActivatedRoute {
56
- // return this._activatedRoute;
57
- // }
58
- // public set activatedRoute(value: ActivatedRoute) {
59
- // this._activatedRoute = value;
60
- // }
65
+ export class DCLessonEditorComponent {
66
+ // Services
67
+ #activatedRoute = inject(ActivatedRoute); // Re-inject as it's needed for navigation
68
+ #lessonNotionService = inject(LessonNotionService);
69
+ #lessonUtilsService = inject(LessonUtilsService);
70
+ #router = inject(Router);
71
+ // Removed #dialogService injection
72
+ #lessonService = inject(LESSONS_TOKEN);
73
+ #toastService = inject(TOAST_ALERTS_TOKEN);
74
+ // ViewChild
61
75
  @ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef;
62
76
  @ViewChild('dhtml', { static: true }) dhtml: ElementRef;
77
+ // Signals States
78
+ readonly lessonId = toSignal(inject(ActivatedRoute).paramMap.pipe(map((params: ParamMap) => params.get('id'))));
79
+ lesson = signal<ILesson | undefined>(undefined); // Initialize as undefined
80
+ isCropperVisible = signal(false);
81
+ isLoadingLesson = signal(false);
82
+ // Removed items signal
83
+ // Computed Signals
84
+ coverBackground = computed(() => {
85
+ const currentLesson = this.lesson();
86
+ const coverImage = currentLesson?.media?.images?.find((img: any) => img.type === 'cover') as LessonImage | undefined;
87
+ const imageUrl = coverImage?.url || '/assets/images/default_banner.webp';
88
+ return `${GradientCss}, url("${imageUrl}")`;
89
+ });
90
+
91
+ // Computed signal to get dynamic components as an array for easier iteration in the template
92
+ readonly dynamicComponentsArray = computed(() => {
93
+ const currentLesson = this.lesson();
94
+ if (currentLesson?.dynamicComponents) {
95
+ return Object.values(currentLesson.dynamicComponents);
96
+ }
97
+ return []; // Return empty array if no lesson or no dynamic components
98
+ });
63
99
 
100
+ // States
101
+ public components: { [key: string]: ComponentRef<LessonComponentInterface> } = {}; // Current Dynamic components
64
102
  public editor = BalloonEditor;
103
+ public lessonComponentEnum = LessonComponentEnum;
104
+
65
105
  public coverStorageSettings: StorageImageSettings = {
66
106
  path: 'lessons/covers',
67
107
  fileName: 'cover',
68
108
  cropSettings: { resizeToWidth: 850, aspectRatio: AspectType.RectangleLarge, resolutions: [ResolutionType.Medium] },
69
109
  };
70
110
 
71
- public lessonComponentEnum = LessonComponentEnum;
72
- public lessonId: string = this._activatedRoute.snapshot.paramMap.get('id');
73
- public lesson: any = { textCoded: `<h1>Nueva lección </h1> <p> Texto aquí</p>`, tags: [] };
74
- public components: { [key: string]: ComponentRef<LessonComponentInterface> } = {};
75
- public isRendered: boolean = false;
76
- public cover: string = '';
77
- public isCropperVisible = false;
78
- public items: MenuItem[];
79
-
80
- constructor(
81
- @Inject(LESSONS_TOKEN) private lessonService: LessonsAbstractService,
82
- @Inject(TOAST_ALERTS_TOKEN) private toastService: ToastAlertsAbstractService,
83
- @Inject(NOTION_SERVICE_TOKEN) private notionService: NotionAbstractService,
84
- private _activatedRoute: ActivatedRoute,
85
- private router: Router,
86
- private cdr: ChangeDetectorRef,
87
- private dialogService: DialogService,
88
- ) {}
89
-
90
- ngOnInit(): void {
91
- this.getLessonIfId();
92
-
93
- this.items = [
94
- {
95
- tooltipOptions: { tooltipLabel: 'Reparar con IA: Repara la lección con IA', tooltipPosition: 'bottom' },
96
- icon: 'pi pi-magic',
97
- command: () => this.generateByAI(),
98
- },
99
- {
100
- tooltipOptions: { tooltipLabel: 'Selector: Agrega un selector con multiples opciones', tooltipPosition: 'bottom' },
101
- icon: 'pi pi-caret-down',
102
- command: () => this.openComponentBuilder('selector'),
103
- },
104
- {
105
- tooltipOptions: { tooltipLabel: 'Hablar: Para que una palabra o frase sea reproducible', tooltipPosition: 'bottom' },
106
- icon: 'pi pi-megaphone',
107
- command: () => this.openComponentBuilder('speaker'),
108
- },
109
- {
110
- tooltipOptions: {
111
- tooltipLabel: 'Entrada de texto: Escribe una respuesta en un cuadro de texto',
112
- tooltipPosition: 'bottom',
113
- },
114
- icon: 'pi pi-pencil',
115
- command: () => this.openComponentBuilder('textWriter'),
116
- },
117
- {
118
- tooltipOptions: { tooltipLabel: 'Verbo: Para ver datos de un verbo', tooltipPosition: 'bottom' },
119
- icon: 'pi pi-eye',
120
- command: () => this.openComponentBuilder('verbSummary'),
121
- },
122
- {
123
- tooltipOptions: { tooltipLabel: 'Palabra: Para ver datos de una palabra', tooltipPosition: 'bottom' },
124
- icon: 'pi pi-file-word',
125
- command: () => this.openComponentBuilder('wordSummary'),
126
- },
127
- ];
128
- }
129
-
130
- public async getLessonIfId(): Promise<void> {
131
- if (!this.lessonId) {
132
- this.cover = `${GradientCss}, url("/assets/images/default_banner.webp")`;
133
- return;
134
- }
135
- this.lesson = await this.lessonService.getLesson(this.lessonId);
136
- console.log('lesson', this.lesson);
137
- if (!this.lesson) {
138
- this.toastService.warn({ title: 'No se encontró la lección', subtitle: 'Quiza el id es incorrecto' });
139
- }
140
- this.togleRender();
141
- this.updateCover();
111
+ constructor() {
112
+ // Removed Speed Dial Items initialization
113
+
114
+ // Effect to fetch lesson data when ID changes
115
+ effect(async () => {
116
+ const id = this.lessonId();
117
+ console.log('Lesson ID Signal:', id);
118
+ if (id) {
119
+ this.isLoadingLesson.set(true); // Start loading
120
+ try {
121
+ const fetchedLesson = await this.#lessonService.getLesson(id);
122
+ if (fetchedLesson) {
123
+ this.lesson.set(fetchedLesson);
124
+ } else {
125
+ this.lesson.set(undefined); // Reset if not found
126
+ this.#toastService.warn({ title: 'No se encontró la lección', subtitle: 'Quizá el id es incorrecto' });
127
+ // Optional: Navigate away or show a specific "not found" state
128
+ // this.#router.navigate(['/path/to/lessons']);
129
+ }
130
+ } catch (error) {
131
+ console.error('Error fetching lesson:', error);
132
+ this.lesson.set(undefined); // Reset on error
133
+ this.#toastService.error({ title: 'Error al cargar la lección', subtitle: 'Intenta de nuevo más tarde' });
134
+ } finally {
135
+ this.isLoadingLesson.set(false); // Stop loading
136
+ }
137
+ } else {
138
+ // Handle case for new lesson (ID is null/undefined)
139
+ this.lesson.set({ textCoded: `<h1>Nueva lección </h1> <p> Texto aquí</p>`, tags: [] } as ILesson); // Set default new lesson structure
140
+ this.isLoadingLesson.set(false); // Ensure loading is off
141
+ }
142
+ });
142
143
  }
143
144
 
144
- public updateCover(): void {
145
- const cover: LessonImage = this.lesson.media?.images?.find((img) => img.type === 'cover');
146
- if (cover) {
147
- this.cover = `${GradientCss}, url('${cover.url}')`;
148
- } else {
149
- this.cover = `${GradientCss}, url("/assets/images/default_banner.webp")`;
150
- }
151
- this.cdr.detectChanges();
145
+ /**
146
+ * Updates a specific property on the lesson signal.
147
+ * Used for ngModelChange events to simulate two-way binding with signals.
148
+ * @param property The key of the ILesson property to update.
149
+ * @param value The new value for the property.
150
+ */
151
+ public updateLessonProperty<K extends keyof ILesson>(property: K, value: ILesson[K]): void {
152
+ this.lesson.update((currentLesson) => {
153
+ if (!currentLesson) return undefined;
154
+ return { ...currentLesson, [property]: value };
155
+ });
152
156
  }
153
157
 
154
158
  public onTagRemove(tag: any): void {
155
- this.lesson.tags = this.lesson.tags.filter((text) => text !== tag.text);
159
+ this.lesson.update((currentLesson) => {
160
+ if (!currentLesson) return undefined;
161
+ const updatedTags = currentLesson.tags.filter((text: string) => text !== tag.text);
162
+ return { ...currentLesson, tags: updatedTags };
163
+ });
156
164
  }
157
165
 
158
166
  public onTagAdd(tag: { value: string; input: any }): void {
159
167
  if (tag.value) {
160
- this.lesson.tags.push(tag.value);
168
+ this.lesson.update((currentLesson) => {
169
+ if (!currentLesson) return undefined;
170
+ // Avoid duplicate tags if necessary
171
+ if (currentLesson.tags.includes(tag.value)) {
172
+ return currentLesson;
173
+ }
174
+ const updatedTags = [...currentLesson.tags, tag.value];
175
+ return { ...currentLesson, tags: updatedTags };
176
+ });
161
177
  }
162
- tag.input.nativeElement.value = '';
178
+ tag.input.nativeElement.value = ''; // Clear input
163
179
  }
164
180
 
165
- public async saveLesson(event: Event = null): Promise<ILesson> {
166
- if (event) {
167
- event.preventDefault();
168
- }
169
- // TODO: Optimización importante, guardar una lección guarda todos los datos de nuevo, y no solo los que se modificaron, encontrar una forma de optimizar.
170
- const lesson = await this.lessonService.postLesson(this.lesson);
171
- if (!this.lessonId) {
172
- this.toastService.success({ title: 'Se creó la lección', subtitle: 'Éxito' });
173
- this.router.navigate([lesson.id], { relativeTo: this._activatedRoute });
174
- } else {
175
- this.toastService.info({ title: 'Se guadarón los cambios en la lección', subtitle: 'Guardado' });
176
- this.lesson = lesson;
177
- this.validateAudios();
181
+ public async saveLesson(event?: Event): Promise<ILesson | undefined> {
182
+ event?.preventDefault();
183
+
184
+ const currentLesson = this.lesson();
185
+ if (!currentLesson) {
186
+ this.#toastService.error({ title: 'Error', subtitle: 'No hay datos de lección para guardar' });
187
+ return undefined;
178
188
  }
179
189
 
180
- this.togleRender();
181
- return lesson;
182
- }
190
+ // Clean orphaned components before saving
191
+ const lessonToSave = this.#lessonUtilsService.cleanOrphanedComponents(currentLesson);
183
192
 
184
- public async validateAudios() {
185
- if (!this.lesson.components) {
186
- return;
193
+ // TODO: Implement optimization for saving only changed data.
194
+ // This requires comparing lessonToSave with the initially fetched state.
195
+ this.isLoadingLesson.set(true); // Indicate saving
196
+ try {
197
+ // Use the cleaned lesson object for saving
198
+ const savedLesson = await this.#lessonService.postLesson(lessonToSave);
199
+ const currentId = this.lessonId();
200
+
201
+ if (!currentId) {
202
+ // It was a new lesson, now it has an ID. Navigate.
203
+ this.#toastService.success({ title: 'Se creó la lección', subtitle: 'Éxito' });
204
+ // The effect should automatically fetch the lesson again after navigation due to paramMap change.
205
+ this.#router.navigate(['../', savedLesson.id], { relativeTo: this.#activatedRoute });
206
+ } else {
207
+ // It was an existing lesson, update the signal with the potentially updated data from the backend.
208
+ this.lesson.set(savedLesson);
209
+ this.#toastService.info({ title: 'Se guardaron los cambios en la lección', subtitle: 'Guardado' });
210
+ // Call the service method for validation
211
+ this.#lessonUtilsService.validateAudios(this.lesson());
212
+ }
213
+ return savedLesson;
214
+ } catch (error: any) {
215
+ // Type error
216
+ console.error('Error saving lesson:', error);
217
+ this.#toastService.error({ title: 'Error al guardar', subtitle: 'No se pudieron guardar los cambios' });
218
+ return undefined;
219
+ } finally {
220
+ this.isLoadingLesson.set(false); // Finish saving indication
187
221
  }
188
- // al menos un audio sin generar.
189
- // agregar más condigciones después
190
- // this.lesson.components.forEach((component: SpeakerCompConfiguration) => {
191
- // if (component.component === 'speaker') {
192
- // if (component.settings.voice && !component.audio) {
193
- // // this.toastrService.warn('Se encontraron audios por generar', 'Será rápido');
194
- // // TODO call backend.
195
- // this.lessonService.generateAudiosForLesson(this.lesson.id).then((res) => {
196
- // // this.toastrService.success('Se generaron los audios', 'Listo');
197
- // console.log(res);
198
- // });
199
- // return;
200
- // }
201
- // }
202
- // });
203
- }
222
+ } // Add missing closing brace for saveLesson
223
+
224
+ // Removed openComponentBuilder method
225
+
226
+ /**
227
+ * Handles the event emitted when a component is added via the adder component.
228
+ * @param result The configuration data returned from the component builder dialog. Expected format: { obj: LessonComponentConfiguration }
229
+ */
230
+ public onComponentAdded(result: ComponentBuildData): void {
231
+ debugger;
232
+ // Check if result and result.obj.id exist
233
+ const newComponent = result?.obj;
234
+ if (newComponent?.id) {
235
+ console.log('Component builder closed, result received in editor:', newComponent);
236
+
237
+ // // Transform LessonComponentConfiguration to DynamicContentComponent
238
+ // const dynamicComponentToAdd: DynamicContentComponent = {
239
+ // id: componentConfig.id,
240
+ // component: componentConfig.component,
241
+ // inputs: {
242
+ // config: componentConfig, // Pass the original config object as an input named 'config'
243
+ // // Add other potential inputs if needed based on component type later
244
+ // },
245
+ // };
246
+
247
+ // Update the lesson signal, adding the transformed component to the dynamicComponents object
248
+ this.lesson.update((currentLesson: any) => {
249
+ if (!currentLesson) return undefined;
250
+
251
+ // Ensure dynamicComponents object exists, initialize if not
252
+ const currentDynamicComponents = currentLesson.dynamicComponents || {};
253
+
254
+ // Create the updated dynamicComponents object
255
+ const updatedDynamicComponents: Record<string, LessonComponentConfiguration<any>> = {
256
+ ...currentDynamicComponents,
257
+ [newComponent.id]: newComponent, // Use component's id as the key
258
+ };
259
+
260
+ // Return the updated lesson state
261
+ return { ...currentLesson, dynamicComponents: updatedDynamicComponents };
262
+ });
204
263
 
205
- public togleRender(_event: any = null): void {
206
- this.isRendered = false;
207
- setTimeout(() => {
208
- this.isRendered = true;
209
- this.cdr.detectChanges();
210
- }, 400);
211
- this.cdr.detectChanges();
264
+ // Optionally save the lesson after adding the component
265
+ // this.saveLesson();
266
+ }
212
267
  }
213
268
 
214
- public openComponentBuilder(type: string): Observable<any> {
215
- return this.dialogService.open(LessonComponentBuilders[type], { width: '550px', header: 'Agregar componente', closable: true }).onClose.pipe();
269
+ public openCropper(): void {
270
+ // Correctly define openCropper
271
+ this.isCropperVisible.set(true);
216
272
  }
217
273
 
218
- public uploadCover(imageUploaded: any) {
219
- const image = { type: 'cover', ...imageUploaded };
220
-
221
- if (!this.lesson.media) {
222
- // puede que no exista media
223
- this.lesson.media = {};
224
- this.lesson.media.images = [image];
225
- } else {
226
- // solo sustituir el cover si ya existe, como siempre utilizo el mismo nombre, no tengo que eliminar la anterior si actualizo.
227
- // const currentCover = this.lesson.media.images.find((img) => img.type === 'cover');
274
+ // isLoadingLesson signal is used directly
228
275
 
229
- this.lesson.media.images = this.lesson.media.images.filter((img) => img.type !== 'cover');
230
- this.lesson.media.images.push(image);
276
+ public async generateByAI(): Promise<void> {
277
+ const currentId = this.lessonId();
278
+ if (!currentId) {
279
+ this.#toastService.warn({ title: 'Guardar primero', subtitle: 'Guarda la lección antes de usar IA.' });
280
+ return;
231
281
  }
232
- this.updateCover();
233
- this.saveLesson();
234
- }
235
282
 
236
- openCropper() {
237
- this.isCropperVisible = true;
238
- }
239
-
240
- isLoadingLesson = false;
241
- public async generateByAI() {
242
- this.isLoadingLesson = true;
283
+ this.isLoadingLesson.set(true);
243
284
  try {
244
- await this.saveLesson();
245
- await this.lessonService.postGenerateByAI(this.lesson.id);
246
- await this.getLessonIfId();
285
+ // Ensure latest changes are saved before generating
286
+ const savedLesson = await this.saveLesson();
287
+ if (!savedLesson) {
288
+ // Handle save error - toast is shown in saveLesson
289
+ throw new Error('Failed to save before AI generation');
290
+ }
291
+
292
+ // Call the service method
293
+ const updatedLesson = await this.#lessonUtilsService.generateByAI(currentId);
294
+
295
+ if (updatedLesson) {
296
+ this.lesson.set(updatedLesson); // Update the signal with AI changes from service
297
+ // Toast success is handled by the service
298
+ } else {
299
+ // Toast error is handled by the service
300
+ throw new Error('AI generation failed or lesson fetch failed after generation.');
301
+ }
302
+ } catch (error: any) {
303
+ // Type error
304
+ console.error('Error during AI generation process in component:', error);
305
+ // Service handles specific AI error toasts, maybe add a general one here if needed
306
+ // this.#toastService.error({ title: 'Error General', subtitle: 'Ocurrió un problema durante el proceso de IA.' });
247
307
  } finally {
248
- this.isLoadingLesson = false;
308
+ this.isLoadingLesson.set(false); // Stop loading
249
309
  }
250
310
  }
251
311
 
252
- public onImageSelected(event: any) {
253
- console.log(event);
312
+ /**
313
+ * Handles the image upload event, updates the lesson signal via the service, and saves.
314
+ * @param event The image upload event data.
315
+ */
316
+ public async onImageUploaded(event: any): Promise<void> {
317
+ // Call the service to update the signal
318
+ this.#lessonUtilsService.uploadCover(this.lesson, event);
319
+ // The coverBackground computed signal will update automatically.
320
+ // Save the lesson after the signal has been updated
321
+ await this.saveLesson();
254
322
  }
255
323
 
256
- public async onImageUploaded(event: any) {
257
- this.uploadCover(event);
258
- this.cdr.detectChanges();
259
- }
324
+ /**
325
+ * Imports lesson content from Notion using the LessonNotionService.
326
+ */
327
+ public async importFromNotion(): Promise<void> {
328
+ // Use the service's loading state or manage locally
329
+ this.isLoadingLesson.set(true);
330
+ try {
331
+ const newContent = await this.#lessonNotionService.importAndLinkLessonFromNotion(this.lesson(), this.lessonId());
260
332
 
261
- public async importFromNotion() {
262
- let notionPageId: string;
263
- if (this.lesson?.extras?.notionPageId) {
264
- const response = confirm(`Ya tenemos el id ${this.lesson.extras?.notionPageId} ¿Quieres usar este id?`);
265
- if (!response) {
266
- return;
267
- }
268
- notionPageId = this.lesson.extras?.notionPageId;
269
- } else {
270
- const response = prompt('Ingresa el url de notion para importar la lección, se guardará este id');
271
- if (!response) {
272
- return;
333
+ if (newContent !== null) {
334
+ // Update the lesson signal's textCoded property
335
+ this.updateLessonProperty('textCoded', newContent);
336
+ // Toast success is handled within the service now
273
337
  }
274
- notionPageId = this.extractNotionPageId(response);
275
- if (!notionPageId) return;
276
- this.linkWithNotion(notionPageId);
277
- }
278
-
279
- this.toastService.info({ title: 'Importando lección...', subtitle: 'Espera unos segundos' });
280
-
281
- const md = await this.notionService.getPageInSpecificFormat(notionPageId, NotionExportType.HTML);
282
- console.log(md);
283
- this.lesson.textCoded = md.content;
284
- this.togleRender();
285
- this.toastService.success({ title: 'Listo', subtitle: 'La lección se importó correctamente' });
286
- }
287
-
288
- extractNotionPageId(url: string) {
289
- const notionIdRegex = /[a-f0-9]{32}(?=\?|$)/;
290
- const match = url.match(notionIdRegex);
291
- const notionId = match ? match[0] : null;
292
- if (!notionId) {
293
- this.toastService.error({
294
- title: 'URL inválido',
295
- subtitle: 'Por favor ingresa una URL válida de Notion',
296
- });
338
+ // If newContent is null, the service handled errors/toasts
339
+ } finally {
340
+ // Ensure loading state is reset regardless of service outcome
341
+ // If observing service state: this.isLoadingLesson.set(this.#lessonNotionService.isLoading());
342
+ this.isLoadingLesson.set(false); // Keep local loading for now
297
343
  }
298
- return notionId;
299
344
  }
300
345
 
301
- public async linkWithNotion(notionPageId: string) {
302
- const extra = this.lesson.extras || {};
303
- extra.notionPageId = notionPageId;
304
-
305
- await this.lessonService.postLesson({ ...this.lesson, extras: extra });
306
- this.toastService.success({ title: 'Listo', subtitle: 'Se enlazó la lección con Notion' });
346
+ /**
347
+ * Calls the LessonNotionService to improve the lesson using AI based on Notion content.
348
+ */
349
+ public async improveNotionWithAI(): Promise<void> {
350
+ await this.#lessonNotionService.improveLessonWithNotionAI(this.lesson());
307
351
  }
308
352
 
309
- public async improveNotionWithAI() {
310
- const md = await this.notionService.getPageInSpecificFormat(this.lesson.extras.notionPageId, NotionExportType.HTML);
311
- console.log(md);
312
-
313
- // this.lesson.textCoded = md.content;
314
-
315
- // this.togleRender();
316
- // this.toastService.success({ title: 'Listo', subtitle: 'La lección se mejoró con AI' });
353
+ public showComponentDetails(data: any) {
354
+ alert('showComponentDetails' + JSON.stringify(data));
317
355
  }
318
356
  }
@@ -0,0 +1 @@
1
+ /* Add any specific styles for the metadata editor here if needed */