@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.
- package/fesm2022/dataclouder-ngx-lessons.mjs +1205 -514
- package/fesm2022/dataclouder-ngx-lessons.mjs.map +1 -1
- package/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.d.ts +6 -8
- package/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.d.ts +11 -0
- package/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.d.ts +39 -38
- package/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.d.ts +22 -0
- package/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.d.ts +32 -37
- package/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.d.ts +7 -17
- package/lib/components/lesson-mini-components/components/ComponentBuilder.d.ts +7 -2
- package/lib/components/lesson-mini-components/components/lessons.clases.d.ts +17 -42
- package/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.d.ts +13 -0
- package/lib/components/lesson-mini-components/components/speaker/speaker.component.d.ts +12 -0
- package/lib/services/lesson-ai.service.d.ts +18 -0
- package/lib/services/lesson-notion.service.d.ts +35 -0
- package/lib/services/lesson-utils.service.d.ts +34 -0
- package/package.json +3 -2
- package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.html +40 -35
- package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.scss +15 -2
- package/src/lib/components/dc-lessons/dc-lesson-card/dc-lesson-card.component.ts +16 -18
- package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.css +1 -0
- package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.html +46 -0
- package/src/lib/components/dc-lessons/dc-lesson-component-adder/dc-lesson-component-adder.component.ts +52 -0
- package/src/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.html +54 -92
- package/src/lib/components/dc-lessons/dc-lesson-editor/dc-lesson-editor.component.ts +268 -230
- package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.css +1 -0
- package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.html +72 -0
- package/src/lib/components/dc-lessons/dc-lesson-metadata-editor/dc-lesson-metadata-editor.component.ts +60 -0
- package/src/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.html +23 -27
- package/src/lib/components/dc-lessons/dc-lesson-renderer/dc-lesson-renderer.component.ts +247 -186
- package/src/lib/components/dc-lessons/lesson-form/lesson-form.component.ts +2 -2
- package/src/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.html +3 -3
- package/src/lib/components/dc-lessons/lesson-list/dc-lesson-list.component.ts +28 -46
- package/src/lib/components/lesson-mini-components/components/ComponentBuilder.ts +23 -15
- package/src/lib/components/lesson-mini-components/components/lessons.clases.ts +32 -66
- package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.html +62 -58
- package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.ts +2 -2
- package/src/lib/components/lesson-mini-components/components/selector/selector.component.html +1 -2
- package/src/lib/components/lesson-mini-components/components/selector/selector.component.ts +2 -2
- package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.html +5 -27
- package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.ts +38 -25
- package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.html +9 -7
- package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.ts +30 -26
- package/src/lib/components/lesson-mini-components/components/translationSwitcher/translationSwitcher.component.ts +2 -2
- package/src/lib/components/lesson-mini-components/components/translationSwitcher/translationSwitcherBuilder/translationSwitcherBuilder.component.ts +2 -2
- package/src/lib/services/lesson-ai.service.ts +103 -0
- package/src/lib/services/lesson-notion.service.ts +161 -0
- package/src/lib/services/lesson-utils.service.ts +181 -0
- package/src/lib/components/lesson-mini-components/components/selector/selector-builder/selector-builder.component.spec.ts +0 -25
- package/src/lib/components/lesson-mini-components/components/speaker/speaker-builder/speaker-builder.component.spec.ts +0 -25
- package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.spec.ts +0 -25
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
// ❌ can use this until i copy services to this lib
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { Component, Input, OnInit } from '@angular/core';
|
|
3
|
+
import { ButtonModule } from 'primeng/button';
|
|
4
|
+
import { SpeakerCompConfiguration } from '../lessons.clases';
|
|
5
|
+
import { TTSGenerated } from '@dataclouder/ngx-tts';
|
|
5
6
|
|
|
6
|
-
// @
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
// import { CONVERSATION_AI_TOKEN } from '@dataclouder/ngx-agent-cards';
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-speaker',
|
|
10
|
+
templateUrl: './speaker.component.html',
|
|
11
|
+
styleUrls: ['./speaker.component.scss'],
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [ButtonModule],
|
|
14
|
+
})
|
|
15
|
+
export class SpeakerComponent implements OnInit {
|
|
16
|
+
ngOnInit(): void {
|
|
17
|
+
// throw new Error('Method not implemented.');
|
|
18
|
+
}
|
|
19
|
+
@Input() config: SpeakerCompConfiguration;
|
|
20
|
+
@Input() tts: TTSGenerated | undefined;
|
|
15
21
|
|
|
16
|
-
// voiceOptions = VoiceOptions;
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// }
|
|
29
|
-
// }
|
|
22
|
+
// voiceOptions = VoiceOptions;
|
|
23
|
+
// constructor(private speachService: SpeechService, private audioService: AudioService) {}
|
|
24
|
+
// ngOnInit(): void {}
|
|
25
|
+
public speach() {
|
|
26
|
+
console.log('should speech but will do in next version');
|
|
27
|
+
// if (this.config.audio) {
|
|
28
|
+
// this.audioService.playAudio(this.config.audio.url);
|
|
29
|
+
// } else {
|
|
30
|
+
// this.speachService.speach(this.config.settings.text);
|
|
31
|
+
// }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
|
|
4
4
|
import { ButtonModule } from 'primeng/button';
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ import { SpeakerCompConfiguration } from '../lessons.clases';
|
|
|
8
8
|
@Component({
|
|
9
9
|
selector: 'app-translation-switcher',
|
|
10
10
|
standalone: true,
|
|
11
|
-
imports: [
|
|
11
|
+
imports: [ButtonModule],
|
|
12
12
|
templateUrl: './translationSwitcher.component.html',
|
|
13
13
|
styleUrl: './translationSwitcher.component.css',
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.Default,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
|
2
2
|
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
|
|
5
5
|
import { ButtonModule } from 'primeng/button';
|
|
6
6
|
import { InputTextModule } from 'primeng/inputtext';
|
|
@@ -11,7 +11,7 @@ import { ComponentBuilder } from '../../ComponentBuilder';
|
|
|
11
11
|
@Component({
|
|
12
12
|
selector: 'app-translation-switcher-builder',
|
|
13
13
|
standalone: true,
|
|
14
|
-
imports: [
|
|
14
|
+
imports: [FormsModule, ReactiveFormsModule, ButtonModule, InputTextModule],
|
|
15
15
|
templateUrl: './translationSwitcherBuilder.component.html',
|
|
16
16
|
styleUrl: './translationSwitcherBuilder.component.css',
|
|
17
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -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
|
-
});
|
package/src/lib/components/lesson-mini-components/components/speaker/speaker.component.spec.ts
DELETED
|
@@ -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
|
-
});
|