@dataclouder/ngx-agent-cards 0.2.14 → 0.2.15

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.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, inject, signal, computed, RendererFactory2, ApplicationRef, Injector, EnvironmentInjector, createComponent, ChangeDetectionStrategy, Component, output, Input, input, effect, ViewChild, DestroyRef, HostBinding, untracked, ElementRef, Pipe, ChangeDetectorRef, EventEmitter, Output, viewChild } from '@angular/core';
2
+ import { InjectionToken, Injectable, inject, signal, computed, RendererFactory2, ApplicationRef, Injector, EnvironmentInjector, createComponent, ChangeDetectionStrategy, Component, output, Input, input, effect, ViewChild, DestroyRef, untracked, HostBinding, ElementRef, Pipe, ChangeDetectorRef, EventEmitter, Output, viewChild } from '@angular/core';
3
3
  import * as i1$1 from '@dataclouder/ngx-core';
4
4
  import { MoodStateOptions, LANGUAGES, EntityCommunicationService, TOAST_ALERTS_TOKEN, APP_CONFIG, MoodState, AudioSpeed as AudioSpeed$1, LoadingBarService, EModelQuality, getLangDesc, AudioSpeedReverse, formatCamelCaseString, EntityBaseDetailComponent, AudioNotificationService, SUPPORTED_LANGUAGES, FormUtilsService, LangDescTranslation, FlagPipe, FlagImgPipe, DcTagsFormComponent, EntityBaseFormComponent, getSupportedLanguageOptions, DcManageableFormComponent, DcLearnableFormComponent, EntityBaseListV2Component, DCFilterBarComponent, QuickTableComponent, EmptyStateComponent, EntityBaseSignalFormComponent } from '@dataclouder/ngx-core';
5
5
  import { UserService } from '@dataclouder/ngx-users';
6
6
  import { AiWhisperService, TtsService, NgxAiServicesService, GeneratedAssetsService, VoiceSelectorComponent } from '@dataclouder/ngx-ai-services';
7
7
  import { FirebaseAuthService } from '@dataclouder/ngx-auth';
8
- import { firstValueFrom, Subject, fromEvent, filter, BehaviorSubject, Subscription } from 'rxjs';
8
+ import { firstValueFrom, Subject, fromEvent, BehaviorSubject, filter, Subscription } from 'rxjs';
9
9
  import * as i1 from '@angular/common';
10
10
  import { DOCUMENT, CommonModule, KeyValuePipe, NgTemplateOutlet, DatePipe, SlicePipe } from '@angular/common';
11
11
  import { nanoid } from 'nanoid';
@@ -24,7 +24,7 @@ import { DCMicComponent } from '@dataclouder/ngx-mic';
24
24
  import * as i4 from 'primeng/textarea';
25
25
  import { TextareaModule } from 'primeng/textarea';
26
26
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
27
- import { map, takeUntil } from 'rxjs/operators';
27
+ import { take, takeUntil, map } from 'rxjs/operators';
28
28
  import * as i2$2 from 'primeng/popover';
29
29
  import { PopoverModule } from 'primeng/popover';
30
30
  import * as i1$3 from 'primeng/skeleton';
@@ -283,6 +283,7 @@ var ChatEventType;
283
283
  ChatEventType["WordClicked"] = "wordClicked";
284
284
  ChatEventType["MoodDetected"] = "moodDetected";
285
285
  ChatEventType["AudioStarted"] = "audioStarted";
286
+ ChatEventType["AudioStopped"] = "audioStopped";
286
287
  // Add other potential event types here as needed
287
288
  // e.g., MessageSent = 'messageSent', SettingsChanged = 'settingsChanged', GoalCompleted = 'goalCompleted'
288
289
  })(ChatEventType || (ChatEventType = {}));
@@ -1865,6 +1866,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1865
1866
  }]
1866
1867
  }] });
1867
1868
 
1869
+ class ChatMonitorService {
1870
+ // Actualmente la única comunicación es con el background para cambiar el fondo.
1871
+ // El problema de comunicar así es si tiene que ser singleton, y en control markets tengo varias instancias de dc-chat.
1872
+ // Voy a utilizar el evento por ahora, y ver si combiene luego crear un servicio.
1873
+ // Se requiere comunicar el chat-engine con la app afuera, va a ver 2 formas de hacerlo.
1874
+ // Mediante eventos del chat, o estar subscrito al chat-monitor.
1875
+ // La complejidad del chat monitor es menor porque puedo injectarlo en cualquier lugar.
1876
+ // Los eventos estan bien pero si el vento viene muy interno tengo que propagarlo bastante.
1877
+ constructor() {
1878
+ this.activeAudioMessage = signal(null, ...(ngDevMode ? [{ debugName: "activeAudioMessage" }] : /* istanbul ignore next */ []));
1879
+ this.currentMood = signal(null, ...(ngDevMode ? [{ debugName: "currentMood" }] : /* istanbul ignore next */ []));
1880
+ this.isConversationActive = signal(false, ...(ngDevMode ? [{ debugName: "isConversationActive" }] : /* istanbul ignore next */ []));
1881
+ // Background
1882
+ this.backgroundUrl = signal(undefined, ...(ngDevMode ? [{ debugName: "backgroundUrl" }] : /* istanbul ignore next */ []));
1883
+ // Audio pipeline signals — source of truth for Live2D lip-sync bridge
1884
+ this.messageAudioWillPlay$ = signal(null, ...(ngDevMode ? [{ debugName: "messageAudioWillPlay$" }] : /* istanbul ignore next */ []));
1885
+ this.messageAudioStatus$ = signal(null, ...(ngDevMode ? [{ debugName: "messageAudioStatus$" }] : /* istanbul ignore next */ []));
1886
+ console.log(`%c ChatMonitorService instantiated: ${Math.random()}`, 'color: #00ff00; font-weight: bold; padding: 2px 4px; border: 1px solid #00ff00;');
1887
+ }
1888
+ logCurrentMood(mood) {
1889
+ this.currentMood.set(mood);
1890
+ }
1891
+ cleanMonitorVars() {
1892
+ this.currentMood.set(null);
1893
+ this.activeAudioMessage.set(null);
1894
+ this.isConversationActive.set(false);
1895
+ }
1896
+ setBackground(imageUrl) {
1897
+ if (imageUrl) {
1898
+ this.backgroundUrl.set(imageUrl);
1899
+ }
1900
+ else {
1901
+ this.backgroundUrl.set(undefined);
1902
+ }
1903
+ }
1904
+ logMessageAudioWillPlay(message) {
1905
+ this.messageAudioWillPlay$.set(message);
1906
+ }
1907
+ logMessageAudioStatus(message, status) {
1908
+ this.messageAudioStatus$.set({ message, status });
1909
+ }
1910
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1911
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, providedIn: 'root' }); }
1912
+ }
1913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, decorators: [{
1914
+ type: Injectable,
1915
+ args: [{
1916
+ providedIn: 'root',
1917
+ }]
1918
+ }], ctorParameters: () => [] });
1919
+
1868
1920
  /**
1869
1921
  * @description
1870
1922
  * Service responsible for managing the real-time, dynamic state of a conversation flow.
@@ -1879,6 +1931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1879
1931
  */
1880
1932
  class ConversationFlowStateService {
1881
1933
  constructor() {
1934
+ this.chatMonitorService = inject(ChatMonitorService, { optional: true });
1882
1935
  this.initialState = {
1883
1936
  goal: { value: 0 },
1884
1937
  challenges: [],
@@ -1944,6 +1997,7 @@ class ConversationFlowStateService {
1944
1997
  }
1945
1998
  if (newState.moodState?.value) {
1946
1999
  this.moodUpdated.next(newState.moodState.value);
2000
+ this.chatMonitorService?.logCurrentMood(newState.moodState.value);
1947
2001
  }
1948
2002
  if (newState.goal && newState.goal.deltaPoints !== undefined && newState.goal.deltaPoints !== 0) {
1949
2003
  this.goalEvaluationUpdated.next({ deltaPoints: newState.goal.deltaPoints });
@@ -2004,6 +2058,7 @@ class ConversationFlowStateService {
2004
2058
  }));
2005
2059
  if (moodState.value) {
2006
2060
  this.moodUpdated.next(moodState.value);
2061
+ this.chatMonitorService?.logCurrentMood(moodState.value);
2007
2062
  }
2008
2063
  }
2009
2064
  /**
@@ -2505,58 +2560,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2505
2560
  }]
2506
2561
  }] });
2507
2562
 
2508
- class ChatMonitorService {
2509
- // Actualmente la única comunicación es con el background para cambiar el fondo.
2510
- // El problema de comunicar así es si tiene que ser singleton, y en control markets tengo varias instancias de dc-chat.
2511
- // Voy a utilizar el evento por ahora, y ver si combiene luego crear un servicio.
2512
- // Se requiere comunicar el chat-engine con la app afuera, va a ver 2 formas de hacerlo.
2513
- // Mediante eventos del chat, o estar subscrito al chat-monitor.
2514
- // La complejidad del chat monitor es menor porque puedo injectarlo en cualquier lugar.
2515
- // Los eventos estan bien pero si el vento viene muy interno tengo que propagarlo bastante.
2516
- constructor() {
2517
- this.messageAudioWillPlay = signal(null, ...(ngDevMode ? [{ debugName: "messageAudioWillPlay" }] : /* istanbul ignore next */ []));
2518
- this.messageAudioWillPlay$ = this.messageAudioWillPlay.asReadonly();
2519
- this.currentMood = signal(null, ...(ngDevMode ? [{ debugName: "currentMood" }] : /* istanbul ignore next */ []));
2520
- this.isConversationActive = signal(false, ...(ngDevMode ? [{ debugName: "isConversationActive" }] : /* istanbul ignore next */ []));
2521
- // Background
2522
- this.backgroundUrl = signal(undefined, ...(ngDevMode ? [{ debugName: "backgroundUrl" }] : /* istanbul ignore next */ []));
2523
- console.log(`%c ChatMonitorService instantiated: ${Math.random()}`, 'color: #00ff00; font-weight: bold; padding: 2px 4px; border: 1px solid #00ff00;');
2524
- }
2525
- logMessageAudioWillPlay(message) {
2526
- // console.log('Audio will play....', message);
2527
- this.messageAudioWillPlay.set(message);
2528
- }
2529
- logCurrentMood(mood) {
2530
- this.currentMood.set(mood);
2531
- }
2532
- cleanMonitorVars() {
2533
- this.currentMood.set(null);
2534
- this.messageAudioWillPlay.set(null);
2535
- this.isConversationActive.set(false);
2536
- }
2537
- setBackground(imageUrl) {
2538
- if (imageUrl) {
2539
- this.backgroundUrl.set(imageUrl);
2540
- }
2541
- else {
2542
- this.backgroundUrl.set(undefined);
2543
- }
2544
- }
2545
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2546
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, providedIn: 'root' }); }
2547
- }
2548
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, decorators: [{
2549
- type: Injectable,
2550
- args: [{
2551
- providedIn: 'root',
2552
- }]
2553
- }], ctorParameters: () => [] });
2554
-
2555
2563
  class MessageProcessingService {
2556
2564
  constructor() {
2557
2565
  this.MAX_CHUNK_LENGTH = 190;
2558
2566
  }
2559
- processMessage(message, conversationSettings) {
2567
+ processMessage(message, conversationSettings, voiceSettings) {
2560
2568
  const processedMessage = {
2561
2569
  ...message,
2562
2570
  messageId: message.messageId || nanoid(),
@@ -2568,25 +2576,25 @@ class MessageProcessingService {
2568
2576
  return processedMessage;
2569
2577
  }
2570
2578
  if (processedMessage.role === ChatRole.Assistant) {
2571
- return this.processAssistantMessage(processedMessage, conversationSettings);
2579
+ return this.processAssistantMessage(processedMessage, conversationSettings, voiceSettings);
2572
2580
  }
2573
2581
  return processedMessage;
2574
2582
  }
2575
- processAssistantMessage(message, settings) {
2583
+ processAssistantMessage(message, settings, voiceSettings) {
2576
2584
  if (settings.avatarImages.assistant) {
2577
2585
  message.imgUrl = settings.avatarImages.assistant;
2578
2586
  }
2579
- const mainVoice = settings?.mainVoice?.voice || settings?.tts?.voice;
2587
+ const mainVoice = voiceSettings?.main?.voice || settings?.mainVoice?.voice || settings?.tts?.voice;
2580
2588
  message.voice = this.getVoice(mainVoice);
2581
2589
  switch (settings.textEngine) {
2582
2590
  case TextEngines.SimpleText:
2583
2591
  this.processSimpleText(message, settings);
2584
2592
  break;
2585
2593
  case TextEngines.MarkdownMultiMessages:
2586
- this.processMultiMessages(message, settings);
2594
+ this.processMultiMessages(message, settings, voiceSettings);
2587
2595
  break;
2588
2596
  case TextEngines.MarkdownSSML:
2589
- this.processSSML(message, settings);
2597
+ this.processSSML(message, settings, voiceSettings);
2590
2598
  break;
2591
2599
  }
2592
2600
  return message;
@@ -2605,9 +2613,9 @@ class MessageProcessingService {
2605
2613
  };
2606
2614
  });
2607
2615
  }
2608
- processMultiMessages(message, settings) {
2616
+ processMultiMessages(message, settings, voiceSettings) {
2609
2617
  const htmlSegments = convertToHTML(message.content);
2610
- const secondaryVoice = settings?.secondaryVoice?.voice || settings?.tts?.secondaryVoice || 'en-US-News-L';
2618
+ const secondaryVoice = voiceSettings?.secondary?.voice || settings?.secondaryVoice?.voice || settings?.tts?.secondaryVoice || 'en-US-News-L';
2611
2619
  const processedSegments = [];
2612
2620
  for (const segment of htmlSegments) {
2613
2621
  const isItalics = segment.tag === 'em';
@@ -2656,11 +2664,12 @@ class MessageProcessingService {
2656
2664
  return `<p>${text}</p>`;
2657
2665
  }
2658
2666
  }
2659
- processSSML(message, settings) {
2660
- if (!settings?.secondaryVoice?.voice) {
2667
+ processSSML(message, settings, voiceSettings) {
2668
+ const secondaryVoice = voiceSettings?.secondary?.voice || settings?.secondaryVoice?.voice || settings?.tts?.secondaryVoice;
2669
+ if (!secondaryVoice) {
2661
2670
  throw new Error('Secondary voice is required for SSML');
2662
2671
  }
2663
- const content = this.subsItalicsByTag(message.content, settings.secondaryVoice.voice);
2672
+ const content = this.subsItalicsByTag(message.content, secondaryVoice);
2664
2673
  message.ssml = `<speak>${content}</speak>`;
2665
2674
  }
2666
2675
  splitContent(content, maxLength) {
@@ -3469,7 +3478,7 @@ class ConversationService {
3469
3478
  }
3470
3479
  createNewUserMessage() {
3471
3480
  const message = { content: '...', role: ChatRole.User };
3472
- const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState());
3481
+ const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3473
3482
  this.messagesStateService.addMessage(processedMessage);
3474
3483
  return processedMessage.messageId;
3475
3484
  }
@@ -3576,7 +3585,7 @@ class ConversationService {
3576
3585
  const firstAssistantMsg = conversationSettings.messages.find((message) => message.role === ChatRole.Assistant);
3577
3586
  if (firstAssistantMsg) {
3578
3587
  // Process the first assistant message
3579
- const processedMessage = this.messageProcessingService.processMessage(firstAssistantMsg, this.conversationSettingsState());
3588
+ const processedMessage = this.messageProcessingService.processMessage(firstAssistantMsg, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3580
3589
  this.messagesStateService.updateMessage(firstAssistantMsg.messageId, processedMessage);
3581
3590
  // Find the index of the message with the matching ID
3582
3591
  const messageIndex = conversationSettings.messages.findIndex((message) => message.messageId === firstAssistantMsg.messageId);
@@ -3625,7 +3634,7 @@ class ConversationService {
3625
3634
  }
3626
3635
  else {
3627
3636
  // Means is new meessage, Process and add the new message
3628
- const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState());
3637
+ const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3629
3638
  // Ensure ID exists (processMessage should handle this, but fallback just in case)
3630
3639
  processedMessage.messageId = processedMessage.messageId || nanoid();
3631
3640
  this.messagesStateService.addMessage(processedMessage);
@@ -3696,7 +3705,7 @@ class ConversationService {
3696
3705
  throw new Error('No message returned from AI');
3697
3706
  }
3698
3707
  // Process response
3699
- const newMessage = this.messageProcessingService.processMessage({ content: response.content, role: ChatRole.Assistant }, conversationSettings);
3708
+ const newMessage = this.messageProcessingService.processMessage({ content: response.content, role: ChatRole.Assistant }, conversationSettings, this.agentCardState.agentCard$()?.voice);
3700
3709
  this.messagesStateService.addMessage(newMessage);
3701
3710
  this.isThinkingSignal.set(false);
3702
3711
  // Run Dynamic Flow Evaluations
@@ -3705,9 +3714,10 @@ class ConversationService {
3705
3714
  }
3706
3715
  async handleStreamingResponse(conversation, conversationSettings) {
3707
3716
  const messageId = nanoid();
3708
- const mainVoice = conversationSettings?.mainVoice?.voice || conversationSettings?.tts?.voice;
3717
+ const agentCard = this.agentCardState.agentCard$();
3718
+ const mainVoice = agentCard?.voice?.main?.voice || conversationSettings?.mainVoice?.voice || conversationSettings?.tts?.voice;
3709
3719
  const assistantVoice = this.messageProcessingService.getVoice(mainVoice);
3710
- const secondaryVoice = conversationSettings?.secondaryVoice?.voice || conversationSettings?.tts?.secondaryVoice || 'en-US-News-L';
3720
+ const secondaryVoice = agentCard?.voice?.secondary?.voice || conversationSettings?.secondaryVoice?.voice || conversationSettings?.tts?.secondaryVoice || 'en-US-News-L';
3711
3721
  let assistantMessage = {
3712
3722
  messageId,
3713
3723
  role: ChatRole.Assistant,
@@ -3879,7 +3889,7 @@ class ConversationService {
3879
3889
  isLoading: true,
3880
3890
  };
3881
3891
  // Process to get initial structure and ensure it's ready for display
3882
- const processedPlaceholder = this.messageProcessingService.processMessage(placeholder, this.conversationSettingsState());
3892
+ const processedPlaceholder = this.messageProcessingService.processMessage(placeholder, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3883
3893
  // Use passed updateId or newly generated one from processing
3884
3894
  const messageId = updateId || processedPlaceholder.messageId || nanoid();
3885
3895
  processedPlaceholder.messageId = messageId;
@@ -4003,7 +4013,6 @@ function provideLive2D(config) {
4003
4013
 
4004
4014
  class VideoPlayerService {
4005
4015
  constructor() {
4006
- this.conversationFlowStateService = inject(ConversationFlowStateService);
4007
4016
  this.useNativePlayer = false;
4008
4017
  this.isRewinding = false;
4009
4018
  this.videoQueue = [];
@@ -4013,17 +4022,6 @@ class VideoPlayerService {
4013
4022
  this.agentCard = signal(undefined, ...(ngDevMode ? [{ debugName: "agentCard" }] : /* istanbul ignore next */ []));
4014
4023
  console.log(' CONSTRUCTOR FOR VIDEO PLAYER...');
4015
4024
  this.playAndRewindedSubscription = this.playAndRewinded$.subscribe(() => { });
4016
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
4017
- console.log('CHANGING MOOD.. ', mood);
4018
- const card = this.agentCard();
4019
- if (mood && card?.assets?.motions) {
4020
- const motionUrl = card.assets.motions.find((m) => m.metadata?.moodState === mood)?.url;
4021
- if (motionUrl) {
4022
- this.addVideosToQueue([motionUrl]);
4023
- this.playAndRewind();
4024
- }
4025
- }
4026
- });
4027
4025
  }
4028
4026
  setAgentCard(card) {
4029
4027
  this.agentCard.set(card);
@@ -4155,9 +4153,6 @@ class VideoPlayerService {
4155
4153
  if (this.playAndRewindedSubscription) {
4156
4154
  this.playAndRewindedSubscription.unsubscribe();
4157
4155
  }
4158
- if (this.moodSubscription) {
4159
- this.moodSubscription.unsubscribe();
4160
- }
4161
4156
  }
4162
4157
  playNextInQueueNative() {
4163
4158
  if (this.videoQueue.length > 0) {
@@ -4245,25 +4240,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
4245
4240
 
4246
4241
  class VideoPlayerNativeService {
4247
4242
  constructor() {
4248
- this.conversationFlowStateService = inject(ConversationFlowStateService);
4249
4243
  this.videoQueue = [];
4250
4244
  this.defaultVideoUrl = null;
4251
4245
  this.pendingPlayingHandler = null;
4252
4246
  this.pendingPlayingTarget = null;
4253
4247
  this.agentCard = signal(undefined, ...(ngDevMode ? [{ debugName: "agentCard" }] : /* istanbul ignore next */ []));
4254
4248
  this.isVideoPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isVideoPlaying" }] : /* istanbul ignore next */ []));
4255
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
4256
- const card = this.agentCard();
4257
- if (!mood || !card?.assets?.motions)
4258
- return;
4259
- const motionUrl = card.assets.motions.find((m) => m.metadata?.moodState === mood)?.url;
4260
- if (!motionUrl)
4261
- return;
4262
- // Interrupt current playback. transitionToVideo loads the new clip
4263
- // into the hidden <video> and crossfades only when its first frame
4264
- // is ready, so the active clip keeps showing until then (no flash).
4265
- this.transitionToVideo(motionUrl);
4266
- });
4267
4249
  this.onVideoEndedBound = this.onVideoEnded.bind(this);
4268
4250
  }
4269
4251
  setAgentCard(card) {
@@ -4346,7 +4328,6 @@ class VideoPlayerNativeService {
4346
4328
  this.agentCard.set(undefined);
4347
4329
  }
4348
4330
  ngOnDestroy() {
4349
- this.moodSubscription?.unsubscribe();
4350
4331
  this.destroyPlayer();
4351
4332
  }
4352
4333
  onVideoEnded() {
@@ -4629,563 +4610,831 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
4629
4610
  args: [DCMicComponent]
4630
4611
  }], isAIThinking: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAIThinking", required: false }] }], micSettings: [{ type: i0.Input, args: [{ isSignal: true, alias: "micSettings", required: false }] }], sendMessage: [{ type: i0.Output, args: ["sendMessage"] }], textInputChanged: [{ type: i0.Output, args: ["textInputChanged"] }] } });
4631
4612
 
4632
- // Given a text that can be in markdown but only in asterisk like *italic* or **bold** or ***bold italic***
4633
- // Example Hola que tal es *Hey What's up* en inglés
4634
- // i want to extract in array every word like this.
4635
- // [{word: 'Hola', tag: ''}, {word: 'que', tag: ''}, {word: 'tal', tag: ''}, {word: 'es', tag: ''}, {word: 'Hey', tag: 'italic'}, {word: 'What's', tag: 'italic'}, {word: 'up', tag: 'italic'}, {word: 'en', tag: ''}, {word: 'inglés', tag: ''}]
4636
- function extractTags(text) {
4637
- const result = [];
4638
- const tagStack = [];
4639
- // Regex to match markdown markers (***, **, *) or spaces or sequences of non-space/non-asterisk characters (words with punctuation)
4640
- const regex = /(\*\*\*|\*\*|\*|\s+|[^\s\*]+)/g;
4641
- let match;
4642
- while ((match = regex.exec(text)) !== null) {
4643
- const token = match[0];
4644
- if (token.trim() === '') {
4645
- // Ignore spaces
4646
- continue;
4647
- }
4648
- if (token === '***') {
4649
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold italic') {
4650
- tagStack.pop(); // Closing bold italic
4613
+ class AudioTextSyncService {
4614
+ constructor() {
4615
+ // Maps to store message-specific signals and observables
4616
+ this.highlightedWordsSignalMap = new Map();
4617
+ this.highlightedWords$Map = new Map();
4618
+ // Maps for cleanup and active audio elements
4619
+ this.cleanup$Map = new Map();
4620
+ this.activeAudioMap = new Map();
4621
+ this.currentPlayingAudio = null;
4622
+ this.chatMonitorService = inject(ChatMonitorService, { optional: true });
4623
+ this.destroyRef = inject(DestroyRef);
4624
+ this.currentActiveMessage = signal(null, ...(ngDevMode ? [{ debugName: "currentActiveMessage" }] : /* istanbul ignore next */ []));
4625
+ // Ensure cleanup when service is destroyed
4626
+ this.destroyRef.onDestroy(() => {
4627
+ this.stopAllSyncs();
4628
+ });
4629
+ }
4630
+ /**
4631
+ * Plays the given audio element exclusively, pausing any other active audio.
4632
+ * @param audioElement The audio element to play.
4633
+ * @param message The message associated with this audio playback.
4634
+ */
4635
+ playAudio(audioElement, message) {
4636
+ if (this.currentPlayingAudio && this.currentPlayingAudio !== audioElement) {
4637
+ this.currentPlayingAudio.pause();
4638
+ this.currentPlayingAudio.currentTime = 0;
4639
+ }
4640
+ // Ensure audioUrl is present on the message if it's currently empty but the element has a source
4641
+ if (!message.audioUrl && audioElement.src) {
4642
+ console.log('AudioTextSyncService: fixing empty message.audioUrl with audioElement.src:', audioElement.src);
4643
+ message.audioUrl = audioElement.src;
4644
+ }
4645
+ console.log('AudioTextSyncService: playAudio emitting to ChatMonitorService activeAudioMessage:', {
4646
+ messageId: message.messageId,
4647
+ audioUrl: message.audioUrl,
4648
+ src: audioElement.src
4649
+ });
4650
+ this.currentPlayingAudio = audioElement;
4651
+ this.currentActiveMessage.set(message);
4652
+ this.chatMonitorService?.activeAudioMessage.set(message);
4653
+ this.chatMonitorService?.logMessageAudioWillPlay(message);
4654
+ this.chatMonitorService?.logMessageAudioStatus(message, 'playing');
4655
+ // Setup ended listener to clear state automatically when playback finishes
4656
+ fromEvent(audioElement, 'ended')
4657
+ .pipe(take(1))
4658
+ .subscribe(() => {
4659
+ if (this.currentPlayingAudio === audioElement) {
4660
+ this.currentActiveMessage.set(null);
4661
+ this.chatMonitorService?.activeAudioMessage.set(null);
4662
+ this.chatMonitorService?.logMessageAudioStatus(message, 'finished');
4651
4663
  }
4652
- else {
4653
- tagStack.push('bold italic'); // Opening bold italic
4664
+ });
4665
+ audioElement.play().catch((error) => {
4666
+ console.error('Error playing audio in AudioTextSyncService:', error);
4667
+ this.currentActiveMessage.set(null);
4668
+ this.chatMonitorService?.activeAudioMessage.set(null);
4669
+ this.chatMonitorService?.logMessageAudioStatus(message, 'stopped');
4670
+ });
4671
+ }
4672
+ /**
4673
+ * Pauses the given audio element.
4674
+ * @param audioElement The audio element to pause.
4675
+ */
4676
+ pauseAudio(audioElement) {
4677
+ const message = this.currentActiveMessage();
4678
+ audioElement.pause();
4679
+ if (this.currentPlayingAudio === audioElement) {
4680
+ this.currentActiveMessage.set(null);
4681
+ this.chatMonitorService?.activeAudioMessage.set(null);
4682
+ if (message) {
4683
+ this.chatMonitorService?.logMessageAudioStatus(message, 'stopped');
4654
4684
  }
4655
4685
  }
4656
- else if (token === '**') {
4657
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold') {
4658
- tagStack.pop(); // Closing bold
4686
+ }
4687
+ /**
4688
+ * Synchronizes audio playback with text transcription
4689
+ * @param audioElement The audio element to sync with
4690
+ * @param transcriptionTimestamps Array of word timestamps
4691
+ * @param messageId Unique identifier for the message
4692
+ */
4693
+ syncAudioWithText(audioElement, transcriptionTimestamps, messageId) {
4694
+ // Stop any existing sync for this message
4695
+ this.stopSync(messageId);
4696
+ // Create new signal and subject for this message if they don't exist
4697
+ if (!this.highlightedWordsSignalMap.has(messageId)) {
4698
+ this.highlightedWordsSignalMap.set(messageId, signal([]));
4699
+ }
4700
+ if (!this.highlightedWords$Map.has(messageId)) {
4701
+ this.highlightedWords$Map.set(messageId, new BehaviorSubject([]));
4702
+ }
4703
+ // Create cleanup subject
4704
+ const cleanup$ = new Subject();
4705
+ this.cleanup$Map.set(messageId, cleanup$);
4706
+ // Store the active audio element
4707
+ this.activeAudioMap.set(messageId, audioElement);
4708
+ // Get the signal and subject for this message
4709
+ const messageSignal = this.highlightedWordsSignalMap.get(messageId);
4710
+ const messageSubject = this.highlightedWords$Map.get(messageId);
4711
+ // Initialize the highlighted words state
4712
+ const initialWords = transcriptionTimestamps.map((word, index) => ({
4713
+ word: word.word,
4714
+ index,
4715
+ isHighlighted: false,
4716
+ }));
4717
+ // Update both signal and observable
4718
+ messageSignal.set(initialWords);
4719
+ messageSubject.next(initialWords);
4720
+ // Listen to timeupdate events
4721
+ fromEvent(audioElement, 'timeupdate')
4722
+ .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$), map(() => audioElement.currentTime))
4723
+ .subscribe((currentTime) => {
4724
+ const updatedWords = transcriptionTimestamps.map((word, index) => {
4725
+ const isHighlighted = currentTime >= word.start - 0.15 && currentTime < word.end + 0.15;
4726
+ return {
4727
+ word: word.word,
4728
+ index,
4729
+ isHighlighted,
4730
+ };
4731
+ });
4732
+ // Update both signal and observable for this message
4733
+ messageSignal.set(updatedWords);
4734
+ messageSubject.next(updatedWords);
4735
+ });
4736
+ // Listen to ended event for cleanup
4737
+ fromEvent(audioElement, 'ended')
4738
+ .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$))
4739
+ .subscribe(() => {
4740
+ // Reset highlighting when audio ends
4741
+ const resetWords = initialWords.map((word) => ({
4742
+ ...word,
4743
+ isHighlighted: false,
4744
+ }));
4745
+ messageSignal.set(resetWords);
4746
+ messageSubject.next(resetWords);
4747
+ });
4748
+ }
4749
+ /**
4750
+ * Stops the sync for a specific message and cleans up resources
4751
+ * @param messageId The ID of the message to stop syncing
4752
+ */
4753
+ stopSync(messageId) {
4754
+ if (this.activeAudioMap.has(messageId)) {
4755
+ const audio = this.activeAudioMap.get(messageId);
4756
+ const currentMsg = this.currentActiveMessage();
4757
+ if (audio && this.currentPlayingAudio === audio) {
4758
+ audio.pause();
4759
+ audio.currentTime = 0;
4760
+ this.currentPlayingAudio = null;
4761
+ this.currentActiveMessage.set(null);
4762
+ this.chatMonitorService?.activeAudioMessage.set(null);
4763
+ if (currentMsg) {
4764
+ this.chatMonitorService?.logMessageAudioStatus(currentMsg, 'stopped');
4765
+ }
4659
4766
  }
4660
- else {
4661
- tagStack.push('bold'); // Opening bold
4767
+ // Get the cleanup subject for this message
4768
+ const cleanup$ = this.cleanup$Map.get(messageId);
4769
+ if (cleanup$) {
4770
+ cleanup$.next();
4771
+ cleanup$.complete();
4772
+ this.cleanup$Map.delete(messageId);
4662
4773
  }
4663
- }
4664
- else if (token === '*') {
4665
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'italic') {
4666
- tagStack.pop(); // Closing italic
4774
+ this.activeAudioMap.delete(messageId);
4775
+ // Reset state for this message
4776
+ const messageSignal = this.highlightedWordsSignalMap.get(messageId);
4777
+ const messageSubject = this.highlightedWords$Map.get(messageId);
4778
+ if (messageSignal) {
4779
+ messageSignal.set([]);
4667
4780
  }
4668
- else {
4669
- tagStack.push('italic'); // Opening italic
4781
+ if (messageSubject) {
4782
+ messageSubject.next([]);
4670
4783
  }
4671
4784
  }
4672
- else {
4673
- // It's a word (including punctuation)
4674
- const currentTag = tagStack.length > 0 ? tagStack[tagStack.length - 1] : '';
4675
- result.push({ word: token, tag: currentTag });
4676
- }
4677
- }
4678
- // Note: This implementation assumes correctly matched opening and closing tags.
4679
- // Unclosed tags at the end of the string will be ignored.
4680
- return result;
4681
- }
4682
-
4683
- // Removed AudioState as it's replaced by AudioStatus from agent.models
4684
- class MessageContentDisplayer {
4685
- get hostHighlightColor() {
4686
- return this.highlightColor();
4687
4785
  }
4688
- constructor() {
4689
- // Inputs
4690
- this.message = input.required(...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
4691
- this.markWord = input(...(ngDevMode ? [undefined, { debugName: "markWord" }] : /* istanbul ignore next */ []));
4692
- this.highlightColor = input(...(ngDevMode ? [undefined, { debugName: "highlightColor" }] : /* istanbul ignore next */ []));
4693
- // Outputs
4694
- this.playAudio = output();
4695
- this.audioCompleted = output();
4696
- this.audioStatusChanged = output();
4697
- this.wordClicked = output();
4698
- // Signal States
4699
- this.wordWithMeta = signal([], ...(ngDevMode ? [{ debugName: "wordWithMeta" }] : /* istanbul ignore next */ []));
4700
- this.localAudioStatus = signal(null, ...(ngDevMode ? [{ debugName: "localAudioStatus" }] : /* istanbul ignore next */ []));
4701
- // Signals State computed from the input message
4702
- this.wordsWithMetaState = [];
4703
- this.lastHasTranscription = false;
4704
- this.audioElement = signal(null, ...(ngDevMode ? [{ debugName: "audioElement" }] : /* istanbul ignore next */ []));
4705
- this.destroyRef = inject(DestroyRef);
4706
- this.iconState = computed(() => {
4707
- const msg = this.message();
4708
- if (msg?.isLoading) {
4709
- return 'loading';
4710
- }
4711
- const status = this.localAudioStatus() || msg?.audioStatus;
4712
- if (status === 'playing') {
4713
- return 'playing';
4714
- }
4715
- if (status === 'stopped') {
4716
- return 'paused';
4717
- }
4718
- if (status === 'finished') {
4719
- return 'finished';
4720
- }
4721
- if (status === 'pending') {
4722
- return 'pending';
4723
- }
4724
- if (msg?.audioUrl) {
4725
- return 'playable';
4786
+ /**
4787
+ * Stops all syncs and cleans up all resources
4788
+ */
4789
+ stopAllSyncs() {
4790
+ const currentMsg = this.currentActiveMessage();
4791
+ if (this.currentPlayingAudio) {
4792
+ this.currentPlayingAudio.pause();
4793
+ this.currentPlayingAudio.currentTime = 0;
4794
+ this.currentPlayingAudio = null;
4795
+ this.currentActiveMessage.set(null);
4796
+ this.chatMonitorService?.activeAudioMessage.set(null);
4797
+ if (currentMsg) {
4798
+ this.chatMonitorService?.logMessageAudioStatus(currentMsg, 'stopped');
4726
4799
  }
4727
- return 'idle';
4728
- }, ...(ngDevMode ? [{ debugName: "iconState" }] : /* istanbul ignore next */ []));
4729
- this.isPendingAudio = computed(() => !!this.message() && this.message()?.audioStatus === 'pending', ...(ngDevMode ? [{ debugName: "isPendingAudio" }] : /* istanbul ignore next */ []));
4730
- this.messageText = computed(() => {
4731
- const msg = this.message();
4732
- return msg?.text || msg?.content || 'N/A';
4733
- }, ...(ngDevMode ? [{ debugName: "messageText" }] : /* istanbul ignore next */ []));
4734
- this.classTag = computed(() => this.message()?.tag, ...(ngDevMode ? [{ debugName: "classTag" }] : /* istanbul ignore next */ []));
4735
- // When message is updated, check is has transcriptions whisper format.
4736
- this.transcriptionTimestamps = computed(() => {
4737
- const msg = this.message();
4738
- if (!msg || !msg.transcription) {
4739
- return [];
4800
+ }
4801
+ // Get all message IDs and stop each sync
4802
+ for (const messageId of this.activeAudioMap.keys()) {
4803
+ this.stopSync(messageId);
4804
+ }
4805
+ // Clear all maps
4806
+ this.highlightedWordsSignalMap.clear();
4807
+ this.highlightedWords$Map.clear();
4808
+ this.cleanup$Map.clear();
4809
+ this.activeAudioMap.clear();
4810
+ }
4811
+ /**
4812
+ * Returns the highlighted words signal for a specific message
4813
+ * @param messageId The ID of the message
4814
+ */
4815
+ getHighlightedWordsSignal(messageId) {
4816
+ // Create a new signal for this message if it doesn't exist
4817
+ if (!this.highlightedWordsSignalMap.has(messageId)) {
4818
+ this.highlightedWordsSignalMap.set(messageId, signal([]));
4819
+ }
4820
+ return this.highlightedWordsSignalMap.get(messageId);
4821
+ }
4822
+ /**
4823
+ * Returns the highlighted words observable for a specific message
4824
+ * @param messageId The ID of the message
4825
+ */
4826
+ getHighlightedWords$(messageId) {
4827
+ // Create a new subject for this message if it doesn't exist
4828
+ if (!this.highlightedWords$Map.has(messageId)) {
4829
+ this.highlightedWords$Map.set(messageId, new BehaviorSubject([]));
4830
+ }
4831
+ return this.highlightedWords$Map.get(messageId).asObservable();
4832
+ }
4833
+ /**
4834
+ * Checks if a word at a specific index is currently highlighted for a specific message
4835
+ * @param messageId The ID of the message
4836
+ * @param index The index of the word to check
4837
+ */
4838
+ isWordHighlighted(messageId, index) {
4839
+ const messageSignal = this.getHighlightedWordsSignal(messageId);
4840
+ return messageSignal().some((word) => word.index === index && word.isHighlighted);
4841
+ }
4842
+ /**
4843
+ * Returns an observable that emits true when a word at a specific index is highlighted for a specific message
4844
+ * @param messageId The ID of the message
4845
+ * @param index The index of the word to observe
4846
+ */
4847
+ isWordHighlighted$(messageId, index) {
4848
+ return this.getHighlightedWords$(messageId).pipe(map((words) => words.some((word) => word.index === index && word.isHighlighted)));
4849
+ }
4850
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4851
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, providedIn: 'root' }); }
4852
+ }
4853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, decorators: [{
4854
+ type: Injectable,
4855
+ args: [{
4856
+ providedIn: 'root',
4857
+ }]
4858
+ }], ctorParameters: () => [] });
4859
+
4860
+ /**
4861
+ * @Injectable
4862
+ *
4863
+ * @description
4864
+ * This service orchestrates the playback of chat messages, ensuring that they are played sequentially.
4865
+ * It manages a queue of messages and handles the generation and playback of audio for each message.
4866
+ *
4867
+ * @see ChatMessageOrchestratorComponent
4868
+ */
4869
+ class MessageOrchestrationService {
4870
+ constructor() {
4871
+ // Services
4872
+ this.conversationService = inject(ConversationService);
4873
+ this.chatMonitorService = inject(ChatMonitorService);
4874
+ this.userService = inject(UserService);
4875
+ this.messagesStateService = inject(MessagesStateService);
4876
+ // State Signals
4877
+ this.messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
4878
+ this.messageRole = signal(ChatRole.User, ...(ngDevMode ? [{ debugName: "messageRole" }] : /* istanbul ignore next */ []));
4879
+ this.messageToPlay = signal(null, ...(ngDevMode ? [{ debugName: "messageToPlay" }] : /* istanbul ignore next */ []));
4880
+ // Public Signals for components to consume
4881
+ this.messagesSignal = this.messages.asReadonly();
4882
+ this.messageToPlay$ = this.messageToPlay.asReadonly();
4883
+ // Audio queue management
4884
+ this.audioQueue = [];
4885
+ this.isGenerating = signal(false, ...(ngDevMode ? [{ debugName: "isGenerating" }] : /* istanbul ignore next */ []));
4886
+ this.currentPlayingIndex = signal(null, ...(ngDevMode ? [{ debugName: "currentPlayingIndex" }] : /* istanbul ignore next */ []));
4887
+ this.preGenerationInProgress = signal(false, ...(ngDevMode ? [{ debugName: "preGenerationInProgress" }] : /* istanbul ignore next */ []));
4888
+ console.log('MessageOrchestrationService initialized');
4889
+ }
4890
+ /**
4891
+ * @description
4892
+ * Starts the orchestration process for a list of messages.
4893
+ *
4894
+ * @param messages - An array of `MessageContent` objects to be orchestrated.
4895
+ * @param role - The `ChatRole` of the messages' author.
4896
+ */
4897
+ startOrchestration(messages, role) {
4898
+ untracked(() => {
4899
+ const currentMessages = this.messages();
4900
+ const isAssistant = role === ChatRole.Assistant;
4901
+ // Check if it's a continuation of the same orchestration (e.g., streaming)
4902
+ const isContinuation = currentMessages.length > 0 &&
4903
+ messages.length >= currentMessages.length &&
4904
+ messages[0].messageId === currentMessages[0].messageId;
4905
+ // Update internal state signal with new messages
4906
+ // If it's a continuation, we want to merge properties carefully to avoid flickering or resetting local state like 'playing' status
4907
+ if (isContinuation) {
4908
+ this.messages.update(prev => {
4909
+ return messages.map((newMsg, idx) => {
4910
+ const prevMsg = prev[idx];
4911
+ if (prevMsg) {
4912
+ // Merge properties: keep local transient state like audioStatus if it's currently managed by this service
4913
+ return { ...newMsg, ...prevMsg, ...newMsg }; // prioritize newMsg but keep prevMsg as base if needed
4914
+ }
4915
+ return newMsg;
4916
+ });
4917
+ });
4740
4918
  }
4741
- return matchTranscription(msg.text || msg.content, msg.transcription.words);
4742
- }, ...(ngDevMode ? [{ debugName: "transcriptionTimestamps" }] : /* istanbul ignore next */ []));
4743
- // if transcriptionTimestamps is calculated then hasTranscription is true.
4744
- this.hasTranscription = computed(() => {
4745
- return this.transcriptionTimestamps().length > 0;
4746
- }, ...(ngDevMode ? [{ debugName: "hasTranscription" }] : /* istanbul ignore next */ []));
4747
- effect(() => {
4748
- const currentMsg = this.message();
4749
- if (!currentMsg)
4750
- return;
4751
- const hasTranscription = this.hasTranscription();
4752
- const idChanged = currentMsg.messageId !== this.currentMessageId;
4753
- const audioUrlChanged = currentMsg.audioUrl !== this.lastAudioUrl;
4754
- const transcriptionChanged = hasTranscription !== this.lastHasTranscription;
4755
- if (idChanged || audioUrlChanged || transcriptionChanged) {
4756
- this.currentMessageId = currentMsg.messageId;
4757
- this.lastAudioUrl = currentMsg.audioUrl;
4758
- this.lastHasTranscription = hasTranscription;
4759
- this.localAudioStatus.set(null); // Reset local state on message/audio change
4760
- this.initializeBasedOnMessage(currentMsg);
4919
+ else {
4920
+ this.messages.set([...messages]);
4761
4921
  }
4762
- });
4763
- effect(() => {
4764
- if (this.isPendingAudio()) {
4765
- this.startAudioPlayback();
4922
+ this.messageRole.set(role);
4923
+ if (isAssistant) {
4924
+ const chatSettings = this.userService.user()?.settings?.conversation;
4925
+ if (chatSettings?.synthVoice) {
4926
+ if (!isContinuation) {
4927
+ console.log('Starting new orchestration');
4928
+ this.initializeAudioQueue();
4929
+ this.processNextInQueue();
4930
+ }
4931
+ else {
4932
+ console.log('Updating existing orchestration queue');
4933
+ // Update the queue with new indices
4934
+ for (let i = currentMessages.length; i < messages.length; i++) {
4935
+ if (messages[i].audioStatus !== 'finished' && !this.audioQueue.includes(i)) {
4936
+ this.audioQueue.push(i);
4937
+ }
4938
+ }
4939
+ this.processNextInQueue();
4940
+ }
4941
+ }
4766
4942
  }
4767
4943
  });
4768
- effect(() => {
4769
- this.wordsWithMetaState = this.wordWithMeta();
4770
- });
4771
4944
  }
4772
- trackByIndex(index, item) {
4773
- return index;
4774
- }
4775
- ngOnDestroy() {
4776
- this.cleanupAudio();
4945
+ audioCompleted(message) {
4946
+ const index = this.currentPlayingIndex();
4947
+ if (index !== null) {
4948
+ console.log(`Audio completed for index ${index}, setting status to finished`);
4949
+ this.updateAudioStatus(message, 'finished', index);
4950
+ }
4951
+ else {
4952
+ console.warn('Audio completed but currentPlayingIndex is null');
4953
+ }
4954
+ this.conversationService.currentAudioStatus.set({ message, completed: true });
4955
+ this.currentPlayingIndex.set(null);
4956
+ this.messageToPlay.set(null);
4957
+ if (this.audioQueue.length > 0) {
4958
+ this.processNextInQueue();
4959
+ }
4777
4960
  }
4778
- initializeBasedOnMessage(msg) {
4779
- if (msg.audioUrl) {
4780
- this.initializeAudio(msg.audioUrl);
4961
+ /**
4962
+ * @description
4963
+ * Updates the audio status of a message.
4964
+ *
4965
+ * @param message - The `MessageContent` object whose status has changed.
4966
+ * @param status - The new `AudioStatus`.
4967
+ * @param index - The index of the message in the orchestration queue.
4968
+ */
4969
+ updateAudioStatus(message, status, index) {
4970
+ console.log(`Updating audio status for index ${index} to ${status}. MessageId: ${message.messageId}`);
4971
+ if (status === 'playing') {
4972
+ if (message.messageId) {
4973
+ this.messagesStateService.setActiveMessage(message.messageId, index);
4974
+ }
4975
+ // Update local state: set this one as active, others as inactive
4976
+ this.messages.update((messages) => messages.map((m, idx) => ({
4977
+ ...m,
4978
+ isActive: idx === index,
4979
+ // Also update the current one's status
4980
+ ...(idx === index ? { audioStatus: status } : {}),
4981
+ })));
4781
4982
  }
4782
- if (this.hasTranscription()) {
4783
- const wordsAndTimestamps = [];
4784
- this.transcriptionTimestamps()?.forEach((word, index) => {
4785
- wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
4983
+ else {
4984
+ // For other statuses (stopped, finished), preserve isActive and update status
4985
+ this.changeStates(index, { ...message, audioStatus: status });
4986
+ }
4987
+ // 4. Update global state
4988
+ if (message.messageId) {
4989
+ this.messagesStateService.updateMultiMessage(message.messageId, index, {
4990
+ audioStatus: status,
4786
4991
  });
4787
- const firstState = this.transcriptionTimestamps()?.map((word, index) => ({
4788
- word: word.word,
4789
- index,
4790
- isHighlighted: false,
4791
- tag: '',
4792
- marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
4793
- }));
4794
- console.log('firstState', firstState);
4795
- this.wordWithMeta.set(firstState);
4796
- // Para mostrar las transcripciones iliminadas: el audio tiene que sonar y en la subscripción apareceran los cambios.
4797
4992
  }
4798
- else {
4799
- this.initializePlainTextWords(msg.text || msg.content || '');
4993
+ this.chatMonitorService.logMessageAudioStatus(message, status);
4994
+ }
4995
+ /**
4996
+ * @description
4997
+ * Notifies the `ConversationService` when a word is clicked.
4998
+ *
4999
+ * @param wordData - The `WordData` object containing information about the clicked word.
5000
+ */
5001
+ wordClicked(wordData) {
5002
+ this.conversationService.notifyWordClicked(wordData);
5003
+ }
5004
+ initializeAudioQueue() {
5005
+ const messages = this.messages();
5006
+ if (messages && messages.length > 0) {
5007
+ this.audioQueue = messages
5008
+ .map((m, index) => ({ status: m.audioStatus, index }))
5009
+ .filter((item) => item.status !== 'finished' && item.status !== 'skip')
5010
+ .map((item) => item.index);
5011
+ console.log('Audio queue initialized:', this.audioQueue);
4800
5012
  }
4801
- // Removal of shouldPlayAudio check - playback now handled by isPendingAudio effect
4802
5013
  }
4803
- initializeAudio(audioUrl) {
4804
- let audio = this.audioElement();
4805
- if (!audio) {
4806
- audio = new Audio(audioUrl);
4807
- this.audioElement.set(audio);
4808
- this.setupAudioEventListeners(audio);
5014
+ async processNextInQueue() {
5015
+ if (this.audioQueue.length === 0 || this.isGenerating() || this.currentPlayingIndex() !== null) {
5016
+ return;
4809
5017
  }
4810
- else if (audio.src && audioUrl && !audio.src.endsWith(audioUrl)) {
4811
- audio.src = audioUrl;
5018
+ const nextIndex = this.audioQueue.shift();
5019
+ this.isGenerating.set(true);
5020
+ try {
5021
+ await this.generateAudioForIndex(nextIndex);
5022
+ }
5023
+ catch (error) {
5024
+ console.error('Error generating audio:', error);
5025
+ }
5026
+ finally {
5027
+ this.isGenerating.set(false);
4812
5028
  }
5029
+ this.preGenerateNextIfNeeded();
4813
5030
  }
4814
- setupAudioEventListeners(audioElement) {
4815
- fromEvent(audioElement, 'timeupdate')
4816
- .pipe(takeUntilDestroyed(this.destroyRef), map(() => audioElement.currentTime || 0))
4817
- .subscribe((currentTime) => {
4818
- if (currentTime === 0) {
4819
- this.updateAudioStatus('playing');
5031
+ async preGenerateNextIfNeeded() {
5032
+ if (this.audioQueue.length > 0 && !this.preGenerationInProgress()) {
5033
+ const nextIndex = this.audioQueue[0];
5034
+ const messages = this.messages();
5035
+ const message = messages[nextIndex];
5036
+ // Skip if audio is already generated or generation is not needed
5037
+ if (message.audioUrl || message.isLoading) {
4820
5038
  return;
4821
5039
  }
4822
- if (this.hasTranscription()) {
4823
- const wordsAndTimestamps = [];
4824
- this.transcriptionTimestamps()?.forEach((word, index) => {
4825
- wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
4826
- });
4827
- const updatedWords = wordsAndTimestamps.map((word, index) => ({
4828
- word: word.word,
4829
- index,
4830
- isHighlighted: currentTime >= word.start - 0.15 && currentTime < word.end + 0.15,
4831
- tag: word.tag,
4832
- marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
4833
- }));
4834
- this.wordWithMeta.set(updatedWords);
5040
+ this.preGenerationInProgress.set(true);
5041
+ const loadingMessage = { ...message, isLoading: true };
5042
+ this.changeStates(nextIndex, loadingMessage);
5043
+ try {
5044
+ const messageAudio = await this.generateAudio(message, null);
5045
+ messageAudio.isLoading = false;
5046
+ // Pre-generated audio stays in the background, no status update needed here or set to 'stopped'/'playable'
5047
+ // Actually, if it's pre-rendered it should probably stay as whatever it was but with audioUrl
5048
+ this.changeStates(nextIndex, messageAudio);
4835
5049
  }
4836
- });
4837
- fromEvent(audioElement, 'ended')
4838
- .pipe(takeUntilDestroyed(this.destroyRef))
4839
- .subscribe(() => {
4840
- const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
4841
- this.wordWithMeta.set(resetWords);
4842
- const currentMsg = this.message();
4843
- if (currentMsg) {
4844
- // No direct mutation of input!
4845
- this.updateAudioStatus('finished');
4846
- this.audioCompleted.emit(currentMsg);
5050
+ finally {
5051
+ this.preGenerationInProgress.set(false);
4847
5052
  }
5053
+ }
5054
+ }
5055
+ async generateAudioForIndex(index) {
5056
+ const messages = this.messages();
5057
+ if (messages[index].audioUrl) {
5058
+ const messageAudio = { ...messages[index], audioStatus: 'pending' };
5059
+ this.changeStates(index, messageAudio);
5060
+ this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5061
+ this.messageToPlay.set(messageAudio);
5062
+ this.currentPlayingIndex.set(index);
5063
+ return;
5064
+ }
5065
+ const message = { ...messages[index], isLoading: true };
5066
+ this.changeStates(index, message);
5067
+ try {
5068
+ const messageAudio = await this.generateAudio(messages[index], null);
5069
+ messageAudio.isLoading = false;
5070
+ messageAudio.audioStatus = 'pending';
5071
+ this.changeStates(index, messageAudio);
5072
+ this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5073
+ this.messageToPlay.set(messageAudio);
5074
+ }
5075
+ finally {
5076
+ // Solo limpiamos isLoading sobre el estado ACTUAL del signal (no el snapshot previo al try),
5077
+ // para no pisar el audioStatus:'pending' que acaba de setear el bloque try.
5078
+ this.messages.update(msgs => {
5079
+ const updated = [...msgs];
5080
+ updated[index] = { ...updated[index], isLoading: false };
5081
+ return updated;
5082
+ });
5083
+ }
5084
+ this.currentPlayingIndex.set(index);
5085
+ }
5086
+ changeStates(index, messageAudio) {
5087
+ this.messages.update((messages) => {
5088
+ const newMessages = [...messages];
5089
+ newMessages[index] = { ...messages[index], ...messageAudio };
5090
+ return newMessages;
4848
5091
  });
4849
5092
  }
4850
- updateAudioStatus(status) {
4851
- const currentMsg = this.message();
4852
- // Update local state for immediate feedback
4853
- this.localAudioStatus.set(status);
4854
- // Only update if the status is actually changing relative to the current input
4855
- if (currentMsg && currentMsg.audioStatus !== status) {
4856
- // NOTE: We do NOT mutate currentMsg.audioStatus here.
4857
- // The parent orchestration service will update the state and propagate it back via signals.
4858
- this.audioStatusChanged.emit({ message: currentMsg, status });
5093
+ async generateAudio(message, overwriteText = null) {
5094
+ if (message.audioUrl) {
5095
+ return message;
5096
+ }
5097
+ try {
5098
+ const text = overwriteText || message.text || message.content;
5099
+ const ttsObject = buildObjectTTSRequest({ ...message, text }, { highlightWords: true });
5100
+ const speechAudio = await this.conversationService.getTTSFile(ttsObject);
5101
+ message = extractAudioAndTranscription(message, speechAudio);
5102
+ return message;
4859
5103
  }
4860
- }
4861
- cleanupAudio() {
4862
- const audioElement = this.audioElement();
4863
- if (audioElement) {
4864
- audioElement.pause();
4865
- audioElement.src = '';
4866
- const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
4867
- this.wordWithMeta.set(resetWords);
5104
+ finally {
5105
+ this.isGenerating.set(false);
4868
5106
  }
4869
5107
  }
4870
- onPlayMessage() {
4871
- console.log('onPlayMessage');
4872
- const currentMsg = this.message();
4873
- const audioElement = this.audioElement();
4874
- if (audioElement) {
4875
- if (audioElement.paused) {
4876
- this.startAudioPlayback();
5108
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5109
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService }); }
5110
+ }
5111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, decorators: [{
5112
+ type: Injectable
5113
+ }], ctorParameters: () => [] });
5114
+
5115
+ // Given a text that can be in markdown but only in asterisk like *italic* or **bold** or ***bold italic***
5116
+ // Example Hola que tal es *Hey What's up* en inglés
5117
+ // i want to extract in array every word like this.
5118
+ // [{word: 'Hola', tag: ''}, {word: 'que', tag: ''}, {word: 'tal', tag: ''}, {word: 'es', tag: ''}, {word: 'Hey', tag: 'italic'}, {word: 'What's', tag: 'italic'}, {word: 'up', tag: 'italic'}, {word: 'en', tag: ''}, {word: 'inglés', tag: ''}]
5119
+ function extractTags(text) {
5120
+ const result = [];
5121
+ const tagStack = [];
5122
+ // Regex to match markdown markers (***, **, *) or spaces or sequences of non-space/non-asterisk characters (words with punctuation)
5123
+ const regex = /(\*\*\*|\*\*|\*|\s+|[^\s\*]+)/g;
5124
+ let match;
5125
+ while ((match = regex.exec(text)) !== null) {
5126
+ const token = match[0];
5127
+ if (token.trim() === '') {
5128
+ // Ignore spaces
5129
+ continue;
5130
+ }
5131
+ if (token === '***') {
5132
+ if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold italic') {
5133
+ tagStack.pop(); // Closing bold italic
4877
5134
  }
4878
5135
  else {
4879
- audioElement.pause();
4880
- this.updateAudioStatus('stopped');
5136
+ tagStack.push('bold italic'); // Opening bold italic
4881
5137
  }
4882
5138
  }
4883
- else if (currentMsg?.audioUrl) {
4884
- console.log('onPlayMessage initializeAudio with url');
4885
- this.initializeAudio(currentMsg.audioUrl);
4886
- this.startAudioPlayback();
4887
- }
4888
- else if (currentMsg) {
4889
- console.log('onPlayMessage emit playAudio');
4890
- this.playAudio.emit(currentMsg);
5139
+ else if (token === '**') {
5140
+ if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold') {
5141
+ tagStack.pop(); // Closing bold
5142
+ }
5143
+ else {
5144
+ tagStack.push('bold'); // Opening bold
5145
+ }
4891
5146
  }
4892
- }
4893
- initializePlainTextWords(text) {
4894
- const words = extractTags(text);
4895
- const plainWords = words.map((word, index) => ({
4896
- word: word.word,
4897
- index: index,
4898
- isHighlighted: false,
4899
- tag: word.tag,
4900
- }));
4901
- this.wordWithMeta.set(plainWords);
4902
- }
4903
- onWordClick(wordData) {
4904
- const currentMsg = this.message();
4905
- if (!currentMsg)
4906
- return;
4907
- const clickedWord = 'isHighlighted' in wordData
4908
- ? { word: wordData.word, index: wordData.index, messageId: currentMsg.messageId }
4909
- : { ...wordData, messageId: currentMsg.messageId };
4910
- this.wordClicked.emit(clickedWord);
4911
- }
4912
- startAudioPlayback() {
4913
- const audioElement = this.audioElement();
4914
- if (!audioElement)
4915
- return;
4916
- try {
4917
- audioElement.play();
4918
- console.log('Audio playing');
5147
+ else if (token === '*') {
5148
+ if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'italic') {
5149
+ tagStack.pop(); // Closing italic
5150
+ }
5151
+ else {
5152
+ tagStack.push('italic'); // Opening italic
5153
+ }
4919
5154
  }
4920
- catch (error) {
4921
- console.error('Error playing audio:', error);
5155
+ else {
5156
+ // It's a word (including punctuation)
5157
+ const currentTag = tagStack.length > 0 ? tagStack[tagStack.length - 1] : '';
5158
+ result.push({ word: token, tag: currentTag });
4922
5159
  }
4923
- // Important: we move it to playing status immediately to avoid re-triggering the effect
4924
- this.updateAudioStatus('playing');
4925
5160
  }
4926
- debug() {
4927
- console.log('debug', this.wordWithMeta());
4928
- }
4929
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4930
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageContentDisplayer, isStandalone: true, selector: "message-content-displayer", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, markWord: { classPropertyName: "markWord", publicName: "markWord", isSignal: true, isRequired: false, transformFunction: null }, highlightColor: { classPropertyName: "highlightColor", publicName: "highlightColor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playAudio: "playAudio", audioCompleted: "audioCompleted", audioStatusChanged: "audioStatusChanged", wordClicked: "wordClicked" }, host: { properties: { "style.--highlight-bg-color": "this.hostHighlightColor" } }, ngImport: i0, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) { \n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> } \n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> } \n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5161
+ // Note: This implementation assumes correctly matched opening and closing tags.
5162
+ // Unclosed tags at the end of the string will be ignored.
5163
+ return result;
4931
5164
  }
4932
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, decorators: [{
4933
- type: Component,
4934
- args: [{ selector: 'message-content-displayer', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) { \n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> } \n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> } \n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"] }]
4935
- }], ctorParameters: () => [], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], markWord: [{ type: i0.Input, args: [{ isSignal: true, alias: "markWord", required: false }] }], highlightColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightColor", required: false }] }], hostHighlightColor: [{
4936
- type: HostBinding,
4937
- args: ['style.--highlight-bg-color']
4938
- }], playAudio: [{ type: i0.Output, args: ["playAudio"] }], audioCompleted: [{ type: i0.Output, args: ["audioCompleted"] }], audioStatusChanged: [{ type: i0.Output, args: ["audioStatusChanged"] }], wordClicked: [{ type: i0.Output, args: ["wordClicked"] }] } });
4939
5165
 
4940
5166
  /**
4941
- * @Injectable
5167
+ * @Component
4942
5168
  *
4943
5169
  * @description
4944
- * This service orchestrates the playback of chat messages, ensuring that they are played sequentially.
4945
- * It manages a queue of messages and handles the generation and playback of audio for each message.
5170
+ * Variante "atada al chat" de `MessageContentDisplayer`.
4946
5171
  *
4947
- * @see ChatMessageOrchestratorComponent
5172
+ * A diferencia del `MessageContentDisplayer` (que es agnóstico y se comunica
5173
+ * exclusivamente mediante `@Output()` para poder reutilizarse fuera del chat),
5174
+ * este componente **inyecta directamente** el `MessageOrchestrationService`
5175
+ * (instancia local provista por `ChatMessageOrchestratorComponent`) y le reporta
5176
+ * los cambios de estado de audio sin emitir eventos.
5177
+ *
5178
+ * Punto clave del refactor:
5179
+ * - NO mantiene un `localAudioStatus`. El `iconState` deriva ÚNICAMENTE del
5180
+ * input `message()`, que a su vez baja del signal del orquestador. Esto elimina
5181
+ * el bug donde el icono/estado de una burbuja anterior se quedaba en "playing"
5182
+ * al activar otro mensaje, porque el estado local divergía del global.
5183
+ *
5184
+ * @see MessageOrchestrationService
5185
+ * @see MessageContentDisplayer (versión desacoplada / reutilizable)
4948
5186
  */
4949
- class MessageOrchestrationService {
4950
- constructor() {
4951
- // Services
4952
- this.conversationService = inject(ConversationService);
4953
- this.chatMonitorService = inject(ChatMonitorService);
4954
- this.userService = inject(UserService);
4955
- this.messagesStateService = inject(MessagesStateService);
4956
- // State Signals
4957
- this.messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
4958
- this.messageRole = signal(ChatRole.User, ...(ngDevMode ? [{ debugName: "messageRole" }] : /* istanbul ignore next */ []));
4959
- this.messageToPlay = signal(null, ...(ngDevMode ? [{ debugName: "messageToPlay" }] : /* istanbul ignore next */ []));
4960
- // Public Signals for components to consume
4961
- this.messagesSignal = this.messages.asReadonly();
4962
- this.messageToPlay$ = this.messageToPlay.asReadonly();
4963
- // Audio queue management
4964
- this.audioQueue = [];
4965
- this.isGenerating = signal(false, ...(ngDevMode ? [{ debugName: "isGenerating" }] : /* istanbul ignore next */ []));
4966
- this.currentPlayingIndex = signal(null, ...(ngDevMode ? [{ debugName: "currentPlayingIndex" }] : /* istanbul ignore next */ []));
4967
- this.preGenerationInProgress = signal(false, ...(ngDevMode ? [{ debugName: "preGenerationInProgress" }] : /* istanbul ignore next */ []));
4968
- console.log('MessageOrchestrationService initialized');
5187
+ class MessageContentReactive {
5188
+ get hostHighlightColor() {
5189
+ return this.highlightColor();
4969
5190
  }
4970
- /**
4971
- * @description
4972
- * Starts the orchestration process for a list of messages.
4973
- *
4974
- * @param messages - An array of `MessageContent` objects to be orchestrated.
4975
- * @param role - The `ChatRole` of the messages' author.
4976
- */
4977
- startOrchestration(messages, role) {
4978
- untracked(() => {
4979
- const currentMessages = this.messages();
4980
- const isAssistant = role === ChatRole.Assistant;
4981
- // Check if it's a continuation of the same orchestration (e.g., streaming)
4982
- const isContinuation = currentMessages.length > 0 &&
4983
- messages.length >= currentMessages.length &&
4984
- messages[0].messageId === currentMessages[0].messageId;
4985
- // Update internal state signal with new messages
4986
- // If it's a continuation, we want to merge properties carefully to avoid flickering or resetting local state like 'playing' status
4987
- if (isContinuation) {
4988
- this.messages.update(prev => {
4989
- return messages.map((newMsg, idx) => {
4990
- const prevMsg = prev[idx];
4991
- if (prevMsg) {
4992
- // Merge properties: keep local transient state like audioStatus if it's currently managed by this service
4993
- return { ...newMsg, ...prevMsg, ...newMsg }; // prioritize newMsg but keep prevMsg as base if needed
4994
- }
4995
- return newMsg;
4996
- });
4997
- });
5191
+ constructor() {
5192
+ // Inputs
5193
+ this.message = input.required(...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
5194
+ /** Índice del fragmento dentro del array de multiMessages del orquestador. */
5195
+ this.index = input.required(...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
5196
+ this.markWord = input(...(ngDevMode ? [undefined, { debugName: "markWord" }] : /* istanbul ignore next */ []));
5197
+ this.highlightColor = input(...(ngDevMode ? [undefined, { debugName: "highlightColor" }] : /* istanbul ignore next */ []));
5198
+ // Services (atados al chat — fuente de verdad)
5199
+ this.orchestrationService = inject(MessageOrchestrationService);
5200
+ this.audioTextSyncService = inject(AudioTextSyncService);
5201
+ this.destroyRef = inject(DestroyRef);
5202
+ // Signal States (solo presentación de palabras / karaoke)
5203
+ this.wordWithMeta = signal([], ...(ngDevMode ? [{ debugName: "wordWithMeta" }] : /* istanbul ignore next */ []));
5204
+ this.wordsWithMetaState = [];
5205
+ this.lastHasTranscription = false;
5206
+ this.audioElement = signal(null, ...(ngDevMode ? [{ debugName: "audioElement" }] : /* istanbul ignore next */ []));
5207
+ /**
5208
+ * Estado del icono derivado ÚNICAMENTE del input `message()`.
5209
+ * No hay estado local: el orquestador es la única fuente de verdad.
5210
+ */
5211
+ this.iconState = computed(() => {
5212
+ const msg = this.message();
5213
+ if (msg?.isLoading) {
5214
+ return 'loading';
4998
5215
  }
4999
- else {
5000
- this.messages.set([...messages]);
5216
+ const status = msg?.audioStatus;
5217
+ if (status === 'playing') {
5218
+ return 'playing';
5001
5219
  }
5002
- this.messageRole.set(role);
5003
- if (isAssistant) {
5004
- const chatSettings = this.userService.user()?.settings?.conversation;
5005
- if (chatSettings?.synthVoice) {
5006
- if (!isContinuation) {
5007
- console.log('Starting new orchestration');
5008
- this.initializeAudioQueue();
5009
- this.processNextInQueue();
5010
- }
5011
- else {
5012
- console.log('Updating existing orchestration queue');
5013
- // Update the queue with new indices
5014
- for (let i = currentMessages.length; i < messages.length; i++) {
5015
- if (messages[i].audioStatus !== 'finished' && !this.audioQueue.includes(i)) {
5016
- this.audioQueue.push(i);
5017
- }
5018
- }
5019
- this.processNextInQueue();
5020
- }
5021
- }
5220
+ if (status === 'stopped') {
5221
+ return 'paused';
5222
+ }
5223
+ if (status === 'finished') {
5224
+ return 'finished';
5225
+ }
5226
+ if (status === 'pending') {
5227
+ return 'pending';
5228
+ }
5229
+ if (msg?.audioUrl) {
5230
+ return 'playable';
5231
+ }
5232
+ return 'idle';
5233
+ }, ...(ngDevMode ? [{ debugName: "iconState" }] : /* istanbul ignore next */ []));
5234
+ this.isPendingAudio = computed(() => this.message()?.audioStatus === 'pending', ...(ngDevMode ? [{ debugName: "isPendingAudio" }] : /* istanbul ignore next */ []));
5235
+ this.messageText = computed(() => {
5236
+ const msg = this.message();
5237
+ return msg?.text || msg?.content || 'N/A';
5238
+ }, ...(ngDevMode ? [{ debugName: "messageText" }] : /* istanbul ignore next */ []));
5239
+ this.classTag = computed(() => this.message()?.tag, ...(ngDevMode ? [{ debugName: "classTag" }] : /* istanbul ignore next */ []));
5240
+ this.transcriptionTimestamps = computed(() => {
5241
+ const msg = this.message();
5242
+ if (!msg || !msg.transcription) {
5243
+ return [];
5244
+ }
5245
+ return matchTranscription(msg.text || msg.content, msg.transcription.words);
5246
+ }, ...(ngDevMode ? [{ debugName: "transcriptionTimestamps" }] : /* istanbul ignore next */ []));
5247
+ this.hasTranscription = computed(() => this.transcriptionTimestamps().length > 0, ...(ngDevMode ? [{ debugName: "hasTranscription" }] : /* istanbul ignore next */ []));
5248
+ // Re-inicializa audio/palabras cuando cambia el mensaje, su audio o su transcripción.
5249
+ effect(() => {
5250
+ const currentMsg = this.message();
5251
+ if (!currentMsg)
5252
+ return;
5253
+ const hasTranscription = this.hasTranscription();
5254
+ const idChanged = currentMsg.messageId !== this.currentMessageId;
5255
+ const audioUrlChanged = currentMsg.audioUrl !== this.lastAudioUrl;
5256
+ const transcriptionChanged = hasTranscription !== this.lastHasTranscription;
5257
+ if (idChanged || audioUrlChanged || transcriptionChanged) {
5258
+ this.currentMessageId = currentMsg.messageId;
5259
+ this.lastAudioUrl = currentMsg.audioUrl;
5260
+ this.lastHasTranscription = hasTranscription;
5261
+ this.initializeBasedOnMessage(currentMsg);
5262
+ }
5263
+ });
5264
+ // Cuando el orquestador marca este fragmento como 'pending', se reproduce.
5265
+ effect(() => {
5266
+ if (this.isPendingAudio()) {
5267
+ this.startAudioPlayback();
5022
5268
  }
5023
5269
  });
5270
+ effect(() => {
5271
+ this.wordsWithMetaState = this.wordWithMeta();
5272
+ });
5024
5273
  }
5025
- audioCompleted(message) {
5026
- const index = this.currentPlayingIndex();
5027
- if (index !== null) {
5028
- console.log(`Audio completed for index ${index}, setting status to finished`);
5029
- this.updateAudioStatus(message, 'finished', index);
5030
- }
5031
- else {
5032
- console.warn('Audio completed but currentPlayingIndex is null');
5033
- }
5034
- this.conversationService.currentAudioStatus.set({ message, completed: true });
5035
- this.currentPlayingIndex.set(null);
5036
- this.messageToPlay.set(null);
5037
- if (this.audioQueue.length > 0) {
5038
- this.processNextInQueue();
5039
- }
5274
+ trackByIndex(index, item) {
5275
+ return index;
5040
5276
  }
5041
- /**
5042
- * @description
5043
- * Updates the audio status of a message.
5044
- *
5045
- * @param message - The `MessageContent` object whose status has changed.
5046
- * @param status - The new `AudioStatus`.
5047
- * @param index - The index of the message in the orchestration queue.
5048
- */
5049
- updateAudioStatus(message, status, index) {
5050
- const currentMessage = this.messages()[index];
5051
- console.log(`Updating audio status for index ${index} to ${status}. MessageId: ${message.messageId}`);
5052
- if (status === 'playing') {
5053
- if (message.messageId) {
5054
- this.messagesStateService.setActiveMessage(message.messageId, index);
5277
+ ngOnDestroy() {
5278
+ this.cleanupAudio();
5279
+ }
5280
+ onPlayMessage() {
5281
+ const currentMsg = this.message();
5282
+ const audioElement = this.audioElement();
5283
+ if (audioElement) {
5284
+ if (audioElement.paused) {
5285
+ this.startAudioPlayback();
5286
+ }
5287
+ else {
5288
+ this.audioTextSyncService.pauseAudio(audioElement);
5289
+ this.reportStatus('stopped');
5055
5290
  }
5056
- // Update local state: set this one as active, others as inactive
5057
- this.messages.update((messages) => messages.map((m, idx) => ({
5058
- ...m,
5059
- isActive: idx === index,
5060
- // Also update the current one's status
5061
- ...(idx === index ? { audioStatus: status } : {}),
5062
- })));
5063
- }
5064
- else {
5065
- // For other statuses (stopped, finished), preserve isActive and update status
5066
- this.changeStates(index, { ...message, audioStatus: status });
5067
5291
  }
5068
- // 4. Update global state
5069
- if (message.messageId) {
5070
- this.messagesStateService.updateMultiMessage(message.messageId, index, {
5071
- audioStatus: status,
5072
- });
5292
+ else if (currentMsg?.audioUrl) {
5293
+ this.initializeAudio(currentMsg.audioUrl);
5294
+ this.startAudioPlayback();
5073
5295
  }
5296
+ // Si no hay audioUrl, el orquestador lo generará vía la cola (status 'pending').
5297
+ }
5298
+ onWordClick(wordData) {
5299
+ const currentMsg = this.message();
5300
+ if (!currentMsg)
5301
+ return;
5302
+ this.orchestrationService.wordClicked({
5303
+ word: wordData.word,
5304
+ index: wordData.index,
5305
+ messageId: currentMsg.messageId,
5306
+ });
5074
5307
  }
5308
+ // --- Reporte de estado directo al orquestador (sin @Output) ---
5075
5309
  /**
5076
- * @description
5077
- * Notifies the `ConversationService` when a word is clicked.
5078
- *
5079
- * @param wordData - The `WordData` object containing information about the clicked word.
5310
+ * Reporta el nuevo estado de audio al orquestador, que es la fuente de verdad.
5311
+ * Guarda contra reportes redundantes (el evento `timeupdate` se dispara en cada tick).
5080
5312
  */
5081
- wordClicked(wordData) {
5082
- this.conversationService.notifyWordClicked(wordData);
5083
- }
5084
- initializeAudioQueue() {
5085
- const messages = this.messages();
5086
- if (messages && messages.length > 0) {
5087
- this.audioQueue = messages
5088
- .map((m, index) => ({ status: m.audioStatus, index }))
5089
- .filter((item) => item.status !== 'finished' && item.status !== 'skip')
5090
- .map((item) => item.index);
5091
- console.log('Audio queue initialized:', this.audioQueue);
5313
+ reportStatus(status) {
5314
+ const currentMsg = this.message();
5315
+ if (currentMsg && currentMsg.audioStatus !== status) {
5316
+ this.orchestrationService.updateAudioStatus(currentMsg, status, this.index());
5092
5317
  }
5093
5318
  }
5094
- async processNextInQueue() {
5095
- if (this.audioQueue.length === 0 || this.isGenerating() || this.currentPlayingIndex() !== null) {
5096
- return;
5319
+ // --- Audio lifecycle ---
5320
+ initializeBasedOnMessage(msg) {
5321
+ if (msg.audioUrl) {
5322
+ this.initializeAudio(msg.audioUrl);
5097
5323
  }
5098
- const nextIndex = this.audioQueue.shift();
5099
- this.isGenerating.set(true);
5100
- try {
5101
- await this.generateAudioForIndex(nextIndex);
5324
+ if (this.hasTranscription()) {
5325
+ const firstState = this.transcriptionTimestamps()?.map((word, index) => ({
5326
+ word: word.word,
5327
+ index,
5328
+ isHighlighted: false,
5329
+ tag: '',
5330
+ marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
5331
+ }));
5332
+ this.wordWithMeta.set(firstState);
5102
5333
  }
5103
- catch (error) {
5104
- console.error('Error generating audio:', error);
5334
+ else {
5335
+ this.initializePlainTextWords(msg.text || msg.content || '');
5105
5336
  }
5106
- finally {
5107
- this.isGenerating.set(false);
5337
+ }
5338
+ initializeAudio(audioUrl) {
5339
+ let audio = this.audioElement();
5340
+ if (!audio) {
5341
+ audio = new Audio(audioUrl);
5342
+ this.audioElement.set(audio);
5343
+ this.setupAudioEventListeners(audio);
5344
+ }
5345
+ else if (audio.src && audioUrl && !audio.src.endsWith(audioUrl)) {
5346
+ audio.src = audioUrl;
5108
5347
  }
5109
- this.preGenerateNextIfNeeded();
5110
5348
  }
5111
- async preGenerateNextIfNeeded() {
5112
- if (this.audioQueue.length > 0 && !this.preGenerationInProgress()) {
5113
- const nextIndex = this.audioQueue[0];
5114
- const messages = this.messages();
5115
- const message = messages[nextIndex];
5116
- // Skip if audio is already generated or generation is not needed
5117
- if (message.audioUrl || message.isLoading) {
5118
- return;
5349
+ setupAudioEventListeners(audioElement) {
5350
+ fromEvent(audioElement, 'timeupdate')
5351
+ .pipe(takeUntilDestroyed(this.destroyRef), map(() => audioElement.currentTime || 0))
5352
+ .subscribe((currentTime) => {
5353
+ if (this.hasTranscription()) {
5354
+ const wordsAndTimestamps = [];
5355
+ this.transcriptionTimestamps()?.forEach((word, index) => {
5356
+ wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
5357
+ });
5358
+ const updatedWords = wordsAndTimestamps.map((word, index) => ({
5359
+ word: word.word,
5360
+ index,
5361
+ isHighlighted: currentTime >= word.start - 0.15 && currentTime < word.end + 0.15,
5362
+ tag: word.tag,
5363
+ marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
5364
+ }));
5365
+ this.wordWithMeta.set(updatedWords);
5119
5366
  }
5120
- this.preGenerationInProgress.set(true);
5121
- const loadingMessage = { ...message, isLoading: true };
5122
- this.changeStates(nextIndex, loadingMessage);
5123
- try {
5124
- const messageAudio = await this.generateAudio(message, null);
5125
- messageAudio.isLoading = false;
5126
- // Pre-generated audio stays in the background, no status update needed here or set to 'stopped'/'playable'
5127
- // Actually, if it's pre-rendered it should probably stay as whatever it was but with audioUrl
5128
- this.changeStates(nextIndex, messageAudio);
5367
+ });
5368
+ fromEvent(audioElement, 'play')
5369
+ .pipe(takeUntilDestroyed(this.destroyRef))
5370
+ .subscribe(() => {
5371
+ this.reportStatus('playing');
5372
+ });
5373
+ fromEvent(audioElement, 'pause')
5374
+ .pipe(takeUntilDestroyed(this.destroyRef))
5375
+ .subscribe(() => {
5376
+ const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
5377
+ this.wordWithMeta.set(resetWords);
5378
+ if (audioElement.currentTime !== audioElement.duration) {
5379
+ this.reportStatus('stopped');
5129
5380
  }
5130
- finally {
5131
- this.preGenerationInProgress.set(false);
5381
+ });
5382
+ fromEvent(audioElement, 'ended')
5383
+ .pipe(takeUntilDestroyed(this.destroyRef))
5384
+ .subscribe(() => {
5385
+ const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
5386
+ this.wordWithMeta.set(resetWords);
5387
+ const currentMsg = this.message();
5388
+ if (currentMsg) {
5389
+ // El servicio marca 'finished' y avanza la cola en un solo paso.
5390
+ this.orchestrationService.audioCompleted(currentMsg);
5132
5391
  }
5133
- }
5392
+ });
5134
5393
  }
5135
- async generateAudioForIndex(index) {
5136
- const messages = this.messages();
5137
- if (messages[index].audioUrl) {
5138
- const messageAudio = { ...messages[index], audioStatus: 'pending' };
5139
- this.changeStates(index, messageAudio);
5140
- this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5141
- this.messageToPlay.set(messageAudio);
5142
- this.currentPlayingIndex.set(index);
5394
+ startAudioPlayback() {
5395
+ const audioElement = this.audioElement();
5396
+ if (!audioElement)
5397
+ return;
5398
+ const currentMsg = this.message();
5399
+ if (!currentMsg)
5143
5400
  return;
5144
- }
5145
- const message = { ...messages[index], isLoading: true };
5146
- this.changeStates(index, message);
5147
5401
  try {
5148
- const messageAudio = await this.generateAudio(messages[index], null);
5149
- messageAudio.isLoading = false;
5150
- messageAudio.audioStatus = 'pending';
5151
- this.changeStates(index, messageAudio);
5152
- this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5153
- this.messageToPlay.set(messageAudio);
5402
+ this.audioTextSyncService.playAudio(audioElement, currentMsg);
5154
5403
  }
5155
- finally {
5156
- const message = { ...messages[index], isLoading: false };
5157
- this.changeStates(index, message);
5404
+ catch (error) {
5405
+ console.error('Error playing audio:', error);
5158
5406
  }
5159
- this.currentPlayingIndex.set(index);
5160
- }
5161
- changeStates(index, messageAudio) {
5162
- this.messages.update((messages) => {
5163
- const newMessages = [...messages];
5164
- newMessages[index] = { ...messages[index], ...messageAudio };
5165
- return newMessages;
5166
- });
5407
+ this.reportStatus('playing');
5167
5408
  }
5168
- async generateAudio(message, overwriteText = null) {
5169
- if (message.audioUrl) {
5170
- return message;
5171
- }
5172
- try {
5173
- const text = overwriteText || message.text || message.content;
5174
- const ttsObject = buildObjectTTSRequest({ ...message, text }, { highlightWords: true });
5175
- const speechAudio = await this.conversationService.getTTSFile(ttsObject);
5176
- message = extractAudioAndTranscription(message, speechAudio);
5177
- return message;
5178
- }
5179
- finally {
5180
- this.isGenerating.set(false);
5409
+ cleanupAudio() {
5410
+ const audioElement = this.audioElement();
5411
+ if (audioElement) {
5412
+ this.audioTextSyncService.pauseAudio(audioElement);
5413
+ audioElement.src = '';
5414
+ const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
5415
+ this.wordWithMeta.set(resetWords);
5181
5416
  }
5182
5417
  }
5183
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5184
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService }); }
5418
+ initializePlainTextWords(text) {
5419
+ const words = extractTags(text);
5420
+ const plainWords = words.map((word, index) => ({
5421
+ word: word.word,
5422
+ index: index,
5423
+ isHighlighted: false,
5424
+ tag: word.tag,
5425
+ }));
5426
+ this.wordWithMeta.set(plainWords);
5427
+ }
5428
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentReactive, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5429
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageContentReactive, isStandalone: true, selector: "message-content-reactive", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, markWord: { classPropertyName: "markWord", publicName: "markWord", isSignal: true, isRequired: false, transformFunction: null }, highlightColor: { classPropertyName: "highlightColor", publicName: "highlightColor", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--highlight-bg-color": "this.hostHighlightColor" } }, ngImport: i0, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) {\n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> }\n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> }\n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> }\n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> }\n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5185
5430
  }
5186
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, decorators: [{
5187
- type: Injectable
5188
- }], ctorParameters: () => [] });
5431
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentReactive, decorators: [{
5432
+ type: Component,
5433
+ args: [{ selector: 'message-content-reactive', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) {\n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> }\n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> }\n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> }\n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> }\n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"] }]
5434
+ }], ctorParameters: () => [], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], markWord: [{ type: i0.Input, args: [{ isSignal: true, alias: "markWord", required: false }] }], highlightColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightColor", required: false }] }], hostHighlightColor: [{
5435
+ type: HostBinding,
5436
+ args: ['style.--highlight-bg-color']
5437
+ }] } });
5189
5438
 
5190
5439
  /**
5191
5440
  * @Component
@@ -5194,8 +5443,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
5194
5443
  * This component orchestrates the display and audio playback of chat messages.
5195
5444
  * It uses the `MessageOrchestrationService` to manage the message queue and audio playback.
5196
5445
  *
5446
+ * Children (`MessageContentReactive`) inject the same `MessageOrchestrationService`
5447
+ * instance directly and report their state to it, so this component no longer needs
5448
+ * to relay per-segment audio events via `@Output()`.
5449
+ *
5197
5450
  * @see MessageOrchestrationService
5198
- * @see MessageContentDisplayer
5451
+ * @see MessageContentReactive
5199
5452
  */
5200
5453
  class ChatMessageOrchestratorComponent {
5201
5454
  constructor() {
@@ -5221,41 +5474,12 @@ class ChatMessageOrchestratorComponent {
5221
5474
  this.orchestrationService.startOrchestration(this.messages(), this.messageRole());
5222
5475
  });
5223
5476
  }
5224
- /**
5225
- * @description
5226
- * Handles the `audioCompleted` event from the `MessageContentDisplayer`.
5227
- *
5228
- * @param message - The `MessageContent` object whose audio has finished playing.
5229
- */
5230
- onAudioCompleted(message) {
5231
- this.orchestrationService.audioCompleted(message);
5232
- this.audioCompleted.emit(message);
5233
- }
5234
- /**
5235
- * @description
5236
- * Handles the `audioStatusChanged` event from the `MessageContentDisplayer`.
5237
- *
5238
- * @param event - The event object containing the `MessageContent` and its new `AudioStatus`.
5239
- * @param index - The index of the message in the array.
5240
- */
5241
- onAudioStatusChanged(event, index) {
5242
- this.orchestrationService.updateAudioStatus(event.message, event.status, index);
5243
- }
5244
- /**
5245
- * @description
5246
- * Handles the `wordClicked` event from the `MessageContentDisplayer`.
5247
- *
5248
- * @param wordData - The `WordData` object containing information about the clicked word.
5249
- */
5250
- onWordClicked(wordData) {
5251
- this.orchestrationService.wordClicked(wordData);
5252
- }
5253
5477
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMessageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5254
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ChatMessageOrchestratorComponent, isStandalone: true, selector: "dc-message-orchestrator", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: true, transformFunction: null }, messageRole: { classPropertyName: "messageRole", publicName: "messageRole", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { audioCompleted: "audioCompleted" }, providers: [MessageOrchestrationService], ngImport: i0, template: "<div class=\"message-orchestrator-container\">\n @for (message of messagesSignal(); track $index) {\n <message-content-displayer [message]=\"message\" (audioCompleted)=\"onAudioCompleted($event)\" (audioStatusChanged)=\"onAudioStatusChanged($event, $index)\" (wordClicked)=\"onWordClicked($event)\" />\n }\n</div>\n", styles: [":host{display:block}.word-options-popup{position:absolute;border:1px solid #ccc;background:#fff;padding:5px;z-index:1000}message-content-displayer{text-shadow:1px 1px 4px rgba(255,255,255,.4)}\n"], dependencies: [{ kind: "component", type: MessageContentDisplayer, selector: "message-content-displayer", inputs: ["message", "markWord", "highlightColor"], outputs: ["playAudio", "audioCompleted", "audioStatusChanged", "wordClicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5478
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ChatMessageOrchestratorComponent, isStandalone: true, selector: "dc-message-orchestrator", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: true, transformFunction: null }, messageRole: { classPropertyName: "messageRole", publicName: "messageRole", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { audioCompleted: "audioCompleted" }, providers: [MessageOrchestrationService], ngImport: i0, template: "<div class=\"message-orchestrator-container\">\n @for (message of messagesSignal(); track $index) {\n <message-content-reactive [message]=\"message\" [index]=\"$index\" />\n }\n</div>\n", styles: [":host{display:block}.word-options-popup{position:absolute;border:1px solid #ccc;background:#fff;padding:5px;z-index:1000}message-content-displayer{text-shadow:1px 1px 4px rgba(255,255,255,.4)}\n"], dependencies: [{ kind: "component", type: MessageContentReactive, selector: "message-content-reactive", inputs: ["message", "index", "markWord", "highlightColor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5255
5479
  }
5256
5480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMessageOrchestratorComponent, decorators: [{
5257
5481
  type: Component,
5258
- args: [{ selector: 'dc-message-orchestrator', imports: [MessageContentDisplayer], changeDetection: ChangeDetectionStrategy.OnPush, providers: [MessageOrchestrationService], template: "<div class=\"message-orchestrator-container\">\n @for (message of messagesSignal(); track $index) {\n <message-content-displayer [message]=\"message\" (audioCompleted)=\"onAudioCompleted($event)\" (audioStatusChanged)=\"onAudioStatusChanged($event, $index)\" (wordClicked)=\"onWordClicked($event)\" />\n }\n</div>\n", styles: [":host{display:block}.word-options-popup{position:absolute;border:1px solid #ccc;background:#fff;padding:5px;z-index:1000}message-content-displayer{text-shadow:1px 1px 4px rgba(255,255,255,.4)}\n"] }]
5482
+ args: [{ selector: 'dc-message-orchestrator', imports: [MessageContentReactive], changeDetection: ChangeDetectionStrategy.OnPush, providers: [MessageOrchestrationService], template: "<div class=\"message-orchestrator-container\">\n @for (message of messagesSignal(); track $index) {\n <message-content-reactive [message]=\"message\" [index]=\"$index\" />\n }\n</div>\n", styles: [":host{display:block}.word-options-popup{position:absolute;border:1px solid #ccc;background:#fff;padding:5px;z-index:1000}message-content-displayer{text-shadow:1px 1px 4px rgba(255,255,255,.4)}\n"] }]
5259
5483
  }], ctorParameters: () => [], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: true }] }], messageRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageRole", required: true }] }], audioCompleted: [{ type: i0.Output, args: ["audioCompleted"] }] } });
5260
5484
 
5261
5485
  const EMOJI_TO_LABEL = new Map(MoodStateOptions.map((o) => [o.emoji, o.label]));
@@ -5571,169 +5795,262 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
5571
5795
  args: ['tooltipRef']
5572
5796
  }] } });
5573
5797
 
5574
- class AudioTextSyncService {
5798
+ // Removed AudioState as it's replaced by AudioStatus from agent.models
5799
+ class MessageContentDisplayer {
5800
+ get hostHighlightColor() {
5801
+ return this.highlightColor();
5802
+ }
5575
5803
  constructor() {
5576
- // Maps to store message-specific signals and observables
5577
- this.highlightedWordsSignalMap = new Map();
5578
- this.highlightedWords$Map = new Map();
5579
- // Maps for cleanup and active audio elements
5580
- this.cleanup$Map = new Map();
5581
- this.activeAudioMap = new Map();
5804
+ // Inputs
5805
+ this.message = input.required(...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
5806
+ this.markWord = input(...(ngDevMode ? [undefined, { debugName: "markWord" }] : /* istanbul ignore next */ []));
5807
+ this.highlightColor = input(...(ngDevMode ? [undefined, { debugName: "highlightColor" }] : /* istanbul ignore next */ []));
5808
+ // Outputs
5809
+ this.playAudio = output();
5810
+ this.audioCompleted = output();
5811
+ this.audioStatusChanged = output();
5812
+ this.wordClicked = output();
5813
+ // Signal States
5814
+ this.wordWithMeta = signal([], ...(ngDevMode ? [{ debugName: "wordWithMeta" }] : /* istanbul ignore next */ []));
5815
+ this.localAudioStatus = signal(null, ...(ngDevMode ? [{ debugName: "localAudioStatus" }] : /* istanbul ignore next */ []));
5816
+ // Signals State computed from the input message
5817
+ this.wordsWithMetaState = [];
5818
+ this.lastHasTranscription = false;
5819
+ this.audioElement = signal(null, ...(ngDevMode ? [{ debugName: "audioElement" }] : /* istanbul ignore next */ []));
5582
5820
  this.destroyRef = inject(DestroyRef);
5583
- // Ensure cleanup when service is destroyed
5584
- this.destroyRef.onDestroy(() => {
5585
- this.stopAllSyncs();
5821
+ this.iconState = computed(() => {
5822
+ const msg = this.message();
5823
+ if (msg?.isLoading) {
5824
+ return 'loading';
5825
+ }
5826
+ const status = this.localAudioStatus() || msg?.audioStatus;
5827
+ if (status === 'playing') {
5828
+ return 'playing';
5829
+ }
5830
+ if (status === 'stopped') {
5831
+ return 'paused';
5832
+ }
5833
+ if (status === 'finished') {
5834
+ return 'finished';
5835
+ }
5836
+ if (status === 'pending') {
5837
+ return 'pending';
5838
+ }
5839
+ if (msg?.audioUrl) {
5840
+ return 'playable';
5841
+ }
5842
+ return 'idle';
5843
+ }, ...(ngDevMode ? [{ debugName: "iconState" }] : /* istanbul ignore next */ []));
5844
+ this.isPendingAudio = computed(() => !!this.message() && this.message()?.audioStatus === 'pending', ...(ngDevMode ? [{ debugName: "isPendingAudio" }] : /* istanbul ignore next */ []));
5845
+ this.messageText = computed(() => {
5846
+ const msg = this.message();
5847
+ return msg?.text || msg?.content || 'N/A';
5848
+ }, ...(ngDevMode ? [{ debugName: "messageText" }] : /* istanbul ignore next */ []));
5849
+ this.classTag = computed(() => this.message()?.tag, ...(ngDevMode ? [{ debugName: "classTag" }] : /* istanbul ignore next */ []));
5850
+ // When message is updated, check is has transcriptions whisper format.
5851
+ this.transcriptionTimestamps = computed(() => {
5852
+ const msg = this.message();
5853
+ if (!msg || !msg.transcription) {
5854
+ return [];
5855
+ }
5856
+ return matchTranscription(msg.text || msg.content, msg.transcription.words);
5857
+ }, ...(ngDevMode ? [{ debugName: "transcriptionTimestamps" }] : /* istanbul ignore next */ []));
5858
+ // if transcriptionTimestamps is calculated then hasTranscription is true.
5859
+ this.hasTranscription = computed(() => {
5860
+ return this.transcriptionTimestamps().length > 0;
5861
+ }, ...(ngDevMode ? [{ debugName: "hasTranscription" }] : /* istanbul ignore next */ []));
5862
+ effect(() => {
5863
+ const currentMsg = this.message();
5864
+ if (!currentMsg)
5865
+ return;
5866
+ const hasTranscription = this.hasTranscription();
5867
+ const idChanged = currentMsg.messageId !== this.currentMessageId;
5868
+ const audioUrlChanged = currentMsg.audioUrl !== this.lastAudioUrl;
5869
+ const transcriptionChanged = hasTranscription !== this.lastHasTranscription;
5870
+ if (idChanged || audioUrlChanged || transcriptionChanged) {
5871
+ this.currentMessageId = currentMsg.messageId;
5872
+ this.lastAudioUrl = currentMsg.audioUrl;
5873
+ this.lastHasTranscription = hasTranscription;
5874
+ this.localAudioStatus.set(null); // Reset local state on message/audio change
5875
+ this.initializeBasedOnMessage(currentMsg);
5876
+ }
5877
+ });
5878
+ effect(() => {
5879
+ if (this.isPendingAudio()) {
5880
+ this.startAudioPlayback();
5881
+ }
5882
+ });
5883
+ effect(() => {
5884
+ this.wordsWithMetaState = this.wordWithMeta();
5586
5885
  });
5587
5886
  }
5588
- /**
5589
- * Synchronizes audio playback with text transcription
5590
- * @param audioElement The audio element to sync with
5591
- * @param transcriptionTimestamps Array of word timestamps
5592
- * @param messageId Unique identifier for the message
5593
- */
5594
- syncAudioWithText(audioElement, transcriptionTimestamps, messageId) {
5595
- // Stop any existing sync for this message
5596
- this.stopSync(messageId);
5597
- // Create new signal and subject for this message if they don't exist
5598
- if (!this.highlightedWordsSignalMap.has(messageId)) {
5599
- this.highlightedWordsSignalMap.set(messageId, signal([]));
5887
+ trackByIndex(index, item) {
5888
+ return index;
5889
+ }
5890
+ ngOnDestroy() {
5891
+ this.cleanupAudio();
5892
+ }
5893
+ initializeBasedOnMessage(msg) {
5894
+ if (msg.audioUrl) {
5895
+ this.initializeAudio(msg.audioUrl);
5600
5896
  }
5601
- if (!this.highlightedWords$Map.has(messageId)) {
5602
- this.highlightedWords$Map.set(messageId, new BehaviorSubject([]));
5897
+ if (this.hasTranscription()) {
5898
+ const wordsAndTimestamps = [];
5899
+ this.transcriptionTimestamps()?.forEach((word, index) => {
5900
+ wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
5901
+ });
5902
+ const firstState = this.transcriptionTimestamps()?.map((word, index) => ({
5903
+ word: word.word,
5904
+ index,
5905
+ isHighlighted: false,
5906
+ tag: '',
5907
+ marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
5908
+ }));
5909
+ console.log('firstState', firstState);
5910
+ this.wordWithMeta.set(firstState);
5911
+ // Para mostrar las transcripciones iliminadas: el audio tiene que sonar y en la subscripción apareceran los cambios.
5603
5912
  }
5604
- // Create cleanup subject
5605
- const cleanup$ = new Subject();
5606
- this.cleanup$Map.set(messageId, cleanup$);
5607
- // Store the active audio element
5608
- this.activeAudioMap.set(messageId, audioElement);
5609
- // Get the signal and subject for this message
5610
- const messageSignal = this.highlightedWordsSignalMap.get(messageId);
5611
- const messageSubject = this.highlightedWords$Map.get(messageId);
5612
- // Initialize the highlighted words state
5613
- const initialWords = transcriptionTimestamps.map((word, index) => ({
5614
- word: word.word,
5615
- index,
5616
- isHighlighted: false,
5617
- }));
5618
- // Update both signal and observable
5619
- messageSignal.set(initialWords);
5620
- messageSubject.next(initialWords);
5621
- // Listen to timeupdate events
5913
+ else {
5914
+ this.initializePlainTextWords(msg.text || msg.content || '');
5915
+ }
5916
+ // Removal of shouldPlayAudio check - playback now handled by isPendingAudio effect
5917
+ }
5918
+ initializeAudio(audioUrl) {
5919
+ let audio = this.audioElement();
5920
+ if (!audio) {
5921
+ audio = new Audio(audioUrl);
5922
+ this.audioElement.set(audio);
5923
+ this.setupAudioEventListeners(audio);
5924
+ }
5925
+ else if (audio.src && audioUrl && !audio.src.endsWith(audioUrl)) {
5926
+ audio.src = audioUrl;
5927
+ }
5928
+ }
5929
+ setupAudioEventListeners(audioElement) {
5622
5930
  fromEvent(audioElement, 'timeupdate')
5623
- .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$), map(() => audioElement.currentTime))
5931
+ .pipe(takeUntilDestroyed(this.destroyRef), map(() => audioElement.currentTime || 0))
5624
5932
  .subscribe((currentTime) => {
5625
- const updatedWords = transcriptionTimestamps.map((word, index) => {
5626
- const isHighlighted = currentTime >= word.start - 0.15 && currentTime < word.end + 0.15;
5627
- return {
5933
+ if (currentTime === 0) {
5934
+ this.updateAudioStatus('playing');
5935
+ return;
5936
+ }
5937
+ if (this.hasTranscription()) {
5938
+ const wordsAndTimestamps = [];
5939
+ this.transcriptionTimestamps()?.forEach((word, index) => {
5940
+ wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
5941
+ });
5942
+ const updatedWords = wordsAndTimestamps.map((word, index) => ({
5628
5943
  word: word.word,
5629
5944
  index,
5630
- isHighlighted,
5631
- };
5632
- });
5633
- // Update both signal and observable for this message
5634
- messageSignal.set(updatedWords);
5635
- messageSubject.next(updatedWords);
5945
+ isHighlighted: currentTime >= word.start - 0.15 && currentTime < word.end + 0.15,
5946
+ tag: word.tag,
5947
+ marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
5948
+ }));
5949
+ this.wordWithMeta.set(updatedWords);
5950
+ }
5636
5951
  });
5637
- // Listen to ended event for cleanup
5638
5952
  fromEvent(audioElement, 'ended')
5639
- .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$))
5953
+ .pipe(takeUntilDestroyed(this.destroyRef))
5640
5954
  .subscribe(() => {
5641
- // Reset highlighting when audio ends
5642
- const resetWords = initialWords.map((word) => ({
5643
- ...word,
5644
- isHighlighted: false,
5645
- }));
5646
- messageSignal.set(resetWords);
5647
- messageSubject.next(resetWords);
5648
- });
5649
- }
5650
- /**
5651
- * Stops the sync for a specific message and cleans up resources
5652
- * @param messageId The ID of the message to stop syncing
5653
- */
5654
- stopSync(messageId) {
5655
- if (this.activeAudioMap.has(messageId)) {
5656
- // Get the cleanup subject for this message
5657
- const cleanup$ = this.cleanup$Map.get(messageId);
5658
- if (cleanup$) {
5659
- cleanup$.next();
5660
- cleanup$.complete();
5661
- this.cleanup$Map.delete(messageId);
5662
- }
5663
- this.activeAudioMap.delete(messageId);
5664
- // Reset state for this message
5665
- const messageSignal = this.highlightedWordsSignalMap.get(messageId);
5666
- const messageSubject = this.highlightedWords$Map.get(messageId);
5667
- if (messageSignal) {
5668
- messageSignal.set([]);
5669
- }
5670
- if (messageSubject) {
5671
- messageSubject.next([]);
5955
+ const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
5956
+ this.wordWithMeta.set(resetWords);
5957
+ const currentMsg = this.message();
5958
+ if (currentMsg) {
5959
+ // No direct mutation of input!
5960
+ this.updateAudioStatus('finished');
5961
+ this.audioCompleted.emit(currentMsg);
5672
5962
  }
5673
- }
5963
+ });
5674
5964
  }
5675
- /**
5676
- * Stops all syncs and cleans up all resources
5677
- */
5678
- stopAllSyncs() {
5679
- // Get all message IDs and stop each sync
5680
- for (const messageId of this.activeAudioMap.keys()) {
5681
- this.stopSync(messageId);
5965
+ updateAudioStatus(status) {
5966
+ const currentMsg = this.message();
5967
+ // Update local state for immediate feedback
5968
+ this.localAudioStatus.set(status);
5969
+ // Only update if the status is actually changing relative to the current input
5970
+ if (currentMsg && currentMsg.audioStatus !== status) {
5971
+ // NOTE: We do NOT mutate currentMsg.audioStatus here.
5972
+ // The parent orchestration service will update the state and propagate it back via signals.
5973
+ this.audioStatusChanged.emit({ message: currentMsg, status });
5682
5974
  }
5683
- // Clear all maps
5684
- this.highlightedWordsSignalMap.clear();
5685
- this.highlightedWords$Map.clear();
5686
- this.cleanup$Map.clear();
5687
- this.activeAudioMap.clear();
5688
5975
  }
5689
- /**
5690
- * Returns the highlighted words signal for a specific message
5691
- * @param messageId The ID of the message
5692
- */
5693
- getHighlightedWordsSignal(messageId) {
5694
- // Create a new signal for this message if it doesn't exist
5695
- if (!this.highlightedWordsSignalMap.has(messageId)) {
5696
- this.highlightedWordsSignalMap.set(messageId, signal([]));
5976
+ cleanupAudio() {
5977
+ const audioElement = this.audioElement();
5978
+ if (audioElement) {
5979
+ audioElement.pause();
5980
+ audioElement.src = '';
5981
+ const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
5982
+ this.wordWithMeta.set(resetWords);
5697
5983
  }
5698
- return this.highlightedWordsSignalMap.get(messageId);
5699
5984
  }
5700
- /**
5701
- * Returns the highlighted words observable for a specific message
5702
- * @param messageId The ID of the message
5703
- */
5704
- getHighlightedWords$(messageId) {
5705
- // Create a new subject for this message if it doesn't exist
5706
- if (!this.highlightedWords$Map.has(messageId)) {
5707
- this.highlightedWords$Map.set(messageId, new BehaviorSubject([]));
5985
+ onPlayMessage() {
5986
+ console.log('onPlayMessage');
5987
+ const currentMsg = this.message();
5988
+ const audioElement = this.audioElement();
5989
+ if (audioElement) {
5990
+ if (audioElement.paused) {
5991
+ this.startAudioPlayback();
5992
+ }
5993
+ else {
5994
+ audioElement.pause();
5995
+ this.updateAudioStatus('stopped');
5996
+ }
5997
+ }
5998
+ else if (currentMsg?.audioUrl) {
5999
+ console.log('onPlayMessage initializeAudio with url');
6000
+ this.initializeAudio(currentMsg.audioUrl);
6001
+ this.startAudioPlayback();
6002
+ }
6003
+ else if (currentMsg) {
6004
+ console.log('onPlayMessage emit playAudio');
6005
+ this.playAudio.emit(currentMsg);
5708
6006
  }
5709
- return this.highlightedWords$Map.get(messageId).asObservable();
5710
6007
  }
5711
- /**
5712
- * Checks if a word at a specific index is currently highlighted for a specific message
5713
- * @param messageId The ID of the message
5714
- * @param index The index of the word to check
5715
- */
5716
- isWordHighlighted(messageId, index) {
5717
- const messageSignal = this.getHighlightedWordsSignal(messageId);
5718
- return messageSignal().some((word) => word.index === index && word.isHighlighted);
6008
+ initializePlainTextWords(text) {
6009
+ const words = extractTags(text);
6010
+ const plainWords = words.map((word, index) => ({
6011
+ word: word.word,
6012
+ index: index,
6013
+ isHighlighted: false,
6014
+ tag: word.tag,
6015
+ }));
6016
+ this.wordWithMeta.set(plainWords);
5719
6017
  }
5720
- /**
5721
- * Returns an observable that emits true when a word at a specific index is highlighted for a specific message
5722
- * @param messageId The ID of the message
5723
- * @param index The index of the word to observe
5724
- */
5725
- isWordHighlighted$(messageId, index) {
5726
- return this.getHighlightedWords$(messageId).pipe(map((words) => words.some((word) => word.index === index && word.isHighlighted)));
6018
+ onWordClick(wordData) {
6019
+ const currentMsg = this.message();
6020
+ if (!currentMsg)
6021
+ return;
6022
+ const clickedWord = 'isHighlighted' in wordData
6023
+ ? { word: wordData.word, index: wordData.index, messageId: currentMsg.messageId }
6024
+ : { ...wordData, messageId: currentMsg.messageId };
6025
+ this.wordClicked.emit(clickedWord);
5727
6026
  }
5728
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5729
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, providedIn: 'root' }); }
6027
+ startAudioPlayback() {
6028
+ const audioElement = this.audioElement();
6029
+ if (!audioElement)
6030
+ return;
6031
+ try {
6032
+ audioElement.play();
6033
+ console.log('Audio playing');
6034
+ }
6035
+ catch (error) {
6036
+ console.error('Error playing audio:', error);
6037
+ }
6038
+ // Important: we move it to playing status immediately to avoid re-triggering the effect
6039
+ this.updateAudioStatus('playing');
6040
+ }
6041
+ debug() {
6042
+ console.log('debug', this.wordWithMeta());
6043
+ }
6044
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6045
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageContentDisplayer, isStandalone: true, selector: "message-content-displayer", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, markWord: { classPropertyName: "markWord", publicName: "markWord", isSignal: true, isRequired: false, transformFunction: null }, highlightColor: { classPropertyName: "highlightColor", publicName: "highlightColor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playAudio: "playAudio", audioCompleted: "audioCompleted", audioStatusChanged: "audioStatusChanged", wordClicked: "wordClicked" }, host: { properties: { "style.--highlight-bg-color": "this.hostHighlightColor" } }, ngImport: i0, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) { \n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> } \n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> } \n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5730
6046
  }
5731
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, decorators: [{
5732
- type: Injectable,
5733
- args: [{
5734
- providedIn: 'root',
5735
- }]
5736
- }], ctorParameters: () => [] });
6047
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, decorators: [{
6048
+ type: Component,
6049
+ args: [{ selector: 'message-content-displayer', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audio-text-sync-container\" [class.is-active]=\"message().isActive\">\n <i (click)=\"onPlayMessage()\" class=\"play-button\">\n @switch (iconState()) { \n @case ('loading') { <i class=\"spin-animation pi pi-spinner-dotted\"></i> } \n @case ('playing') { <i class=\"pi pi-volume-up\"></i> }\n @case ('paused') { <i class=\"pi pi-pause-circle\"></i> } \n @case ('finished') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n @case ('pending') { <i class=\"pi pi-angle-right\"></i> }\n @case ('playable') { <i class=\"pi pi-play-circle\" style=\"color: white\"></i> } \n }\n </i>\n\n <!-- Display transcription with highlighting -->\n <p style=\"width: 100%\" [class]=\"'text-content ' + classTag()\">\n @for (word of wordWithMeta(); track trackByIndex($index, word)) {\n <span\n [class]=\"word.tag\"\n [class.highlight]=\"word.isHighlighted\"\n [class.marked]=\"word.marked\"\n (click)=\"onWordClick(word)\"\n [attr.id]=\"'word-' + message().messageId + '-' + word.index\"\n >{{ word.word }}\n </span>\n }\n </p>\n</div>\n@if (message().imageUrl) {\n<div class=\"tour-image-container\" style=\"display: flex; justify-content: center\">\n <img [src]=\"message().imageUrl\" alt=\"Tour Image\" style=\"max-width: 220px; height: auto\" />\n</div>\n}\n", styles: [":host{display:block}.audio-text-sync-container{display:flex;align-items:flex-start;gap:2px;align-items:center;transition:background-color .3s ease}.audio-text-sync-container.is-active{background-color:#3cd8ff1a;border-radius:8px;padding-left:4px}.play-button{cursor:pointer;display:flex;align-items:center;justify-content:center;min-width:24px;margin-top:4px}.play-button:hover{opacity:.8}.text-content{flex:1}.highlight{background-color:var(--highlight-bg-color, rgb(60, 216, 255));border-radius:4px}::ng-deep .marked{position:relative;color:#f8bfbc}::ng-deep .marked:before{content:\"\";z-index:-2;left:-.1em;top:.1em;border-width:2px;border-style:solid;border-color:#f27068;position:absolute;border-right-color:transparent;width:100%;height:1em;transform:rotate(2deg);opacity:.7;border-radius:50%;padding:.1em .25em}::ng-deep .marked:after{content:\"\";z-index:-1;left:-.1em;top:.4em;padding:.1em .25em;border-width:2px;border-style:solid;border-color:#f27068;border-left-color:transparent;border-top-color:transparent;position:absolute;width:100%;height:1em;transform:rotate(-1deg);opacity:.7;border-radius:50%}.spin-animation{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.em{font-style:italic;color:var(--chat-message-italic-color, rgb(184, 208, 252))}.strong{font-weight:700;color:inherit}.italic{font-style:italic;color:var(--chat-message-italic-color, rgb(252, 198, 184))}.em_strong{font-weight:700;font-style:italic;color:inherit}\n"] }]
6050
+ }], ctorParameters: () => [], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], markWord: [{ type: i0.Input, args: [{ isSignal: true, alias: "markWord", required: false }] }], highlightColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightColor", required: false }] }], hostHighlightColor: [{
6051
+ type: HostBinding,
6052
+ args: ['style.--highlight-bg-color']
6053
+ }], playAudio: [{ type: i0.Output, args: ["playAudio"] }], audioCompleted: [{ type: i0.Output, args: ["audioCompleted"] }], audioStatusChanged: [{ type: i0.Output, args: ["audioStatusChanged"] }], wordClicked: [{ type: i0.Output, args: ["wordClicked"] }] } });
5737
6054
 
5738
6055
  class DcImmersiveChatComponent {
5739
6056
  constructor() {
@@ -6137,16 +6454,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
6137
6454
 
6138
6455
  class MessagesStateInspectorComponent {
6139
6456
  constructor() {
6140
- this.messagesStateService = inject(MessagesStateService);
6141
- this.messages = this.messagesStateService.getMessagesSignal();
6457
+ this.service = input(...(ngDevMode ? [undefined, { debugName: "service" }] : /* istanbul ignore next */ []));
6458
+ this.fallback = inject(MessagesStateService);
6459
+ this.messages = computed(() => (this.service() ?? this.fallback).getMessagesSignal()(), ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
6142
6460
  }
6143
6461
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessagesStateInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6144
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessagesStateInspectorComponent, isStandalone: true, selector: "dc-messages-state-inspector", ngImport: i0, template: "<div class=\"messages-state-inspector\">\n @for (msg of messages(); track msg.messageId; let i = $index) {\n <details class=\"message-group mb-2 border rounded p-2\">\n <summary class=\"cursor-pointer font-bold p-1 hover:bg-gray-100 flex justify-between items-center\">\n <span>\n <span class=\"text-xs text-gray-400 mr-2\">#{{ i + 1 }}</span>\n <span [class]=\"'role-badge ' + msg.role\">{{ msg.role }}</span>\n <span class=\"ml-2 text-sm truncate max-w-xs\">{{ msg.content | slice:0:50 }}{{ msg.content.length > 50 ? '...' : '' }}</span>\n </span>\n <span class=\"text-xs text-gray-400\">{{ msg.messageId }}</span>\n </summary>\n\n <div class=\"message-details mt-2 pl-4 border-l-2 border-primary\">\n <!-- Standard Properties -->\n <div class=\"grid grid-cols-2 gap-2 text-sm\">\n <div><strong>Role:</strong> {{ msg.role }}</div>\n <div><strong>Section:</strong> {{ msg.section || 'N/A' }}</div>\n <div><strong>Message ID:</strong> {{ msg.messageId }}</div>\n <div class=\"col-span-2\"><strong>Content:</strong> <pre class=\"whitespace-pre-wrap text-xs bg-gray-50 p-2 mt-1\">{{ msg.content }}</pre></div>\n @if (msg.translation) {\n <div class=\"col-span-2 text-blue-600\"><strong>Translation:</strong> {{ msg.translation }}</div>\n }\n </div>\n\n <!-- JSON Preview of other props -->\n <details class=\"mt-2\">\n <summary class=\"text-xs text-gray-500 cursor-pointer\">All Metadata</summary>\n <pre class=\"text-[10px] bg-slate-800 text-white p-2 rounded mt-1 overflow-auto max-h-64\">{{ msg | safeJson }}</pre>\n </details>\n\n <!-- MultiMessages recursive check -->\n @if (msg.multiMessages && msg.multiMessages.length > 0) {\n <div class=\"mt-3\">\n <strong class=\"text-xs uppercase text-gray-500\">Multi Messages ({{ msg.multiMessages.length }})</strong>\n <div class=\"pl-4 border-l-2 border-dashed border-gray-300\">\n @for (subMsg of msg.multiMessages; track $index) {\n <details class=\"mt-1 border rounded p-1 bg-gray-50\">\n <summary class=\"text-xs cursor-pointer truncate\">Sub: {{ subMsg.text | slice:0:30 }}...</summary>\n <pre class=\"text-[10px] mt-1 p-1 bg-white border overflow-auto\">{{ subMsg | safeJson }}</pre>\n </details>\n }\n </div>\n </div>\n }\n\n <!-- Evaluation -->\n @if (msg.evaluation) {\n <div class=\"mt-2 p-2 bg-yellow-50 rounded text-xs space-y-2\">\n <strong>Evaluation</strong>\n\n @if (msg.evaluation['flow']; as flow) {\n <div class=\"flow-eval space-y-2\">\n @if (flow.goal) {\n <div class=\"p-2 bg-white rounded border\">\n <div class=\"flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Goal</strong>\n @if (flow.goal.deltaPoints != null) {\n <span\n class=\"px-1.5 rounded text-[11px] font-bold\"\n [class.bg-green-100]=\"flow.goal.deltaPoints > 0\"\n [class.text-green-700]=\"flow.goal.deltaPoints > 0\"\n [class.bg-red-100]=\"flow.goal.deltaPoints < 0\"\n [class.text-red-700]=\"flow.goal.deltaPoints < 0\"\n [class.bg-gray-100]=\"flow.goal.deltaPoints === 0\"\n [class.text-gray-600]=\"flow.goal.deltaPoints === 0\">\n {{ flow.goal.deltaPoints > 0 ? '+' : '' }}{{ flow.goal.deltaPoints }} pts\n </span>\n }\n </div>\n @if (flow.goal.previousValue != null && flow.goal.newValue != null) {\n <div class=\"mt-1 text-[11px] text-gray-600\">\n <span>{{ flow.goal.previousValue }}</span>\n <span class=\"mx-1\">\u2192</span>\n <span class=\"font-bold text-gray-900\">{{ flow.goal.newValue }}</span>\n <span class=\"text-gray-400\"> / 100</span>\n </div>\n <div class=\"w-full bg-gray-200 rounded h-1 mt-1 overflow-hidden\">\n <div class=\"bg-primary h-1\" [style.width.%]=\"flow.goal.newValue\" [style.background-color]=\"'#3b82f6'\"></div>\n </div>\n }\n @if (flow.intervention?.message) {\n <div class=\"mt-1 text-[11px] italic text-gray-700\">\"{{ flow.intervention.message }}\"</div>\n }\n </div>\n }\n\n @if (flow.challenges?.length) {\n <div class=\"p-2 bg-white rounded border\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Challenges</strong>\n <ul class=\"mt-1 space-y-0.5\">\n @for (ch of flow.challenges; track ch.name) {\n <li class=\"flex items-center gap-1 text-[11px]\">\n <span [class.text-green-600]=\"ch.value\" [class.text-gray-400]=\"!ch.value\">\n {{ ch.value ? '\u2713' : '\u25CB' }}\n </span>\n <span [class.font-semibold]=\"ch.value\">{{ ch.name }}</span>\n </li>\n }\n </ul>\n </div>\n }\n\n @if (flow.moodState?.value) {\n <div class=\"p-2 bg-white rounded border flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Mood</strong>\n <span class=\"text-[11px] font-semibold\">{{ flow.moodState.value }}</span>\n </div>\n }\n\n @if (flow.evaluatedAt) {\n <div class=\"text-[10px] text-gray-400 text-right\">{{ flow.evaluatedAt }}</div>\n }\n </div>\n }\n\n <details class=\"mt-1\">\n <summary class=\"text-[10px] text-gray-500 cursor-pointer\">Raw evaluation</summary>\n <pre class=\"text-[10px]\">{{ msg.evaluation | safeJson }}</pre>\n </details>\n </div>\n }\n\n <!-- Tags -->\n @if (msg.tags && msg.tags.length > 0) {\n <div class=\"mt-2 flex flex-wrap gap-1\">\n <strong>Tags:</strong>\n @for (tag of msg.tags; track tag) {\n <span class=\"bg-gray-200 px-1 rounded text-[10px]\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n </details>\n } @empty {\n <div class=\"p-4 text-center text-gray-400\">No messages in state.</div>\n }\n</div>\n\n<style>\n .role-badge {\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.7rem;\n text-transform: uppercase;\n font-weight: bold;\n }\n .role-badge.user { background: #e0f2fe; color: #0369a1; }\n .role-badge.assistant { background: #f0fdf4; color: #15803d; }\n .role-badge.assistantHelper { background: #faf5ff; color: #7e22ce; }\n .role-badge.system { background: #f1f5f9; color: #475569; }\n .border-primary { border-color: var(--p-primary-color, #3b82f6); }\n</style>\n", styles: [".role-badge{padding:2px 6px;border-radius:4px;font-size:.7rem;text-transform:uppercase;font-weight:700}.role-badge.user{background:#e0f2fe;color:#0369a1}.role-badge.assistant{background:#f0fdf4;color:#15803d}.role-badge.assistantHelper{background:#faf5ff;color:#7e22ce}.role-badge.system{background:#f1f5f9;color:#475569}.border-primary{border-color:var(--p-primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }, { kind: "pipe", type: SafeJsonPipe, name: "safeJson" }] }); }
6462
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessagesStateInspectorComponent, isStandalone: true, selector: "dc-messages-state-inspector", inputs: { service: { classPropertyName: "service", publicName: "service", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"messages-state-inspector\">\n @for (msg of messages(); track msg.messageId; let i = $index) {\n <details class=\"message-group mb-2 border rounded p-2\">\n <summary class=\"cursor-pointer font-bold p-1 hover:bg-gray-100 flex justify-between items-center\">\n <span>\n <span class=\"text-xs text-gray-400 mr-2\">#{{ i + 1 }}</span>\n <span [class]=\"'role-badge ' + msg.role\">{{ msg.role }}</span>\n <span class=\"ml-2 text-sm truncate max-w-xs\">{{ msg.content | slice:0:50 }}{{ msg.content.length > 50 ? '...' : '' }}</span>\n </span>\n <span class=\"text-xs text-gray-400\">{{ msg.messageId }}</span>\n </summary>\n\n <div class=\"message-details mt-2 pl-4 border-l-2 border-primary\">\n <!-- Standard Properties -->\n <div class=\"grid grid-cols-2 gap-2 text-sm\">\n <div><strong>Role:</strong> {{ msg.role }}</div>\n <div><strong>Section:</strong> {{ msg.section || 'N/A' }}</div>\n <div><strong>Message ID:</strong> {{ msg.messageId }}</div>\n <div class=\"col-span-2\"><strong>Content:</strong> <pre class=\"whitespace-pre-wrap text-xs bg-gray-50 p-2 mt-1\">{{ msg.content }}</pre></div>\n @if (msg.translation) {\n <div class=\"col-span-2 text-blue-600\"><strong>Translation:</strong> {{ msg.translation }}</div>\n }\n </div>\n\n <!-- JSON Preview of other props -->\n <details class=\"mt-2\">\n <summary class=\"text-xs text-gray-500 cursor-pointer\">All Metadata</summary>\n <pre class=\"text-[10px] bg-slate-800 text-white p-2 rounded mt-1 overflow-auto max-h-64\">{{ msg | safeJson }}</pre>\n </details>\n\n <!-- MultiMessages recursive check -->\n @if (msg.multiMessages && msg.multiMessages.length > 0) {\n <div class=\"mt-3\">\n <strong class=\"text-xs uppercase text-gray-500\">Multi Messages ({{ msg.multiMessages.length }})</strong>\n <div class=\"pl-4 border-l-2 border-dashed border-gray-300\">\n @for (subMsg of msg.multiMessages; track $index) {\n <details class=\"mt-1 border rounded p-1 bg-gray-50\">\n <summary class=\"text-xs cursor-pointer truncate\">Sub: {{ subMsg.text | slice:0:30 }}...</summary>\n <pre class=\"text-[10px] mt-1 p-1 bg-white border overflow-auto\">{{ subMsg | safeJson }}</pre>\n </details>\n }\n </div>\n </div>\n }\n\n <!-- Evaluation -->\n @if (msg.evaluation) {\n <div class=\"mt-2 p-2 bg-yellow-50 rounded text-xs space-y-2\">\n <strong>Evaluation</strong>\n\n @if (msg.evaluation['flow']; as flow) {\n <div class=\"flow-eval space-y-2\">\n @if (flow.goal) {\n <div class=\"p-2 bg-white rounded border\">\n <div class=\"flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Goal</strong>\n @if (flow.goal.deltaPoints != null) {\n <span\n class=\"px-1.5 rounded text-[11px] font-bold\"\n [class.bg-green-100]=\"flow.goal.deltaPoints > 0\"\n [class.text-green-700]=\"flow.goal.deltaPoints > 0\"\n [class.bg-red-100]=\"flow.goal.deltaPoints < 0\"\n [class.text-red-700]=\"flow.goal.deltaPoints < 0\"\n [class.bg-gray-100]=\"flow.goal.deltaPoints === 0\"\n [class.text-gray-600]=\"flow.goal.deltaPoints === 0\">\n {{ flow.goal.deltaPoints > 0 ? '+' : '' }}{{ flow.goal.deltaPoints }} pts\n </span>\n }\n </div>\n @if (flow.goal.previousValue != null && flow.goal.newValue != null) {\n <div class=\"mt-1 text-[11px] text-gray-600\">\n <span>{{ flow.goal.previousValue }}</span>\n <span class=\"mx-1\">\u2192</span>\n <span class=\"font-bold text-gray-900\">{{ flow.goal.newValue }}</span>\n <span class=\"text-gray-400\"> / 100</span>\n </div>\n <div class=\"w-full bg-gray-200 rounded h-1 mt-1 overflow-hidden\">\n <div class=\"bg-primary h-1\" [style.width.%]=\"flow.goal.newValue\" [style.background-color]=\"'#3b82f6'\"></div>\n </div>\n }\n @if (flow.intervention?.message) {\n <div class=\"mt-1 text-[11px] italic text-gray-700\">\"{{ flow.intervention.message }}\"</div>\n }\n </div>\n }\n\n @if (flow.challenges?.length) {\n <div class=\"p-2 bg-white rounded border\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Challenges</strong>\n <ul class=\"mt-1 space-y-0.5\">\n @for (ch of flow.challenges; track ch.name) {\n <li class=\"flex items-center gap-1 text-[11px]\">\n <span [class.text-green-600]=\"ch.value\" [class.text-gray-400]=\"!ch.value\">\n {{ ch.value ? '\u2713' : '\u25CB' }}\n </span>\n <span [class.font-semibold]=\"ch.value\">{{ ch.name }}</span>\n </li>\n }\n </ul>\n </div>\n }\n\n @if (flow.moodState?.value) {\n <div class=\"p-2 bg-white rounded border flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Mood</strong>\n <span class=\"text-[11px] font-semibold\">{{ flow.moodState.value }}</span>\n </div>\n }\n\n @if (flow.evaluatedAt) {\n <div class=\"text-[10px] text-gray-400 text-right\">{{ flow.evaluatedAt }}</div>\n }\n </div>\n }\n\n <details class=\"mt-1\">\n <summary class=\"text-[10px] text-gray-500 cursor-pointer\">Raw evaluation</summary>\n <pre class=\"text-[10px]\">{{ msg.evaluation | safeJson }}</pre>\n </details>\n </div>\n }\n\n <!-- Tags -->\n @if (msg.tags && msg.tags.length > 0) {\n <div class=\"mt-2 flex flex-wrap gap-1\">\n <strong>Tags:</strong>\n @for (tag of msg.tags; track tag) {\n <span class=\"bg-gray-200 px-1 rounded text-[10px]\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n </details>\n } @empty {\n <div class=\"p-4 text-center text-gray-400\">No messages in state.</div>\n }\n</div>\n\n<style>\n .role-badge {\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.7rem;\n text-transform: uppercase;\n font-weight: bold;\n }\n .role-badge.user { background: #e0f2fe; color: #0369a1; }\n .role-badge.assistant { background: #f0fdf4; color: #15803d; }\n .role-badge.assistantHelper { background: #faf5ff; color: #7e22ce; }\n .role-badge.system { background: #f1f5f9; color: #475569; }\n .border-primary { border-color: var(--p-primary-color, #3b82f6); }\n</style>\n", styles: [".role-badge{padding:2px 6px;border-radius:4px;font-size:.7rem;text-transform:uppercase;font-weight:700}.role-badge.user{background:#e0f2fe;color:#0369a1}.role-badge.assistant{background:#f0fdf4;color:#15803d}.role-badge.assistantHelper{background:#faf5ff;color:#7e22ce}.role-badge.system{background:#f1f5f9;color:#475569}.border-primary{border-color:var(--p-primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }, { kind: "pipe", type: SafeJsonPipe, name: "safeJson" }] }); }
6145
6463
  }
6146
6464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessagesStateInspectorComponent, decorators: [{
6147
6465
  type: Component,
6148
6466
  args: [{ selector: 'dc-messages-state-inspector', standalone: true, imports: [CommonModule, SafeJsonPipe], template: "<div class=\"messages-state-inspector\">\n @for (msg of messages(); track msg.messageId; let i = $index) {\n <details class=\"message-group mb-2 border rounded p-2\">\n <summary class=\"cursor-pointer font-bold p-1 hover:bg-gray-100 flex justify-between items-center\">\n <span>\n <span class=\"text-xs text-gray-400 mr-2\">#{{ i + 1 }}</span>\n <span [class]=\"'role-badge ' + msg.role\">{{ msg.role }}</span>\n <span class=\"ml-2 text-sm truncate max-w-xs\">{{ msg.content | slice:0:50 }}{{ msg.content.length > 50 ? '...' : '' }}</span>\n </span>\n <span class=\"text-xs text-gray-400\">{{ msg.messageId }}</span>\n </summary>\n\n <div class=\"message-details mt-2 pl-4 border-l-2 border-primary\">\n <!-- Standard Properties -->\n <div class=\"grid grid-cols-2 gap-2 text-sm\">\n <div><strong>Role:</strong> {{ msg.role }}</div>\n <div><strong>Section:</strong> {{ msg.section || 'N/A' }}</div>\n <div><strong>Message ID:</strong> {{ msg.messageId }}</div>\n <div class=\"col-span-2\"><strong>Content:</strong> <pre class=\"whitespace-pre-wrap text-xs bg-gray-50 p-2 mt-1\">{{ msg.content }}</pre></div>\n @if (msg.translation) {\n <div class=\"col-span-2 text-blue-600\"><strong>Translation:</strong> {{ msg.translation }}</div>\n }\n </div>\n\n <!-- JSON Preview of other props -->\n <details class=\"mt-2\">\n <summary class=\"text-xs text-gray-500 cursor-pointer\">All Metadata</summary>\n <pre class=\"text-[10px] bg-slate-800 text-white p-2 rounded mt-1 overflow-auto max-h-64\">{{ msg | safeJson }}</pre>\n </details>\n\n <!-- MultiMessages recursive check -->\n @if (msg.multiMessages && msg.multiMessages.length > 0) {\n <div class=\"mt-3\">\n <strong class=\"text-xs uppercase text-gray-500\">Multi Messages ({{ msg.multiMessages.length }})</strong>\n <div class=\"pl-4 border-l-2 border-dashed border-gray-300\">\n @for (subMsg of msg.multiMessages; track $index) {\n <details class=\"mt-1 border rounded p-1 bg-gray-50\">\n <summary class=\"text-xs cursor-pointer truncate\">Sub: {{ subMsg.text | slice:0:30 }}...</summary>\n <pre class=\"text-[10px] mt-1 p-1 bg-white border overflow-auto\">{{ subMsg | safeJson }}</pre>\n </details>\n }\n </div>\n </div>\n }\n\n <!-- Evaluation -->\n @if (msg.evaluation) {\n <div class=\"mt-2 p-2 bg-yellow-50 rounded text-xs space-y-2\">\n <strong>Evaluation</strong>\n\n @if (msg.evaluation['flow']; as flow) {\n <div class=\"flow-eval space-y-2\">\n @if (flow.goal) {\n <div class=\"p-2 bg-white rounded border\">\n <div class=\"flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Goal</strong>\n @if (flow.goal.deltaPoints != null) {\n <span\n class=\"px-1.5 rounded text-[11px] font-bold\"\n [class.bg-green-100]=\"flow.goal.deltaPoints > 0\"\n [class.text-green-700]=\"flow.goal.deltaPoints > 0\"\n [class.bg-red-100]=\"flow.goal.deltaPoints < 0\"\n [class.text-red-700]=\"flow.goal.deltaPoints < 0\"\n [class.bg-gray-100]=\"flow.goal.deltaPoints === 0\"\n [class.text-gray-600]=\"flow.goal.deltaPoints === 0\">\n {{ flow.goal.deltaPoints > 0 ? '+' : '' }}{{ flow.goal.deltaPoints }} pts\n </span>\n }\n </div>\n @if (flow.goal.previousValue != null && flow.goal.newValue != null) {\n <div class=\"mt-1 text-[11px] text-gray-600\">\n <span>{{ flow.goal.previousValue }}</span>\n <span class=\"mx-1\">\u2192</span>\n <span class=\"font-bold text-gray-900\">{{ flow.goal.newValue }}</span>\n <span class=\"text-gray-400\"> / 100</span>\n </div>\n <div class=\"w-full bg-gray-200 rounded h-1 mt-1 overflow-hidden\">\n <div class=\"bg-primary h-1\" [style.width.%]=\"flow.goal.newValue\" [style.background-color]=\"'#3b82f6'\"></div>\n </div>\n }\n @if (flow.intervention?.message) {\n <div class=\"mt-1 text-[11px] italic text-gray-700\">\"{{ flow.intervention.message }}\"</div>\n }\n </div>\n }\n\n @if (flow.challenges?.length) {\n <div class=\"p-2 bg-white rounded border\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Challenges</strong>\n <ul class=\"mt-1 space-y-0.5\">\n @for (ch of flow.challenges; track ch.name) {\n <li class=\"flex items-center gap-1 text-[11px]\">\n <span [class.text-green-600]=\"ch.value\" [class.text-gray-400]=\"!ch.value\">\n {{ ch.value ? '\u2713' : '\u25CB' }}\n </span>\n <span [class.font-semibold]=\"ch.value\">{{ ch.name }}</span>\n </li>\n }\n </ul>\n </div>\n }\n\n @if (flow.moodState?.value) {\n <div class=\"p-2 bg-white rounded border flex items-center justify-between\">\n <strong class=\"text-[11px] uppercase text-gray-600\">Mood</strong>\n <span class=\"text-[11px] font-semibold\">{{ flow.moodState.value }}</span>\n </div>\n }\n\n @if (flow.evaluatedAt) {\n <div class=\"text-[10px] text-gray-400 text-right\">{{ flow.evaluatedAt }}</div>\n }\n </div>\n }\n\n <details class=\"mt-1\">\n <summary class=\"text-[10px] text-gray-500 cursor-pointer\">Raw evaluation</summary>\n <pre class=\"text-[10px]\">{{ msg.evaluation | safeJson }}</pre>\n </details>\n </div>\n }\n\n <!-- Tags -->\n @if (msg.tags && msg.tags.length > 0) {\n <div class=\"mt-2 flex flex-wrap gap-1\">\n <strong>Tags:</strong>\n @for (tag of msg.tags; track tag) {\n <span class=\"bg-gray-200 px-1 rounded text-[10px]\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n </details>\n } @empty {\n <div class=\"p-4 text-center text-gray-400\">No messages in state.</div>\n }\n</div>\n\n<style>\n .role-badge {\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.7rem;\n text-transform: uppercase;\n font-weight: bold;\n }\n .role-badge.user { background: #e0f2fe; color: #0369a1; }\n .role-badge.assistant { background: #f0fdf4; color: #15803d; }\n .role-badge.assistantHelper { background: #faf5ff; color: #7e22ce; }\n .role-badge.system { background: #f1f5f9; color: #475569; }\n .border-primary { border-color: var(--p-primary-color, #3b82f6); }\n</style>\n" }]
6149
- }] });
6467
+ }], propDecorators: { service: [{ type: i0.Input, args: [{ isSignal: true, alias: "service", required: false }] }] } });
6150
6468
 
6151
6469
  class DcAgentCardDetailComponent extends EntityBaseDetailComponent {
6152
6470
  constructor() {
@@ -6186,11 +6504,11 @@ class DcAgentCardDetailComponent extends EntityBaseDetailComponent {
6186
6504
  this.selectedMotion.set(null);
6187
6505
  }
6188
6506
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6189
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcAgentCardDetailComponent, isStandalone: true, selector: "dc-agent-card-detail", inputs: { entityData: { classPropertyName: "entityData", publicName: "entityData", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "@if (isLoading()) {\n<div class=\"loading-state\">\n <p-progressSpinner strokeWidth=\"4\" />\n</div>\n} @else if (entity()) {\n<div class=\"preview-container\">\n\n <!-- Banner -->\n @if (bannerUrl) {\n <div class=\"preview-banner\">\n <img class=\"banner-image\" [src]=\"bannerUrl\" alt=\"banner\" />\n </div>\n }\n\n <!-- Header: image + name + meta -->\n <div class=\"preview-header\" [class.has-banner]=\"bannerUrl\">\n @if (entity()?.assets?.image?.url) {\n <img class=\"preview-image\" [src]=\"entity()!.assets!.image!.url\" [alt]=\"characterData?.name || entity()?.name\" />\n }\n <div class=\"preview-header-info\">\n <h2 class=\"preview-name\">{{ characterData?.name || entity()?.name }}</h2>\n\n <div class=\"preview-meta-tags\">\n @if (entity()?.lang) {\n <p-tag [value]=\"entity()!.lang!\" severity=\"secondary\" icon=\"pi pi-flag\" />\n }\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-circle-fill\" />\n }\n @if (characterData?.gender) {\n <p-tag [value]=\"characterData!.gender\" severity=\"secondary\" icon=\"pi pi-user\" />\n }\n @if (entity()?.metaApp?.takenCount) {\n <p-tag [value]=\"'Taken: ' + entity()!.metaApp!.takenCount\" severity=\"success\" icon=\"pi pi-users\" />\n }\n @if (learnableInfo?.level) {\n <p-tag [value]=\"'Level ' + learnableInfo.level\" severity=\"warn\" icon=\"pi pi-star\" />\n }\n @if (learnableInfo?.takenCount) {\n <p-tag [value]=\"'Played: ' + learnableInfo.takenCount\" severity=\"secondary\" icon=\"pi pi-play\" />\n }\n </div>\n\n @if (characterData?.tags?.length) {\n <div class=\"preview-tags\">\n @for (tag of characterData!.tags; track tag) {\n <p-tag [value]=\"tag\" severity=\"secondary\" />\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Entity-level description -->\n @if (entity()?.description) {\n <div class=\"preview-entity-description\">\n <p>{{ entity()!.description }}</p>\n </div>\n }\n\n <!-- Motion Videos -->\n @if (motions.length) {\n <div class=\"preview-section motions-top-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-video\"></i> Motion Videos ({{ motions.length }})</h4>\n <div class=\"motions-grid\">\n @for (motion of motions; track $index) {\n @if (motion.metadata?.moodState || motion.metadata?.event) {\n <button class=\"motion-chip\" (click)=\"openMotionVideo(motion)\">\n <i class=\"pi pi-play-circle\"></i>\n <span>{{ motion.metadata?.moodState || motion.metadata?.event }}</span>\n </button>\n }\n }\n </div>\n </div>\n }\n\n <p-divider />\n\n @if (characterData?.hook) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-bookmark\"></i> Hook</h4>\n <p>{{ characterData!.hook }}</p>\n </div>\n }\n\n @if (characterData?.instructions) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-info-circle\"></i> Instructions</h4>\n <p>{{ characterData!.instructions }}</p>\n </div>\n }\n\n @if (characterData?.scenario) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-map\"></i> Scenario</h4>\n <p>{{ characterData!.scenario }}</p>\n </div>\n }\n\n @if (characterData?.creator_notes) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-file\"></i> Creator Notes</h4>\n <p>{{ characterData!.creator_notes }}</p>\n </div>\n }\n\n @if (characterData?.description) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-align-left\"></i> Description</h4>\n <p>{{ characterData!.description }}</p>\n </div>\n }\n\n @if (characterData?.greetings?.length) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-comments\"></i> Greetings</h4>\n @for (greeting of characterData!.greetings; track $index) {\n <p class=\"greeting-item\"><i class=\"pi pi-chevron-right\"></i> {{ greeting }}</p>\n }\n </div>\n }\n\n\n @if (persona) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-id-card\"></i> Persona</h4>\n <div class=\"persona-grid\">\n @if (persona.identity) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Identity</span>\n <span>{{ persona.identity }}</span>\n </div>\n }\n @if (persona.physical) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Physical</span>\n <span>{{ persona.physical }}</span>\n </div>\n }\n @if (persona.personality) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Personality</span>\n <span>{{ persona.personality }}</span>\n </div>\n }\n @if (persona.communication) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Communication</span>\n <span>{{ persona.communication }}</span>\n </div>\n }\n @if (persona.background) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Background</span>\n <span>{{ persona.background }}</span>\n </div>\n }\n @if (persona.psychology) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Psychology</span>\n <span>{{ persona.psychology }}</span>\n </div>\n }\n @if (persona.capabilities) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Capabilities</span>\n <span>{{ persona.capabilities }}</span>\n </div>\n }\n @if (persona.social) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Social</span>\n <span>{{ persona.social }}</span>\n </div>\n }\n @if (persona.preferences) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Preferences</span>\n <span>{{ persona.preferences }}</span>\n </div>\n }\n @if (persona.situation) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Situation</span>\n <span>{{ persona.situation }}</span>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Conversation Settings -->\n @if (entity()?.conversationSettings) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Conversation Settings</h4>\n <div class=\"settings-chips\">\n @if (entity()?.conversationSettings?.conversationType) {\n <p-tag [value]=\"entity()!.conversationSettings!.conversationType!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.conversationSettings?.textEngine) {\n <p-tag [value]=\"entity()!.conversationSettings!.textEngine!\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n @if (entity()?.conversationSettings?.autoStart) {\n <p-tag value=\"Auto Start\" severity=\"success\" icon=\"pi pi-play\" />\n }\n @if (entity()?.conversationSettings?.userMustStart) {\n <p-tag value=\"User Starts\" severity=\"warn\" icon=\"pi pi-user\" />\n }\n </div>\n </div>\n }\n\n <!-- Voices -->\n @if (entity()?.conversationSettings?.mainVoice?.voice) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-volume-up\"></i> Voices</h4>\n <div class=\"voice-info\">\n <p><span class=\"field-label\">Main:</span> {{ entity()!.conversationSettings!.mainVoice!.voice }}</p>\n @if (entity()?.conversationSettings?.secondaryVoice?.voice) {\n <p><span class=\"field-label\">Secondary:</span> {{ entity()!.conversationSettings!.secondaryVoice!.voice }}</p>\n }\n </div>\n </div>\n }\n\n</div>\n}\n\n@if (entity()?.manageable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Manageable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.manageable?.isPublic) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if (entity()?.learnable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Learnable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.learnable?.level) {\n <p-tag [value]=\"'Level ' + entity()!.learnable!.level!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.learnable?.tags) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if(entity()?.conversationFlow) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-tags\"></i> Conversation Flow</h4>\n\n @if (entity()?.conversationFlow?.moodState) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-face-smile\"></i> Mood State</h5>\n <div class=\"settings-chips\">\n @if (entity()!.conversationFlow!.moodState!.enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"danger\" icon=\"pi pi-times\" />\n }\n @if (entity()!.conversationFlow!.moodState!.useAssetStatesOnly) {\n <p-tag value=\"Asset States Only\" severity=\"warn\" icon=\"pi pi-images\" />\n }\n </div>\n @if (entity()!.conversationFlow!.moodState!.detectableStates?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.5rem;\">\n @for (state of entity()!.conversationFlow!.moodState!.detectableStates; track $index) {\n <p-tag [value]=\"state\" severity=\"info\" icon=\"pi pi-tag\" />\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.dynamicConditions?.length) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-sliders-v\"></i> Dynamic Conditions</h5>\n @for (condition of entity()!.conversationFlow!.dynamicConditions!; track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <div class=\"settings-chips\">\n <p-tag [value]=\"condition.what\" severity=\"info\" icon=\"pi pi-filter\" />\n <p-tag [value]=\"condition.when\" severity=\"secondary\" />\n <p-tag [value]=\"condition.value + ''\" severity=\"warn\" />\n </div>\n @if (condition.do?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @for (action of condition.do!; track $index) {\n <p-tag [value]=\"action.actionType\" severity=\"contrast\" icon=\"pi pi-bolt\" />\n @if (action.prompt) {\n <span class=\"field-label\" style=\"font-size: 0.75rem;\">{{ action.prompt | slice:0:60 }}\u2026</span>\n }\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.triggerTasks) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-bolt\"></i> Trigger Tasks</h5>\n @for (entry of objectEntries(entity()!.conversationFlow!.triggerTasks!); track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <span class=\"field-label\">{{ entry[0] }}</span>\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @if (entry[1].enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"secondary\" icon=\"pi pi-times\" />\n }\n @if (entry[1].disableFeature) {\n <p-tag value=\"Feature Disabled\" severity=\"danger\" icon=\"pi pi-ban\" />\n }\n </div>\n @if (entry[1].task) {\n <p style=\"font-size: 0.8rem; margin: 0.25rem 0 0;\">{{ entry[1].task | slice:0:120 }}\u2026</p>\n }\n </div>\n }\n </div>\n }\n</div>\n}\n\n<!-- Video Dialog -->\n<p-dialog\n [visible]=\"videoDialogVisible()\"\n (visibleChange)=\"$event ? null : closeMotionVideo()\"\n [header]=\"selectedMotion()?.metadata?.moodState || selectedMotion()?.metadata?.event || 'Video'\"\n [modal]=\"true\"\n [dismissableMask]=\"true\"\n [closable]=\"true\"\n styleClass=\"video-dialog\"\n [style]=\"{ width: '560px' }\">\n\n @if (selectedMotion()?.url) {\n <video\n class=\"motion-video\"\n [src]=\"selectedMotion().url\"\n controls\n autoplay>\n </video>\n }\n</p-dialog>\n", styles: [".preview-container{padding:.5rem}.preview-banner{width:100%;height:210px;overflow:hidden;border-radius:8px;margin-bottom:0}.banner-image{width:100%;height:100%;object-fit:cover;display:block}.preview-header{display:flex;gap:1.5rem;align-items:flex-start;padding:1rem 0 .5rem}.preview-header.has-banner{padding-top:.75rem}.preview-image{width:120px;height:170px;object-fit:cover;border-radius:8px;flex-shrink:0;box-shadow:0 2px 8px #00000026}.preview-header-info{display:flex;flex-direction:column;gap:.5rem}.preview-name{margin:0 0 .25rem;font-size:1.5rem;font-weight:700}.preview-meta-tags{display:flex;flex-wrap:wrap;gap:.35rem}.preview-tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.preview-entity-description{color:var(--p-text-muted-color);font-size:.9rem;margin:.5rem 0}.preview-entity-description p{margin:0}.preview-section{margin-bottom:1rem}.section-title{margin:0 0 .5rem;font-size:.95rem;font-weight:600;color:var(--p-text-muted-color);display:flex;align-items:center;gap:.4rem}.greeting-item{margin:.25rem 0;padding-left:.5rem;border-left:2px solid var(--p-primary-color)}.first-message{background:var(--p-surface-100);border-radius:6px;padding:.75rem;font-style:italic}.persona-grid{display:flex;flex-direction:column;gap:.5rem}.persona-field{display:flex;flex-direction:column;gap:.15rem}.field-label{font-size:.8rem;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.05em}.motions-top-section{margin-top:.75rem;margin-bottom:0}.motions-grid{display:flex;flex-wrap:wrap;gap:.4rem}.motion-chip{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .65rem;border-radius:20px;border:1px solid var(--p-surface-300);background:var(--p-surface-100);color:var(--p-text-color);font-size:.8rem;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,transform .1s;text-transform:capitalize}.motion-chip i{font-size:.85rem;color:var(--p-primary-color)}.motion-chip:hover{background:var(--p-primary-50, #f0f4ff);border-color:var(--p-primary-color);transform:translateY(-1px)}.motion-chip:active{transform:translateY(0)}.motion-video{width:100%;border-radius:6px;display:block}.settings-chips{display:flex;flex-wrap:wrap;gap:.4rem}.voice-info{display:flex;flex-direction:column;gap:.25rem}.loading-state{display:flex;justify-content:center;padding:2rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CardModule }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i1$4.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: DividerModule }, { kind: "component", type: i2$3.Divider, selector: "p-divider", inputs: ["styleClass", "layout", "type", "align"] }, { kind: "ngmodule", type: ProgressSpinnerModule }, { kind: "component", type: i2$4.ProgressSpinner, selector: "p-progressSpinner, p-progress-spinner, p-progressspinner", inputs: ["styleClass", "strokeWidth", "fill", "animationDuration", "ariaLabel"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6507
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcAgentCardDetailComponent, isStandalone: true, selector: "dc-agent-card-detail", inputs: { entityData: { classPropertyName: "entityData", publicName: "entityData", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "@if (isLoading()) {\n<div class=\"loading-state\">\n <p-progressSpinner strokeWidth=\"4\" />\n</div>\n} @else if (entity()) {\n<div class=\"preview-container\">\n\n <!-- Banner -->\n @if (bannerUrl) {\n <div class=\"preview-banner\">\n <img class=\"banner-image\" [src]=\"bannerUrl\" alt=\"banner\" />\n </div>\n }\n\n <!-- Header: image + name + meta -->\n <div class=\"preview-header\" [class.has-banner]=\"bannerUrl\">\n @if (entity()?.assets?.image?.url) {\n <img class=\"preview-image\" [src]=\"entity()!.assets!.image!.url\" [alt]=\"characterData?.name || entity()?.name\" />\n }\n <div class=\"preview-header-info\">\n <h2 class=\"preview-name\">{{ characterData?.name || entity()?.name }}</h2>\n\n <div class=\"preview-meta-tags\">\n @if (entity()?.lang) {\n <p-tag [value]=\"entity()!.lang!\" severity=\"secondary\" icon=\"pi pi-flag\" />\n }\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-circle-fill\" />\n }\n @if (characterData?.gender) {\n <p-tag [value]=\"characterData!.gender\" severity=\"secondary\" icon=\"pi pi-user\" />\n }\n @if (entity()?.metaApp?.takenCount) {\n <p-tag [value]=\"'Taken: ' + entity()!.metaApp!.takenCount\" severity=\"success\" icon=\"pi pi-users\" />\n }\n @if (learnableInfo?.level) {\n <p-tag [value]=\"'Level ' + learnableInfo.level\" severity=\"warn\" icon=\"pi pi-star\" />\n }\n @if (learnableInfo?.takenCount) {\n <p-tag [value]=\"'Played: ' + learnableInfo.takenCount\" severity=\"secondary\" icon=\"pi pi-play\" />\n }\n </div>\n\n @if (characterData?.tags?.length) {\n <div class=\"preview-tags\">\n @for (tag of characterData!.tags; track tag) {\n <p-tag [value]=\"tag\" severity=\"secondary\" />\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Entity-level description -->\n @if (entity()?.description) {\n <div class=\"preview-entity-description\">\n <p>{{ entity()!.description }}</p>\n </div>\n }\n\n <!-- Motion Videos -->\n @if (motions.length) {\n <div class=\"preview-section motions-top-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-video\"></i> Motion Videos ({{ motions.length }})</h4>\n <div class=\"motions-grid\">\n @for (motion of motions; track $index) {\n @if (motion.metadata?.moodState || motion.metadata?.event) {\n <button class=\"motion-chip\" (click)=\"openMotionVideo(motion)\">\n <i class=\"pi pi-play-circle\"></i>\n <span>{{ motion.metadata?.moodState || motion.metadata?.event }}</span>\n </button>\n }\n }\n </div>\n </div>\n }\n\n <p-divider />\n\n @if (characterData?.hook) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-bookmark\"></i> Hook</h4>\n <p>{{ characterData!.hook }}</p>\n </div>\n }\n\n @if (characterData?.instructions) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-info-circle\"></i> Instructions</h4>\n <p>{{ characterData!.instructions }}</p>\n </div>\n }\n\n @if (characterData?.scenario) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-map\"></i> Scenario</h4>\n <p>{{ characterData!.scenario }}</p>\n </div>\n }\n\n @if (characterData?.creator_notes) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-file\"></i> Creator Notes</h4>\n <p>{{ characterData!.creator_notes }}</p>\n </div>\n }\n\n @if (characterData?.description) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-align-left\"></i> Description</h4>\n <p>{{ characterData!.description }}</p>\n </div>\n }\n\n @if (characterData?.greetings?.length) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-comments\"></i> Greetings</h4>\n @for (greeting of characterData!.greetings; track $index) {\n <p class=\"greeting-item\"><i class=\"pi pi-chevron-right\"></i> {{ greeting }}</p>\n }\n </div>\n }\n\n\n @if (persona) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-id-card\"></i> Persona</h4>\n <div class=\"persona-grid\">\n @if (persona.identity) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Identity</span>\n <span>{{ persona.identity }}</span>\n </div>\n }\n @if (persona.physical) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Physical</span>\n <span>{{ persona.physical }}</span>\n </div>\n }\n @if (persona.personality) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Personality</span>\n <span>{{ persona.personality }}</span>\n </div>\n }\n @if (persona.communication) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Communication</span>\n <span>{{ persona.communication }}</span>\n </div>\n }\n @if (persona.background) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Background</span>\n <span>{{ persona.background }}</span>\n </div>\n }\n @if (persona.psychology) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Psychology</span>\n <span>{{ persona.psychology }}</span>\n </div>\n }\n @if (persona.capabilities) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Capabilities</span>\n <span>{{ persona.capabilities }}</span>\n </div>\n }\n @if (persona.social) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Social</span>\n <span>{{ persona.social }}</span>\n </div>\n }\n @if (persona.preferences) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Preferences</span>\n <span>{{ persona.preferences }}</span>\n </div>\n }\n @if (persona.situation) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Situation</span>\n <span>{{ persona.situation }}</span>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Conversation Settings -->\n @if (entity()?.conversationSettings) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Conversation Settings</h4>\n <div class=\"settings-chips\">\n @if (entity()?.conversationSettings?.conversationType) {\n <p-tag [value]=\"entity()!.conversationSettings!.conversationType!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.conversationSettings?.textEngine) {\n <p-tag [value]=\"entity()!.conversationSettings!.textEngine!\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n @if (entity()?.conversationSettings?.autoStart) {\n <p-tag value=\"Auto Start\" severity=\"success\" icon=\"pi pi-play\" />\n }\n @if (entity()?.conversationSettings?.userMustStart) {\n <p-tag value=\"User Starts\" severity=\"warn\" icon=\"pi pi-user\" />\n }\n </div>\n </div>\n }\n\n <!-- Voices -->\n @if (entity()?.voice?.main?.voice || entity()?.conversationSettings?.mainVoice?.voice) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-volume-up\"></i> Voices</h4>\n <div class=\"voice-info\">\n <p><span class=\"field-label\">Main:</span> {{ entity()?.voice?.main?.voice || entity()?.conversationSettings?.mainVoice?.voice }}</p>\n @if (entity()?.voice?.secondary?.voice || entity()?.conversationSettings?.secondaryVoice?.voice) {\n <p><span class=\"field-label\">Secondary:</span> {{ entity()?.voice?.secondary?.voice || entity()?.conversationSettings?.secondaryVoice?.voice }}</p>\n }\n </div>\n </div>\n }\n\n</div>\n}\n\n@if (entity()?.manageable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Manageable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.manageable?.isPublic) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if (entity()?.learnable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Learnable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.learnable?.level) {\n <p-tag [value]=\"'Level ' + entity()!.learnable!.level!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.learnable?.tags) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if(entity()?.conversationFlow) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-tags\"></i> Conversation Flow</h4>\n\n @if (entity()?.conversationFlow?.moodState) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-face-smile\"></i> Mood State</h5>\n <div class=\"settings-chips\">\n @if (entity()!.conversationFlow!.moodState!.enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"danger\" icon=\"pi pi-times\" />\n }\n @if (entity()!.conversationFlow!.moodState!.useAssetStatesOnly) {\n <p-tag value=\"Asset States Only\" severity=\"warn\" icon=\"pi pi-images\" />\n }\n </div>\n @if (entity()!.conversationFlow!.moodState!.detectableStates?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.5rem;\">\n @for (state of entity()!.conversationFlow!.moodState!.detectableStates; track $index) {\n <p-tag [value]=\"state\" severity=\"info\" icon=\"pi pi-tag\" />\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.dynamicConditions?.length) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-sliders-v\"></i> Dynamic Conditions</h5>\n @for (condition of entity()!.conversationFlow!.dynamicConditions!; track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <div class=\"settings-chips\">\n <p-tag [value]=\"condition.what\" severity=\"info\" icon=\"pi pi-filter\" />\n <p-tag [value]=\"condition.when\" severity=\"secondary\" />\n <p-tag [value]=\"condition.value + ''\" severity=\"warn\" />\n </div>\n @if (condition.do?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @for (action of condition.do!; track $index) {\n <p-tag [value]=\"action.actionType\" severity=\"contrast\" icon=\"pi pi-bolt\" />\n @if (action.prompt) {\n <span class=\"field-label\" style=\"font-size: 0.75rem;\">{{ action.prompt | slice:0:60 }}\u2026</span>\n }\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.triggerTasks) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-bolt\"></i> Trigger Tasks</h5>\n @for (entry of objectEntries(entity()!.conversationFlow!.triggerTasks!); track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <span class=\"field-label\">{{ entry[0] }}</span>\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @if (entry[1].enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"secondary\" icon=\"pi pi-times\" />\n }\n @if (entry[1].disableFeature) {\n <p-tag value=\"Feature Disabled\" severity=\"danger\" icon=\"pi pi-ban\" />\n }\n </div>\n @if (entry[1].task) {\n <p style=\"font-size: 0.8rem; margin: 0.25rem 0 0;\">{{ entry[1].task | slice:0:120 }}\u2026</p>\n }\n </div>\n }\n </div>\n }\n</div>\n}\n\n<!-- Video Dialog -->\n<p-dialog\n [visible]=\"videoDialogVisible()\"\n (visibleChange)=\"$event ? null : closeMotionVideo()\"\n [header]=\"selectedMotion()?.metadata?.moodState || selectedMotion()?.metadata?.event || 'Video'\"\n [modal]=\"true\"\n [dismissableMask]=\"true\"\n [closable]=\"true\"\n styleClass=\"video-dialog\"\n [style]=\"{ width: '560px' }\">\n\n @if (selectedMotion()?.url) {\n <video\n class=\"motion-video\"\n [src]=\"selectedMotion().url\"\n controls\n autoplay>\n </video>\n }\n</p-dialog>\n", styles: [".preview-container{padding:.5rem}.preview-banner{width:100%;height:210px;overflow:hidden;border-radius:8px;margin-bottom:0}.banner-image{width:100%;height:100%;object-fit:cover;display:block}.preview-header{display:flex;gap:1.5rem;align-items:flex-start;padding:1rem 0 .5rem}.preview-header.has-banner{padding-top:.75rem}.preview-image{width:120px;height:170px;object-fit:cover;border-radius:8px;flex-shrink:0;box-shadow:0 2px 8px #00000026}.preview-header-info{display:flex;flex-direction:column;gap:.5rem}.preview-name{margin:0 0 .25rem;font-size:1.5rem;font-weight:700}.preview-meta-tags{display:flex;flex-wrap:wrap;gap:.35rem}.preview-tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.preview-entity-description{color:var(--p-text-muted-color);font-size:.9rem;margin:.5rem 0}.preview-entity-description p{margin:0}.preview-section{margin-bottom:1rem}.section-title{margin:0 0 .5rem;font-size:.95rem;font-weight:600;color:var(--p-text-muted-color);display:flex;align-items:center;gap:.4rem}.greeting-item{margin:.25rem 0;padding-left:.5rem;border-left:2px solid var(--p-primary-color)}.first-message{background:var(--p-surface-100);border-radius:6px;padding:.75rem;font-style:italic}.persona-grid{display:flex;flex-direction:column;gap:.5rem}.persona-field{display:flex;flex-direction:column;gap:.15rem}.field-label{font-size:.8rem;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.05em}.motions-top-section{margin-top:.75rem;margin-bottom:0}.motions-grid{display:flex;flex-wrap:wrap;gap:.4rem}.motion-chip{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .65rem;border-radius:20px;border:1px solid var(--p-surface-300);background:var(--p-surface-100);color:var(--p-text-color);font-size:.8rem;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,transform .1s;text-transform:capitalize}.motion-chip i{font-size:.85rem;color:var(--p-primary-color)}.motion-chip:hover{background:var(--p-primary-50, #f0f4ff);border-color:var(--p-primary-color);transform:translateY(-1px)}.motion-chip:active{transform:translateY(0)}.motion-video{width:100%;border-radius:6px;display:block}.settings-chips{display:flex;flex-wrap:wrap;gap:.4rem}.voice-info{display:flex;flex-direction:column;gap:.25rem}.loading-state{display:flex;justify-content:center;padding:2rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CardModule }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i1$4.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: DividerModule }, { kind: "component", type: i2$3.Divider, selector: "p-divider", inputs: ["styleClass", "layout", "type", "align"] }, { kind: "ngmodule", type: ProgressSpinnerModule }, { kind: "component", type: i2$4.ProgressSpinner, selector: "p-progressSpinner, p-progress-spinner, p-progressspinner", inputs: ["styleClass", "strokeWidth", "fill", "animationDuration", "ariaLabel"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6190
6508
  }
6191
6509
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardDetailComponent, decorators: [{
6192
6510
  type: Component,
6193
- args: [{ selector: 'dc-agent-card-detail', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, CardModule, TagModule, DividerModule, ProgressSpinnerModule, DialogModule, ButtonModule], template: "@if (isLoading()) {\n<div class=\"loading-state\">\n <p-progressSpinner strokeWidth=\"4\" />\n</div>\n} @else if (entity()) {\n<div class=\"preview-container\">\n\n <!-- Banner -->\n @if (bannerUrl) {\n <div class=\"preview-banner\">\n <img class=\"banner-image\" [src]=\"bannerUrl\" alt=\"banner\" />\n </div>\n }\n\n <!-- Header: image + name + meta -->\n <div class=\"preview-header\" [class.has-banner]=\"bannerUrl\">\n @if (entity()?.assets?.image?.url) {\n <img class=\"preview-image\" [src]=\"entity()!.assets!.image!.url\" [alt]=\"characterData?.name || entity()?.name\" />\n }\n <div class=\"preview-header-info\">\n <h2 class=\"preview-name\">{{ characterData?.name || entity()?.name }}</h2>\n\n <div class=\"preview-meta-tags\">\n @if (entity()?.lang) {\n <p-tag [value]=\"entity()!.lang!\" severity=\"secondary\" icon=\"pi pi-flag\" />\n }\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-circle-fill\" />\n }\n @if (characterData?.gender) {\n <p-tag [value]=\"characterData!.gender\" severity=\"secondary\" icon=\"pi pi-user\" />\n }\n @if (entity()?.metaApp?.takenCount) {\n <p-tag [value]=\"'Taken: ' + entity()!.metaApp!.takenCount\" severity=\"success\" icon=\"pi pi-users\" />\n }\n @if (learnableInfo?.level) {\n <p-tag [value]=\"'Level ' + learnableInfo.level\" severity=\"warn\" icon=\"pi pi-star\" />\n }\n @if (learnableInfo?.takenCount) {\n <p-tag [value]=\"'Played: ' + learnableInfo.takenCount\" severity=\"secondary\" icon=\"pi pi-play\" />\n }\n </div>\n\n @if (characterData?.tags?.length) {\n <div class=\"preview-tags\">\n @for (tag of characterData!.tags; track tag) {\n <p-tag [value]=\"tag\" severity=\"secondary\" />\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Entity-level description -->\n @if (entity()?.description) {\n <div class=\"preview-entity-description\">\n <p>{{ entity()!.description }}</p>\n </div>\n }\n\n <!-- Motion Videos -->\n @if (motions.length) {\n <div class=\"preview-section motions-top-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-video\"></i> Motion Videos ({{ motions.length }})</h4>\n <div class=\"motions-grid\">\n @for (motion of motions; track $index) {\n @if (motion.metadata?.moodState || motion.metadata?.event) {\n <button class=\"motion-chip\" (click)=\"openMotionVideo(motion)\">\n <i class=\"pi pi-play-circle\"></i>\n <span>{{ motion.metadata?.moodState || motion.metadata?.event }}</span>\n </button>\n }\n }\n </div>\n </div>\n }\n\n <p-divider />\n\n @if (characterData?.hook) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-bookmark\"></i> Hook</h4>\n <p>{{ characterData!.hook }}</p>\n </div>\n }\n\n @if (characterData?.instructions) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-info-circle\"></i> Instructions</h4>\n <p>{{ characterData!.instructions }}</p>\n </div>\n }\n\n @if (characterData?.scenario) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-map\"></i> Scenario</h4>\n <p>{{ characterData!.scenario }}</p>\n </div>\n }\n\n @if (characterData?.creator_notes) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-file\"></i> Creator Notes</h4>\n <p>{{ characterData!.creator_notes }}</p>\n </div>\n }\n\n @if (characterData?.description) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-align-left\"></i> Description</h4>\n <p>{{ characterData!.description }}</p>\n </div>\n }\n\n @if (characterData?.greetings?.length) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-comments\"></i> Greetings</h4>\n @for (greeting of characterData!.greetings; track $index) {\n <p class=\"greeting-item\"><i class=\"pi pi-chevron-right\"></i> {{ greeting }}</p>\n }\n </div>\n }\n\n\n @if (persona) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-id-card\"></i> Persona</h4>\n <div class=\"persona-grid\">\n @if (persona.identity) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Identity</span>\n <span>{{ persona.identity }}</span>\n </div>\n }\n @if (persona.physical) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Physical</span>\n <span>{{ persona.physical }}</span>\n </div>\n }\n @if (persona.personality) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Personality</span>\n <span>{{ persona.personality }}</span>\n </div>\n }\n @if (persona.communication) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Communication</span>\n <span>{{ persona.communication }}</span>\n </div>\n }\n @if (persona.background) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Background</span>\n <span>{{ persona.background }}</span>\n </div>\n }\n @if (persona.psychology) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Psychology</span>\n <span>{{ persona.psychology }}</span>\n </div>\n }\n @if (persona.capabilities) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Capabilities</span>\n <span>{{ persona.capabilities }}</span>\n </div>\n }\n @if (persona.social) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Social</span>\n <span>{{ persona.social }}</span>\n </div>\n }\n @if (persona.preferences) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Preferences</span>\n <span>{{ persona.preferences }}</span>\n </div>\n }\n @if (persona.situation) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Situation</span>\n <span>{{ persona.situation }}</span>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Conversation Settings -->\n @if (entity()?.conversationSettings) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Conversation Settings</h4>\n <div class=\"settings-chips\">\n @if (entity()?.conversationSettings?.conversationType) {\n <p-tag [value]=\"entity()!.conversationSettings!.conversationType!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.conversationSettings?.textEngine) {\n <p-tag [value]=\"entity()!.conversationSettings!.textEngine!\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n @if (entity()?.conversationSettings?.autoStart) {\n <p-tag value=\"Auto Start\" severity=\"success\" icon=\"pi pi-play\" />\n }\n @if (entity()?.conversationSettings?.userMustStart) {\n <p-tag value=\"User Starts\" severity=\"warn\" icon=\"pi pi-user\" />\n }\n </div>\n </div>\n }\n\n <!-- Voices -->\n @if (entity()?.conversationSettings?.mainVoice?.voice) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-volume-up\"></i> Voices</h4>\n <div class=\"voice-info\">\n <p><span class=\"field-label\">Main:</span> {{ entity()!.conversationSettings!.mainVoice!.voice }}</p>\n @if (entity()?.conversationSettings?.secondaryVoice?.voice) {\n <p><span class=\"field-label\">Secondary:</span> {{ entity()!.conversationSettings!.secondaryVoice!.voice }}</p>\n }\n </div>\n </div>\n }\n\n</div>\n}\n\n@if (entity()?.manageable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Manageable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.manageable?.isPublic) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if (entity()?.learnable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Learnable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.learnable?.level) {\n <p-tag [value]=\"'Level ' + entity()!.learnable!.level!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.learnable?.tags) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if(entity()?.conversationFlow) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-tags\"></i> Conversation Flow</h4>\n\n @if (entity()?.conversationFlow?.moodState) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-face-smile\"></i> Mood State</h5>\n <div class=\"settings-chips\">\n @if (entity()!.conversationFlow!.moodState!.enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"danger\" icon=\"pi pi-times\" />\n }\n @if (entity()!.conversationFlow!.moodState!.useAssetStatesOnly) {\n <p-tag value=\"Asset States Only\" severity=\"warn\" icon=\"pi pi-images\" />\n }\n </div>\n @if (entity()!.conversationFlow!.moodState!.detectableStates?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.5rem;\">\n @for (state of entity()!.conversationFlow!.moodState!.detectableStates; track $index) {\n <p-tag [value]=\"state\" severity=\"info\" icon=\"pi pi-tag\" />\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.dynamicConditions?.length) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-sliders-v\"></i> Dynamic Conditions</h5>\n @for (condition of entity()!.conversationFlow!.dynamicConditions!; track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <div class=\"settings-chips\">\n <p-tag [value]=\"condition.what\" severity=\"info\" icon=\"pi pi-filter\" />\n <p-tag [value]=\"condition.when\" severity=\"secondary\" />\n <p-tag [value]=\"condition.value + ''\" severity=\"warn\" />\n </div>\n @if (condition.do?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @for (action of condition.do!; track $index) {\n <p-tag [value]=\"action.actionType\" severity=\"contrast\" icon=\"pi pi-bolt\" />\n @if (action.prompt) {\n <span class=\"field-label\" style=\"font-size: 0.75rem;\">{{ action.prompt | slice:0:60 }}\u2026</span>\n }\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.triggerTasks) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-bolt\"></i> Trigger Tasks</h5>\n @for (entry of objectEntries(entity()!.conversationFlow!.triggerTasks!); track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <span class=\"field-label\">{{ entry[0] }}</span>\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @if (entry[1].enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"secondary\" icon=\"pi pi-times\" />\n }\n @if (entry[1].disableFeature) {\n <p-tag value=\"Feature Disabled\" severity=\"danger\" icon=\"pi pi-ban\" />\n }\n </div>\n @if (entry[1].task) {\n <p style=\"font-size: 0.8rem; margin: 0.25rem 0 0;\">{{ entry[1].task | slice:0:120 }}\u2026</p>\n }\n </div>\n }\n </div>\n }\n</div>\n}\n\n<!-- Video Dialog -->\n<p-dialog\n [visible]=\"videoDialogVisible()\"\n (visibleChange)=\"$event ? null : closeMotionVideo()\"\n [header]=\"selectedMotion()?.metadata?.moodState || selectedMotion()?.metadata?.event || 'Video'\"\n [modal]=\"true\"\n [dismissableMask]=\"true\"\n [closable]=\"true\"\n styleClass=\"video-dialog\"\n [style]=\"{ width: '560px' }\">\n\n @if (selectedMotion()?.url) {\n <video\n class=\"motion-video\"\n [src]=\"selectedMotion().url\"\n controls\n autoplay>\n </video>\n }\n</p-dialog>\n", styles: [".preview-container{padding:.5rem}.preview-banner{width:100%;height:210px;overflow:hidden;border-radius:8px;margin-bottom:0}.banner-image{width:100%;height:100%;object-fit:cover;display:block}.preview-header{display:flex;gap:1.5rem;align-items:flex-start;padding:1rem 0 .5rem}.preview-header.has-banner{padding-top:.75rem}.preview-image{width:120px;height:170px;object-fit:cover;border-radius:8px;flex-shrink:0;box-shadow:0 2px 8px #00000026}.preview-header-info{display:flex;flex-direction:column;gap:.5rem}.preview-name{margin:0 0 .25rem;font-size:1.5rem;font-weight:700}.preview-meta-tags{display:flex;flex-wrap:wrap;gap:.35rem}.preview-tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.preview-entity-description{color:var(--p-text-muted-color);font-size:.9rem;margin:.5rem 0}.preview-entity-description p{margin:0}.preview-section{margin-bottom:1rem}.section-title{margin:0 0 .5rem;font-size:.95rem;font-weight:600;color:var(--p-text-muted-color);display:flex;align-items:center;gap:.4rem}.greeting-item{margin:.25rem 0;padding-left:.5rem;border-left:2px solid var(--p-primary-color)}.first-message{background:var(--p-surface-100);border-radius:6px;padding:.75rem;font-style:italic}.persona-grid{display:flex;flex-direction:column;gap:.5rem}.persona-field{display:flex;flex-direction:column;gap:.15rem}.field-label{font-size:.8rem;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.05em}.motions-top-section{margin-top:.75rem;margin-bottom:0}.motions-grid{display:flex;flex-wrap:wrap;gap:.4rem}.motion-chip{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .65rem;border-radius:20px;border:1px solid var(--p-surface-300);background:var(--p-surface-100);color:var(--p-text-color);font-size:.8rem;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,transform .1s;text-transform:capitalize}.motion-chip i{font-size:.85rem;color:var(--p-primary-color)}.motion-chip:hover{background:var(--p-primary-50, #f0f4ff);border-color:var(--p-primary-color);transform:translateY(-1px)}.motion-chip:active{transform:translateY(0)}.motion-video{width:100%;border-radius:6px;display:block}.settings-chips{display:flex;flex-wrap:wrap;gap:.4rem}.voice-info{display:flex;flex-direction:column;gap:.25rem}.loading-state{display:flex;justify-content:center;padding:2rem}\n"] }]
6511
+ args: [{ selector: 'dc-agent-card-detail', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, CardModule, TagModule, DividerModule, ProgressSpinnerModule, DialogModule, ButtonModule], template: "@if (isLoading()) {\n<div class=\"loading-state\">\n <p-progressSpinner strokeWidth=\"4\" />\n</div>\n} @else if (entity()) {\n<div class=\"preview-container\">\n\n <!-- Banner -->\n @if (bannerUrl) {\n <div class=\"preview-banner\">\n <img class=\"banner-image\" [src]=\"bannerUrl\" alt=\"banner\" />\n </div>\n }\n\n <!-- Header: image + name + meta -->\n <div class=\"preview-header\" [class.has-banner]=\"bannerUrl\">\n @if (entity()?.assets?.image?.url) {\n <img class=\"preview-image\" [src]=\"entity()!.assets!.image!.url\" [alt]=\"characterData?.name || entity()?.name\" />\n }\n <div class=\"preview-header-info\">\n <h2 class=\"preview-name\">{{ characterData?.name || entity()?.name }}</h2>\n\n <div class=\"preview-meta-tags\">\n @if (entity()?.lang) {\n <p-tag [value]=\"entity()!.lang!\" severity=\"secondary\" icon=\"pi pi-flag\" />\n }\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-circle-fill\" />\n }\n @if (characterData?.gender) {\n <p-tag [value]=\"characterData!.gender\" severity=\"secondary\" icon=\"pi pi-user\" />\n }\n @if (entity()?.metaApp?.takenCount) {\n <p-tag [value]=\"'Taken: ' + entity()!.metaApp!.takenCount\" severity=\"success\" icon=\"pi pi-users\" />\n }\n @if (learnableInfo?.level) {\n <p-tag [value]=\"'Level ' + learnableInfo.level\" severity=\"warn\" icon=\"pi pi-star\" />\n }\n @if (learnableInfo?.takenCount) {\n <p-tag [value]=\"'Played: ' + learnableInfo.takenCount\" severity=\"secondary\" icon=\"pi pi-play\" />\n }\n </div>\n\n @if (characterData?.tags?.length) {\n <div class=\"preview-tags\">\n @for (tag of characterData!.tags; track tag) {\n <p-tag [value]=\"tag\" severity=\"secondary\" />\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Entity-level description -->\n @if (entity()?.description) {\n <div class=\"preview-entity-description\">\n <p>{{ entity()!.description }}</p>\n </div>\n }\n\n <!-- Motion Videos -->\n @if (motions.length) {\n <div class=\"preview-section motions-top-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-video\"></i> Motion Videos ({{ motions.length }})</h4>\n <div class=\"motions-grid\">\n @for (motion of motions; track $index) {\n @if (motion.metadata?.moodState || motion.metadata?.event) {\n <button class=\"motion-chip\" (click)=\"openMotionVideo(motion)\">\n <i class=\"pi pi-play-circle\"></i>\n <span>{{ motion.metadata?.moodState || motion.metadata?.event }}</span>\n </button>\n }\n }\n </div>\n </div>\n }\n\n <p-divider />\n\n @if (characterData?.hook) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-bookmark\"></i> Hook</h4>\n <p>{{ characterData!.hook }}</p>\n </div>\n }\n\n @if (characterData?.instructions) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-info-circle\"></i> Instructions</h4>\n <p>{{ characterData!.instructions }}</p>\n </div>\n }\n\n @if (characterData?.scenario) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-map\"></i> Scenario</h4>\n <p>{{ characterData!.scenario }}</p>\n </div>\n }\n\n @if (characterData?.creator_notes) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-file\"></i> Creator Notes</h4>\n <p>{{ characterData!.creator_notes }}</p>\n </div>\n }\n\n @if (characterData?.description) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-align-left\"></i> Description</h4>\n <p>{{ characterData!.description }}</p>\n </div>\n }\n\n @if (characterData?.greetings?.length) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-comments\"></i> Greetings</h4>\n @for (greeting of characterData!.greetings; track $index) {\n <p class=\"greeting-item\"><i class=\"pi pi-chevron-right\"></i> {{ greeting }}</p>\n }\n </div>\n }\n\n\n @if (persona) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-id-card\"></i> Persona</h4>\n <div class=\"persona-grid\">\n @if (persona.identity) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Identity</span>\n <span>{{ persona.identity }}</span>\n </div>\n }\n @if (persona.physical) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Physical</span>\n <span>{{ persona.physical }}</span>\n </div>\n }\n @if (persona.personality) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Personality</span>\n <span>{{ persona.personality }}</span>\n </div>\n }\n @if (persona.communication) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Communication</span>\n <span>{{ persona.communication }}</span>\n </div>\n }\n @if (persona.background) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Background</span>\n <span>{{ persona.background }}</span>\n </div>\n }\n @if (persona.psychology) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Psychology</span>\n <span>{{ persona.psychology }}</span>\n </div>\n }\n @if (persona.capabilities) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Capabilities</span>\n <span>{{ persona.capabilities }}</span>\n </div>\n }\n @if (persona.social) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Social</span>\n <span>{{ persona.social }}</span>\n </div>\n }\n @if (persona.preferences) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Preferences</span>\n <span>{{ persona.preferences }}</span>\n </div>\n }\n @if (persona.situation) {\n <div class=\"persona-field\">\n <span class=\"field-label\">Situation</span>\n <span>{{ persona.situation }}</span>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Conversation Settings -->\n @if (entity()?.conversationSettings) {\n <p-divider />\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Conversation Settings</h4>\n <div class=\"settings-chips\">\n @if (entity()?.conversationSettings?.conversationType) {\n <p-tag [value]=\"entity()!.conversationSettings!.conversationType!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.conversationSettings?.textEngine) {\n <p-tag [value]=\"entity()!.conversationSettings!.textEngine!\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n @if (entity()?.conversationSettings?.autoStart) {\n <p-tag value=\"Auto Start\" severity=\"success\" icon=\"pi pi-play\" />\n }\n @if (entity()?.conversationSettings?.userMustStart) {\n <p-tag value=\"User Starts\" severity=\"warn\" icon=\"pi pi-user\" />\n }\n </div>\n </div>\n }\n\n <!-- Voices -->\n @if (entity()?.voice?.main?.voice || entity()?.conversationSettings?.mainVoice?.voice) {\n <div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-volume-up\"></i> Voices</h4>\n <div class=\"voice-info\">\n <p><span class=\"field-label\">Main:</span> {{ entity()?.voice?.main?.voice || entity()?.conversationSettings?.mainVoice?.voice }}</p>\n @if (entity()?.voice?.secondary?.voice || entity()?.conversationSettings?.secondaryVoice?.voice) {\n <p><span class=\"field-label\">Secondary:</span> {{ entity()?.voice?.secondary?.voice || entity()?.conversationSettings?.secondaryVoice?.voice }}</p>\n }\n </div>\n </div>\n }\n\n</div>\n}\n\n@if (entity()?.manageable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Manageable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.manageable?.status) {\n <p-tag [value]=\"entity()!.manageable!.status!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.manageable?.isPublic) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if (entity()?.learnable) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-sliders-h\"></i> Learnable</h4>\n <div class=\"settings-chips\">\n @if (entity()?.learnable?.level) {\n <p-tag [value]=\"'Level ' + entity()!.learnable!.level!\" severity=\"info\" icon=\"pi pi-comments\" />\n }\n @if (entity()?.learnable?.tags) {\n <p-tag [value]=\"'Public'\" severity=\"secondary\" icon=\"pi pi-code\" />\n }\n </div>\n</div>\n}\n\n@if(entity()?.conversationFlow) {\n<p-divider />\n<div class=\"preview-section\">\n <h4 class=\"section-title\"><i class=\"pi pi-tags\"></i> Conversation Flow</h4>\n\n @if (entity()?.conversationFlow?.moodState) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-face-smile\"></i> Mood State</h5>\n <div class=\"settings-chips\">\n @if (entity()!.conversationFlow!.moodState!.enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"danger\" icon=\"pi pi-times\" />\n }\n @if (entity()!.conversationFlow!.moodState!.useAssetStatesOnly) {\n <p-tag value=\"Asset States Only\" severity=\"warn\" icon=\"pi pi-images\" />\n }\n </div>\n @if (entity()!.conversationFlow!.moodState!.detectableStates?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.5rem;\">\n @for (state of entity()!.conversationFlow!.moodState!.detectableStates; track $index) {\n <p-tag [value]=\"state\" severity=\"info\" icon=\"pi pi-tag\" />\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.dynamicConditions?.length) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-sliders-v\"></i> Dynamic Conditions</h5>\n @for (condition of entity()!.conversationFlow!.dynamicConditions!; track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <div class=\"settings-chips\">\n <p-tag [value]=\"condition.what\" severity=\"info\" icon=\"pi pi-filter\" />\n <p-tag [value]=\"condition.when\" severity=\"secondary\" />\n <p-tag [value]=\"condition.value + ''\" severity=\"warn\" />\n </div>\n @if (condition.do?.length) {\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @for (action of condition.do!; track $index) {\n <p-tag [value]=\"action.actionType\" severity=\"contrast\" icon=\"pi pi-bolt\" />\n @if (action.prompt) {\n <span class=\"field-label\" style=\"font-size: 0.75rem;\">{{ action.prompt | slice:0:60 }}\u2026</span>\n }\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (entity()?.conversationFlow?.triggerTasks) {\n <div class=\"preview-section\">\n <h5 class=\"section-title\"><i class=\"pi pi-bolt\"></i> Trigger Tasks</h5>\n @for (entry of objectEntries(entity()!.conversationFlow!.triggerTasks!); track $index) {\n <div class=\"persona-field\" style=\"margin-bottom: 0.5rem;\">\n <span class=\"field-label\">{{ entry[0] }}</span>\n <div class=\"settings-chips\" style=\"margin-top: 0.25rem;\">\n @if (entry[1].enabled) {\n <p-tag value=\"Enabled\" severity=\"success\" icon=\"pi pi-check\" />\n } @else {\n <p-tag value=\"Disabled\" severity=\"secondary\" icon=\"pi pi-times\" />\n }\n @if (entry[1].disableFeature) {\n <p-tag value=\"Feature Disabled\" severity=\"danger\" icon=\"pi pi-ban\" />\n }\n </div>\n @if (entry[1].task) {\n <p style=\"font-size: 0.8rem; margin: 0.25rem 0 0;\">{{ entry[1].task | slice:0:120 }}\u2026</p>\n }\n </div>\n }\n </div>\n }\n</div>\n}\n\n<!-- Video Dialog -->\n<p-dialog\n [visible]=\"videoDialogVisible()\"\n (visibleChange)=\"$event ? null : closeMotionVideo()\"\n [header]=\"selectedMotion()?.metadata?.moodState || selectedMotion()?.metadata?.event || 'Video'\"\n [modal]=\"true\"\n [dismissableMask]=\"true\"\n [closable]=\"true\"\n styleClass=\"video-dialog\"\n [style]=\"{ width: '560px' }\">\n\n @if (selectedMotion()?.url) {\n <video\n class=\"motion-video\"\n [src]=\"selectedMotion().url\"\n controls\n autoplay>\n </video>\n }\n</p-dialog>\n", styles: [".preview-container{padding:.5rem}.preview-banner{width:100%;height:210px;overflow:hidden;border-radius:8px;margin-bottom:0}.banner-image{width:100%;height:100%;object-fit:cover;display:block}.preview-header{display:flex;gap:1.5rem;align-items:flex-start;padding:1rem 0 .5rem}.preview-header.has-banner{padding-top:.75rem}.preview-image{width:120px;height:170px;object-fit:cover;border-radius:8px;flex-shrink:0;box-shadow:0 2px 8px #00000026}.preview-header-info{display:flex;flex-direction:column;gap:.5rem}.preview-name{margin:0 0 .25rem;font-size:1.5rem;font-weight:700}.preview-meta-tags{display:flex;flex-wrap:wrap;gap:.35rem}.preview-tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.preview-entity-description{color:var(--p-text-muted-color);font-size:.9rem;margin:.5rem 0}.preview-entity-description p{margin:0}.preview-section{margin-bottom:1rem}.section-title{margin:0 0 .5rem;font-size:.95rem;font-weight:600;color:var(--p-text-muted-color);display:flex;align-items:center;gap:.4rem}.greeting-item{margin:.25rem 0;padding-left:.5rem;border-left:2px solid var(--p-primary-color)}.first-message{background:var(--p-surface-100);border-radius:6px;padding:.75rem;font-style:italic}.persona-grid{display:flex;flex-direction:column;gap:.5rem}.persona-field{display:flex;flex-direction:column;gap:.15rem}.field-label{font-size:.8rem;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.05em}.motions-top-section{margin-top:.75rem;margin-bottom:0}.motions-grid{display:flex;flex-wrap:wrap;gap:.4rem}.motion-chip{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .65rem;border-radius:20px;border:1px solid var(--p-surface-300);background:var(--p-surface-100);color:var(--p-text-color);font-size:.8rem;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,transform .1s;text-transform:capitalize}.motion-chip i{font-size:.85rem;color:var(--p-primary-color)}.motion-chip:hover{background:var(--p-primary-50, #f0f4ff);border-color:var(--p-primary-color);transform:translateY(-1px)}.motion-chip:active{transform:translateY(0)}.motion-video{width:100%;border-radius:6px;display:block}.settings-chips{display:flex;flex-wrap:wrap;gap:.4rem}.voice-info{display:flex;flex-direction:column;gap:.25rem}.loading-state{display:flex;justify-content:center;padding:2rem}\n"] }]
6194
6512
  }], ctorParameters: () => [], propDecorators: { entityData: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityData", required: false }] }] } });
6195
6513
 
6196
6514
  class ConversationSummaryComponent {
@@ -6292,11 +6610,11 @@ class ConversationInspector {
6292
6610
  this.viewMode.set(this.viewMode() === 'markdown' ? 'regular' : 'markdown');
6293
6611
  }
6294
6612
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ConversationInspector, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6295
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ConversationInspector, isStandalone: true, selector: "dc-conversation-info", outputs: { completeEvent: "completeEvent" }, ngImport: i0, template: "<div class=\"info-content\">\n <p-tabs value=\"0\">\n <p-tablist>\n <p-tab value=\"0\">\uD83D\uDCCA Summary</p-tab>\n <p-tab value=\"1\">\uD83D\uDD00 Dynamic Flow</p-tab>\n <p-tab value=\"6\">\uD83D\uDCE6 Messages State</p-tab>\n <p-tab value=\"7\">\uD83D\uDC64 Preview Card</p-tab>\n <p-tab value=\"5\">\uD83D\uDCBE Conversation State</p-tab>\n <p-tab value=\"2\">\uD83D\uDCAC Conversation Messages</p-tab>\n <p-tab value=\"3\">\uD83D\uDCB0 Pricing</p-tab>\n <p-tab value=\"4\">\uD83D\uDC1B More</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"0\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n }\n <dc-conversation-summary [agentCard]=\"agentCard\" [parentInjector]=\"config.data.injector\" (completeEvent)=\"complete()\"></dc-conversation-summary>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"1\">\n <ng-template #content>\n <!-- Dynamic Flow -->\n\n <p-divider>Conversation Flow</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (dynamicFlowService.conversationFlowConfig()?.goal?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Goal</p>\n </div>\n @if(dynamicFlowService.conversationFlowConfig()?.goal?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger User Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger Assistant Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <h3>Herramientas</h3>\n @for (tool of agentCardStateService.conversationFlow$()?.tools; track tool.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (tool.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">{{ tool.name }}</p>\n </div>\n @if(tool.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ tool.description }} </div>\n }\n </div>\n }\n\n <h3>Challenges Retos</h3>\n @for (challenge of dynamicFlowService.conversationFlowConfig()?.challenges; track challenge.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (challenge.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n\n <p class=\"font-semibold\"> {{ challenge.emoji }} {{ challenge.name }}</p>\n </div>\n @if(challenge.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ challenge.description }} </div>\n }\n </div>\n }\n <h5>Conditions</h5>\n\n @for (condition of dynamicFlowService.conversationFlowConfig()?.dynamicConditions; track $index) {\n <div class=\"pl-2 mt-1 text-sm text-gray-600 italic\"> When the {{ condition.what }} is {{ condition.when }} than {{ condition.value }} Do </div>\n <ol>\n @for (doAction of condition.do; track $index) {\n <li class=\"pl-5 mt-1 text-sm text-gray-600 italic\">\n {{ $index }})\n <span style=\"color: brown\"> {{ doAction.actionType }}</span>\n <span style=\"color: green\"> {{ doAction.systemPromptType }}</span>\n <span style=\"color: green\"> {{ doAction.dynamicFlowTaskType }}</span>\n\n to\n <span style=\"color: rgb(49, 23, 177)\"> {{ doAction.enabled }}</span>\n \"{{ doAction.prompt }}\"\n </li>\n }\n </ol>\n }\n\n <p>Challenges</p>\n <pre>{{ agentCardStateService.conversationFlow$()?.challenges | safeJson }}</pre>\n\n <p-divider>Performance Evaluation</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Performance Prompt</p>\n <span class=\"text-xs text-gray-400 ml-2\" pTooltip=\"Minimum messages required to trigger evaluation\">(MIN_MESSAGES_EVALUATE: 4 hardcoded)</span>\n </div>\n @if(agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n @if(agentCardStateService.conversationFlow$()?.performancePrompt; as prompt) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> \n <markdown>\n {{ prompt }} \n </markdown>\n </div>\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> No custom performance prompt defined (using default). </div>\n }\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> Performance analysis disabled. </div>\n }\n </div>\n\n <p-divider>System Evaluation Prompt</p-divider>\n <details>\n <summary>Show Prompt</summary>\n <pre>{{ systemPromptForFlow() }}</pre>\n </details>\n\n <!-- Dynamic Flow ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"5\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n\n @if (flowState.challenges?.length) {\n <div class=\"mb-4 p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFC6</span> Completed Challenges\n </h3>\n <ul class=\"list-disc pl-5 space-y-1\">\n @for (challenge of flowState.challenges; track challenge) {\n <li>{{ challenge }}</li>\n }\n </ul>\n </div>\n }\n\n <details class=\"mt-4\">\n <summary class=\"cursor-pointer text-sm font-medium\">Raw JSON State</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ flowState | safeJson }}</pre>\n </details>\n } @else {\n <p class=\"italic mt-2 opacity-50\">No conversation state available.</p>\n }\n\n <details class=\"mt-2\">\n <summary class=\"cursor-pointer text-sm font-medium\">Current Prompt</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ systemPromptForFlow() }}</pre>\n </details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"2\">\n <ng-template #content>\n <div>\n <p-button label=\"Exportar todo\" (click)=\"exportChat('all')\" />\n <p-button label=\"Exportar conversaci\u00F3n\" (click)=\"exportChat('conversation')\" />\n <p-button [label]=\"'Vista ' + viewMode()\" (click)=\"toggleViewMode()\" />\n </div>\n <dc-prompt-preview [messages]=\"messageStateService.getMessagesSignal()()\" [view]=\"viewMode()\"></dc-prompt-preview>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"6\">\n <ng-template #content>\n <dc-messages-state-inspector></dc-messages-state-inspector>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"3\">\n <ng-template #content>\n <dc-cost-details></dc-cost-details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"4\">\n <ng-template #content>\n <!-- MORE INFO -->\n\n <details>\n <summary>Estado Global Conversation Flow</summary>\n <p>Goal</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.goal | safeJson }}</pre>\n <p>Trigger User Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onUserMessage | safeJson }}</pre>\n <p>Trigger Assistant Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onAssistantMessage | safeJson }}</pre>\n <p>Conditions</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.dynamicConditions | safeJson }}</pre>\n <p>Tools</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.tools | safeJson }}</pre>\n <p>Challenges</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.challenges | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Conversation Settings (Debug)</summary>\n <pre>{{ conversationService.conversationSettings$() | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Agent Card</summary>\n <pre>{{ agentCard | safeJson }}</pre>\n </details>\n\n <details>\n <summary>User Settings</summary>\n <pre>{{ chatUserSettings | safeJson }}</pre>\n </details>\n\n <!-- MORE INFO ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"7\">\n <ng-template #content>\n <dc-agent-card-detail [entityData]=\"agentCardStateService.agentCard$()\" />\n </ng-template>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n</div>\n", styles: [":host{overflow-y:scroll}.info-content{display:flex;flex-direction:column;gap:1rem;padding:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "ngmodule", type: SliderModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: DividerModule }, { kind: "component", type: i2$3.Divider, selector: "p-divider", inputs: ["styleClass", "layout", "type", "align"] }, { kind: "component", type: PromptPreviewComponent, selector: "dc-prompt-preview", inputs: ["messages", "jailBrake", "view"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i2$5.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i2$5.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i2$5.TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "component", type: i2$5.TabList, selector: "p-tablist" }, { kind: "component", type: i2$5.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: CostDetailsComponent, selector: "dc-cost-details" }, { kind: "component", type: MessagesStateInspectorComponent, selector: "dc-messages-state-inspector" }, { kind: "component", type: DcAgentCardDetailComponent, selector: "dc-agent-card-detail", inputs: ["entityData"] }, { kind: "component", type: ConversationSummaryComponent, selector: "dc-conversation-summary", inputs: ["agentCard", "parentInjector"], outputs: ["completeEvent"] }, { kind: "pipe", type: SafeJsonPipe, name: "safeJson" }] }); }
6613
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ConversationInspector, isStandalone: true, selector: "dc-conversation-info", outputs: { completeEvent: "completeEvent" }, ngImport: i0, template: "<div class=\"info-content\">\n <p-tabs value=\"0\">\n <p-tablist>\n <p-tab value=\"0\">\uD83D\uDCCA Summary</p-tab>\n <p-tab value=\"1\">\uD83D\uDD00 Dynamic Flow</p-tab>\n <p-tab value=\"6\">\uD83D\uDCE6 Messages State</p-tab>\n <p-tab value=\"7\">\uD83D\uDC64 Preview Card</p-tab>\n <p-tab value=\"5\">\uD83D\uDCBE Conversation State</p-tab>\n <p-tab value=\"2\">\uD83D\uDCAC Conversation Messages</p-tab>\n <p-tab value=\"3\">\uD83D\uDCB0 Pricing</p-tab>\n <p-tab value=\"4\">\uD83D\uDC1B More</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"0\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n }\n <dc-conversation-summary [agentCard]=\"agentCard\" [parentInjector]=\"config.data.injector\" (completeEvent)=\"complete()\"></dc-conversation-summary>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"1\">\n <ng-template #content>\n <!-- Dynamic Flow -->\n\n <p-divider>Conversation Flow</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (dynamicFlowService.conversationFlowConfig()?.goal?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Goal</p>\n </div>\n @if(dynamicFlowService.conversationFlowConfig()?.goal?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger User Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger Assistant Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <h3>Herramientas</h3>\n @for (tool of agentCardStateService.conversationFlow$()?.tools; track tool.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (tool.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">{{ tool.name }}</p>\n </div>\n @if(tool.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ tool.description }} </div>\n }\n </div>\n }\n\n <h3>Challenges Retos</h3>\n @for (challenge of dynamicFlowService.conversationFlowConfig()?.challenges; track challenge.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (challenge.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n\n <p class=\"font-semibold\"> {{ challenge.emoji }} {{ challenge.name }}</p>\n </div>\n @if(challenge.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ challenge.description }} </div>\n }\n </div>\n }\n <h5>Conditions</h5>\n\n @for (condition of dynamicFlowService.conversationFlowConfig()?.dynamicConditions; track $index) {\n <div class=\"pl-2 mt-1 text-sm text-gray-600 italic\"> When the {{ condition.what }} is {{ condition.when }} than {{ condition.value }} Do </div>\n <ol>\n @for (doAction of condition.do; track $index) {\n <li class=\"pl-5 mt-1 text-sm text-gray-600 italic\">\n {{ $index }})\n <span style=\"color: brown\"> {{ doAction.actionType }}</span>\n <span style=\"color: green\"> {{ doAction.systemPromptType }}</span>\n <span style=\"color: green\"> {{ doAction.dynamicFlowTaskType }}</span>\n\n to\n <span style=\"color: rgb(49, 23, 177)\"> {{ doAction.enabled }}</span>\n \"{{ doAction.prompt }}\"\n </li>\n }\n </ol>\n }\n\n <p>Challenges</p>\n <pre>{{ agentCardStateService.conversationFlow$()?.challenges | safeJson }}</pre>\n\n <p-divider>Performance Evaluation</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Performance Prompt</p>\n <span class=\"text-xs text-gray-400 ml-2\" pTooltip=\"Minimum messages required to trigger evaluation\">(MIN_MESSAGES_EVALUATE: 4 hardcoded)</span>\n </div>\n @if(agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n @if(agentCardStateService.conversationFlow$()?.performancePrompt; as prompt) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> \n <markdown>\n {{ prompt }} \n </markdown>\n </div>\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> No custom performance prompt defined (using default). </div>\n }\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> Performance analysis disabled. </div>\n }\n </div>\n\n <p-divider>System Evaluation Prompt</p-divider>\n <details>\n <summary>Show Prompt</summary>\n <pre>{{ systemPromptForFlow() }}</pre>\n </details>\n\n <!-- Dynamic Flow ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"5\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n\n @if (flowState.challenges?.length) {\n <div class=\"mb-4 p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFC6</span> Completed Challenges\n </h3>\n <ul class=\"list-disc pl-5 space-y-1\">\n @for (challenge of flowState.challenges; track challenge) {\n <li>{{ challenge }}</li>\n }\n </ul>\n </div>\n }\n\n <details class=\"mt-4\">\n <summary class=\"cursor-pointer text-sm font-medium\">Raw JSON State</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ flowState | safeJson }}</pre>\n </details>\n } @else {\n <p class=\"italic mt-2 opacity-50\">No conversation state available.</p>\n }\n\n <details class=\"mt-2\">\n <summary class=\"cursor-pointer text-sm font-medium\">Current Prompt</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ systemPromptForFlow() }}</pre>\n </details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"2\">\n <ng-template #content>\n <div>\n <p-button label=\"Exportar todo\" (click)=\"exportChat('all')\" />\n <p-button label=\"Exportar conversaci\u00F3n\" (click)=\"exportChat('conversation')\" />\n <p-button [label]=\"'Vista ' + viewMode()\" (click)=\"toggleViewMode()\" />\n </div>\n <dc-prompt-preview [messages]=\"messageStateService.getMessagesSignal()()\" [view]=\"viewMode()\"></dc-prompt-preview>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"6\">\n <ng-template #content>\n <dc-messages-state-inspector [service]=\"messageStateService\"></dc-messages-state-inspector>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"3\">\n <ng-template #content>\n <dc-cost-details></dc-cost-details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"4\">\n <ng-template #content>\n <!-- MORE INFO -->\n\n <details>\n <summary>Estado Global Conversation Flow</summary>\n <p>Goal</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.goal | safeJson }}</pre>\n <p>Trigger User Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onUserMessage | safeJson }}</pre>\n <p>Trigger Assistant Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onAssistantMessage | safeJson }}</pre>\n <p>Conditions</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.dynamicConditions | safeJson }}</pre>\n <p>Tools</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.tools | safeJson }}</pre>\n <p>Challenges</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.challenges | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Conversation Settings (Debug)</summary>\n <pre>{{ conversationService.conversationSettings$() | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Agent Card</summary>\n <pre>{{ agentCard | safeJson }}</pre>\n </details>\n\n <details>\n <summary>User Settings</summary>\n <pre>{{ chatUserSettings | safeJson }}</pre>\n </details>\n\n <!-- MORE INFO ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"7\">\n <ng-template #content>\n <dc-agent-card-detail [entityData]=\"agentCardStateService.agentCard$()\" />\n </ng-template>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n</div>\n", styles: [":host{overflow-y:scroll}.info-content{display:flex;flex-direction:column;gap:1rem;padding:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "ngmodule", type: SliderModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: DividerModule }, { kind: "component", type: i2$3.Divider, selector: "p-divider", inputs: ["styleClass", "layout", "type", "align"] }, { kind: "component", type: PromptPreviewComponent, selector: "dc-prompt-preview", inputs: ["messages", "jailBrake", "view"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i2$5.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i2$5.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i2$5.TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "component", type: i2$5.TabList, selector: "p-tablist" }, { kind: "component", type: i2$5.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: CostDetailsComponent, selector: "dc-cost-details" }, { kind: "component", type: MessagesStateInspectorComponent, selector: "dc-messages-state-inspector", inputs: ["service"] }, { kind: "component", type: DcAgentCardDetailComponent, selector: "dc-agent-card-detail", inputs: ["entityData"] }, { kind: "component", type: ConversationSummaryComponent, selector: "dc-conversation-summary", inputs: ["agentCard", "parentInjector"], outputs: ["completeEvent"] }, { kind: "pipe", type: SafeJsonPipe, name: "safeJson" }] }); }
6296
6614
  }
6297
6615
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ConversationInspector, decorators: [{
6298
6616
  type: Component,
6299
- args: [{ selector: 'dc-conversation-info', standalone: true, imports: [CommonModule, SafeJsonPipe, MarkdownComponent, SliderModule, ButtonModule, FormsModule, DividerModule, PromptPreviewComponent, TabsModule, CostDetailsComponent, MessagesStateInspectorComponent, DcAgentCardDetailComponent, ConversationSummaryComponent], template: "<div class=\"info-content\">\n <p-tabs value=\"0\">\n <p-tablist>\n <p-tab value=\"0\">\uD83D\uDCCA Summary</p-tab>\n <p-tab value=\"1\">\uD83D\uDD00 Dynamic Flow</p-tab>\n <p-tab value=\"6\">\uD83D\uDCE6 Messages State</p-tab>\n <p-tab value=\"7\">\uD83D\uDC64 Preview Card</p-tab>\n <p-tab value=\"5\">\uD83D\uDCBE Conversation State</p-tab>\n <p-tab value=\"2\">\uD83D\uDCAC Conversation Messages</p-tab>\n <p-tab value=\"3\">\uD83D\uDCB0 Pricing</p-tab>\n <p-tab value=\"4\">\uD83D\uDC1B More</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"0\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n }\n <dc-conversation-summary [agentCard]=\"agentCard\" [parentInjector]=\"config.data.injector\" (completeEvent)=\"complete()\"></dc-conversation-summary>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"1\">\n <ng-template #content>\n <!-- Dynamic Flow -->\n\n <p-divider>Conversation Flow</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (dynamicFlowService.conversationFlowConfig()?.goal?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Goal</p>\n </div>\n @if(dynamicFlowService.conversationFlowConfig()?.goal?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger User Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger Assistant Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <h3>Herramientas</h3>\n @for (tool of agentCardStateService.conversationFlow$()?.tools; track tool.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (tool.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">{{ tool.name }}</p>\n </div>\n @if(tool.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ tool.description }} </div>\n }\n </div>\n }\n\n <h3>Challenges Retos</h3>\n @for (challenge of dynamicFlowService.conversationFlowConfig()?.challenges; track challenge.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (challenge.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n\n <p class=\"font-semibold\"> {{ challenge.emoji }} {{ challenge.name }}</p>\n </div>\n @if(challenge.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ challenge.description }} </div>\n }\n </div>\n }\n <h5>Conditions</h5>\n\n @for (condition of dynamicFlowService.conversationFlowConfig()?.dynamicConditions; track $index) {\n <div class=\"pl-2 mt-1 text-sm text-gray-600 italic\"> When the {{ condition.what }} is {{ condition.when }} than {{ condition.value }} Do </div>\n <ol>\n @for (doAction of condition.do; track $index) {\n <li class=\"pl-5 mt-1 text-sm text-gray-600 italic\">\n {{ $index }})\n <span style=\"color: brown\"> {{ doAction.actionType }}</span>\n <span style=\"color: green\"> {{ doAction.systemPromptType }}</span>\n <span style=\"color: green\"> {{ doAction.dynamicFlowTaskType }}</span>\n\n to\n <span style=\"color: rgb(49, 23, 177)\"> {{ doAction.enabled }}</span>\n \"{{ doAction.prompt }}\"\n </li>\n }\n </ol>\n }\n\n <p>Challenges</p>\n <pre>{{ agentCardStateService.conversationFlow$()?.challenges | safeJson }}</pre>\n\n <p-divider>Performance Evaluation</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Performance Prompt</p>\n <span class=\"text-xs text-gray-400 ml-2\" pTooltip=\"Minimum messages required to trigger evaluation\">(MIN_MESSAGES_EVALUATE: 4 hardcoded)</span>\n </div>\n @if(agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n @if(agentCardStateService.conversationFlow$()?.performancePrompt; as prompt) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> \n <markdown>\n {{ prompt }} \n </markdown>\n </div>\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> No custom performance prompt defined (using default). </div>\n }\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> Performance analysis disabled. </div>\n }\n </div>\n\n <p-divider>System Evaluation Prompt</p-divider>\n <details>\n <summary>Show Prompt</summary>\n <pre>{{ systemPromptForFlow() }}</pre>\n </details>\n\n <!-- Dynamic Flow ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"5\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n\n @if (flowState.challenges?.length) {\n <div class=\"mb-4 p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFC6</span> Completed Challenges\n </h3>\n <ul class=\"list-disc pl-5 space-y-1\">\n @for (challenge of flowState.challenges; track challenge) {\n <li>{{ challenge }}</li>\n }\n </ul>\n </div>\n }\n\n <details class=\"mt-4\">\n <summary class=\"cursor-pointer text-sm font-medium\">Raw JSON State</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ flowState | safeJson }}</pre>\n </details>\n } @else {\n <p class=\"italic mt-2 opacity-50\">No conversation state available.</p>\n }\n\n <details class=\"mt-2\">\n <summary class=\"cursor-pointer text-sm font-medium\">Current Prompt</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ systemPromptForFlow() }}</pre>\n </details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"2\">\n <ng-template #content>\n <div>\n <p-button label=\"Exportar todo\" (click)=\"exportChat('all')\" />\n <p-button label=\"Exportar conversaci\u00F3n\" (click)=\"exportChat('conversation')\" />\n <p-button [label]=\"'Vista ' + viewMode()\" (click)=\"toggleViewMode()\" />\n </div>\n <dc-prompt-preview [messages]=\"messageStateService.getMessagesSignal()()\" [view]=\"viewMode()\"></dc-prompt-preview>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"6\">\n <ng-template #content>\n <dc-messages-state-inspector></dc-messages-state-inspector>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"3\">\n <ng-template #content>\n <dc-cost-details></dc-cost-details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"4\">\n <ng-template #content>\n <!-- MORE INFO -->\n\n <details>\n <summary>Estado Global Conversation Flow</summary>\n <p>Goal</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.goal | safeJson }}</pre>\n <p>Trigger User Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onUserMessage | safeJson }}</pre>\n <p>Trigger Assistant Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onAssistantMessage | safeJson }}</pre>\n <p>Conditions</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.dynamicConditions | safeJson }}</pre>\n <p>Tools</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.tools | safeJson }}</pre>\n <p>Challenges</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.challenges | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Conversation Settings (Debug)</summary>\n <pre>{{ conversationService.conversationSettings$() | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Agent Card</summary>\n <pre>{{ agentCard | safeJson }}</pre>\n </details>\n\n <details>\n <summary>User Settings</summary>\n <pre>{{ chatUserSettings | safeJson }}</pre>\n </details>\n\n <!-- MORE INFO ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"7\">\n <ng-template #content>\n <dc-agent-card-detail [entityData]=\"agentCardStateService.agentCard$()\" />\n </ng-template>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n</div>\n", styles: [":host{overflow-y:scroll}.info-content{display:flex;flex-direction:column;gap:1rem;padding:1rem}\n"] }]
6617
+ args: [{ selector: 'dc-conversation-info', standalone: true, imports: [CommonModule, SafeJsonPipe, MarkdownComponent, SliderModule, ButtonModule, FormsModule, DividerModule, PromptPreviewComponent, TabsModule, CostDetailsComponent, MessagesStateInspectorComponent, DcAgentCardDetailComponent, ConversationSummaryComponent], template: "<div class=\"info-content\">\n <p-tabs value=\"0\">\n <p-tablist>\n <p-tab value=\"0\">\uD83D\uDCCA Summary</p-tab>\n <p-tab value=\"1\">\uD83D\uDD00 Dynamic Flow</p-tab>\n <p-tab value=\"6\">\uD83D\uDCE6 Messages State</p-tab>\n <p-tab value=\"7\">\uD83D\uDC64 Preview Card</p-tab>\n <p-tab value=\"5\">\uD83D\uDCBE Conversation State</p-tab>\n <p-tab value=\"2\">\uD83D\uDCAC Conversation Messages</p-tab>\n <p-tab value=\"3\">\uD83D\uDCB0 Pricing</p-tab>\n <p-tab value=\"4\">\uD83D\uDC1B More</p-tab>\n </p-tablist>\n <p-tabpanels>\n <p-tabpanel value=\"0\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n }\n <dc-conversation-summary [agentCard]=\"agentCard\" [parentInjector]=\"config.data.injector\" (completeEvent)=\"complete()\"></dc-conversation-summary>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"1\">\n <ng-template #content>\n <!-- Dynamic Flow -->\n\n <p-divider>Conversation Flow</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (dynamicFlowService.conversationFlowConfig()?.goal?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Goal</p>\n </div>\n @if(dynamicFlowService.conversationFlowConfig()?.goal?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger User Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onUserMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Trigger Assistant Message</p>\n </div>\n @if(agentCardStateService.conversationFlow$()?.triggerTasks?.onAssistantMessage?.task; as task) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ task }} </div>\n }\n </div>\n\n <h3>Herramientas</h3>\n @for (tool of agentCardStateService.conversationFlow$()?.tools; track tool.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (tool.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">{{ tool.name }}</p>\n </div>\n @if(tool.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ tool.description }} </div>\n }\n </div>\n }\n\n <h3>Challenges Retos</h3>\n @for (challenge of dynamicFlowService.conversationFlowConfig()?.challenges; track challenge.name) {\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (challenge.enabled) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n\n <p class=\"font-semibold\"> {{ challenge.emoji }} {{ challenge.name }}</p>\n </div>\n @if(challenge.description) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> {{ challenge.description }} </div>\n }\n </div>\n }\n <h5>Conditions</h5>\n\n @for (condition of dynamicFlowService.conversationFlowConfig()?.dynamicConditions; track $index) {\n <div class=\"pl-2 mt-1 text-sm text-gray-600 italic\"> When the {{ condition.what }} is {{ condition.when }} than {{ condition.value }} Do </div>\n <ol>\n @for (doAction of condition.do; track $index) {\n <li class=\"pl-5 mt-1 text-sm text-gray-600 italic\">\n {{ $index }})\n <span style=\"color: brown\"> {{ doAction.actionType }}</span>\n <span style=\"color: green\"> {{ doAction.systemPromptType }}</span>\n <span style=\"color: green\"> {{ doAction.dynamicFlowTaskType }}</span>\n\n to\n <span style=\"color: rgb(49, 23, 177)\"> {{ doAction.enabled }}</span>\n \"{{ doAction.prompt }}\"\n </li>\n }\n </ol>\n }\n\n <p>Challenges</p>\n <pre>{{ agentCardStateService.conversationFlow$()?.challenges | safeJson }}</pre>\n\n <p-divider>Performance Evaluation</p-divider>\n <div class=\"mb-2\">\n <div class=\"flex items-center gap-2\">\n @if (agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n <div class=\"w-3 h-3 bg-green-500 rounded-full\" pTooltip=\"Active\"></div>\n } @else {\n <div class=\"w-3 h-3 bg-red-500 rounded-full\" pTooltip=\"Inactive\"></div>\n }\n <p class=\"font-semibold\">Performance Prompt</p>\n <span class=\"text-xs text-gray-400 ml-2\" pTooltip=\"Minimum messages required to trigger evaluation\">(MIN_MESSAGES_EVALUATE: 4 hardcoded)</span>\n </div>\n @if(agentCardStateService.conversationFlow$()?.enablePerformanceAnalysis !== false) {\n @if(agentCardStateService.conversationFlow$()?.performancePrompt; as prompt) {\n <div class=\"pl-5 mt-1 text-sm text-gray-600 italic\"> \n <markdown>\n {{ prompt }} \n </markdown>\n </div>\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> No custom performance prompt defined (using default). </div>\n }\n } @else {\n <div class=\"pl-5 mt-1 text-sm text-gray-400 italic\"> Performance analysis disabled. </div>\n }\n </div>\n\n <p-divider>System Evaluation Prompt</p-divider>\n <details>\n <summary>Show Prompt</summary>\n <pre>{{ systemPromptForFlow() }}</pre>\n </details>\n\n <!-- Dynamic Flow ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"5\">\n <ng-template #content>\n @if (conversationFlowStateService.flowState(); as flowState) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 mt-2\">\n <!-- Goal State -->\n <div class=\"p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFAF</span> Goal Progress\n </h3>\n <div class=\"flex items-end gap-4 mb-3\">\n <div class=\"text-5xl font-black leading-none\">\n {{ flowState.goal?.value || 0 }}\n </div>\n @if (flowState.goal?.deltaPoints !== undefined) {\n <div class=\"text-xl font-bold mb-1\" \n [class.text-green-600]=\"flowState.goal.deltaPoints > 0\"\n [class.text-red-600]=\"flowState.goal.deltaPoints < 0\"\n [class.opacity-50]=\"flowState.goal.deltaPoints === 0\">\n {{ flowState.goal.deltaPoints > 0 ? '+' : '' }}{{ flowState.goal.deltaPoints }}\n </div>\n }\n </div>\n \n @if (flowState.intervention?.message) {\n <div class=\"mt-4 p-3 rounded border-l-4 text-sm italic shadow-sm opacity-80\">\n \"{{ flowState.intervention.message }}\"\n </div>\n }\n </div>\n\n <!-- Mood State -->\n <div class=\"p-4 border rounded-lg shadow-sm flex flex-col\">\n <h3 class=\"text-lg font-bold mb-2 flex items-center gap-2\">\n <span>\uD83C\uDFAD</span> Agent Mood\n </h3>\n <div class=\"flex-grow flex items-center justify-center\">\n @if (flowState.moodState?.value) {\n <div class=\"text-4xl font-black uppercase tracking-widest text-center drop-shadow-sm\">\n {{ flowState.moodState.value }}\n </div>\n } @else {\n <div class=\"text-xl font-medium italic opacity-50\">Neutral</div>\n }\n </div>\n </div>\n </div>\n\n @if (flowState.challenges?.length) {\n <div class=\"mb-4 p-4 border rounded-lg shadow-sm\">\n <h3 class=\"text-lg font-bold mb-3 flex items-center gap-2\">\n <span>\uD83C\uDFC6</span> Completed Challenges\n </h3>\n <ul class=\"list-disc pl-5 space-y-1\">\n @for (challenge of flowState.challenges; track challenge) {\n <li>{{ challenge }}</li>\n }\n </ul>\n </div>\n }\n\n <details class=\"mt-4\">\n <summary class=\"cursor-pointer text-sm font-medium\">Raw JSON State</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ flowState | safeJson }}</pre>\n </details>\n } @else {\n <p class=\"italic mt-2 opacity-50\">No conversation state available.</p>\n }\n\n <details class=\"mt-2\">\n <summary class=\"cursor-pointer text-sm font-medium\">Current Prompt</summary>\n <pre class=\"mt-2 p-2 rounded text-xs overflow-auto\">{{ systemPromptForFlow() }}</pre>\n </details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"2\">\n <ng-template #content>\n <div>\n <p-button label=\"Exportar todo\" (click)=\"exportChat('all')\" />\n <p-button label=\"Exportar conversaci\u00F3n\" (click)=\"exportChat('conversation')\" />\n <p-button [label]=\"'Vista ' + viewMode()\" (click)=\"toggleViewMode()\" />\n </div>\n <dc-prompt-preview [messages]=\"messageStateService.getMessagesSignal()()\" [view]=\"viewMode()\"></dc-prompt-preview>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"6\">\n <ng-template #content>\n <dc-messages-state-inspector [service]=\"messageStateService\"></dc-messages-state-inspector>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"3\">\n <ng-template #content>\n <dc-cost-details></dc-cost-details>\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"4\">\n <ng-template #content>\n <!-- MORE INFO -->\n\n <details>\n <summary>Estado Global Conversation Flow</summary>\n <p>Goal</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.goal | safeJson }}</pre>\n <p>Trigger User Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onUserMessage | safeJson }}</pre>\n <p>Trigger Assistant Message</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.triggerTasks?.onAssistantMessage | safeJson }}</pre>\n <p>Conditions</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.dynamicConditions | safeJson }}</pre>\n <p>Tools</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.tools | safeJson }}</pre>\n <p>Challenges</p>\n <pre>{{ dynamicFlowService.conversationFlowConfig()?.challenges | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Conversation Settings (Debug)</summary>\n <pre>{{ conversationService.conversationSettings$() | safeJson }}</pre>\n </details>\n\n <details>\n <summary>Agent Card</summary>\n <pre>{{ agentCard | safeJson }}</pre>\n </details>\n\n <details>\n <summary>User Settings</summary>\n <pre>{{ chatUserSettings | safeJson }}</pre>\n </details>\n\n <!-- MORE INFO ends Here -->\n </ng-template>\n </p-tabpanel>\n <p-tabpanel value=\"7\">\n <ng-template #content>\n <dc-agent-card-detail [entityData]=\"agentCardStateService.agentCard$()\" />\n </ng-template>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n</div>\n", styles: [":host{overflow-y:scroll}.info-content{display:flex;flex-direction:column;gap:1rem;padding:1rem}\n"] }]
6300
6618
  }], ctorParameters: () => [], propDecorators: { completeEvent: [{ type: i0.Output, args: ["completeEvent"] }] } });
6301
6619
 
6302
6620
  class ConversationInfoService {
@@ -6347,7 +6665,9 @@ class DCChatComponent {
6347
6665
  // 📥 Input Signals
6348
6666
  this.parseDict = input({}, ...(ngDevMode ? [{ debugName: "parseDict" }] : /* istanbul ignore next */ []));
6349
6667
  // 📤 Outputs
6668
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
6350
6669
  this.chatEvent = output(); // Notifies about various events happening inside the chat
6670
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
6351
6671
  this.goalCompleted = output(); // notifies when user completes goal (score reaches 100)
6352
6672
  // readonly wordClicked = output<WordData>(); // Replaced by sendMessage with ChatEventType.WordClicked
6353
6673
  // Signals States
@@ -6360,6 +6680,7 @@ class DCChatComponent {
6360
6680
  effect(() => {
6361
6681
  const score = this.conversationFlowStateService.flowState().goal.value;
6362
6682
  if (score >= 100) {
6683
+ // TODO: check if i can use the monitor in the father so subscribe to this.
6363
6684
  this.goalCompleted.emit();
6364
6685
  }
6365
6686
  });
@@ -6373,19 +6694,6 @@ class DCChatComponent {
6373
6694
  // this.conversationService.notifyWordClicked(null);
6374
6695
  }
6375
6696
  });
6376
- // Effect to watch for audio playback events
6377
- effect(() => {
6378
- const message = this.chatMonitorService.messageAudioWillPlay$();
6379
- if (message) {
6380
- this.chatEvent.emit({ type: ChatEventType.AudioStarted, payload: message });
6381
- }
6382
- });
6383
- // Subscribe to mood updates
6384
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
6385
- if (mood) {
6386
- this.chatEvent.emit({ type: ChatEventType.MoodDetected, payload: mood });
6387
- }
6388
- });
6389
6697
  }
6390
6698
  async ngOnInit() {
6391
6699
  this.conversationService.setDestroyed(false);
@@ -6406,7 +6714,6 @@ class DCChatComponent {
6406
6714
  // Mark conversation as destroyed to prevent async operations
6407
6715
  this.conversationService.setDestroyed(true);
6408
6716
  this.chatMonitorService.cleanMonitorVars();
6409
- this.moodSubscription?.unsubscribe();
6410
6717
  // Ensure the microphone is stopped when the chat component is destroyed
6411
6718
  this.chatFooterComponent?.stopMic();
6412
6719
  this.checkPerformanceAnalysis();
@@ -6476,7 +6783,8 @@ class DCChatComponent {
6476
6783
  ConversationFlowStateService,
6477
6784
  DynamicFlowService,
6478
6785
  AgentCardStateService,
6479
- ChatMonitorService,
6786
+ // ChatMonitorService removido del providers[] local: ahora la instancia la provee el
6787
+ // componente ancestro (ConversationCardDetailsComponent) para compartirla con dc-agent-card-avatar.
6480
6788
  ContextEngineService,
6481
6789
  ], viewQueries: [{ propertyName: "chatFooterComponent", first: true, predicate: ChatFooterComponent, descendants: true }], ngImport: i0, template: "<div class=\"chat-container\">\n <dc-chat-header\n [agentCard]=\"agentCard\"\n [viewMode]=\"viewMode()\"\n (showInfoEvent)=\"showInfo()\"\n (settingsClickEvent)=\"changeUserChatSettings()\"\n (restartConversationEvent)=\"restartConversation($event)\"\n (completeEvent)=\"complete()\"\n (viewModeChanged)=\"viewMode.set($event)\">\n </dc-chat-header>\n\n @if (viewMode() === 'chat') {\n <dc-chat-messages-list [chatUserSettings]=\"chatUserSettings\"> </dc-chat-messages-list>\n <dc-chat-footer [micSettings]=\"micSettings\"> </dc-chat-footer>\n } @else {\n <dc-immersive-chat [micSettings]=\"micSettings\"></dc-immersive-chat>\n }\n\n <dc-polito-notification></dc-polito-notification>\n <dc-observer-intervention [customConfig]=\"observerConfig\"></dc-observer-intervention>\n</div>\n", styles: ["::ng-deep .p-drawer-content{padding:0!important}::ng-deep .p-dialog-content{display:contents}.chat-container{display:flex;flex-direction:column;height:100%;max-height:100vh;overflow:hidden;background-color:transparent;position:relative}dc-chat-messages-list{flex:1;overflow-y:auto;padding:.5rem}.score-container{padding:.5rem 1rem;background-color:var(--surface-card, #ffffff);border-top:1px solid var(--surface-border, #dee2e6)}.info-content{max-height:70vh;overflow-y:auto;padding:1rem}.info-content h3{margin-top:1rem;margin-bottom:.5rem;font-size:1.2rem}.info-content pre{background-color:var(--surface-hover, #f1f1f1);padding:1rem;border-radius:4px;overflow-x:auto;font-size:.9rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ChatHeaderComponent, selector: "dc-chat-header", inputs: ["alternativeConversation", "agentCard", "viewMode"], outputs: ["restartConversationEvent", "showInfoEvent", "settingsClickEvent", "viewModeChanged"] }, { kind: "component", type: ChatFooterComponent, selector: "dc-chat-footer", inputs: ["isAIThinking", "micSettings"], outputs: ["sendMessage", "textInputChanged"] }, { kind: "component", type: ChatMessagesListComponent, selector: "dc-chat-messages-list", inputs: ["chatUserSettings"] }, { kind: "ngmodule", type: ProgressBarModule }, { kind: "component", type: DcImmersiveChatComponent, selector: "dc-immersive-chat", inputs: ["micSettings"] }, { kind: "component", type: PolitoNotificationComponent, selector: "dc-polito-notification" }, { kind: "component", type: DcObserverInterventionComponent, selector: "dc-observer-intervention", inputs: ["customConfig"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6482
6790
  }
@@ -6503,7 +6811,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
6503
6811
  ConversationFlowStateService,
6504
6812
  DynamicFlowService,
6505
6813
  AgentCardStateService,
6506
- ChatMonitorService,
6814
+ // ChatMonitorService removido del providers[] local: ahora la instancia la provee el
6815
+ // componente ancestro (ConversationCardDetailsComponent) para compartirla con dc-agent-card-avatar.
6507
6816
  ContextEngineService,
6508
6817
  ], template: "<div class=\"chat-container\">\n <dc-chat-header\n [agentCard]=\"agentCard\"\n [viewMode]=\"viewMode()\"\n (showInfoEvent)=\"showInfo()\"\n (settingsClickEvent)=\"changeUserChatSettings()\"\n (restartConversationEvent)=\"restartConversation($event)\"\n (completeEvent)=\"complete()\"\n (viewModeChanged)=\"viewMode.set($event)\">\n </dc-chat-header>\n\n @if (viewMode() === 'chat') {\n <dc-chat-messages-list [chatUserSettings]=\"chatUserSettings\"> </dc-chat-messages-list>\n <dc-chat-footer [micSettings]=\"micSettings\"> </dc-chat-footer>\n } @else {\n <dc-immersive-chat [micSettings]=\"micSettings\"></dc-immersive-chat>\n }\n\n <dc-polito-notification></dc-polito-notification>\n <dc-observer-intervention [customConfig]=\"observerConfig\"></dc-observer-intervention>\n</div>\n", styles: ["::ng-deep .p-drawer-content{padding:0!important}::ng-deep .p-dialog-content{display:contents}.chat-container{display:flex;flex-direction:column;height:100%;max-height:100vh;overflow:hidden;background-color:transparent;position:relative}dc-chat-messages-list{flex:1;overflow-y:auto;padding:.5rem}.score-container{padding:.5rem 1rem;background-color:var(--surface-card, #ffffff);border-top:1px solid var(--surface-border, #dee2e6)}.info-content{max-height:70vh;overflow-y:auto;padding:1rem}.info-content h3{margin-top:1rem;margin-bottom:.5rem;font-size:1.2rem}.info-content pre{background-color:var(--surface-hover, #f1f1f1);padding:1rem;border-radius:4px;overflow-x:auto;font-size:.9rem}\n"] }]
6509
6818
  }], ctorParameters: () => [], propDecorators: { chatFooterComponent: [{
@@ -7496,7 +7805,7 @@ class CharacterFormGroupService {
7496
7805
  conversationFlow: this.createConversationFlowFormGroup(),
7497
7806
  manageable: this.formUtils.createManageableFormGroup(),
7498
7807
  learnable: this.formUtils.createLearnableFormGroup(),
7499
- voiceCloning: this.createVoiceCloningFormGroup(),
7808
+ voice: this.createAgentVoiceFormGroup(),
7500
7809
  agenticConfig: this.createAgenticConfigFormGroup(),
7501
7810
  live2d: [null],
7502
7811
  avatarType: [null],
@@ -7504,6 +7813,22 @@ class CharacterFormGroupService {
7504
7813
  }
7505
7814
  patchFormWithConversationData(form, agentCard) {
7506
7815
  form.patchValue(agentCard);
7816
+ if (!agentCard.voice) {
7817
+ const voicePatch = {};
7818
+ if (agentCard.conversationSettings?.mainVoice) {
7819
+ voicePatch.main = agentCard.conversationSettings.mainVoice;
7820
+ }
7821
+ if (agentCard.conversationSettings?.secondaryVoice) {
7822
+ voicePatch.secondary = agentCard.conversationSettings.secondaryVoice;
7823
+ }
7824
+ if (agentCard.voiceCloning) {
7825
+ voicePatch.cloning = {
7826
+ sample: agentCard.voiceCloning.sample,
7827
+ transcription: agentCard.voiceCloning.transcription
7828
+ };
7829
+ }
7830
+ form.get('voice')?.patchValue(voicePatch);
7831
+ }
7507
7832
  // Patch dynamicConditions FormArray after the main form is patched
7508
7833
  const dynamicConditionsFormArray = form.get('conversationFlow.dynamicConditions');
7509
7834
  dynamicConditionsFormArray.clear(); // Clear any existing default items
@@ -7673,8 +7998,6 @@ class CharacterFormGroupService {
7673
7998
  userMustStart: [false],
7674
7999
  streamResponses: [false],
7675
8000
  displayMode: ['chat'],
7676
- mainVoice: this.createVoiceTTSFormGroup(),
7677
- secondaryVoice: this.createVoiceTTSFormGroup(),
7678
8001
  model: createAIModelFormGroup(),
7679
8002
  rules: this.fb.array([]),
7680
8003
  chatMode: ['regular'],
@@ -7777,11 +8100,14 @@ class CharacterFormGroupService {
7777
8100
  emoji: [criteria?.emoji || ''],
7778
8101
  });
7779
8102
  }
7780
- createVoiceCloningFormGroup() {
8103
+ createAgentVoiceFormGroup(voice) {
7781
8104
  return this.fb.group({
7782
- sample: [],
7783
- transcription: [''],
7784
- main: this.createVoiceTTSFormGroup(),
8105
+ main: this.createVoiceTTSFormGroup(voice?.main),
8106
+ secondary: this.createVoiceTTSFormGroup(voice?.secondary),
8107
+ cloning: this.fb.group({
8108
+ sample: [voice?.cloning?.sample || null],
8109
+ transcription: [voice?.cloning?.transcription || ''],
8110
+ })
7785
8111
  });
7786
8112
  }
7787
8113
  createAgenticConfigFormGroup() {
@@ -8328,47 +8654,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8328
8654
  type: Output
8329
8655
  }] } });
8330
8656
 
8331
- class DcVoiceTtsFormComponent {
8332
- constructor() {
8333
- this.dialogService = inject(DialogService);
8334
- this.cdr = inject(ChangeDetectorRef);
8335
- this.form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
8336
- this.title = input('Voice TTS Settings', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
8337
- this.voiceTTSOptions = input([], ...(ngDevMode ? [{ debugName: "voiceTTSOptions" }] : /* istanbul ignore next */ []));
8338
- this.fullForm = input(false, ...(ngDevMode ? [{ debugName: "fullForm" }] : /* istanbul ignore next */ []));
8339
- this.providerOptions = [
8340
- { label: 'Google', value: 'google' },
8341
- { label: 'Fish Audio', value: 'fish-audio' },
8342
- ];
8343
- }
8344
- openVoiceSelector() {
8345
- const voiceControl = this.form().get('voice');
8346
- const dialogRef = this.dialogService.open(VoiceSelectorComponent, {
8347
- header: 'Select Voice',
8348
- width: '50vw',
8349
- maximizable: true,
8350
- height: '500px',
8351
- contentStyle: { 'max-height': '90vh', overflow: 'auto', 'min-height': '500px' },
8352
- baseZIndex: 10000,
8353
- closable: true,
8354
- modal: true,
8355
- data: { currentVoiceId: voiceControl?.value },
8356
- });
8357
- dialogRef.onClose.subscribe((selectedVoiceId) => {
8358
- if (selectedVoiceId) {
8359
- voiceControl?.setValue(selectedVoiceId);
8360
- this.cdr.detectChanges();
8361
- }
8362
- });
8363
- }
8364
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8365
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcVoiceTtsFormComponent, isStandalone: true, selector: "dc-voice-tts-form", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, voiceTTSOptions: { classPropertyName: "voiceTTSOptions", publicName: "voiceTTSOptions", isSignal: true, isRequired: false, transformFunction: null }, fullForm: { classPropertyName: "fullForm", publicName: "fullForm", isSignal: true, isRequired: false, transformFunction: null } }, providers: [DialogService], ngImport: i0, template: "<h3> {{ title() }}</h3>\n<div [formGroup]=\"form()\" class=\"form-grid\">\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"provider\">Provider <span pTooltip=\"TTS provider\">\u2139\uFE0F</span></label>\n <p-select id=\"provider\" [options]=\"providerOptions\" formControlName=\"provider\" optionLabel=\"label\" optionValue=\"value\" placeholder=\"Select Provider\" class=\"w-full\"></p-select>\n </div>\n }\n\n <div class=\"form-field\">\n <label for=\"voice\">Voice <span pTooltip=\"Select the voice for text-to-speech\">\u2139\uFE0F</span></label>\n <p-inputgroup>\n <p-inputgroup-addon>\n <p-button [rounded]=\"true\" [text]=\"true\" icon=\"pi pi-exclamation-circle\" (click)=\"openVoiceSelector()\" />\n </p-inputgroup-addon>\n <p-select\n id=\"voice\"\n [editable]=\"true\"\n [options]=\"voiceTTSOptions()\"\n formControlName=\"voice\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [placeholder]=\"'Select Voice'\" />\n </p-inputgroup>\n </div>\n\n <div class=\"form-field\">\n <label for=\"speedRate\">Speed Rate <span pTooltip=\"Adjust the rate of speech delivery\">\u2139\uFE0F</span></label>\n <br />\n <input pInputText id=\"speedRate\" type=\"number\" formControlName=\"speedRate\" step=\"0.1\" />\n </div>\n\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"model\">Model <span pTooltip=\"Model ID if the provider requires it\">\u2139\uFE0F</span></label>\n <input pInputText id=\"model\" type=\"text\" formControlName=\"model\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Language code override (optional, usually included in the voice ID)\">\u2139\uFE0F</span></label>\n <input pInputText id=\"lang\" type=\"text\" formControlName=\"lang\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"effect\">Effect <span pTooltip=\"Platform audio effect\">\u2139\uFE0F</span></label>\n <input pInputText id=\"effect\" type=\"text\" formControlName=\"effect\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputGroupModule }, { kind: "component", type: i3$4.InputGroup, selector: "p-inputgroup, p-inputGroup, p-input-group", inputs: ["styleClass"] }, { kind: "ngmodule", type: InputGroupAddonModule }, { kind: "component", type: i4$4.InputGroupAddon, selector: "p-inputgroup-addon, p-inputGroupAddon", inputs: ["style", "styleClass"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3$3.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: CardModule }] }); }
8366
- }
8367
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, decorators: [{
8368
- type: Component,
8369
- args: [{ selector: 'dc-voice-tts-form', standalone: true, imports: [ReactiveFormsModule, DynamicDialogModule, ButtonModule, InputGroupModule, InputGroupAddonModule, SelectModule, InputTextModule, CardModule], providers: [DialogService], template: "<h3> {{ title() }}</h3>\n<div [formGroup]=\"form()\" class=\"form-grid\">\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"provider\">Provider <span pTooltip=\"TTS provider\">\u2139\uFE0F</span></label>\n <p-select id=\"provider\" [options]=\"providerOptions\" formControlName=\"provider\" optionLabel=\"label\" optionValue=\"value\" placeholder=\"Select Provider\" class=\"w-full\"></p-select>\n </div>\n }\n\n <div class=\"form-field\">\n <label for=\"voice\">Voice <span pTooltip=\"Select the voice for text-to-speech\">\u2139\uFE0F</span></label>\n <p-inputgroup>\n <p-inputgroup-addon>\n <p-button [rounded]=\"true\" [text]=\"true\" icon=\"pi pi-exclamation-circle\" (click)=\"openVoiceSelector()\" />\n </p-inputgroup-addon>\n <p-select\n id=\"voice\"\n [editable]=\"true\"\n [options]=\"voiceTTSOptions()\"\n formControlName=\"voice\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [placeholder]=\"'Select Voice'\" />\n </p-inputgroup>\n </div>\n\n <div class=\"form-field\">\n <label for=\"speedRate\">Speed Rate <span pTooltip=\"Adjust the rate of speech delivery\">\u2139\uFE0F</span></label>\n <br />\n <input pInputText id=\"speedRate\" type=\"number\" formControlName=\"speedRate\" step=\"0.1\" />\n </div>\n\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"model\">Model <span pTooltip=\"Model ID if the provider requires it\">\u2139\uFE0F</span></label>\n <input pInputText id=\"model\" type=\"text\" formControlName=\"model\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Language code override (optional, usually included in the voice ID)\">\u2139\uFE0F</span></label>\n <input pInputText id=\"lang\" type=\"text\" formControlName=\"lang\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"effect\">Effect <span pTooltip=\"Platform audio effect\">\u2139\uFE0F</span></label>\n <input pInputText id=\"effect\" type=\"text\" formControlName=\"effect\" />\n </div>\n }\n</div>\n" }]
8370
- }], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], voiceTTSOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "voiceTTSOptions", required: false }] }], fullForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullForm", required: false }] }] } });
8371
-
8372
8657
  class PromptConversationTypesDialogComponent {
8373
8658
  constructor() {
8374
8659
  this.dialogRef = inject(DynamicDialogRef);
@@ -8438,12 +8723,6 @@ class DcConversationSettingsFormComponent {
8438
8723
  get rulesFormArray() {
8439
8724
  return this.form.get('rules');
8440
8725
  }
8441
- get mainVoiceFormGroup() {
8442
- return this.form.get('mainVoice');
8443
- }
8444
- get secondaryVoiceFormGroup() {
8445
- return this.form.get('secondaryVoice');
8446
- }
8447
8726
  get modelFormGroup() {
8448
8727
  return this.form.get('model');
8449
8728
  }
@@ -8475,7 +8754,7 @@ class DcConversationSettingsFormComponent {
8475
8754
  this.getRules();
8476
8755
  }
8477
8756
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcConversationSettingsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8478
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcConversationSettingsFormComponent, isStandalone: true, selector: "dc-conversation-settings-form", inputs: { form: "form", textEngineOptions: "textEngineOptions", conversationOptions: "conversationOptions", voiceTTSOptions: "voiceTTSOptions" }, providers: [DialogService], ngImport: i0, template: "<div [formGroup]=\"form\">\n <h3 class=\"text-xl font-bold border-b pb-2\">\n Chat Conversation Settings\n <span pTooltip=\"Additional information about the conversation use in the chat\" class=\"text-blue-500 cursor-help text-sm\">\u2139\uFE0F</span>\n </h3>\n\n <div>\n <label for=\"rules\" class=\"block text-sm font-medium\">\n Conversation Rules\n <span pTooltip=\"Add rules to the conversation\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select id=\"rules\" [options]=\"rules\" (onChange)=\"addRule($event.value)\" placeholder=\"Select a Rule\" optionLabel=\"name\" class=\"w-full\"></p-select>\n\n <div formArrayName=\"rules\" class=\"mt-4 space-y-2\">\n @for (rule of rulesFormArray.controls; track rule; let i = $index) {\n <div [formGroupName]=\"i\" class=\"flex items-center justify-between p-2 border rounded-md\">\n <span>{{ rule.value.name }}</span>\n <button pButton type=\"button\" icon=\"pi pi-trash\" class=\"p-button-danger p-button-text\" (click)=\"removeRule(i)\"></button>\n </div>\n }\n </div>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <div>\n <label for=\"conversationType\" class=\"block text-sm font-medium\">\n Conversation Type\n <span class=\"cursor-pointer text-blue-500\" (click)=\"openConversationTypeDialog()\" pTooltip=\"Choose the type of conversation interaction\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"conversationType\"\n [options]=\"conversationOptions\"\n formControlName=\"conversationType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Conversation Type'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"textEngine\" class=\"block text-sm font-medium\">\n Text Engine\n <span\n class=\"cursor-pointer text-blue-500\"\n (click)=\"textEngineDialog.toggle($event)\"\n pTooltip=\"Sistema de generaci\u00F3n de texto y audios. Client: el cliente llama al servidor en cada dialogo de voz/personaje, es optimo para historias, Server SSML: se sintetiza todo el audio en uno solo con los distintos cambios de voz/personaje, util para la reflexi\u00F3n porque es bilingue, utiliza dialogos en ingles y espa\u00F1ol en el mismo dialogo/audio\"\n >\u2139\uFE0F</span\n >\n </label>\n <p-select\n id=\"textEngine\"\n [options]=\"textEngineOptions\"\n formControlName=\"textEngine\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Text Engine'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Auto Start\n <span pTooltip=\"Start conversation automatically\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"autoStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"md:col-span-2\">\n <label for=\"displayMode\" class=\"block text-sm font-medium text-red-500\">\n Display Mode (Deprecated Soon)\n <span pTooltip=\"[Deprecated] Use Chat Mode and Avatar Display Mode instead.\" class=\"text-red-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"displayMode\"\n [options]=\"displayModeOptions\"\n formControlName=\"displayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"chatMode\" class=\"block text-sm font-medium\">\n Chat Mode\n <span pTooltip=\"regular: standard scrolling message list | immersive: voice-first Heads-Up Display (HUD)\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"chatMode\"\n [options]=\"chatModeOptions\"\n formControlName=\"chatMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Chat Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"avatarDisplayMode\" class=\"block text-sm font-medium\">\n Avatar Display Mode\n <span pTooltip=\"card: enclosed box layout | transparent: floats overlaying background | fullscreen: stretches model to fill page behind chat\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"avatarDisplayMode\"\n [options]=\"avatarDisplayModeOptions\"\n formControlName=\"avatarDisplayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n User Must Start\n <span pTooltip=\"If true the user must start the conversation, ignoring first message if any.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"userMustStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Stream Responses\n <span pTooltip=\"Enable real-time token streaming for LLM responses.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"streamResponses\"></p-toggleSwitch>\n </div>\n </div>\n\n <hr />\n\n <dc-voice-tts-form [form]=\"mainVoiceFormGroup\" [title]=\"'Main Voice TTS Settings'\" [voiceTTSOptions]=\"voiceTTSOptions\"></dc-voice-tts-form>\n\n <dc-voice-tts-form [form]=\"secondaryVoiceFormGroup\" [title]=\"'Secondary Voice TTS Settings'\" [voiceTTSOptions]=\"voiceTTSOptions\"></dc-voice-tts-form>\n\n <hr />\n\n <dc-model-selector [modelForm]=\"modelFormGroup\" [shortForm]=\"true\"></dc-model-selector>\n <hr />\n</div>\n\n<p-popover #textEngineDialog [style]=\"{ width: '350px' }\" header=\"Text Engine Information\">\n <ng-template pTemplate=\"content\">\n <div class=\"p-4\">\n <h3 class=\"text-md font-semibold mb-3 text-gray-800\">Text Engine Types</h3>\n <ul class=\"space-y-3 text-sm text-gray-600\">\n <li>\n <strong class=\"font-semibold text-gray-900\">Texto Simple:</strong>\n La conversaci\u00F3n es como chatgpt, preguntas y responde, es la m\u00E1s b\u00E1sica.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">Multi Mensajes:</strong>\n Utiliza markdown (recomendable entenderlo), para dar formato al texto. El sistema puede partir dialogos con distinto formato (normal, cursiva,\n negritas) para generar distintas voces y estilos para el narrador y personaje.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">MD SSML:</strong>\n Markdown con SSML. Similar a Multi Mensajes, pero se presenta en un solo mensaje y la voz se genera para toda la linea. \u00DAtil para conversaciones\n biling\u00FCes.\n </li>\n </ul>\n </div>\n </ng-template>\n</p-popover>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "directive", type: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: ToggleSwitchModule }, { kind: "component", type: i4$3.ToggleSwitch, selector: "p-toggleswitch, p-toggleSwitch, p-toggle-switch", inputs: ["styleClass", "tabindex", "inputId", "readonly", "trueValue", "falseValue", "ariaLabel", "size", "ariaLabelledBy", "autofocus"], outputs: ["onChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i2$2.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "component", type: ModelSelectorComponent, selector: "dc-model-selector", inputs: ["modelForm", "shortForm"] }, { kind: "component", type: DcVoiceTtsFormComponent, selector: "dc-voice-tts-form", inputs: ["form", "title", "voiceTTSOptions", "fullForm"] }] }); }
8757
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcConversationSettingsFormComponent, isStandalone: true, selector: "dc-conversation-settings-form", inputs: { form: "form", textEngineOptions: "textEngineOptions", conversationOptions: "conversationOptions", voiceTTSOptions: "voiceTTSOptions" }, providers: [DialogService], ngImport: i0, template: "<div [formGroup]=\"form\">\n <h3 class=\"text-xl font-bold border-b pb-2\">\n Chat Conversation Settings\n <span pTooltip=\"Additional information about the conversation use in the chat\" class=\"text-blue-500 cursor-help text-sm\">\u2139\uFE0F</span>\n </h3>\n\n <div>\n <label for=\"rules\" class=\"block text-sm font-medium\">\n Conversation Rules\n <span pTooltip=\"Add rules to the conversation\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select id=\"rules\" [options]=\"rules\" (onChange)=\"addRule($event.value)\" placeholder=\"Select a Rule\" optionLabel=\"name\" class=\"w-full\"></p-select>\n\n <div formArrayName=\"rules\" class=\"mt-4 space-y-2\">\n @for (rule of rulesFormArray.controls; track rule; let i = $index) {\n <div [formGroupName]=\"i\" class=\"flex items-center justify-between p-2 border rounded-md\">\n <span>{{ rule.value.name }}</span>\n <button pButton type=\"button\" icon=\"pi pi-trash\" class=\"p-button-danger p-button-text\" (click)=\"removeRule(i)\"></button>\n </div>\n }\n </div>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <div>\n <label for=\"conversationType\" class=\"block text-sm font-medium\">\n Conversation Type\n <span class=\"cursor-pointer text-blue-500\" (click)=\"openConversationTypeDialog()\" pTooltip=\"Choose the type of conversation interaction\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"conversationType\"\n [options]=\"conversationOptions\"\n formControlName=\"conversationType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Conversation Type'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"textEngine\" class=\"block text-sm font-medium\">\n Text Engine\n <span\n class=\"cursor-pointer text-blue-500\"\n (click)=\"textEngineDialog.toggle($event)\"\n pTooltip=\"Sistema de generaci\u00F3n de texto y audios. Client: el cliente llama al servidor en cada dialogo de voz/personaje, es optimo para historias, Server SSML: se sintetiza todo el audio en uno solo con los distintos cambios de voz/personaje, util para la reflexi\u00F3n porque es bilingue, utiliza dialogos en ingles y espa\u00F1ol en el mismo dialogo/audio\"\n >\u2139\uFE0F</span\n >\n </label>\n <p-select\n id=\"textEngine\"\n [options]=\"textEngineOptions\"\n formControlName=\"textEngine\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Text Engine'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Auto Start\n <span pTooltip=\"Start conversation automatically\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"autoStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"md:col-span-2\">\n <label for=\"displayMode\" class=\"block text-sm font-medium text-red-500\">\n Display Mode (Deprecated Soon)\n <span pTooltip=\"[Deprecated] Use Chat Mode and Avatar Display Mode instead.\" class=\"text-red-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"displayMode\"\n [options]=\"displayModeOptions\"\n formControlName=\"displayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"chatMode\" class=\"block text-sm font-medium\">\n Chat Mode\n <span pTooltip=\"regular: standard scrolling message list | immersive: voice-first Heads-Up Display (HUD)\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"chatMode\"\n [options]=\"chatModeOptions\"\n formControlName=\"chatMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Chat Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"avatarDisplayMode\" class=\"block text-sm font-medium\">\n Avatar Display Mode\n <span pTooltip=\"card: enclosed box layout | transparent: floats overlaying background | fullscreen: stretches model to fill page behind chat\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"avatarDisplayMode\"\n [options]=\"avatarDisplayModeOptions\"\n formControlName=\"avatarDisplayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n User Must Start\n <span pTooltip=\"If true the user must start the conversation, ignoring first message if any.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"userMustStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Stream Responses\n <span pTooltip=\"Enable real-time token streaming for LLM responses.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"streamResponses\"></p-toggleSwitch>\n </div>\n </div>\n\n <hr />\n\n <dc-model-selector [modelForm]=\"modelFormGroup\" [shortForm]=\"true\"></dc-model-selector>\n <hr />\n</div>\n\n<p-popover #textEngineDialog [style]=\"{ width: '350px' }\" header=\"Text Engine Information\">\n <ng-template pTemplate=\"content\">\n <div class=\"p-4\">\n <h3 class=\"text-md font-semibold mb-3 text-gray-800\">Text Engine Types</h3>\n <ul class=\"space-y-3 text-sm text-gray-600\">\n <li>\n <strong class=\"font-semibold text-gray-900\">Texto Simple:</strong>\n La conversaci\u00F3n es como chatgpt, preguntas y responde, es la m\u00E1s b\u00E1sica.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">Multi Mensajes:</strong>\n Utiliza markdown (recomendable entenderlo), para dar formato al texto. El sistema puede partir dialogos con distinto formato (normal, cursiva,\n negritas) para generar distintas voces y estilos para el narrador y personaje.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">MD SSML:</strong>\n Markdown con SSML. Similar a Multi Mensajes, pero se presenta en un solo mensaje y la voz se genera para toda la linea. \u00DAtil para conversaciones\n biling\u00FCes.\n </li>\n </ul>\n </div>\n </ng-template>\n</p-popover>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "directive", type: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: ToggleSwitchModule }, { kind: "component", type: i4$3.ToggleSwitch, selector: "p-toggleswitch, p-toggleSwitch, p-toggle-switch", inputs: ["styleClass", "tabindex", "inputId", "readonly", "trueValue", "falseValue", "ariaLabel", "size", "ariaLabelledBy", "autofocus"], outputs: ["onChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i2$2.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "component", type: ModelSelectorComponent, selector: "dc-model-selector", inputs: ["modelForm", "shortForm"] }] }); }
8479
8758
  }
8480
8759
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcConversationSettingsFormComponent, decorators: [{
8481
8760
  type: Component,
@@ -8489,8 +8768,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8489
8768
  PopoverModule,
8490
8769
  DynamicDialogModule,
8491
8770
  ModelSelectorComponent,
8492
- DcVoiceTtsFormComponent,
8493
- ], providers: [DialogService], template: "<div [formGroup]=\"form\">\n <h3 class=\"text-xl font-bold border-b pb-2\">\n Chat Conversation Settings\n <span pTooltip=\"Additional information about the conversation use in the chat\" class=\"text-blue-500 cursor-help text-sm\">\u2139\uFE0F</span>\n </h3>\n\n <div>\n <label for=\"rules\" class=\"block text-sm font-medium\">\n Conversation Rules\n <span pTooltip=\"Add rules to the conversation\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select id=\"rules\" [options]=\"rules\" (onChange)=\"addRule($event.value)\" placeholder=\"Select a Rule\" optionLabel=\"name\" class=\"w-full\"></p-select>\n\n <div formArrayName=\"rules\" class=\"mt-4 space-y-2\">\n @for (rule of rulesFormArray.controls; track rule; let i = $index) {\n <div [formGroupName]=\"i\" class=\"flex items-center justify-between p-2 border rounded-md\">\n <span>{{ rule.value.name }}</span>\n <button pButton type=\"button\" icon=\"pi pi-trash\" class=\"p-button-danger p-button-text\" (click)=\"removeRule(i)\"></button>\n </div>\n }\n </div>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <div>\n <label for=\"conversationType\" class=\"block text-sm font-medium\">\n Conversation Type\n <span class=\"cursor-pointer text-blue-500\" (click)=\"openConversationTypeDialog()\" pTooltip=\"Choose the type of conversation interaction\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"conversationType\"\n [options]=\"conversationOptions\"\n formControlName=\"conversationType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Conversation Type'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"textEngine\" class=\"block text-sm font-medium\">\n Text Engine\n <span\n class=\"cursor-pointer text-blue-500\"\n (click)=\"textEngineDialog.toggle($event)\"\n pTooltip=\"Sistema de generaci\u00F3n de texto y audios. Client: el cliente llama al servidor en cada dialogo de voz/personaje, es optimo para historias, Server SSML: se sintetiza todo el audio en uno solo con los distintos cambios de voz/personaje, util para la reflexi\u00F3n porque es bilingue, utiliza dialogos en ingles y espa\u00F1ol en el mismo dialogo/audio\"\n >\u2139\uFE0F</span\n >\n </label>\n <p-select\n id=\"textEngine\"\n [options]=\"textEngineOptions\"\n formControlName=\"textEngine\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Text Engine'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Auto Start\n <span pTooltip=\"Start conversation automatically\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"autoStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"md:col-span-2\">\n <label for=\"displayMode\" class=\"block text-sm font-medium text-red-500\">\n Display Mode (Deprecated Soon)\n <span pTooltip=\"[Deprecated] Use Chat Mode and Avatar Display Mode instead.\" class=\"text-red-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"displayMode\"\n [options]=\"displayModeOptions\"\n formControlName=\"displayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"chatMode\" class=\"block text-sm font-medium\">\n Chat Mode\n <span pTooltip=\"regular: standard scrolling message list | immersive: voice-first Heads-Up Display (HUD)\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"chatMode\"\n [options]=\"chatModeOptions\"\n formControlName=\"chatMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Chat Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"avatarDisplayMode\" class=\"block text-sm font-medium\">\n Avatar Display Mode\n <span pTooltip=\"card: enclosed box layout | transparent: floats overlaying background | fullscreen: stretches model to fill page behind chat\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"avatarDisplayMode\"\n [options]=\"avatarDisplayModeOptions\"\n formControlName=\"avatarDisplayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n User Must Start\n <span pTooltip=\"If true the user must start the conversation, ignoring first message if any.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"userMustStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Stream Responses\n <span pTooltip=\"Enable real-time token streaming for LLM responses.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"streamResponses\"></p-toggleSwitch>\n </div>\n </div>\n\n <hr />\n\n <dc-voice-tts-form [form]=\"mainVoiceFormGroup\" [title]=\"'Main Voice TTS Settings'\" [voiceTTSOptions]=\"voiceTTSOptions\"></dc-voice-tts-form>\n\n <dc-voice-tts-form [form]=\"secondaryVoiceFormGroup\" [title]=\"'Secondary Voice TTS Settings'\" [voiceTTSOptions]=\"voiceTTSOptions\"></dc-voice-tts-form>\n\n <hr />\n\n <dc-model-selector [modelForm]=\"modelFormGroup\" [shortForm]=\"true\"></dc-model-selector>\n <hr />\n</div>\n\n<p-popover #textEngineDialog [style]=\"{ width: '350px' }\" header=\"Text Engine Information\">\n <ng-template pTemplate=\"content\">\n <div class=\"p-4\">\n <h3 class=\"text-md font-semibold mb-3 text-gray-800\">Text Engine Types</h3>\n <ul class=\"space-y-3 text-sm text-gray-600\">\n <li>\n <strong class=\"font-semibold text-gray-900\">Texto Simple:</strong>\n La conversaci\u00F3n es como chatgpt, preguntas y responde, es la m\u00E1s b\u00E1sica.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">Multi Mensajes:</strong>\n Utiliza markdown (recomendable entenderlo), para dar formato al texto. El sistema puede partir dialogos con distinto formato (normal, cursiva,\n negritas) para generar distintas voces y estilos para el narrador y personaje.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">MD SSML:</strong>\n Markdown con SSML. Similar a Multi Mensajes, pero se presenta en un solo mensaje y la voz se genera para toda la linea. \u00DAtil para conversaciones\n biling\u00FCes.\n </li>\n </ul>\n </div>\n </ng-template>\n</p-popover>\n" }]
8771
+ ], providers: [DialogService], template: "<div [formGroup]=\"form\">\n <h3 class=\"text-xl font-bold border-b pb-2\">\n Chat Conversation Settings\n <span pTooltip=\"Additional information about the conversation use in the chat\" class=\"text-blue-500 cursor-help text-sm\">\u2139\uFE0F</span>\n </h3>\n\n <div>\n <label for=\"rules\" class=\"block text-sm font-medium\">\n Conversation Rules\n <span pTooltip=\"Add rules to the conversation\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select id=\"rules\" [options]=\"rules\" (onChange)=\"addRule($event.value)\" placeholder=\"Select a Rule\" optionLabel=\"name\" class=\"w-full\"></p-select>\n\n <div formArrayName=\"rules\" class=\"mt-4 space-y-2\">\n @for (rule of rulesFormArray.controls; track rule; let i = $index) {\n <div [formGroupName]=\"i\" class=\"flex items-center justify-between p-2 border rounded-md\">\n <span>{{ rule.value.name }}</span>\n <button pButton type=\"button\" icon=\"pi pi-trash\" class=\"p-button-danger p-button-text\" (click)=\"removeRule(i)\"></button>\n </div>\n }\n </div>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <div>\n <label for=\"conversationType\" class=\"block text-sm font-medium\">\n Conversation Type\n <span class=\"cursor-pointer text-blue-500\" (click)=\"openConversationTypeDialog()\" pTooltip=\"Choose the type of conversation interaction\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"conversationType\"\n [options]=\"conversationOptions\"\n formControlName=\"conversationType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Conversation Type'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"textEngine\" class=\"block text-sm font-medium\">\n Text Engine\n <span\n class=\"cursor-pointer text-blue-500\"\n (click)=\"textEngineDialog.toggle($event)\"\n pTooltip=\"Sistema de generaci\u00F3n de texto y audios. Client: el cliente llama al servidor en cada dialogo de voz/personaje, es optimo para historias, Server SSML: se sintetiza todo el audio en uno solo con los distintos cambios de voz/personaje, util para la reflexi\u00F3n porque es bilingue, utiliza dialogos en ingles y espa\u00F1ol en el mismo dialogo/audio\"\n >\u2139\uFE0F</span\n >\n </label>\n <p-select\n id=\"textEngine\"\n [options]=\"textEngineOptions\"\n formControlName=\"textEngine\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Text Engine'\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Auto Start\n <span pTooltip=\"Start conversation automatically\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"autoStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"md:col-span-2\">\n <label for=\"displayMode\" class=\"block text-sm font-medium text-red-500\">\n Display Mode (Deprecated Soon)\n <span pTooltip=\"[Deprecated] Use Chat Mode and Avatar Display Mode instead.\" class=\"text-red-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"displayMode\"\n [options]=\"displayModeOptions\"\n formControlName=\"displayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"chatMode\" class=\"block text-sm font-medium\">\n Chat Mode\n <span pTooltip=\"regular: standard scrolling message list | immersive: voice-first Heads-Up Display (HUD)\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"chatMode\"\n [options]=\"chatModeOptions\"\n formControlName=\"chatMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Chat Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div>\n <label for=\"avatarDisplayMode\" class=\"block text-sm font-medium\">\n Avatar Display Mode\n <span pTooltip=\"card: enclosed box layout | transparent: floats overlaying background | fullscreen: stretches model to fill page behind chat\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-select\n id=\"avatarDisplayMode\"\n [options]=\"avatarDisplayModeOptions\"\n formControlName=\"avatarDisplayMode\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Display Mode\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n User Must Start\n <span pTooltip=\"If true the user must start the conversation, ignoring first message if any.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"userMustStart\"></p-toggleSwitch>\n </div>\n\n <div class=\"flex items-center justify-between md:col-span-2\">\n <label class=\"text-sm font-medium\">\n Stream Responses\n <span pTooltip=\"Enable real-time token streaming for LLM responses.\" class=\"text-blue-500 cursor-help\">\u2139\uFE0F</span>\n </label>\n <p-toggleSwitch formControlName=\"streamResponses\"></p-toggleSwitch>\n </div>\n </div>\n\n <hr />\n\n <dc-model-selector [modelForm]=\"modelFormGroup\" [shortForm]=\"true\"></dc-model-selector>\n <hr />\n</div>\n\n<p-popover #textEngineDialog [style]=\"{ width: '350px' }\" header=\"Text Engine Information\">\n <ng-template pTemplate=\"content\">\n <div class=\"p-4\">\n <h3 class=\"text-md font-semibold mb-3 text-gray-800\">Text Engine Types</h3>\n <ul class=\"space-y-3 text-sm text-gray-600\">\n <li>\n <strong class=\"font-semibold text-gray-900\">Texto Simple:</strong>\n La conversaci\u00F3n es como chatgpt, preguntas y responde, es la m\u00E1s b\u00E1sica.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">Multi Mensajes:</strong>\n Utiliza markdown (recomendable entenderlo), para dar formato al texto. El sistema puede partir dialogos con distinto formato (normal, cursiva,\n negritas) para generar distintas voces y estilos para el narrador y personaje.\n </li>\n <li>\n <strong class=\"font-semibold text-gray-900\">MD SSML:</strong>\n Markdown con SSML. Similar a Multi Mensajes, pero se presenta en un solo mensaje y la voz se genera para toda la linea. \u00DAtil para conversaciones\n biling\u00FCes.\n </li>\n </ul>\n </div>\n </ng-template>\n</p-popover>\n" }]
8494
8772
  }], propDecorators: { form: [{
8495
8773
  type: Input
8496
8774
  }], textEngineOptions: [{
@@ -8501,6 +8779,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8501
8779
  type: Input
8502
8780
  }] } });
8503
8781
 
8782
+ class DcVoiceTtsFormComponent {
8783
+ constructor() {
8784
+ this.dialogService = inject(DialogService);
8785
+ this.cdr = inject(ChangeDetectorRef);
8786
+ this.form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
8787
+ this.title = input('Voice TTS Settings', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
8788
+ this.voiceTTSOptions = input([], ...(ngDevMode ? [{ debugName: "voiceTTSOptions" }] : /* istanbul ignore next */ []));
8789
+ this.fullForm = input(false, ...(ngDevMode ? [{ debugName: "fullForm" }] : /* istanbul ignore next */ []));
8790
+ this.providerOptions = [
8791
+ { label: 'Google', value: 'google' },
8792
+ { label: 'Fish Audio', value: 'fish-audio' },
8793
+ ];
8794
+ }
8795
+ openVoiceSelector() {
8796
+ const voiceControl = this.form().get('voice');
8797
+ const dialogRef = this.dialogService.open(VoiceSelectorComponent, {
8798
+ header: 'Select Voice',
8799
+ width: '50vw',
8800
+ maximizable: true,
8801
+ height: '500px',
8802
+ contentStyle: { 'max-height': '90vh', overflow: 'auto', 'min-height': '500px' },
8803
+ baseZIndex: 10000,
8804
+ closable: true,
8805
+ modal: true,
8806
+ data: { currentVoiceId: voiceControl?.value },
8807
+ });
8808
+ dialogRef.onClose.subscribe((selectedVoiceId) => {
8809
+ if (selectedVoiceId) {
8810
+ voiceControl?.setValue(selectedVoiceId);
8811
+ this.cdr.detectChanges();
8812
+ }
8813
+ });
8814
+ }
8815
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8816
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcVoiceTtsFormComponent, isStandalone: true, selector: "dc-voice-tts-form", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, voiceTTSOptions: { classPropertyName: "voiceTTSOptions", publicName: "voiceTTSOptions", isSignal: true, isRequired: false, transformFunction: null }, fullForm: { classPropertyName: "fullForm", publicName: "fullForm", isSignal: true, isRequired: false, transformFunction: null } }, providers: [DialogService], ngImport: i0, template: "<h3> {{ title() }}</h3>\n<div [formGroup]=\"form()\" class=\"form-grid\">\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"provider\">Provider <span pTooltip=\"TTS provider\">\u2139\uFE0F</span></label>\n <p-select id=\"provider\" [options]=\"providerOptions\" formControlName=\"provider\" optionLabel=\"label\" optionValue=\"value\" placeholder=\"Select Provider\" class=\"w-full\"></p-select>\n </div>\n }\n\n <div class=\"form-field\">\n <label for=\"voice\">Voice <span pTooltip=\"Select the voice for text-to-speech\">\u2139\uFE0F</span></label>\n <p-inputgroup>\n <p-inputgroup-addon>\n <p-button [rounded]=\"true\" [text]=\"true\" icon=\"pi pi-exclamation-circle\" (click)=\"openVoiceSelector()\" />\n </p-inputgroup-addon>\n <p-select\n id=\"voice\"\n [editable]=\"true\"\n [options]=\"voiceTTSOptions()\"\n formControlName=\"voice\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [placeholder]=\"'Select Voice'\" />\n </p-inputgroup>\n </div>\n\n <div class=\"form-field\">\n <label for=\"speedRate\">Speed Rate <span pTooltip=\"Adjust the rate of speech delivery\">\u2139\uFE0F</span></label>\n <br />\n <input pInputText id=\"speedRate\" type=\"number\" formControlName=\"speedRate\" step=\"0.1\" />\n </div>\n\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"model\">Model <span pTooltip=\"Model ID if the provider requires it\">\u2139\uFE0F</span></label>\n <input pInputText id=\"model\" type=\"text\" formControlName=\"model\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Language code override (optional, usually included in the voice ID)\">\u2139\uFE0F</span></label>\n <input pInputText id=\"lang\" type=\"text\" formControlName=\"lang\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"effect\">Effect <span pTooltip=\"Platform audio effect\">\u2139\uFE0F</span></label>\n <input pInputText id=\"effect\" type=\"text\" formControlName=\"effect\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputGroupModule }, { kind: "component", type: i3$4.InputGroup, selector: "p-inputgroup, p-inputGroup, p-input-group", inputs: ["styleClass"] }, { kind: "ngmodule", type: InputGroupAddonModule }, { kind: "component", type: i4$4.InputGroupAddon, selector: "p-inputgroup-addon, p-inputGroupAddon", inputs: ["style", "styleClass"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3$3.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: CardModule }] }); }
8817
+ }
8818
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, decorators: [{
8819
+ type: Component,
8820
+ args: [{ selector: 'dc-voice-tts-form', standalone: true, imports: [ReactiveFormsModule, DynamicDialogModule, ButtonModule, InputGroupModule, InputGroupAddonModule, SelectModule, InputTextModule, CardModule], providers: [DialogService], template: "<h3> {{ title() }}</h3>\n<div [formGroup]=\"form()\" class=\"form-grid\">\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"provider\">Provider <span pTooltip=\"TTS provider\">\u2139\uFE0F</span></label>\n <p-select id=\"provider\" [options]=\"providerOptions\" formControlName=\"provider\" optionLabel=\"label\" optionValue=\"value\" placeholder=\"Select Provider\" class=\"w-full\"></p-select>\n </div>\n }\n\n <div class=\"form-field\">\n <label for=\"voice\">Voice <span pTooltip=\"Select the voice for text-to-speech\">\u2139\uFE0F</span></label>\n <p-inputgroup>\n <p-inputgroup-addon>\n <p-button [rounded]=\"true\" [text]=\"true\" icon=\"pi pi-exclamation-circle\" (click)=\"openVoiceSelector()\" />\n </p-inputgroup-addon>\n <p-select\n id=\"voice\"\n [editable]=\"true\"\n [options]=\"voiceTTSOptions()\"\n formControlName=\"voice\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [placeholder]=\"'Select Voice'\" />\n </p-inputgroup>\n </div>\n\n <div class=\"form-field\">\n <label for=\"speedRate\">Speed Rate <span pTooltip=\"Adjust the rate of speech delivery\">\u2139\uFE0F</span></label>\n <br />\n <input pInputText id=\"speedRate\" type=\"number\" formControlName=\"speedRate\" step=\"0.1\" />\n </div>\n\n @if (fullForm()) {\n <div class=\"form-field\">\n <label for=\"model\">Model <span pTooltip=\"Model ID if the provider requires it\">\u2139\uFE0F</span></label>\n <input pInputText id=\"model\" type=\"text\" formControlName=\"model\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Language code override (optional, usually included in the voice ID)\">\u2139\uFE0F</span></label>\n <input pInputText id=\"lang\" type=\"text\" formControlName=\"lang\" />\n </div>\n\n <div class=\"form-field\">\n <label for=\"effect\">Effect <span pTooltip=\"Platform audio effect\">\u2139\uFE0F</span></label>\n <input pInputText id=\"effect\" type=\"text\" formControlName=\"effect\" />\n </div>\n }\n</div>\n" }]
8821
+ }], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], voiceTTSOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "voiceTTSOptions", required: false }] }], fullForm: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullForm", required: false }] }] } });
8822
+
8504
8823
  class DCAgentCardFormComponent extends EntityBaseFormComponent {
8505
8824
  constructor() {
8506
8825
  super(...arguments);
@@ -8539,14 +8858,14 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8539
8858
  this.isGenerating = signal(false, ...(ngDevMode ? [{ debugName: "isGenerating" }] : /* istanbul ignore next */ []));
8540
8859
  this.isCloningVoice = signal(false, ...(ngDevMode ? [{ debugName: "isCloningVoice" }] : /* istanbul ignore next */ []));
8541
8860
  this.canCloneVoice = computed(() => {
8542
- const sampleUrl = this.form.get('voiceCloning.sample')?.value?.url;
8543
- const voiceId = this.form.get('voiceCloning.main.voice')?.value;
8861
+ const sampleUrl = this.form.get('voice.cloning.sample')?.value?.url;
8862
+ const voiceId = this.form.get('voice.main.voice')?.value;
8544
8863
  const canCloneVoiceVar = !!sampleUrl && !voiceId;
8545
8864
  return canCloneVoiceVar;
8546
8865
  }, ...(ngDevMode ? [{ debugName: "canCloneVoice" }] : /* istanbul ignore next */ []));
8547
8866
  this.fishModelId = computed(() => {
8548
- const voiceId = this.form.get('voiceCloning.main.voice')?.value;
8549
- const provider = this.form.get('voiceCloning.main.provider')?.value;
8867
+ const voiceId = this.form.get('voice.main.voice')?.value;
8868
+ const provider = this.form.get('voice.main.provider')?.value;
8550
8869
  return voiceId && provider === 'fish-audio' ? voiceId : null;
8551
8870
  }, ...(ngDevMode ? [{ debugName: "fishModelId" }] : /* istanbul ignore next */ []));
8552
8871
  }
@@ -8769,15 +9088,15 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8769
9088
  }
8770
9089
  onFileUploaded(event) {
8771
9090
  console.log('File uploaded', event);
8772
- this.form.controls.voiceCloning.patchValue({ sample: event });
9091
+ this.form.get('voice.cloning')?.patchValue({ sample: event });
8773
9092
  }
8774
9093
  async cloneVoice() {
8775
- const sample = this.form.get('voiceCloning.sample')?.value;
9094
+ const sample = this.form.get('voice.cloning.sample')?.value;
8776
9095
  if (!sample?.url) {
8777
9096
  this.toastService?.error({ title: 'Falta el sample', subtitle: 'Sube un audio antes de crear el modelo' });
8778
9097
  return;
8779
9098
  }
8780
- const text = this.form.get('voiceCloning.transcription')?.value || '';
9099
+ const text = this.form.get('voice.cloning.transcription')?.value || '';
8781
9100
  const agentName = this.form.get('name')?.value || 'Polilan';
8782
9101
  const title = `${agentName} — voice`;
8783
9102
  this.isCloningVoice.set(true);
@@ -8788,9 +9107,9 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8788
9107
  text,
8789
9108
  trainMode: 'fast',
8790
9109
  });
8791
- const mainCtrl = this.form.get('voiceCloning.main');
8792
- mainCtrl?.patchValue({ voice: res._id, provider: 'fish-audio' });
8793
- mainCtrl?.markAsDirty();
9110
+ const newMainCtrl = this.form.get('voice.main');
9111
+ newMainCtrl?.patchValue({ voice: res._id, provider: 'fish-audio' });
9112
+ newMainCtrl?.markAsDirty();
8794
9113
  this.toastService?.success({ title: 'Modelo de voz creado', subtitle: res._id });
8795
9114
  }
8796
9115
  catch (e) {
@@ -8811,7 +9130,7 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8811
9130
  this.router.navigate(['../../cards-creator'], { relativeTo: this.route });
8812
9131
  }
8813
9132
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DCAgentCardFormComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
8814
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DCAgentCardFormComponent, isStandalone: true, selector: "dc-agent-form", inputs: { inputSettings: { classPropertyName: "inputSettings", publicName: "inputSettings", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSave: "onSave", onSelectLive2d: "onSelectLive2d" }, providers: [DialogService], usesInheritance: true, ngImport: i0, template: "<p-card>\n <div class=\"top-buttons\">\n <button pButton severity=\"info\" (click)=\"checkPrompt()\" label=\"\uD83D\uDC41\uFE0F Ver instrucciones finales \uD83D\uDCD3\"></button>\n\n <button pButton severity=\"info\" (click)=\"goToDetails()\" label=\"\uD83D\uDCAC Conversar\"></button>\n <button pButton severity=\"primary\" (click)=\"save()\" label=\"\uD83D\uDCBE Guardar cambios\"></button>\n </div>\n\n <div class=\"top-buttons\">\n <p-button [loading]=\"isGenerating()\" severity=\"help\" (click)=\"generateCharacter()\" label=\"Generar \uD83E\uDDBE\"></p-button>\n <p-button severity=\"help\" (click)=\"goToCardsCreator()\" label=\" Creador masivo \"></p-button>\n\n <p-button severity=\"info\" (click)=\"downloadConversation()\" label=\"\uD83D\uDCC1 Exportar \u2B07\uFE0F\"></p-button>\n <p-button severity=\"info\" (click)=\"importConversation()\" label=\"\uD83C\uDCCF Importar \u2B06\uFE0F\"></p-button>\n <p-button severity=\"danger\" (click)=\"deleteCard()\" icon=\"pi pi-trash\"></p-button>\n </div>\n\n <br />\n <br />\n <form [formGroup]=\"form\" class=\"conversation-form\">\n <div class=\"form-grid\">\n <div class=\"left-column\">\n <div title=\"Main data\" >\n <div style=\"display: flex; gap: 15px\">\n <div class=\"form-field\">\n <label for=\"version\">Version: {{ form.get('version').value }} <span pTooltip=\"Version number of the conversation\">\u2139\uFE0F</span></label>\n </div>\n\n <div class=\"form-field\">\n <label for=\"id\"\n >ID: <span pTooltip=\"Unique identifier for this conversation\"> {{ form.get('id').value }} \u2139\uFE0F</span></label\n >\n </div>\n </div>\n\n\n <div class=\"form-field\">\n <label for=\"name\">Card Name <span pTooltip=\"Name of the Agent Card\">\u2139\uFE0F</span></label>\n <input pInputText id=\"name\" type=\"text\" formControlName=\"name\" />\n @if(form.get('name').errors?.['required'] && form.get('name').touched){\n <div class=\"error\"> Name is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"description\"> Description <span pTooltip=\"Description of the conversation\">\u2139\uFE0F</span></label>\n <input pInputText id=\"description\" type=\"text\" formControlName=\"description\" />\n @if(form.get('description').errors?.['required'] && form.get('description').touched){\n <div class=\"error\"> Description is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Select the primary language for the conversation\">\u2139\uFE0F</span></label>\n <p-select\n id=\"lang\"\n [options]=\"languageOptions\"\n [filter]=\"true\"\n formControlName=\"lang\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Language'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"agentType\">Agent Type <span pTooltip=\"Select the type of agent\">\u2139\uFE0F</span></label>\n <p-select id=\"agentType\" [options]=\"agentTypeOptions\" formControlName=\"agentType\" [placeholder]=\"'Select Agent Type'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"avatarType\">Avatar Type <span pTooltip=\"image: static picture | video: double looping video (ping-pong) | live2d: interactive 2D model with real-time audio lip-sync | vroid: 3D model (future)\">\u2139\uFE0F</span></label>\n <p-select\n id=\"avatarType\"\n [options]=\"avatarTypeOptions\"\n formControlName=\"avatarType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Type\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label>Avatar Live2D <span pTooltip=\"Selecciona el modelo Live2D para este agente\">\u2139\uFE0F</span></label>\n <div style=\"display: flex; align-items: center; gap: 12px; margin-top: 4px;\">\n @if (form.get('live2d')?.value; as live2d) {\n <div style=\"display: flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--p-border-color); border-radius: 6px; background: var(--p-content-background);\">\n <img width=\"36px\" height=\"36px\" style=\"border-radius: 50%; object-fit: cover;\" [src]=\"live2d.image?.url || 'defaults/images/face-3.jpg'\" alt=\"live2d preview\" />\n <span style=\"font-weight: 500;\">{{ live2d.name }}</span>\n <p-button icon=\"pi pi-times\" [text]=\"true\" severity=\"danger\" (onClick)=\"form.patchValue({ live2d: null })\" />\n </div>\n }\n <p-button (onClick)=\"onSelectLive2d.emit()\" label=\"Seleccionar Live2D\" icon=\"pi pi-search\" [outlined]=\"true\" />\n </div>\n </div>\n </div>\n\n <p-accordion [multiple]=\"true\" >\n <p-accordion-panel value=\"manageable\">\n <p-accordion-header>\n <span>Manageable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Controla visibilidad y acceso</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-manageable-form [form]=\"form.controls.manageable\"></dc-manageable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"learnable\">\n <p-accordion-header>\n <span>Learnable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Aprendizaje y conocimiento</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-learnable-form [form]=\"form.controls.learnable\"></dc-learnable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"conversationSettings\">\n <p-accordion-header>\n <span>Conversation Settings</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Modelo, voz y configuraci\u00F3n del chat</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-settings-form\n [form]=\"form.controls.conversationSettings\"\n [textEngineOptions]=\"textEngineOptions\"\n [conversationOptions]=\"conversationOptions\"\n [voiceTTSOptions]=\"voiceTTSOptions\">\n </dc-conversation-settings-form>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.conversationFlow){\n <p-accordion-panel value=\"conversationFlow\">\n <p-accordion-header>\n <span>Conversation Flow</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Objetivos, triggers y condiciones din\u00E1micas</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-flow-form [formGroup]=\"form.controls.conversationFlow\"></dc-conversation-flow-form>\n </p-accordion-content>\n </p-accordion-panel>\n }\n <p-accordion-panel value=\"accounts\">\n <p-accordion-header>\n <span>Gesti\u00F3n de cuentas</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n @if(form.controls.accounts) {\n <account-platform-form [formArray]=\"form.controls.accounts\"></account-platform-form>\n }\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"voiceCloning\">\n <p-accordion-header>\n <span>Clonaci\u00F3n de voz</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"voiceCloning\">\n <div class=\"form-field\">\n <label for=\"transcription\">Transcription</label>\n <textarea pTextarea id=\"transcription\" formControlName=\"transcription\" rows=\"3\"></textarea>\n </div>\n <div class=\"form-field\">\n <dc-simple-uploader\n buttonLabel=\"Subir audio\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (fileUploaded)=\"onFileUploaded($event)\"></dc-simple-uploader>\n @if(form.get('voiceCloning.sample').value?.url){\n <audio controls [src]=\"form.get('voiceCloning.sample').value.url\"></audio>\n }\n </div>\n <div class=\"form-field voice-clone-status\">\n @if(!form.get('voiceCloning.sample').value?.url){\n <small class=\"muted\">Sube un audio para entrenar un modelo de voz.</small>\n }\n <!-- @if(canCloneVoice()){ -->\n <p-button\n icon=\"pi pi-bolt\"\n severity=\"help\"\n [loading]=\"isCloningVoice()\"\n label=\"Crear modelo en Fish Audio\"\n (click)=\"cloneVoice()\">\n </p-button>\n <!-- } -->\n @if(fishModelId()){\n <div class=\"model-tag\">\n <span>Modelo Fish Audio: <code>{{ fishModelId() }}</code></span>\n <p-button\n icon=\"pi pi-refresh\"\n severity=\"secondary\"\n [text]=\"true\"\n [loading]=\"isCloningVoice()\"\n label=\"Regenerar\"\n (click)=\"cloneVoice()\">\n </p-button>\n </div>\n }\n </div>\n <dc-voice-tts-form\n [form]=\"$any(form.get('voiceCloning.main'))\"\n [title]=\"'Voice Cloning \u2014 Main Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.agenticConfig) {\n <p-accordion-panel value=\"agenticConfig\">\n <p-accordion-header>\n <span>Configuraci\u00F3n Ag\u00E9ntica</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Motor ag\u00E9ntico, patrones y herramientas</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"agenticConfig\">\n <div class=\"form-field flex items-center gap-2 mb-4\">\n <p-checkbox id=\"agenticEnabled\" formControlName=\"enabled\" [binary]=\"true\"></p-checkbox>\n <label for=\"agenticEnabled\" class=\"cursor-pointer\">Habilitar Agente Aut\u00F3nomo</label>\n </div>\n\n @if(form.get('agenticConfig.enabled').value) {\n <div class=\"form-field mb-4\">\n <label for=\"agenticEngine\" class=\"block text-sm font-medium mb-1\">Motor Ag\u00E9ntico</label>\n <p-select\n id=\"agenticEngine\"\n [options]=\"agenticEngineOptions\"\n formControlName=\"engine\"\n [placeholder]=\"'Seleccionar Motor'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"agenticPattern\" class=\"block text-sm font-medium mb-1\">Patr\u00F3n Ag\u00E9ntico</label>\n <p-select\n id=\"agenticPattern\"\n [options]=\"agenticPatternOptions\"\n formControlName=\"pattern\"\n [placeholder]=\"'Seleccionar Patr\u00F3n'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"maxIterations\" class=\"block text-sm font-medium mb-1\">M\u00E1ximo de Iteraciones</label>\n <input pInputText id=\"maxIterations\" type=\"number\" formControlName=\"maxIterations\" class=\"w-full\" />\n </div>\n\n <div class=\"form-field mb-4\">\n <label class=\"block text-sm font-medium mb-2\">Herramientas Permitidas</label>\n <div class=\"flex flex-col gap-2 mt-2 pl-2\">\n @for (toolOpt of allowedToolsOptions; track toolOpt.value) {\n <div class=\"flex items-center gap-2\">\n <p-checkbox\n [inputId]=\"'tool_' + toolOpt.value\"\n [binary]=\"true\"\n [ngModel]=\"isToolAllowed(toolOpt.value)\"\n [ngModelOptions]=\"{standalone: true}\"\n (onChange)=\"toggleTool(toolOpt.value, $event.checked)\">\n </p-checkbox>\n <label [for]=\"'tool_' + toolOpt.value\" class=\"cursor-pointer text-sm\">{{ toolOpt.label }}</label>\n </div>\n }\n </div>\n </div>\n\n @if(form.get('agenticConfig.pattern').value === 'reflection') {\n <div class=\"form-field mb-4\">\n <label for=\"reflectionPrompt\" class=\"block text-sm font-medium mb-1\">Prompt de Reflexi\u00F3n</label>\n <textarea pTextarea id=\"reflectionPrompt\" formControlName=\"reflectionPrompt\" rows=\"3\" [autoResize]=\"true\" class=\"w-full\"></textarea>\n </div>\n }\n\n @if(form.get('agenticConfig.engine').value === 'os_hermes') {\n <div class=\"form-field mb-4\">\n <label for=\"connectionUrl\" class=\"block text-sm font-medium mb-1\">URL de Conexi\u00F3n</label>\n <input pInputText id=\"connectionUrl\" type=\"text\" formControlName=\"connectionUrl\" placeholder=\"ws://...\" class=\"w-full\" />\n </div>\n }\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Razonamiento</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.reasoningModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Ejecuci\u00F3n</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.executionModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n }\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n\n </div>\n\n <div class=\"right-column\">\n @if(entity() && entityId()){\n <assets-loader\n [assets]=\"entity().assets\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (assetsChange)=\"onAssetsChange($event)\"\n (assetUpdate)=\"onUpdateAsset($event)\"\n (onFileSelected)=\"onImageSelected($event)\"></assets-loader>\n } @if(form.controls.characterCard){\n\n <br />\n <dc-character-card-form [characterCardForm]=\"form.controls.characterCard\" (generateMissingDataRequest)=\"generateMissingData()\">\n </dc-character-card-form>\n }\n </div>\n </div>\n </form>\n\n <div class=\"float-button\">\n <p-button icon=\"pi pi-eye\" (click)=\"inspect()\" severity=\"secondary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Inspeccionar\"> </p-button>\n\n <p-button icon=\"pi pi-save\" (click)=\"save()\" severity=\"primary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Guardar (Ctrl + S)\"> </p-button>\n </div>\n</p-card>\n", styles: [".conversation-form{max-width:100%;padding:20px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.conversation-form .card-group{padding:20px;border-radius:6px;margin-bottom:24px}.conversation-form .card-group h3{margin:0 0 20px;color:#2c3e50;font-size:1.25rem}.conversation-form .form-grid{display:flex;flex-wrap:wrap;gap:2rem;width:100%}.conversation-form .form-field{margin-bottom:1.5rem;display:flex;flex-direction:column;gap:.5rem}.conversation-form .form-field label{font-weight:500}.conversation-form .form-field.checkbox{flex-direction:row;align-items:center;gap:.5rem}.conversation-form .form-field.checkbox input[type=checkbox]{width:auto}.conversation-form .form-field .error{color:#dc3545;font-size:.875rem;margin-top:.25rem}.conversation-form .form-field .remove-button{position:absolute;border:none;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;top:-10px;right:-10px}.conversation-form .left-column,.conversation-form .right-column{display:flex;flex-direction:column;flex:1 1 calc(50% - 1rem);min-width:300px}@media(max-width:768px){.conversation-form .left-column,.conversation-form .right-column{flex:1 1 100%}}.conversation-form .card-group{padding:1rem;margin-bottom:1.5rem}.conversation-form .card-group h3{margin-top:0;margin-bottom:1rem}.top-buttons{display:flex;justify-content:space-between;margin-bottom:2rem;gap:1rem}.top-buttons button{flex:1}::ng-deep em{font-weight:900;color:#014a93}.float-button{position:fixed;bottom:4rem;right:2rem;z-index:1000;display:flex;gap:1px}.float-button :host ::ng-deep .p-button{width:4rem;height:4rem;border-radius:50%}.voice-clone-status{display:flex;flex-direction:column;gap:.5rem;margin:.75rem 0}.voice-clone-status .muted{color:var(--p-text-muted-color)}.voice-clone-status .model-tag{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--p-surface-100, #f3f4f6);border-radius:6px;font-size:.9rem}.voice-clone-status .model-tag code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormArrayDirective, selector: "[formArray]", inputs: ["formArray"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "component", type: AssetsLoaderComponent, selector: "assets-loader", inputs: ["assets", "storagePath"], outputs: ["assetsChange", "assetUpdate", "onFileSelected"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3$3.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$1.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "component", type: AccountPlatformForm, selector: "account-platform-form", inputs: ["formArray"] }, { kind: "ngmodule", type: CardModule }, { kind: "component", type: i2$6.Card, selector: "p-card", inputs: ["header", "subheader", "style", "styleClass"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "component", type: i8.Accordion, selector: "p-accordion", inputs: ["value", "multiple", "styleClass", "expandIcon", "collapseIcon", "selectOnFocus", "transitionOptions", "motionOptions"], outputs: ["valueChange", "onClose", "onOpen"] }, { kind: "component", type: i8.AccordionPanel, selector: "p-accordion-panel, p-accordionpanel", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: i8.AccordionHeader, selector: "p-accordion-header, p-accordionheader" }, { kind: "component", type: i8.AccordionContent, selector: "p-accordion-content, p-accordioncontent" }, { kind: "component", type: DCConversationFlowFormComponent, selector: "dc-conversation-flow-form", inputs: ["formGroup"] }, { kind: "component", type: DcCharacterCardFormComponent, selector: "dc-character-card-form", inputs: ["characterCardForm"], outputs: ["generateMissingDataRequest"] }, { kind: "ngmodule", type: MessageModule }, { kind: "component", type: DcConversationSettingsFormComponent, selector: "dc-conversation-settings-form", inputs: ["form", "textEngineOptions", "conversationOptions", "voiceTTSOptions"] }, { kind: "component", type: DcVoiceTtsFormComponent, selector: "dc-voice-tts-form", inputs: ["form", "title", "voiceTTSOptions", "fullForm"] }, { kind: "component", type: DcManageableFormComponent, selector: "dc-manageable-form", inputs: ["form"] }, { kind: "component", type: DcLearnableFormComponent, selector: "dc-learnable-form", inputs: ["form"] }, { kind: "component", type: SimpleUploaderComponent, selector: "dc-simple-uploader", inputs: ["storagePath", "buttonLabel", "accept", "disabled", "metadata", "provider"], outputs: ["fileUploaded", "uploadError"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "component", type: ModelSelectorComponent, selector: "dc-model-selector", inputs: ["modelForm", "shortForm"] }] }); }
9133
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DCAgentCardFormComponent, isStandalone: true, selector: "dc-agent-form", inputs: { inputSettings: { classPropertyName: "inputSettings", publicName: "inputSettings", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSave: "onSave", onSelectLive2d: "onSelectLive2d" }, providers: [DialogService], usesInheritance: true, ngImport: i0, template: "<p-card>\n <div class=\"top-buttons\">\n <button pButton severity=\"info\" (click)=\"checkPrompt()\" label=\"\uD83D\uDC41\uFE0F Ver instrucciones finales \uD83D\uDCD3\"></button>\n\n <button pButton severity=\"info\" (click)=\"goToDetails()\" label=\"\uD83D\uDCAC Conversar\"></button>\n <button pButton severity=\"primary\" (click)=\"save()\" label=\"\uD83D\uDCBE Guardar cambios\"></button>\n </div>\n\n <div class=\"top-buttons\">\n <p-button [loading]=\"isGenerating()\" severity=\"help\" (click)=\"generateCharacter()\" label=\"Generar \uD83E\uDDBE\"></p-button>\n <p-button severity=\"help\" (click)=\"goToCardsCreator()\" label=\" Creador masivo \"></p-button>\n\n <p-button severity=\"info\" (click)=\"downloadConversation()\" label=\"\uD83D\uDCC1 Exportar \u2B07\uFE0F\"></p-button>\n <p-button severity=\"info\" (click)=\"importConversation()\" label=\"\uD83C\uDCCF Importar \u2B06\uFE0F\"></p-button>\n <p-button severity=\"danger\" (click)=\"deleteCard()\" icon=\"pi pi-trash\"></p-button>\n </div>\n\n <br />\n <br />\n <form [formGroup]=\"form\" class=\"conversation-form\">\n <div class=\"form-grid\">\n <div class=\"left-column\">\n <div title=\"Main data\" >\n <div style=\"display: flex; gap: 15px\">\n <div class=\"form-field\">\n <label for=\"version\">Version: {{ form.get('version').value }} <span pTooltip=\"Version number of the conversation\">\u2139\uFE0F</span></label>\n </div>\n\n <div class=\"form-field\">\n <label for=\"id\"\n >ID: <span pTooltip=\"Unique identifier for this conversation\"> {{ form.get('id').value }} \u2139\uFE0F</span></label\n >\n </div>\n </div>\n\n\n <div class=\"form-field\">\n <label for=\"name\">Card Name <span pTooltip=\"Name of the Agent Card\">\u2139\uFE0F</span></label>\n <input pInputText id=\"name\" type=\"text\" formControlName=\"name\" />\n @if(form.get('name').errors?.['required'] && form.get('name').touched){\n <div class=\"error\"> Name is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"description\"> Description <span pTooltip=\"Description of the conversation\">\u2139\uFE0F</span></label>\n <input pInputText id=\"description\" type=\"text\" formControlName=\"description\" />\n @if(form.get('description').errors?.['required'] && form.get('description').touched){\n <div class=\"error\"> Description is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Select the primary language for the conversation\">\u2139\uFE0F</span></label>\n <p-select\n id=\"lang\"\n [options]=\"languageOptions\"\n [filter]=\"true\"\n formControlName=\"lang\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Language'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"agentType\">Agent Type <span pTooltip=\"Select the type of agent\">\u2139\uFE0F</span></label>\n <p-select id=\"agentType\" [options]=\"agentTypeOptions\" formControlName=\"agentType\" [placeholder]=\"'Select Agent Type'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"avatarType\">Avatar Type <span pTooltip=\"image: static picture | video: double looping video (ping-pong) | live2d: interactive 2D model with real-time audio lip-sync | vroid: 3D model (future)\">\u2139\uFE0F</span></label>\n <p-select\n id=\"avatarType\"\n [options]=\"avatarTypeOptions\"\n formControlName=\"avatarType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Type\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label>Avatar Live2D <span pTooltip=\"Selecciona el modelo Live2D para este agente\">\u2139\uFE0F</span></label>\n <div style=\"display: flex; align-items: center; gap: 12px; margin-top: 4px;\">\n @if (form.get('live2d')?.value; as live2d) {\n <div style=\"display: flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--p-border-color); border-radius: 6px; background: var(--p-content-background);\">\n <img width=\"36px\" height=\"36px\" style=\"border-radius: 50%; object-fit: cover;\" [src]=\"live2d.image?.url || 'defaults/images/face-3.jpg'\" alt=\"live2d preview\" />\n <span style=\"font-weight: 500;\">{{ live2d.name }}</span>\n <p-button icon=\"pi pi-times\" [text]=\"true\" severity=\"danger\" (onClick)=\"form.patchValue({ live2d: null })\" />\n </div>\n }\n <p-button (onClick)=\"onSelectLive2d.emit()\" label=\"Seleccionar Live2D\" icon=\"pi pi-search\" [outlined]=\"true\" />\n </div>\n </div>\n </div>\n\n <p-accordion [multiple]=\"true\" >\n <p-accordion-panel value=\"manageable\">\n <p-accordion-header>\n <span>Manageable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Controla visibilidad y acceso</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-manageable-form [form]=\"form.controls.manageable\"></dc-manageable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"learnable\">\n <p-accordion-header>\n <span>Learnable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Aprendizaje y conocimiento</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-learnable-form [form]=\"form.controls.learnable\"></dc-learnable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"conversationSettings\">\n <p-accordion-header>\n <span>Conversation Settings</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Modelo, voz y configuraci\u00F3n del chat</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-settings-form\n [form]=\"form.controls.conversationSettings\"\n [textEngineOptions]=\"textEngineOptions\"\n [conversationOptions]=\"conversationOptions\"\n [voiceTTSOptions]=\"voiceTTSOptions\">\n </dc-conversation-settings-form>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.conversationFlow){\n <p-accordion-panel value=\"conversationFlow\">\n <p-accordion-header>\n <span>Conversation Flow</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Objetivos, triggers y condiciones din\u00E1micas</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-flow-form [formGroup]=\"form.controls.conversationFlow\"></dc-conversation-flow-form>\n </p-accordion-content>\n </p-accordion-panel>\n }\n <p-accordion-panel value=\"accounts\">\n <p-accordion-header>\n <span>Gesti\u00F3n de cuentas</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n @if(form.controls.accounts) {\n <account-platform-form [formArray]=\"form.controls.accounts\"></account-platform-form>\n }\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"voice\">\n <p-accordion-header>\n <span>Voz</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Sintesis y Clonaci\u00F3n</span>\n\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"voice\">\n <div formGroupName=\"cloning\">\n <div class=\"form-field\">\n <label for=\"new_transcription\">Transcription (Voice.Cloning)</label>\n <textarea pTextarea id=\"new_transcription\" formControlName=\"transcription\" rows=\"3\"></textarea>\n </div>\n <div class=\"form-field\">\n <dc-simple-uploader\n buttonLabel=\"Subir audio (Voice.Cloning)\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (fileUploaded)=\"onFileUploaded($event)\"></dc-simple-uploader>\n @if(form.get('voice.cloning.sample')?.value?.url){\n <audio controls [src]=\"form.get('voice.cloning.sample').value.url\"></audio>\n }\n </div>\n </div>\n <div class=\"form-field voice-clone-status\">\n @if(!form.get('voice.cloning.sample')?.value?.url){\n <small class=\"muted\">Sube un audio para entrenar un modelo de voz en la nueva propiedad.</small>\n }\n <p-button\n icon=\"pi pi-bolt\"\n severity=\"help\"\n [loading]=\"isCloningVoice()\"\n label=\"Crear modelo en Fish Audio (Voice)\"\n (click)=\"cloneVoice()\">\n </p-button>\n </div>\n \n <dc-voice-tts-form\n [form]=\"$any(form.get('voice.main'))\"\n [title]=\"'Main Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n \n <dc-voice-tts-form\n [form]=\"$any(form.get('voice.secondary'))\"\n [title]=\"'Secondary Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.agenticConfig) {\n <p-accordion-panel value=\"agenticConfig\">\n <p-accordion-header>\n <span>Configuraci\u00F3n Ag\u00E9ntica</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Motor ag\u00E9ntico, patrones y herramientas</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"agenticConfig\">\n <div class=\"form-field flex items-center gap-2 mb-4\">\n <p-checkbox id=\"agenticEnabled\" formControlName=\"enabled\" [binary]=\"true\"></p-checkbox>\n <label for=\"agenticEnabled\" class=\"cursor-pointer\">Habilitar Agente Aut\u00F3nomo</label>\n </div>\n\n @if(form.get('agenticConfig.enabled').value) {\n <div class=\"form-field mb-4\">\n <label for=\"agenticEngine\" class=\"block text-sm font-medium mb-1\">Motor Ag\u00E9ntico</label>\n <p-select\n id=\"agenticEngine\"\n [options]=\"agenticEngineOptions\"\n formControlName=\"engine\"\n [placeholder]=\"'Seleccionar Motor'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"agenticPattern\" class=\"block text-sm font-medium mb-1\">Patr\u00F3n Ag\u00E9ntico</label>\n <p-select\n id=\"agenticPattern\"\n [options]=\"agenticPatternOptions\"\n formControlName=\"pattern\"\n [placeholder]=\"'Seleccionar Patr\u00F3n'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"maxIterations\" class=\"block text-sm font-medium mb-1\">M\u00E1ximo de Iteraciones</label>\n <input pInputText id=\"maxIterations\" type=\"number\" formControlName=\"maxIterations\" class=\"w-full\" />\n </div>\n\n <div class=\"form-field mb-4\">\n <label class=\"block text-sm font-medium mb-2\">Herramientas Permitidas</label>\n <div class=\"flex flex-col gap-2 mt-2 pl-2\">\n @for (toolOpt of allowedToolsOptions; track toolOpt.value) {\n <div class=\"flex items-center gap-2\">\n <p-checkbox\n [inputId]=\"'tool_' + toolOpt.value\"\n [binary]=\"true\"\n [ngModel]=\"isToolAllowed(toolOpt.value)\"\n [ngModelOptions]=\"{standalone: true}\"\n (onChange)=\"toggleTool(toolOpt.value, $event.checked)\">\n </p-checkbox>\n <label [for]=\"'tool_' + toolOpt.value\" class=\"cursor-pointer text-sm\">{{ toolOpt.label }}</label>\n </div>\n }\n </div>\n </div>\n\n @if(form.get('agenticConfig.pattern').value === 'reflection') {\n <div class=\"form-field mb-4\">\n <label for=\"reflectionPrompt\" class=\"block text-sm font-medium mb-1\">Prompt de Reflexi\u00F3n</label>\n <textarea pTextarea id=\"reflectionPrompt\" formControlName=\"reflectionPrompt\" rows=\"3\" [autoResize]=\"true\" class=\"w-full\"></textarea>\n </div>\n }\n\n @if(form.get('agenticConfig.engine').value === 'os_hermes') {\n <div class=\"form-field mb-4\">\n <label for=\"connectionUrl\" class=\"block text-sm font-medium mb-1\">URL de Conexi\u00F3n</label>\n <input pInputText id=\"connectionUrl\" type=\"text\" formControlName=\"connectionUrl\" placeholder=\"ws://...\" class=\"w-full\" />\n </div>\n }\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Razonamiento</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.reasoningModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Ejecuci\u00F3n</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.executionModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n }\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n\n </div>\n\n <div class=\"right-column\">\n @if(entity() && entityId()){\n <assets-loader\n [assets]=\"entity().assets\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (assetsChange)=\"onAssetsChange($event)\"\n (assetUpdate)=\"onUpdateAsset($event)\"\n (onFileSelected)=\"onImageSelected($event)\"></assets-loader>\n } @if(form.controls.characterCard){\n\n <br />\n <dc-character-card-form [characterCardForm]=\"form.controls.characterCard\" (generateMissingDataRequest)=\"generateMissingData()\">\n </dc-character-card-form>\n }\n </div>\n </div>\n </form>\n\n <div class=\"float-button\">\n <p-button icon=\"pi pi-eye\" (click)=\"inspect()\" severity=\"secondary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Inspeccionar\"> </p-button>\n\n <p-button icon=\"pi pi-save\" (click)=\"save()\" severity=\"primary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Guardar (Ctrl + S)\"> </p-button>\n </div>\n</p-card>\n", styles: [".conversation-form{max-width:100%;padding:20px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.conversation-form .card-group{padding:20px;border-radius:6px;margin-bottom:24px}.conversation-form .card-group h3{margin:0 0 20px;color:#2c3e50;font-size:1.25rem}.conversation-form .form-grid{display:flex;flex-wrap:wrap;gap:2rem;width:100%}.conversation-form .form-field{margin-bottom:1.5rem;display:flex;flex-direction:column;gap:.5rem}.conversation-form .form-field label{font-weight:500}.conversation-form .form-field.checkbox{flex-direction:row;align-items:center;gap:.5rem}.conversation-form .form-field.checkbox input[type=checkbox]{width:auto}.conversation-form .form-field .error{color:#dc3545;font-size:.875rem;margin-top:.25rem}.conversation-form .form-field .remove-button{position:absolute;border:none;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;top:-10px;right:-10px}.conversation-form .left-column,.conversation-form .right-column{display:flex;flex-direction:column;flex:1 1 calc(50% - 1rem);min-width:300px}@media(max-width:768px){.conversation-form .left-column,.conversation-form .right-column{flex:1 1 100%}}.conversation-form .card-group{padding:1rem;margin-bottom:1.5rem}.conversation-form .card-group h3{margin-top:0;margin-bottom:1rem}.top-buttons{display:flex;justify-content:space-between;margin-bottom:2rem;gap:1rem}.top-buttons button{flex:1}::ng-deep em{font-weight:900;color:#014a93}.float-button{position:fixed;bottom:4rem;right:2rem;z-index:1000;display:flex;gap:1px}.float-button :host ::ng-deep .p-button{width:4rem;height:4rem;border-radius:50%}.voice-clone-status{display:flex;flex-direction:column;gap:.5rem;margin:.75rem 0}.voice-clone-status .muted{color:var(--p-text-muted-color)}.voice-clone-status .model-tag{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--p-surface-100, #f3f4f6);border-radius:6px;font-size:.9rem}.voice-clone-status .model-tag code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormArrayDirective, selector: "[formArray]", inputs: ["formArray"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "component", type: AssetsLoaderComponent, selector: "assets-loader", inputs: ["assets", "storagePath"], outputs: ["assetsChange", "assetUpdate", "onFileSelected"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i3$3.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$1.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: DynamicDialogModule }, { kind: "component", type: AccountPlatformForm, selector: "account-platform-form", inputs: ["formArray"] }, { kind: "ngmodule", type: CardModule }, { kind: "component", type: i2$6.Card, selector: "p-card", inputs: ["header", "subheader", "style", "styleClass"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "component", type: i8.Accordion, selector: "p-accordion", inputs: ["value", "multiple", "styleClass", "expandIcon", "collapseIcon", "selectOnFocus", "transitionOptions", "motionOptions"], outputs: ["valueChange", "onClose", "onOpen"] }, { kind: "component", type: i8.AccordionPanel, selector: "p-accordion-panel, p-accordionpanel", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: i8.AccordionHeader, selector: "p-accordion-header, p-accordionheader" }, { kind: "component", type: i8.AccordionContent, selector: "p-accordion-content, p-accordioncontent" }, { kind: "component", type: DCConversationFlowFormComponent, selector: "dc-conversation-flow-form", inputs: ["formGroup"] }, { kind: "component", type: DcCharacterCardFormComponent, selector: "dc-character-card-form", inputs: ["characterCardForm"], outputs: ["generateMissingDataRequest"] }, { kind: "ngmodule", type: MessageModule }, { kind: "component", type: DcConversationSettingsFormComponent, selector: "dc-conversation-settings-form", inputs: ["form", "textEngineOptions", "conversationOptions", "voiceTTSOptions"] }, { kind: "component", type: DcVoiceTtsFormComponent, selector: "dc-voice-tts-form", inputs: ["form", "title", "voiceTTSOptions", "fullForm"] }, { kind: "component", type: DcManageableFormComponent, selector: "dc-manageable-form", inputs: ["form"] }, { kind: "component", type: DcLearnableFormComponent, selector: "dc-learnable-form", inputs: ["form"] }, { kind: "component", type: SimpleUploaderComponent, selector: "dc-simple-uploader", inputs: ["storagePath", "buttonLabel", "accept", "disabled", "metadata", "provider"], outputs: ["fileUploaded", "uploadError"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i4.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "component", type: ModelSelectorComponent, selector: "dc-model-selector", inputs: ["modelForm", "shortForm"] }] }); }
8815
9134
  }
8816
9135
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DCAgentCardFormComponent, decorators: [{
8817
9136
  type: Component,
@@ -8839,7 +9158,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8839
9158
  SimpleUploaderComponent,
8840
9159
  TextareaModule,
8841
9160
  ModelSelectorComponent,
8842
- ], template: "<p-card>\n <div class=\"top-buttons\">\n <button pButton severity=\"info\" (click)=\"checkPrompt()\" label=\"\uD83D\uDC41\uFE0F Ver instrucciones finales \uD83D\uDCD3\"></button>\n\n <button pButton severity=\"info\" (click)=\"goToDetails()\" label=\"\uD83D\uDCAC Conversar\"></button>\n <button pButton severity=\"primary\" (click)=\"save()\" label=\"\uD83D\uDCBE Guardar cambios\"></button>\n </div>\n\n <div class=\"top-buttons\">\n <p-button [loading]=\"isGenerating()\" severity=\"help\" (click)=\"generateCharacter()\" label=\"Generar \uD83E\uDDBE\"></p-button>\n <p-button severity=\"help\" (click)=\"goToCardsCreator()\" label=\" Creador masivo \"></p-button>\n\n <p-button severity=\"info\" (click)=\"downloadConversation()\" label=\"\uD83D\uDCC1 Exportar \u2B07\uFE0F\"></p-button>\n <p-button severity=\"info\" (click)=\"importConversation()\" label=\"\uD83C\uDCCF Importar \u2B06\uFE0F\"></p-button>\n <p-button severity=\"danger\" (click)=\"deleteCard()\" icon=\"pi pi-trash\"></p-button>\n </div>\n\n <br />\n <br />\n <form [formGroup]=\"form\" class=\"conversation-form\">\n <div class=\"form-grid\">\n <div class=\"left-column\">\n <div title=\"Main data\" >\n <div style=\"display: flex; gap: 15px\">\n <div class=\"form-field\">\n <label for=\"version\">Version: {{ form.get('version').value }} <span pTooltip=\"Version number of the conversation\">\u2139\uFE0F</span></label>\n </div>\n\n <div class=\"form-field\">\n <label for=\"id\"\n >ID: <span pTooltip=\"Unique identifier for this conversation\"> {{ form.get('id').value }} \u2139\uFE0F</span></label\n >\n </div>\n </div>\n\n\n <div class=\"form-field\">\n <label for=\"name\">Card Name <span pTooltip=\"Name of the Agent Card\">\u2139\uFE0F</span></label>\n <input pInputText id=\"name\" type=\"text\" formControlName=\"name\" />\n @if(form.get('name').errors?.['required'] && form.get('name').touched){\n <div class=\"error\"> Name is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"description\"> Description <span pTooltip=\"Description of the conversation\">\u2139\uFE0F</span></label>\n <input pInputText id=\"description\" type=\"text\" formControlName=\"description\" />\n @if(form.get('description').errors?.['required'] && form.get('description').touched){\n <div class=\"error\"> Description is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Select the primary language for the conversation\">\u2139\uFE0F</span></label>\n <p-select\n id=\"lang\"\n [options]=\"languageOptions\"\n [filter]=\"true\"\n formControlName=\"lang\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Language'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"agentType\">Agent Type <span pTooltip=\"Select the type of agent\">\u2139\uFE0F</span></label>\n <p-select id=\"agentType\" [options]=\"agentTypeOptions\" formControlName=\"agentType\" [placeholder]=\"'Select Agent Type'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"avatarType\">Avatar Type <span pTooltip=\"image: static picture | video: double looping video (ping-pong) | live2d: interactive 2D model with real-time audio lip-sync | vroid: 3D model (future)\">\u2139\uFE0F</span></label>\n <p-select\n id=\"avatarType\"\n [options]=\"avatarTypeOptions\"\n formControlName=\"avatarType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Type\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label>Avatar Live2D <span pTooltip=\"Selecciona el modelo Live2D para este agente\">\u2139\uFE0F</span></label>\n <div style=\"display: flex; align-items: center; gap: 12px; margin-top: 4px;\">\n @if (form.get('live2d')?.value; as live2d) {\n <div style=\"display: flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--p-border-color); border-radius: 6px; background: var(--p-content-background);\">\n <img width=\"36px\" height=\"36px\" style=\"border-radius: 50%; object-fit: cover;\" [src]=\"live2d.image?.url || 'defaults/images/face-3.jpg'\" alt=\"live2d preview\" />\n <span style=\"font-weight: 500;\">{{ live2d.name }}</span>\n <p-button icon=\"pi pi-times\" [text]=\"true\" severity=\"danger\" (onClick)=\"form.patchValue({ live2d: null })\" />\n </div>\n }\n <p-button (onClick)=\"onSelectLive2d.emit()\" label=\"Seleccionar Live2D\" icon=\"pi pi-search\" [outlined]=\"true\" />\n </div>\n </div>\n </div>\n\n <p-accordion [multiple]=\"true\" >\n <p-accordion-panel value=\"manageable\">\n <p-accordion-header>\n <span>Manageable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Controla visibilidad y acceso</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-manageable-form [form]=\"form.controls.manageable\"></dc-manageable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"learnable\">\n <p-accordion-header>\n <span>Learnable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Aprendizaje y conocimiento</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-learnable-form [form]=\"form.controls.learnable\"></dc-learnable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"conversationSettings\">\n <p-accordion-header>\n <span>Conversation Settings</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Modelo, voz y configuraci\u00F3n del chat</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-settings-form\n [form]=\"form.controls.conversationSettings\"\n [textEngineOptions]=\"textEngineOptions\"\n [conversationOptions]=\"conversationOptions\"\n [voiceTTSOptions]=\"voiceTTSOptions\">\n </dc-conversation-settings-form>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.conversationFlow){\n <p-accordion-panel value=\"conversationFlow\">\n <p-accordion-header>\n <span>Conversation Flow</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Objetivos, triggers y condiciones din\u00E1micas</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-flow-form [formGroup]=\"form.controls.conversationFlow\"></dc-conversation-flow-form>\n </p-accordion-content>\n </p-accordion-panel>\n }\n <p-accordion-panel value=\"accounts\">\n <p-accordion-header>\n <span>Gesti\u00F3n de cuentas</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n @if(form.controls.accounts) {\n <account-platform-form [formArray]=\"form.controls.accounts\"></account-platform-form>\n }\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"voiceCloning\">\n <p-accordion-header>\n <span>Clonaci\u00F3n de voz</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"voiceCloning\">\n <div class=\"form-field\">\n <label for=\"transcription\">Transcription</label>\n <textarea pTextarea id=\"transcription\" formControlName=\"transcription\" rows=\"3\"></textarea>\n </div>\n <div class=\"form-field\">\n <dc-simple-uploader\n buttonLabel=\"Subir audio\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (fileUploaded)=\"onFileUploaded($event)\"></dc-simple-uploader>\n @if(form.get('voiceCloning.sample').value?.url){\n <audio controls [src]=\"form.get('voiceCloning.sample').value.url\"></audio>\n }\n </div>\n <div class=\"form-field voice-clone-status\">\n @if(!form.get('voiceCloning.sample').value?.url){\n <small class=\"muted\">Sube un audio para entrenar un modelo de voz.</small>\n }\n <!-- @if(canCloneVoice()){ -->\n <p-button\n icon=\"pi pi-bolt\"\n severity=\"help\"\n [loading]=\"isCloningVoice()\"\n label=\"Crear modelo en Fish Audio\"\n (click)=\"cloneVoice()\">\n </p-button>\n <!-- } -->\n @if(fishModelId()){\n <div class=\"model-tag\">\n <span>Modelo Fish Audio: <code>{{ fishModelId() }}</code></span>\n <p-button\n icon=\"pi pi-refresh\"\n severity=\"secondary\"\n [text]=\"true\"\n [loading]=\"isCloningVoice()\"\n label=\"Regenerar\"\n (click)=\"cloneVoice()\">\n </p-button>\n </div>\n }\n </div>\n <dc-voice-tts-form\n [form]=\"$any(form.get('voiceCloning.main'))\"\n [title]=\"'Voice Cloning \u2014 Main Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.agenticConfig) {\n <p-accordion-panel value=\"agenticConfig\">\n <p-accordion-header>\n <span>Configuraci\u00F3n Ag\u00E9ntica</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Motor ag\u00E9ntico, patrones y herramientas</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"agenticConfig\">\n <div class=\"form-field flex items-center gap-2 mb-4\">\n <p-checkbox id=\"agenticEnabled\" formControlName=\"enabled\" [binary]=\"true\"></p-checkbox>\n <label for=\"agenticEnabled\" class=\"cursor-pointer\">Habilitar Agente Aut\u00F3nomo</label>\n </div>\n\n @if(form.get('agenticConfig.enabled').value) {\n <div class=\"form-field mb-4\">\n <label for=\"agenticEngine\" class=\"block text-sm font-medium mb-1\">Motor Ag\u00E9ntico</label>\n <p-select\n id=\"agenticEngine\"\n [options]=\"agenticEngineOptions\"\n formControlName=\"engine\"\n [placeholder]=\"'Seleccionar Motor'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"agenticPattern\" class=\"block text-sm font-medium mb-1\">Patr\u00F3n Ag\u00E9ntico</label>\n <p-select\n id=\"agenticPattern\"\n [options]=\"agenticPatternOptions\"\n formControlName=\"pattern\"\n [placeholder]=\"'Seleccionar Patr\u00F3n'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"maxIterations\" class=\"block text-sm font-medium mb-1\">M\u00E1ximo de Iteraciones</label>\n <input pInputText id=\"maxIterations\" type=\"number\" formControlName=\"maxIterations\" class=\"w-full\" />\n </div>\n\n <div class=\"form-field mb-4\">\n <label class=\"block text-sm font-medium mb-2\">Herramientas Permitidas</label>\n <div class=\"flex flex-col gap-2 mt-2 pl-2\">\n @for (toolOpt of allowedToolsOptions; track toolOpt.value) {\n <div class=\"flex items-center gap-2\">\n <p-checkbox\n [inputId]=\"'tool_' + toolOpt.value\"\n [binary]=\"true\"\n [ngModel]=\"isToolAllowed(toolOpt.value)\"\n [ngModelOptions]=\"{standalone: true}\"\n (onChange)=\"toggleTool(toolOpt.value, $event.checked)\">\n </p-checkbox>\n <label [for]=\"'tool_' + toolOpt.value\" class=\"cursor-pointer text-sm\">{{ toolOpt.label }}</label>\n </div>\n }\n </div>\n </div>\n\n @if(form.get('agenticConfig.pattern').value === 'reflection') {\n <div class=\"form-field mb-4\">\n <label for=\"reflectionPrompt\" class=\"block text-sm font-medium mb-1\">Prompt de Reflexi\u00F3n</label>\n <textarea pTextarea id=\"reflectionPrompt\" formControlName=\"reflectionPrompt\" rows=\"3\" [autoResize]=\"true\" class=\"w-full\"></textarea>\n </div>\n }\n\n @if(form.get('agenticConfig.engine').value === 'os_hermes') {\n <div class=\"form-field mb-4\">\n <label for=\"connectionUrl\" class=\"block text-sm font-medium mb-1\">URL de Conexi\u00F3n</label>\n <input pInputText id=\"connectionUrl\" type=\"text\" formControlName=\"connectionUrl\" placeholder=\"ws://...\" class=\"w-full\" />\n </div>\n }\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Razonamiento</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.reasoningModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Ejecuci\u00F3n</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.executionModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n }\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n\n </div>\n\n <div class=\"right-column\">\n @if(entity() && entityId()){\n <assets-loader\n [assets]=\"entity().assets\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (assetsChange)=\"onAssetsChange($event)\"\n (assetUpdate)=\"onUpdateAsset($event)\"\n (onFileSelected)=\"onImageSelected($event)\"></assets-loader>\n } @if(form.controls.characterCard){\n\n <br />\n <dc-character-card-form [characterCardForm]=\"form.controls.characterCard\" (generateMissingDataRequest)=\"generateMissingData()\">\n </dc-character-card-form>\n }\n </div>\n </div>\n </form>\n\n <div class=\"float-button\">\n <p-button icon=\"pi pi-eye\" (click)=\"inspect()\" severity=\"secondary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Inspeccionar\"> </p-button>\n\n <p-button icon=\"pi pi-save\" (click)=\"save()\" severity=\"primary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Guardar (Ctrl + S)\"> </p-button>\n </div>\n</p-card>\n", styles: [".conversation-form{max-width:100%;padding:20px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.conversation-form .card-group{padding:20px;border-radius:6px;margin-bottom:24px}.conversation-form .card-group h3{margin:0 0 20px;color:#2c3e50;font-size:1.25rem}.conversation-form .form-grid{display:flex;flex-wrap:wrap;gap:2rem;width:100%}.conversation-form .form-field{margin-bottom:1.5rem;display:flex;flex-direction:column;gap:.5rem}.conversation-form .form-field label{font-weight:500}.conversation-form .form-field.checkbox{flex-direction:row;align-items:center;gap:.5rem}.conversation-form .form-field.checkbox input[type=checkbox]{width:auto}.conversation-form .form-field .error{color:#dc3545;font-size:.875rem;margin-top:.25rem}.conversation-form .form-field .remove-button{position:absolute;border:none;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;top:-10px;right:-10px}.conversation-form .left-column,.conversation-form .right-column{display:flex;flex-direction:column;flex:1 1 calc(50% - 1rem);min-width:300px}@media(max-width:768px){.conversation-form .left-column,.conversation-form .right-column{flex:1 1 100%}}.conversation-form .card-group{padding:1rem;margin-bottom:1.5rem}.conversation-form .card-group h3{margin-top:0;margin-bottom:1rem}.top-buttons{display:flex;justify-content:space-between;margin-bottom:2rem;gap:1rem}.top-buttons button{flex:1}::ng-deep em{font-weight:900;color:#014a93}.float-button{position:fixed;bottom:4rem;right:2rem;z-index:1000;display:flex;gap:1px}.float-button :host ::ng-deep .p-button{width:4rem;height:4rem;border-radius:50%}.voice-clone-status{display:flex;flex-direction:column;gap:.5rem;margin:.75rem 0}.voice-clone-status .muted{color:var(--p-text-muted-color)}.voice-clone-status .model-tag{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--p-surface-100, #f3f4f6);border-radius:6px;font-size:.9rem}.voice-clone-status .model-tag code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem}\n"] }]
9161
+ ], template: "<p-card>\n <div class=\"top-buttons\">\n <button pButton severity=\"info\" (click)=\"checkPrompt()\" label=\"\uD83D\uDC41\uFE0F Ver instrucciones finales \uD83D\uDCD3\"></button>\n\n <button pButton severity=\"info\" (click)=\"goToDetails()\" label=\"\uD83D\uDCAC Conversar\"></button>\n <button pButton severity=\"primary\" (click)=\"save()\" label=\"\uD83D\uDCBE Guardar cambios\"></button>\n </div>\n\n <div class=\"top-buttons\">\n <p-button [loading]=\"isGenerating()\" severity=\"help\" (click)=\"generateCharacter()\" label=\"Generar \uD83E\uDDBE\"></p-button>\n <p-button severity=\"help\" (click)=\"goToCardsCreator()\" label=\" Creador masivo \"></p-button>\n\n <p-button severity=\"info\" (click)=\"downloadConversation()\" label=\"\uD83D\uDCC1 Exportar \u2B07\uFE0F\"></p-button>\n <p-button severity=\"info\" (click)=\"importConversation()\" label=\"\uD83C\uDCCF Importar \u2B06\uFE0F\"></p-button>\n <p-button severity=\"danger\" (click)=\"deleteCard()\" icon=\"pi pi-trash\"></p-button>\n </div>\n\n <br />\n <br />\n <form [formGroup]=\"form\" class=\"conversation-form\">\n <div class=\"form-grid\">\n <div class=\"left-column\">\n <div title=\"Main data\" >\n <div style=\"display: flex; gap: 15px\">\n <div class=\"form-field\">\n <label for=\"version\">Version: {{ form.get('version').value }} <span pTooltip=\"Version number of the conversation\">\u2139\uFE0F</span></label>\n </div>\n\n <div class=\"form-field\">\n <label for=\"id\"\n >ID: <span pTooltip=\"Unique identifier for this conversation\"> {{ form.get('id').value }} \u2139\uFE0F</span></label\n >\n </div>\n </div>\n\n\n <div class=\"form-field\">\n <label for=\"name\">Card Name <span pTooltip=\"Name of the Agent Card\">\u2139\uFE0F</span></label>\n <input pInputText id=\"name\" type=\"text\" formControlName=\"name\" />\n @if(form.get('name').errors?.['required'] && form.get('name').touched){\n <div class=\"error\"> Name is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"description\"> Description <span pTooltip=\"Description of the conversation\">\u2139\uFE0F</span></label>\n <input pInputText id=\"description\" type=\"text\" formControlName=\"description\" />\n @if(form.get('description').errors?.['required'] && form.get('description').touched){\n <div class=\"error\"> Description is required </div>\n }\n </div>\n\n <div class=\"form-field\">\n <label for=\"lang\">Language <span pTooltip=\"Select the primary language for the conversation\">\u2139\uFE0F</span></label>\n <p-select\n id=\"lang\"\n [options]=\"languageOptions\"\n [filter]=\"true\"\n formControlName=\"lang\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [placeholder]=\"'Select Language'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"agentType\">Agent Type <span pTooltip=\"Select the type of agent\">\u2139\uFE0F</span></label>\n <p-select id=\"agentType\" [options]=\"agentTypeOptions\" formControlName=\"agentType\" [placeholder]=\"'Select Agent Type'\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label for=\"avatarType\">Avatar Type <span pTooltip=\"image: static picture | video: double looping video (ping-pong) | live2d: interactive 2D model with real-time audio lip-sync | vroid: 3D model (future)\">\u2139\uFE0F</span></label>\n <p-select\n id=\"avatarType\"\n [options]=\"avatarTypeOptions\"\n formControlName=\"avatarType\"\n optionLabel=\"label\"\n optionValue=\"value\"\n placeholder=\"Select Avatar Type\"\n class=\"w-full mt-1\"></p-select>\n </div>\n\n <div class=\"form-field\">\n <label>Avatar Live2D <span pTooltip=\"Selecciona el modelo Live2D para este agente\">\u2139\uFE0F</span></label>\n <div style=\"display: flex; align-items: center; gap: 12px; margin-top: 4px;\">\n @if (form.get('live2d')?.value; as live2d) {\n <div style=\"display: flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--p-border-color); border-radius: 6px; background: var(--p-content-background);\">\n <img width=\"36px\" height=\"36px\" style=\"border-radius: 50%; object-fit: cover;\" [src]=\"live2d.image?.url || 'defaults/images/face-3.jpg'\" alt=\"live2d preview\" />\n <span style=\"font-weight: 500;\">{{ live2d.name }}</span>\n <p-button icon=\"pi pi-times\" [text]=\"true\" severity=\"danger\" (onClick)=\"form.patchValue({ live2d: null })\" />\n </div>\n }\n <p-button (onClick)=\"onSelectLive2d.emit()\" label=\"Seleccionar Live2D\" icon=\"pi pi-search\" [outlined]=\"true\" />\n </div>\n </div>\n </div>\n\n <p-accordion [multiple]=\"true\" >\n <p-accordion-panel value=\"manageable\">\n <p-accordion-header>\n <span>Manageable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Controla visibilidad y acceso</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-manageable-form [form]=\"form.controls.manageable\"></dc-manageable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"learnable\">\n <p-accordion-header>\n <span>Learnable</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Aprendizaje y conocimiento</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-learnable-form [form]=\"form.controls.learnable\"></dc-learnable-form>\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"conversationSettings\">\n <p-accordion-header>\n <span>Conversation Settings</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Modelo, voz y configuraci\u00F3n del chat</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-settings-form\n [form]=\"form.controls.conversationSettings\"\n [textEngineOptions]=\"textEngineOptions\"\n [conversationOptions]=\"conversationOptions\"\n [voiceTTSOptions]=\"voiceTTSOptions\">\n </dc-conversation-settings-form>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.conversationFlow){\n <p-accordion-panel value=\"conversationFlow\">\n <p-accordion-header>\n <span>Conversation Flow</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Objetivos, triggers y condiciones din\u00E1micas</span>\n </p-accordion-header>\n <p-accordion-content>\n <dc-conversation-flow-form [formGroup]=\"form.controls.conversationFlow\"></dc-conversation-flow-form>\n </p-accordion-content>\n </p-accordion-panel>\n }\n <p-accordion-panel value=\"accounts\">\n <p-accordion-header>\n <span>Gesti\u00F3n de cuentas</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Beta</span>\n </p-accordion-header>\n <p-accordion-content>\n @if(form.controls.accounts) {\n <account-platform-form [formArray]=\"form.controls.accounts\"></account-platform-form>\n }\n </p-accordion-content>\n </p-accordion-panel>\n <p-accordion-panel value=\"voice\">\n <p-accordion-header>\n <span>Voz</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Sintesis y Clonaci\u00F3n</span>\n\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"voice\">\n <div formGroupName=\"cloning\">\n <div class=\"form-field\">\n <label for=\"new_transcription\">Transcription (Voice.Cloning)</label>\n <textarea pTextarea id=\"new_transcription\" formControlName=\"transcription\" rows=\"3\"></textarea>\n </div>\n <div class=\"form-field\">\n <dc-simple-uploader\n buttonLabel=\"Subir audio (Voice.Cloning)\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (fileUploaded)=\"onFileUploaded($event)\"></dc-simple-uploader>\n @if(form.get('voice.cloning.sample')?.value?.url){\n <audio controls [src]=\"form.get('voice.cloning.sample').value.url\"></audio>\n }\n </div>\n </div>\n <div class=\"form-field voice-clone-status\">\n @if(!form.get('voice.cloning.sample')?.value?.url){\n <small class=\"muted\">Sube un audio para entrenar un modelo de voz en la nueva propiedad.</small>\n }\n <p-button\n icon=\"pi pi-bolt\"\n severity=\"help\"\n [loading]=\"isCloningVoice()\"\n label=\"Crear modelo en Fish Audio (Voice)\"\n (click)=\"cloneVoice()\">\n </p-button>\n </div>\n \n <dc-voice-tts-form\n [form]=\"$any(form.get('voice.main'))\"\n [title]=\"'Main Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n \n <dc-voice-tts-form\n [form]=\"$any(form.get('voice.secondary'))\"\n [title]=\"'Secondary Voice'\"\n [voiceTTSOptions]=\"voiceTTSOptions\"\n [fullForm]=\"true\">\n </dc-voice-tts-form>\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n @if(form.controls.agenticConfig) {\n <p-accordion-panel value=\"agenticConfig\">\n <p-accordion-header>\n <span>Configuraci\u00F3n Ag\u00E9ntica</span>\n <span style=\"margin-left: 8px; font-size: 0.75rem; color: var(--p-text-muted-color); font-weight: normal\">Motor ag\u00E9ntico, patrones y herramientas</span>\n </p-accordion-header>\n <p-accordion-content>\n <div formGroupName=\"agenticConfig\">\n <div class=\"form-field flex items-center gap-2 mb-4\">\n <p-checkbox id=\"agenticEnabled\" formControlName=\"enabled\" [binary]=\"true\"></p-checkbox>\n <label for=\"agenticEnabled\" class=\"cursor-pointer\">Habilitar Agente Aut\u00F3nomo</label>\n </div>\n\n @if(form.get('agenticConfig.enabled').value) {\n <div class=\"form-field mb-4\">\n <label for=\"agenticEngine\" class=\"block text-sm font-medium mb-1\">Motor Ag\u00E9ntico</label>\n <p-select\n id=\"agenticEngine\"\n [options]=\"agenticEngineOptions\"\n formControlName=\"engine\"\n [placeholder]=\"'Seleccionar Motor'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"agenticPattern\" class=\"block text-sm font-medium mb-1\">Patr\u00F3n Ag\u00E9ntico</label>\n <p-select\n id=\"agenticPattern\"\n [options]=\"agenticPatternOptions\"\n formControlName=\"pattern\"\n [placeholder]=\"'Seleccionar Patr\u00F3n'\"\n class=\"w-full\"></p-select>\n </div>\n\n <div class=\"form-field mb-4\">\n <label for=\"maxIterations\" class=\"block text-sm font-medium mb-1\">M\u00E1ximo de Iteraciones</label>\n <input pInputText id=\"maxIterations\" type=\"number\" formControlName=\"maxIterations\" class=\"w-full\" />\n </div>\n\n <div class=\"form-field mb-4\">\n <label class=\"block text-sm font-medium mb-2\">Herramientas Permitidas</label>\n <div class=\"flex flex-col gap-2 mt-2 pl-2\">\n @for (toolOpt of allowedToolsOptions; track toolOpt.value) {\n <div class=\"flex items-center gap-2\">\n <p-checkbox\n [inputId]=\"'tool_' + toolOpt.value\"\n [binary]=\"true\"\n [ngModel]=\"isToolAllowed(toolOpt.value)\"\n [ngModelOptions]=\"{standalone: true}\"\n (onChange)=\"toggleTool(toolOpt.value, $event.checked)\">\n </p-checkbox>\n <label [for]=\"'tool_' + toolOpt.value\" class=\"cursor-pointer text-sm\">{{ toolOpt.label }}</label>\n </div>\n }\n </div>\n </div>\n\n @if(form.get('agenticConfig.pattern').value === 'reflection') {\n <div class=\"form-field mb-4\">\n <label for=\"reflectionPrompt\" class=\"block text-sm font-medium mb-1\">Prompt de Reflexi\u00F3n</label>\n <textarea pTextarea id=\"reflectionPrompt\" formControlName=\"reflectionPrompt\" rows=\"3\" [autoResize]=\"true\" class=\"w-full\"></textarea>\n </div>\n }\n\n @if(form.get('agenticConfig.engine').value === 'os_hermes') {\n <div class=\"form-field mb-4\">\n <label for=\"connectionUrl\" class=\"block text-sm font-medium mb-1\">URL de Conexi\u00F3n</label>\n <input pInputText id=\"connectionUrl\" type=\"text\" formControlName=\"connectionUrl\" placeholder=\"ws://...\" class=\"w-full\" />\n </div>\n }\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Razonamiento</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.reasoningModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n\n <div class=\"form-field mt-6\">\n <h4 class=\"font-semibold text-base mb-2\">Modelo de Ejecuci\u00F3n</h4>\n <dc-model-selector [modelForm]=\"$any(form.get('agenticConfig.executionModel'))\" [shortForm]=\"true\"></dc-model-selector>\n </div>\n }\n </div>\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n\n </div>\n\n <div class=\"right-column\">\n @if(entity() && entityId()){\n <assets-loader\n [assets]=\"entity().assets\"\n [storagePath]=\"'conversation-cards/' + entityId()\"\n (assetsChange)=\"onAssetsChange($event)\"\n (assetUpdate)=\"onUpdateAsset($event)\"\n (onFileSelected)=\"onImageSelected($event)\"></assets-loader>\n } @if(form.controls.characterCard){\n\n <br />\n <dc-character-card-form [characterCardForm]=\"form.controls.characterCard\" (generateMissingDataRequest)=\"generateMissingData()\">\n </dc-character-card-form>\n }\n </div>\n </div>\n </form>\n\n <div class=\"float-button\">\n <p-button icon=\"pi pi-eye\" (click)=\"inspect()\" severity=\"secondary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Inspeccionar\"> </p-button>\n\n <p-button icon=\"pi pi-save\" (click)=\"save()\" severity=\"primary\" [rounded]=\"true\" [raised]=\"true\" pTooltip=\"Guardar (Ctrl + S)\"> </p-button>\n </div>\n</p-card>\n", styles: [".conversation-form{max-width:100%;padding:20px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.conversation-form .card-group{padding:20px;border-radius:6px;margin-bottom:24px}.conversation-form .card-group h3{margin:0 0 20px;color:#2c3e50;font-size:1.25rem}.conversation-form .form-grid{display:flex;flex-wrap:wrap;gap:2rem;width:100%}.conversation-form .form-field{margin-bottom:1.5rem;display:flex;flex-direction:column;gap:.5rem}.conversation-form .form-field label{font-weight:500}.conversation-form .form-field.checkbox{flex-direction:row;align-items:center;gap:.5rem}.conversation-form .form-field.checkbox input[type=checkbox]{width:auto}.conversation-form .form-field .error{color:#dc3545;font-size:.875rem;margin-top:.25rem}.conversation-form .form-field .remove-button{position:absolute;border:none;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;top:-10px;right:-10px}.conversation-form .left-column,.conversation-form .right-column{display:flex;flex-direction:column;flex:1 1 calc(50% - 1rem);min-width:300px}@media(max-width:768px){.conversation-form .left-column,.conversation-form .right-column{flex:1 1 100%}}.conversation-form .card-group{padding:1rem;margin-bottom:1.5rem}.conversation-form .card-group h3{margin-top:0;margin-bottom:1rem}.top-buttons{display:flex;justify-content:space-between;margin-bottom:2rem;gap:1rem}.top-buttons button{flex:1}::ng-deep em{font-weight:900;color:#014a93}.float-button{position:fixed;bottom:4rem;right:2rem;z-index:1000;display:flex;gap:1px}.float-button :host ::ng-deep .p-button{width:4rem;height:4rem;border-radius:50%}.voice-clone-status{display:flex;flex-direction:column;gap:.5rem;margin:.75rem 0}.voice-clone-status .muted{color:var(--p-text-muted-color)}.voice-clone-status .model-tag{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--p-surface-100, #f3f4f6);border-radius:6px;font-size:.9rem}.voice-clone-status .model-tag code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8rem}\n"] }]
8843
9162
  }], propDecorators: { onSave: [{ type: i0.Output, args: ["onSave"] }], inputSettings: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputSettings", required: false }] }], onSelectLive2d: [{ type: i0.Output, args: ["onSelectLive2d"] }] } });
8844
9163
 
8845
9164
  class TruncatePipe {
@@ -9014,7 +9333,7 @@ class AgentCardListComponent extends EntityBaseListV2Component {
9014
9333
  const adminFilters = [
9015
9334
  {
9016
9335
  name: 'Voz Única',
9017
- field: 'voiceCloning.sample',
9336
+ field: 'voice.cloning.sample',
9018
9337
  type: 'select',
9019
9338
  operator: '$not_null',
9020
9339
  options: [
@@ -9271,7 +9590,7 @@ class Live2dModelComponent {
9271
9590
  this.playAnimation(group);
9272
9591
  }
9273
9592
  }
9274
- speak(audioUrl, options) {
9593
+ async speak(audioUrl, options) {
9275
9594
  if (!this.model)
9276
9595
  return;
9277
9596
  if (options?.focus !== false) {
@@ -9280,8 +9599,21 @@ class Live2dModelComponent {
9280
9599
  this.viewStateChanged.emit(transform);
9281
9600
  }
9282
9601
  }
9602
+ this.stopSpeaking();
9603
+ const targetVolume = options?.volume !== undefined ? options.volume : 1.0;
9604
+ if (this.live2dConfig) {
9605
+ try {
9606
+ const live2dEngine = await this.live2dConfig.Live2DModel();
9607
+ if (live2dEngine && live2dEngine.SoundManager) {
9608
+ live2dEngine.SoundManager.volume = targetVolume;
9609
+ }
9610
+ }
9611
+ catch (e) {
9612
+ console.warn('Failed to set SoundManager volume', e);
9613
+ }
9614
+ }
9283
9615
  this.model.speak(audioUrl, {
9284
- volume: 0,
9616
+ volume: targetVolume,
9285
9617
  crossOrigin: options?.crossOrigin || 'anonymous',
9286
9618
  });
9287
9619
  }
@@ -9440,7 +9772,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
9440
9772
  args: ['canvas']
9441
9773
  }], modelPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "modelPath", required: true }] }], scale: [{ type: i0.Input, args: [{ isSignal: true, alias: "scale", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: false }] }], y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], modelLoaded: [{ type: i0.Output, args: ["modelLoaded"] }], viewStateChanged: [{ type: i0.Output, args: ["viewStateChanged"] }] } });
9442
9774
 
9443
- class DcAgentCardConverseComponent {
9775
+ class DcAgentCardAvatarComponent {
9444
9776
  set isConversationActive(val) {
9445
9777
  this._isConversationActive.set(val);
9446
9778
  }
@@ -9462,8 +9794,8 @@ class DcAgentCardConverseComponent {
9462
9794
  this.videoPlayerService = inject(VideoPlayerNativeService);
9463
9795
  this.agentCardId = '';
9464
9796
  this.useNativePlayer = true;
9465
- this.activeAudioMessage = null;
9466
9797
  this._isConversationActive = signal(false, ...(ngDevMode ? [{ debugName: "_isConversationActive" }] : /* istanbul ignore next */ []));
9798
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
9467
9799
  this.onStartConversation = output();
9468
9800
  this.videoPlayerA = viewChild('videoPlayerA', ...(ngDevMode ? [{ debugName: "videoPlayerA" }] : /* istanbul ignore next */ []));
9469
9801
  this.videoPlayerB = viewChild('videoPlayerB', ...(ngDevMode ? [{ debugName: "videoPlayerB" }] : /* istanbul ignore next */ []));
@@ -9544,25 +9876,48 @@ class DcAgentCardConverseComponent {
9544
9876
  this.videoPlayerService.setAgentCard(card);
9545
9877
  }
9546
9878
  });
9547
- // Live2D Speak (Lip-Sync) effect
9879
+ // Live2D Speak (Lip-Sync) effect — listening directly to ChatMonitorService's activeAudioMessage
9548
9880
  effect(() => {
9549
- const audioMsg = this.activeAudioMessage;
9881
+ const audioMsg = this.chatMonitorService.activeAudioMessage();
9882
+ console.log('DcAgentCardAvatarComponent: Speak effect triggered. activeAudioMessage:', audioMsg ? {
9883
+ messageId: audioMsg.messageId,
9884
+ audioUrl: audioMsg.audioUrl
9885
+ } : null);
9550
9886
  const type = this.avatarType();
9551
9887
  const l2d = this.live2dModel();
9552
- if (type === 'live2d' && audioMsg?.audioUrl && l2d) {
9553
- l2d.stopSpeaking();
9554
- l2d.speak(audioMsg.audioUrl, { volume: 0.0 });
9888
+ if (type === 'live2d' && l2d) {
9889
+ if (audioMsg && audioMsg.audioUrl) {
9890
+ console.log('DcAgentCardAvatarComponent: Live2D speak started with URL:', audioMsg.audioUrl);
9891
+ l2d.stopSpeaking();
9892
+ l2d.speak(audioMsg.audioUrl, { volume: 0.0 });
9893
+ }
9894
+ else {
9895
+ console.log('DcAgentCardAvatarComponent: Live2D speak stopped (no active message or no audioUrl)');
9896
+ l2d.stopSpeaking();
9897
+ }
9555
9898
  }
9556
9899
  });
9557
9900
  // Live2D Mood state emotion updates mapping
9558
9901
  effect(() => {
9559
- const mood = this.conversationFlowStateService.flowState().moodState?.value;
9902
+ const mood = this.chatMonitorService.currentMood();
9560
9903
  const type = this.avatarType();
9561
9904
  const l2d = this.live2dModel();
9562
9905
  if (type === 'live2d' && mood && l2d) {
9563
9906
  l2d.setExpression(mood);
9564
9907
  }
9565
9908
  });
9909
+ // Native Video player mood state updates mapping
9910
+ effect(() => {
9911
+ const mood = this.chatMonitorService.currentMood();
9912
+ const card = this.agentCard();
9913
+ const type = this.avatarType();
9914
+ if (type === 'video' && mood && card?.assets?.motions) {
9915
+ const motionUrl = card.assets.motions.find((m) => m.metadata?.moodState === mood)?.url;
9916
+ if (motionUrl) {
9917
+ this.videoPlayerService.setVideoSource(motionUrl);
9918
+ }
9919
+ }
9920
+ });
9566
9921
  }
9567
9922
  ngOnDestroy() {
9568
9923
  this.videoPlayerService.cleanUp();
@@ -9626,6 +9981,18 @@ class DcAgentCardConverseComponent {
9626
9981
  cardTranslated.conversationSettings.secondaryVoice.voice = transformedSecondaryVoice;
9627
9982
  }
9628
9983
  }
9984
+ if (cardTranslated.voice?.main) {
9985
+ const transformedMainVoice = this._transformVoice(cardTranslated.voice.main.voice, locale);
9986
+ if (transformedMainVoice) {
9987
+ cardTranslated.voice.main.voice = transformedMainVoice;
9988
+ }
9989
+ }
9990
+ if (cardTranslated.voice?.secondary) {
9991
+ const transformedSecondaryVoice = this._transformVoice(cardTranslated.voice.secondary.voice, locale);
9992
+ if (transformedSecondaryVoice) {
9993
+ cardTranslated.voice.secondary.voice = transformedSecondaryVoice;
9994
+ }
9995
+ }
9629
9996
  return cardTranslated;
9630
9997
  }
9631
9998
  _transformVoice(voice, locale) {
@@ -9656,18 +10023,16 @@ class DcAgentCardConverseComponent {
9656
10023
  toggleInfoLayer() {
9657
10024
  this.showInfoLayer.update((value) => !value);
9658
10025
  }
9659
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardConverseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
9660
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcAgentCardConverseComponent, isStandalone: true, selector: "dc-agent-card-converse", inputs: { agentCardId: "agentCardId", useNativePlayer: "useNativePlayer", activeAudioMessage: "activeAudioMessage", isConversationActive: "isConversationActive" }, outputs: { onStartConversation: "onStartConversation" }, viewQueries: [{ propertyName: "videoPlayerA", first: true, predicate: ["videoPlayerA"], descendants: true, isSignal: true }, { propertyName: "videoPlayerB", first: true, predicate: ["videoPlayerB"], descendants: true, isSignal: true }, { propertyName: "live2dModel", first: true, predicate: Live2dModelComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #cardContent>\n <div class=\"card-container\">\n <!-- CASO 1: Renderizado por Video (Ping-pong nativo) -->\n @if (avatarType() === 'video') {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" [style.opacity]=\"videoPlayerService.isVideoPlaying() ? 0 : 1\" style=\"transition: opacity 0.5s ease-in-out;\" alt=\"\" />\n <video #videoPlayerA class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 1; z-index: 1;\" playsinline></video>\n <video #videoPlayerB class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 0; z-index: 2;\" playsinline></video>\n }\n\n <!-- CASO 2: Renderizado por Live2D (PixiJS Canvas) -->\n @else if (avatarType() === 'live2d') {\n @if (model3JsonUrl(); as modelPath) {\n <app-live2d-model\n #live2dModel\n class=\"card-image\"\n style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: all; z-index: 2;\"\n [modelPath]=\"modelPath\"\n [zoom]=\"live2dZoomState()\"\n [x]=\"live2dXState()\"\n [y]=\"live2dYState()\"\n (viewStateChanged)=\"onViewStateChanged($event)\">\n </app-live2d-model>\n } @else {\n <!-- Fallback si no tiene manifest -->\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n }\n\n <!-- CASO 3: Renderizado por VRoid (WebGL 3D - Placeholder) -->\n @else if (avatarType() === 'vroid') {\n <div class=\"card-image\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #222; display: flex; align-items: center; justify-content: center; color: white; z-index: 2;\">\n <span>[3D VRoid Avatar Canvas]</span>\n </div>\n }\n\n <!-- CASO 4: Imagen Est\u00E1tica Est\u00E1ndar -->\n @else {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n\n\n\n @if (!isChatActive()) {\n\n <div class=\"info-button\" (click)=\"toggleInfoLayer()\">\n <p-button icon=\"pi pi-arrow-down-left\" [rounded]=\"true\" [raised]=\"true\" severity=\"primary\" [outlined]=\"true\" />\n </div>\n \n <div style=\"position: absolute; bottom: 20px; right: 50%; transform: translateX(50%); z-index: 3\">\n <p-button\n size=\"large\"\n [label]=\"'dataclouder.agentCards.dcAgentCardDetails.startConversation' | translate\"\n [rounded]=\"true\"\n (click)=\"startConversation()\" />\n </div>\n }\n\n <div class=\"info-layer\" [class.active]=\"showInfoLayer()\">\n <div class=\"info-content\">\n <h1\n ><strong>{{ agentCard()?.name || agentCard()?.characterCard?.data?.name }}</strong></h1\n >\n\n @if (agentCard()?.characterCard?.data?.instructions) {\n <div class=\"scenario\">\n <h4>{{ 'dataclouder.agentCards.dcAgentCardDetails.instructions' | translate }}</h4>\n <p>{{ agentCard()?.characterCard?.data?.instructions | parseCard: agentCard() }}</p>\n </div>\n }\n </div>\n </div>\n </div>\n</ng-template>\n\n<div \n style=\"display: flex; justify-content: center; align-items: center\" \n [class.transparent-mode]=\"avatarDisplay() === 'transparent'\"\n [class.fullscreen-mode]=\"avatarDisplay() === 'fullscreen'\"\n [class.card-mode]=\"avatarDisplay() === 'card'\"\n>\n @if (avatarDisplay() === 'transparent' || avatarDisplay() === 'fullscreen') {\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n } @else {\n <p-card>\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n </p-card>\n }\n</div>\n", styles: ["::ng-deep .p-card{width:420px;height:700px}::ng-deep .p-card .p-card-body{width:100%;height:100%}:host{display:block;width:100%;height:100%}.transparent-mode .card-container{width:420px;height:700px;position:relative}.transparent-mode .card-image{background:transparent}.fullscreen-mode{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.fullscreen-mode .card-container{width:100%;height:100%;position:relative}.fullscreen-mode .card-image{background:transparent}.card-image{height:100%;width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}::ng-deep .plyr--full-ui{width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}.info-button{position:absolute;top:15px;right:15px;z-index:3}.info-button:hover{transform:scale(1.1)}.info-layer{height:100%;width:100%;position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;z-index:2;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);background-color:var(--chat-user-message-bg);color:#fff;opacity:1;clip-path:circle(0% at top right);transition:clip-path .5s cubic-bezier(.25,1,.5,1);pointer-events:none}.info-layer.active{clip-path:circle(150% at top right);pointer-events:auto}.info-content{padding:15px;text-align:center;max-width:90%}.info-content h1{margin-top:0;font-size:18px;margin-bottom:10px}.info-content p{font-size:12px;margin:0}::ng-deep .info-button .p-button{background-color:#ffffff47}\n"], dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CardModule }, { kind: "component", type: i2$6.Card, selector: "p-card", inputs: ["header", "subheader", "style", "styleClass"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Live2dModelComponent, selector: "app-live2d-model", inputs: ["modelPath", "scale", "zoom", "x", "y"], outputs: ["modelLoaded", "viewStateChanged"] }, { kind: "pipe", type: ParseCardPipe, name: "parseCard" }, { kind: "pipe", type: i3$6.TranslatePipe, name: "translate" }] }); }
10026
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
10027
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DcAgentCardAvatarComponent, isStandalone: true, selector: "dc-agent-card-avatar", inputs: { agentCardId: "agentCardId", useNativePlayer: "useNativePlayer", isConversationActive: "isConversationActive" }, outputs: { onStartConversation: "onStartConversation" }, viewQueries: [{ propertyName: "videoPlayerA", first: true, predicate: ["videoPlayerA"], descendants: true, isSignal: true }, { propertyName: "videoPlayerB", first: true, predicate: ["videoPlayerB"], descendants: true, isSignal: true }, { propertyName: "live2dModel", first: true, predicate: Live2dModelComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #cardContent>\n <div class=\"card-container\">\n <!-- CASO 1: Renderizado por Video (Ping-pong nativo) -->\n @if (avatarType() === 'video') {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" [style.opacity]=\"videoPlayerService.isVideoPlaying() ? 0 : 1\" style=\"transition: opacity 0.5s ease-in-out;\" alt=\"\" />\n <video #videoPlayerA class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 1; z-index: 1;\" playsinline></video>\n <video #videoPlayerB class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 0; z-index: 2;\" playsinline></video>\n }\n\n <!-- CASO 2: Renderizado por Live2D (PixiJS Canvas) -->\n @else if (avatarType() === 'live2d') {\n @if (model3JsonUrl(); as modelPath) {\n <app-live2d-model\n #live2dModel\n class=\"card-image\"\n style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: all; z-index: 2;\"\n [modelPath]=\"modelPath\"\n [zoom]=\"live2dZoomState()\"\n [x]=\"live2dXState()\"\n [y]=\"live2dYState()\"\n (viewStateChanged)=\"onViewStateChanged($event)\">\n </app-live2d-model>\n } @else {\n <!-- Fallback si no tiene manifest -->\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n }\n\n <!-- CASO 3: Renderizado por VRoid (WebGL 3D - Placeholder) -->\n @else if (avatarType() === 'vroid') {\n <div class=\"card-image\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #222; display: flex; align-items: center; justify-content: center; color: white; z-index: 2;\">\n <span>[3D VRoid Avatar Canvas]</span>\n </div>\n }\n\n <!-- CASO 4: Imagen Est\u00E1tica Est\u00E1ndar -->\n @else {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n\n\n\n @if (!isChatActive()) {\n\n <div class=\"info-button\" (click)=\"toggleInfoLayer()\">\n <p-button icon=\"pi pi-arrow-down-left\" [rounded]=\"true\" [raised]=\"true\" severity=\"primary\" [outlined]=\"true\" />\n </div>\n \n <div style=\"position: absolute; bottom: 20px; right: 50%; transform: translateX(50%); z-index: 3\">\n <p-button\n size=\"large\"\n [label]=\"'dataclouder.agentCards.dcAgentCardDetails.startConversation' | translate\"\n [rounded]=\"true\"\n (click)=\"startConversation()\" />\n </div>\n }\n\n <div class=\"info-layer\" [class.active]=\"showInfoLayer()\">\n <div class=\"info-content\">\n <h1\n ><strong>{{ agentCard()?.name || agentCard()?.characterCard?.data?.name }}</strong></h1\n >\n\n @if (agentCard()?.characterCard?.data?.instructions) {\n <div class=\"scenario\">\n <h4>{{ 'dataclouder.agentCards.dcAgentCardDetails.instructions' | translate }}</h4>\n <p>{{ agentCard()?.characterCard?.data?.instructions | parseCard: agentCard() }}</p>\n </div>\n }\n </div>\n </div>\n </div>\n</ng-template>\n\n<div \n style=\"display: flex; justify-content: center; align-items: center\" \n [class.transparent-mode]=\"avatarDisplay() === 'transparent'\"\n [class.fullscreen-mode]=\"avatarDisplay() === 'fullscreen'\"\n [class.card-mode]=\"avatarDisplay() === 'card'\"\n>\n @if (avatarDisplay() === 'transparent' || avatarDisplay() === 'fullscreen') {\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n } @else {\n <p-card>\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n </p-card>\n }\n</div>\n", styles: ["::ng-deep .p-card{width:420px;height:700px}::ng-deep .p-card .p-card-body{width:100%;height:100%}:host{display:block;width:100%;height:100%}.transparent-mode .card-container{width:420px;height:700px;position:relative}.transparent-mode .card-image{background:transparent}.fullscreen-mode{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.fullscreen-mode .card-container{width:100%;height:100%;position:relative}.fullscreen-mode .card-image{background:transparent}.card-image{height:100%;width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}::ng-deep .plyr--full-ui{width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}.info-button{position:absolute;top:15px;right:15px;z-index:3}.info-button:hover{transform:scale(1.1)}.info-layer{height:100%;width:100%;position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;z-index:2;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);background-color:var(--chat-user-message-bg);color:#fff;opacity:1;clip-path:circle(0% at top right);transition:clip-path .5s cubic-bezier(.25,1,.5,1);pointer-events:none}.info-layer.active{clip-path:circle(150% at top right);pointer-events:auto}.info-content{padding:15px;text-align:center;max-width:90%}.info-content h1{margin-top:0;font-size:18px;margin-bottom:10px}.info-content p{font-size:12px;margin:0}::ng-deep .info-button .p-button{background-color:#ffffff47}\n"], dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CardModule }, { kind: "component", type: i2$6.Card, selector: "p-card", inputs: ["header", "subheader", "style", "styleClass"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Live2dModelComponent, selector: "app-live2d-model", inputs: ["modelPath", "scale", "zoom", "x", "y"], outputs: ["modelLoaded", "viewStateChanged"] }, { kind: "pipe", type: ParseCardPipe, name: "parseCard" }, { kind: "pipe", type: i3$6.TranslatePipe, name: "translate" }] }); }
9661
10028
  }
9662
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardConverseComponent, decorators: [{
10029
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardAvatarComponent, decorators: [{
9663
10030
  type: Component,
9664
- args: [{ selector: 'dc-agent-card-converse', standalone: true, imports: [ButtonModule, CardModule, ParseCardPipe, TranslateModule, NgTemplateOutlet, Live2dModelComponent], template: "<ng-template #cardContent>\n <div class=\"card-container\">\n <!-- CASO 1: Renderizado por Video (Ping-pong nativo) -->\n @if (avatarType() === 'video') {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" [style.opacity]=\"videoPlayerService.isVideoPlaying() ? 0 : 1\" style=\"transition: opacity 0.5s ease-in-out;\" alt=\"\" />\n <video #videoPlayerA class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 1; z-index: 1;\" playsinline></video>\n <video #videoPlayerB class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 0; z-index: 2;\" playsinline></video>\n }\n\n <!-- CASO 2: Renderizado por Live2D (PixiJS Canvas) -->\n @else if (avatarType() === 'live2d') {\n @if (model3JsonUrl(); as modelPath) {\n <app-live2d-model\n #live2dModel\n class=\"card-image\"\n style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: all; z-index: 2;\"\n [modelPath]=\"modelPath\"\n [zoom]=\"live2dZoomState()\"\n [x]=\"live2dXState()\"\n [y]=\"live2dYState()\"\n (viewStateChanged)=\"onViewStateChanged($event)\">\n </app-live2d-model>\n } @else {\n <!-- Fallback si no tiene manifest -->\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n }\n\n <!-- CASO 3: Renderizado por VRoid (WebGL 3D - Placeholder) -->\n @else if (avatarType() === 'vroid') {\n <div class=\"card-image\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #222; display: flex; align-items: center; justify-content: center; color: white; z-index: 2;\">\n <span>[3D VRoid Avatar Canvas]</span>\n </div>\n }\n\n <!-- CASO 4: Imagen Est\u00E1tica Est\u00E1ndar -->\n @else {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n\n\n\n @if (!isChatActive()) {\n\n <div class=\"info-button\" (click)=\"toggleInfoLayer()\">\n <p-button icon=\"pi pi-arrow-down-left\" [rounded]=\"true\" [raised]=\"true\" severity=\"primary\" [outlined]=\"true\" />\n </div>\n \n <div style=\"position: absolute; bottom: 20px; right: 50%; transform: translateX(50%); z-index: 3\">\n <p-button\n size=\"large\"\n [label]=\"'dataclouder.agentCards.dcAgentCardDetails.startConversation' | translate\"\n [rounded]=\"true\"\n (click)=\"startConversation()\" />\n </div>\n }\n\n <div class=\"info-layer\" [class.active]=\"showInfoLayer()\">\n <div class=\"info-content\">\n <h1\n ><strong>{{ agentCard()?.name || agentCard()?.characterCard?.data?.name }}</strong></h1\n >\n\n @if (agentCard()?.characterCard?.data?.instructions) {\n <div class=\"scenario\">\n <h4>{{ 'dataclouder.agentCards.dcAgentCardDetails.instructions' | translate }}</h4>\n <p>{{ agentCard()?.characterCard?.data?.instructions | parseCard: agentCard() }}</p>\n </div>\n }\n </div>\n </div>\n </div>\n</ng-template>\n\n<div \n style=\"display: flex; justify-content: center; align-items: center\" \n [class.transparent-mode]=\"avatarDisplay() === 'transparent'\"\n [class.fullscreen-mode]=\"avatarDisplay() === 'fullscreen'\"\n [class.card-mode]=\"avatarDisplay() === 'card'\"\n>\n @if (avatarDisplay() === 'transparent' || avatarDisplay() === 'fullscreen') {\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n } @else {\n <p-card>\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n </p-card>\n }\n</div>\n", styles: ["::ng-deep .p-card{width:420px;height:700px}::ng-deep .p-card .p-card-body{width:100%;height:100%}:host{display:block;width:100%;height:100%}.transparent-mode .card-container{width:420px;height:700px;position:relative}.transparent-mode .card-image{background:transparent}.fullscreen-mode{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.fullscreen-mode .card-container{width:100%;height:100%;position:relative}.fullscreen-mode .card-image{background:transparent}.card-image{height:100%;width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}::ng-deep .plyr--full-ui{width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}.info-button{position:absolute;top:15px;right:15px;z-index:3}.info-button:hover{transform:scale(1.1)}.info-layer{height:100%;width:100%;position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;z-index:2;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);background-color:var(--chat-user-message-bg);color:#fff;opacity:1;clip-path:circle(0% at top right);transition:clip-path .5s cubic-bezier(.25,1,.5,1);pointer-events:none}.info-layer.active{clip-path:circle(150% at top right);pointer-events:auto}.info-content{padding:15px;text-align:center;max-width:90%}.info-content h1{margin-top:0;font-size:18px;margin-bottom:10px}.info-content p{font-size:12px;margin:0}::ng-deep .info-button .p-button{background-color:#ffffff47}\n"] }]
10031
+ args: [{ selector: 'dc-agent-card-avatar', standalone: true, imports: [ButtonModule, CardModule, ParseCardPipe, TranslateModule, NgTemplateOutlet, Live2dModelComponent], template: "<ng-template #cardContent>\n <div class=\"card-container\">\n <!-- CASO 1: Renderizado por Video (Ping-pong nativo) -->\n @if (avatarType() === 'video') {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" [style.opacity]=\"videoPlayerService.isVideoPlaying() ? 0 : 1\" style=\"transition: opacity 0.5s ease-in-out;\" alt=\"\" />\n <video #videoPlayerA class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 1; z-index: 1;\" playsinline></video>\n <video #videoPlayerB class=\"card-image\" style=\"position: absolute; top: 0; left: 0; transition: opacity 0.3s ease-in-out; opacity: 0; z-index: 2;\" playsinline></video>\n }\n\n <!-- CASO 2: Renderizado por Live2D (PixiJS Canvas) -->\n @else if (avatarType() === 'live2d') {\n @if (model3JsonUrl(); as modelPath) {\n <app-live2d-model\n #live2dModel\n class=\"card-image\"\n style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: all; z-index: 2;\"\n [modelPath]=\"modelPath\"\n [zoom]=\"live2dZoomState()\"\n [x]=\"live2dXState()\"\n [y]=\"live2dYState()\"\n (viewStateChanged)=\"onViewStateChanged($event)\">\n </app-live2d-model>\n } @else {\n <!-- Fallback si no tiene manifest -->\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n }\n\n <!-- CASO 3: Renderizado por VRoid (WebGL 3D - Placeholder) -->\n @else if (avatarType() === 'vroid') {\n <div class=\"card-image\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #222; display: flex; align-items: center; justify-content: center; color: white; z-index: 2;\">\n <span>[3D VRoid Avatar Canvas]</span>\n </div>\n }\n\n <!-- CASO 4: Imagen Est\u00E1tica Est\u00E1ndar -->\n @else {\n <img class=\"card-image\" [src]=\"agentCard()?.assets?.image?.url\" alt=\"\" />\n }\n\n\n\n @if (!isChatActive()) {\n\n <div class=\"info-button\" (click)=\"toggleInfoLayer()\">\n <p-button icon=\"pi pi-arrow-down-left\" [rounded]=\"true\" [raised]=\"true\" severity=\"primary\" [outlined]=\"true\" />\n </div>\n \n <div style=\"position: absolute; bottom: 20px; right: 50%; transform: translateX(50%); z-index: 3\">\n <p-button\n size=\"large\"\n [label]=\"'dataclouder.agentCards.dcAgentCardDetails.startConversation' | translate\"\n [rounded]=\"true\"\n (click)=\"startConversation()\" />\n </div>\n }\n\n <div class=\"info-layer\" [class.active]=\"showInfoLayer()\">\n <div class=\"info-content\">\n <h1\n ><strong>{{ agentCard()?.name || agentCard()?.characterCard?.data?.name }}</strong></h1\n >\n\n @if (agentCard()?.characterCard?.data?.instructions) {\n <div class=\"scenario\">\n <h4>{{ 'dataclouder.agentCards.dcAgentCardDetails.instructions' | translate }}</h4>\n <p>{{ agentCard()?.characterCard?.data?.instructions | parseCard: agentCard() }}</p>\n </div>\n }\n </div>\n </div>\n </div>\n</ng-template>\n\n<div \n style=\"display: flex; justify-content: center; align-items: center\" \n [class.transparent-mode]=\"avatarDisplay() === 'transparent'\"\n [class.fullscreen-mode]=\"avatarDisplay() === 'fullscreen'\"\n [class.card-mode]=\"avatarDisplay() === 'card'\"\n>\n @if (avatarDisplay() === 'transparent' || avatarDisplay() === 'fullscreen') {\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n } @else {\n <p-card>\n <ng-container *ngTemplateOutlet=\"cardContent\"></ng-container>\n </p-card>\n }\n</div>\n", styles: ["::ng-deep .p-card{width:420px;height:700px}::ng-deep .p-card .p-card-body{width:100%;height:100%}:host{display:block;width:100%;height:100%}.transparent-mode .card-container{width:420px;height:700px;position:relative}.transparent-mode .card-image{background:transparent}.fullscreen-mode{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.fullscreen-mode .card-container{width:100%;height:100%;position:relative}.fullscreen-mode .card-image{background:transparent}.card-image{height:100%;width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}::ng-deep .plyr--full-ui{width:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;transition:filter .3s ease}.info-button{position:absolute;top:15px;right:15px;z-index:3}.info-button:hover{transform:scale(1.1)}.info-layer{height:100%;width:100%;position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;z-index:2;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);background-color:var(--chat-user-message-bg);color:#fff;opacity:1;clip-path:circle(0% at top right);transition:clip-path .5s cubic-bezier(.25,1,.5,1);pointer-events:none}.info-layer.active{clip-path:circle(150% at top right);pointer-events:auto}.info-content{padding:15px;text-align:center;max-width:90%}.info-content h1{margin-top:0;font-size:18px;margin-bottom:10px}.info-content p{font-size:12px;margin:0}::ng-deep .info-button .p-button{background-color:#ffffff47}\n"] }]
9665
10032
  }], ctorParameters: () => [], propDecorators: { agentCardId: [{
9666
10033
  type: Input
9667
10034
  }], useNativePlayer: [{
9668
10035
  type: Input
9669
- }], activeAudioMessage: [{
9670
- type: Input
9671
10036
  }], isConversationActive: [{
9672
10037
  type: Input
9673
10038
  }], onStartConversation: [{ type: i0.Output, args: ["onStartConversation"] }], videoPlayerA: [{ type: i0.ViewChild, args: ['videoPlayerA', { isSignal: true }] }], videoPlayerB: [{ type: i0.ViewChild, args: ['videoPlayerB', { isSignal: true }] }], live2dModel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => Live2dModelComponent), { isSignal: true }] }] } });
@@ -10289,5 +10654,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
10289
10654
  * Generated bundle index. Do not edit.
10290
10655
  */
10291
10656
 
10292
- export { ACCDataGenerationComponent, AGENT_CARDS_STATE_SERVICE, AIGenerationService, AgentCardListComponent, AgentCardUI, AgentCardsGenerationService, AgenticEngine, AgenticPattern, AudioService, AudioSpeed, BACKGROUND_SERVICE_TOKEN, CHAT_OBSERVER_CONFIG, CONVERSATION_AI_TOKEN, CardRouteService, CardRoutesComponent, CardsCreatorComponent, ChatEngineTestComponent, ChatEventType, ChatMessage, ChatMessageOrchestratorComponent, ChatMonitorService, ChatRole, ConditionOperator, ConditionType, ContextEngineService, ContextType, ConversationDTO, ConversationEvents, ConversationFlowStateService, ConversationMessagesDTO, ConversationPromptBuilderService, ConversationRuleService, ConversationRulesComponent, ConversationStatus, ConversationType, ConversationTypeOptions, DCAgentCardFormComponent, DCChatComponent, DCConversationUserChatSettingsComponent, DcAgentCardConverseComponent, DcAgentCardDetailComponent, DcObserverInterventionComponent, DefaultAgentCardsService, DoActionTypeOptions, DynamicFlowService, DynamicFlowTaskTypeOptions, EAccountsPlatform, EAgentType, EDoActionType, EDynamicFlowTaskType, EntityThen, EntityWhatOptions, EntityWhenOptions, EvalResultStringDefinition, GlobalToolsService, LIVE2D_CONFIG, Live2dModelComponent, MessageContent, MessageContentDisplayer, MessagesStateService, ModelSelectorComponent, PolitoNotificationComponent, PopupService, PromptPreviewComponent, SectionType, SystemPromptType, TextEngineOptions, TextEngines, VIDEO_PLAYER_SERVICE_TOKEN, VideoPlayerNativeService, VideoPlayerService, VoiceTTSOption, VoiceTTSOptions, WordTimestamps, XKillsAgentCardsService, buildObjectTTSRequest, characterCardStringDataDefinition, convertToHTML, createAIModelFormGroup, defaultconvUserSettings, extractAudioAndTranscription, extractJsonFromResponse, getMoodStateLabelsAsString, getMoodStatePrompt, markdownToHtml, matchTranscription, provideAgentCardService, provideLive2D, removeEmojis, removeEmojisAndSpecialCharacters, removeSpecialCharacters };
10657
+ export { ACCDataGenerationComponent, AGENT_CARDS_STATE_SERVICE, AIGenerationService, AgentCardListComponent, AgentCardUI, AgentCardsGenerationService, AgenticEngine, AgenticPattern, AudioService, AudioSpeed, BACKGROUND_SERVICE_TOKEN, CHAT_OBSERVER_CONFIG, CONVERSATION_AI_TOKEN, CardRouteService, CardRoutesComponent, CardsCreatorComponent, ChatEngineTestComponent, ChatEventType, ChatMessage, ChatMessageOrchestratorComponent, ChatMonitorService, ChatRole, ConditionOperator, ConditionType, ContextEngineService, ContextType, ConversationDTO, ConversationEvents, ConversationFlowStateService, ConversationMessagesDTO, ConversationPromptBuilderService, ConversationRuleService, ConversationRulesComponent, ConversationStatus, ConversationType, ConversationTypeOptions, DCAgentCardFormComponent, DCChatComponent, DCConversationUserChatSettingsComponent, DcAgentCardAvatarComponent, DcAgentCardDetailComponent, DcObserverInterventionComponent, DefaultAgentCardsService, DoActionTypeOptions, DynamicFlowService, DynamicFlowTaskTypeOptions, EAccountsPlatform, EAgentType, EDoActionType, EDynamicFlowTaskType, EntityThen, EntityWhatOptions, EntityWhenOptions, EvalResultStringDefinition, GlobalToolsService, LIVE2D_CONFIG, Live2dModelComponent, MessageContent, MessageContentDisplayer, MessagesStateService, ModelSelectorComponent, PolitoNotificationComponent, PopupService, PromptPreviewComponent, SectionType, SystemPromptType, TextEngineOptions, TextEngines, VIDEO_PLAYER_SERVICE_TOKEN, VideoPlayerNativeService, VideoPlayerService, VoiceTTSOption, VoiceTTSOptions, WordTimestamps, XKillsAgentCardsService, buildObjectTTSRequest, characterCardStringDataDefinition, convertToHTML, createAIModelFormGroup, defaultconvUserSettings, extractAudioAndTranscription, extractJsonFromResponse, getMoodStateLabelsAsString, getMoodStatePrompt, markdownToHtml, matchTranscription, provideAgentCardService, provideLive2D, removeEmojis, removeEmojisAndSpecialCharacters, removeSpecialCharacters };
10293
10658
  //# sourceMappingURL=dataclouder-ngx-agent-cards.mjs.map