@dataclouder/ngx-agent-cards 0.2.13 → 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';
@@ -79,9 +79,6 @@ import { PaginatorModule } from 'primeng/paginator';
79
79
  import * as i3$5 from 'primeng/speeddial';
80
80
  import { SpeedDialModule } from 'primeng/speeddial';
81
81
  import { isEmpty } from 'es-toolkit/compat';
82
- import { Live2DModel } from 'untitled-pixi-live2d-engine/cubism';
83
- import * as PIXI from 'pixi.js';
84
- import { Application } from 'pixi.js';
85
82
  import * as i3$6 from '@ngx-translate/core';
86
83
  import { TranslateModule } from '@ngx-translate/core';
87
84
  import { form, required, submit, FormField } from '@angular/forms/signals';
@@ -286,6 +283,7 @@ var ChatEventType;
286
283
  ChatEventType["WordClicked"] = "wordClicked";
287
284
  ChatEventType["MoodDetected"] = "moodDetected";
288
285
  ChatEventType["AudioStarted"] = "audioStarted";
286
+ ChatEventType["AudioStopped"] = "audioStopped";
289
287
  // Add other potential event types here as needed
290
288
  // e.g., MessageSent = 'messageSent', SettingsChanged = 'settingsChanged', GoalCompleted = 'goalCompleted'
291
289
  })(ChatEventType || (ChatEventType = {}));
@@ -1868,6 +1866,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1868
1866
  }]
1869
1867
  }] });
1870
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
+
1871
1920
  /**
1872
1921
  * @description
1873
1922
  * Service responsible for managing the real-time, dynamic state of a conversation flow.
@@ -1882,6 +1931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1882
1931
  */
1883
1932
  class ConversationFlowStateService {
1884
1933
  constructor() {
1934
+ this.chatMonitorService = inject(ChatMonitorService, { optional: true });
1885
1935
  this.initialState = {
1886
1936
  goal: { value: 0 },
1887
1937
  challenges: [],
@@ -1947,6 +1997,7 @@ class ConversationFlowStateService {
1947
1997
  }
1948
1998
  if (newState.moodState?.value) {
1949
1999
  this.moodUpdated.next(newState.moodState.value);
2000
+ this.chatMonitorService?.logCurrentMood(newState.moodState.value);
1950
2001
  }
1951
2002
  if (newState.goal && newState.goal.deltaPoints !== undefined && newState.goal.deltaPoints !== 0) {
1952
2003
  this.goalEvaluationUpdated.next({ deltaPoints: newState.goal.deltaPoints });
@@ -2007,6 +2058,7 @@ class ConversationFlowStateService {
2007
2058
  }));
2008
2059
  if (moodState.value) {
2009
2060
  this.moodUpdated.next(moodState.value);
2061
+ this.chatMonitorService?.logCurrentMood(moodState.value);
2010
2062
  }
2011
2063
  }
2012
2064
  /**
@@ -2508,58 +2560,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2508
2560
  }]
2509
2561
  }] });
2510
2562
 
2511
- class ChatMonitorService {
2512
- // Actualmente la única comunicación es con el background para cambiar el fondo.
2513
- // El problema de comunicar así es si tiene que ser singleton, y en control markets tengo varias instancias de dc-chat.
2514
- // Voy a utilizar el evento por ahora, y ver si combiene luego crear un servicio.
2515
- // Se requiere comunicar el chat-engine con la app afuera, va a ver 2 formas de hacerlo.
2516
- // Mediante eventos del chat, o estar subscrito al chat-monitor.
2517
- // La complejidad del chat monitor es menor porque puedo injectarlo en cualquier lugar.
2518
- // Los eventos estan bien pero si el vento viene muy interno tengo que propagarlo bastante.
2519
- constructor() {
2520
- this.messageAudioWillPlay = signal(null, ...(ngDevMode ? [{ debugName: "messageAudioWillPlay" }] : /* istanbul ignore next */ []));
2521
- this.messageAudioWillPlay$ = this.messageAudioWillPlay.asReadonly();
2522
- this.currentMood = signal(null, ...(ngDevMode ? [{ debugName: "currentMood" }] : /* istanbul ignore next */ []));
2523
- this.isConversationActive = signal(false, ...(ngDevMode ? [{ debugName: "isConversationActive" }] : /* istanbul ignore next */ []));
2524
- // Background
2525
- this.backgroundUrl = signal(undefined, ...(ngDevMode ? [{ debugName: "backgroundUrl" }] : /* istanbul ignore next */ []));
2526
- console.log(`%c ChatMonitorService instantiated: ${Math.random()}`, 'color: #00ff00; font-weight: bold; padding: 2px 4px; border: 1px solid #00ff00;');
2527
- }
2528
- logMessageAudioWillPlay(message) {
2529
- // console.log('Audio will play....', message);
2530
- this.messageAudioWillPlay.set(message);
2531
- }
2532
- logCurrentMood(mood) {
2533
- this.currentMood.set(mood);
2534
- }
2535
- cleanMonitorVars() {
2536
- this.currentMood.set(null);
2537
- this.messageAudioWillPlay.set(null);
2538
- this.isConversationActive.set(false);
2539
- }
2540
- setBackground(imageUrl) {
2541
- if (imageUrl) {
2542
- this.backgroundUrl.set(imageUrl);
2543
- }
2544
- else {
2545
- this.backgroundUrl.set(undefined);
2546
- }
2547
- }
2548
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2549
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, providedIn: 'root' }); }
2550
- }
2551
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMonitorService, decorators: [{
2552
- type: Injectable,
2553
- args: [{
2554
- providedIn: 'root',
2555
- }]
2556
- }], ctorParameters: () => [] });
2557
-
2558
2563
  class MessageProcessingService {
2559
2564
  constructor() {
2560
2565
  this.MAX_CHUNK_LENGTH = 190;
2561
2566
  }
2562
- processMessage(message, conversationSettings) {
2567
+ processMessage(message, conversationSettings, voiceSettings) {
2563
2568
  const processedMessage = {
2564
2569
  ...message,
2565
2570
  messageId: message.messageId || nanoid(),
@@ -2571,25 +2576,25 @@ class MessageProcessingService {
2571
2576
  return processedMessage;
2572
2577
  }
2573
2578
  if (processedMessage.role === ChatRole.Assistant) {
2574
- return this.processAssistantMessage(processedMessage, conversationSettings);
2579
+ return this.processAssistantMessage(processedMessage, conversationSettings, voiceSettings);
2575
2580
  }
2576
2581
  return processedMessage;
2577
2582
  }
2578
- processAssistantMessage(message, settings) {
2583
+ processAssistantMessage(message, settings, voiceSettings) {
2579
2584
  if (settings.avatarImages.assistant) {
2580
2585
  message.imgUrl = settings.avatarImages.assistant;
2581
2586
  }
2582
- const mainVoice = settings?.mainVoice?.voice || settings?.tts?.voice;
2587
+ const mainVoice = voiceSettings?.main?.voice || settings?.mainVoice?.voice || settings?.tts?.voice;
2583
2588
  message.voice = this.getVoice(mainVoice);
2584
2589
  switch (settings.textEngine) {
2585
2590
  case TextEngines.SimpleText:
2586
2591
  this.processSimpleText(message, settings);
2587
2592
  break;
2588
2593
  case TextEngines.MarkdownMultiMessages:
2589
- this.processMultiMessages(message, settings);
2594
+ this.processMultiMessages(message, settings, voiceSettings);
2590
2595
  break;
2591
2596
  case TextEngines.MarkdownSSML:
2592
- this.processSSML(message, settings);
2597
+ this.processSSML(message, settings, voiceSettings);
2593
2598
  break;
2594
2599
  }
2595
2600
  return message;
@@ -2608,9 +2613,9 @@ class MessageProcessingService {
2608
2613
  };
2609
2614
  });
2610
2615
  }
2611
- processMultiMessages(message, settings) {
2616
+ processMultiMessages(message, settings, voiceSettings) {
2612
2617
  const htmlSegments = convertToHTML(message.content);
2613
- 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';
2614
2619
  const processedSegments = [];
2615
2620
  for (const segment of htmlSegments) {
2616
2621
  const isItalics = segment.tag === 'em';
@@ -2659,11 +2664,12 @@ class MessageProcessingService {
2659
2664
  return `<p>${text}</p>`;
2660
2665
  }
2661
2666
  }
2662
- processSSML(message, settings) {
2663
- if (!settings?.secondaryVoice?.voice) {
2667
+ processSSML(message, settings, voiceSettings) {
2668
+ const secondaryVoice = voiceSettings?.secondary?.voice || settings?.secondaryVoice?.voice || settings?.tts?.secondaryVoice;
2669
+ if (!secondaryVoice) {
2664
2670
  throw new Error('Secondary voice is required for SSML');
2665
2671
  }
2666
- const content = this.subsItalicsByTag(message.content, settings.secondaryVoice.voice);
2672
+ const content = this.subsItalicsByTag(message.content, secondaryVoice);
2667
2673
  message.ssml = `<speak>${content}</speak>`;
2668
2674
  }
2669
2675
  splitContent(content, maxLength) {
@@ -3472,7 +3478,7 @@ class ConversationService {
3472
3478
  }
3473
3479
  createNewUserMessage() {
3474
3480
  const message = { content: '...', role: ChatRole.User };
3475
- const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState());
3481
+ const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3476
3482
  this.messagesStateService.addMessage(processedMessage);
3477
3483
  return processedMessage.messageId;
3478
3484
  }
@@ -3579,7 +3585,7 @@ class ConversationService {
3579
3585
  const firstAssistantMsg = conversationSettings.messages.find((message) => message.role === ChatRole.Assistant);
3580
3586
  if (firstAssistantMsg) {
3581
3587
  // Process the first assistant message
3582
- const processedMessage = this.messageProcessingService.processMessage(firstAssistantMsg, this.conversationSettingsState());
3588
+ const processedMessage = this.messageProcessingService.processMessage(firstAssistantMsg, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3583
3589
  this.messagesStateService.updateMessage(firstAssistantMsg.messageId, processedMessage);
3584
3590
  // Find the index of the message with the matching ID
3585
3591
  const messageIndex = conversationSettings.messages.findIndex((message) => message.messageId === firstAssistantMsg.messageId);
@@ -3628,7 +3634,7 @@ class ConversationService {
3628
3634
  }
3629
3635
  else {
3630
3636
  // Means is new meessage, Process and add the new message
3631
- const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState());
3637
+ const processedMessage = this.messageProcessingService.processMessage(message, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3632
3638
  // Ensure ID exists (processMessage should handle this, but fallback just in case)
3633
3639
  processedMessage.messageId = processedMessage.messageId || nanoid();
3634
3640
  this.messagesStateService.addMessage(processedMessage);
@@ -3699,7 +3705,7 @@ class ConversationService {
3699
3705
  throw new Error('No message returned from AI');
3700
3706
  }
3701
3707
  // Process response
3702
- 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);
3703
3709
  this.messagesStateService.addMessage(newMessage);
3704
3710
  this.isThinkingSignal.set(false);
3705
3711
  // Run Dynamic Flow Evaluations
@@ -3708,9 +3714,10 @@ class ConversationService {
3708
3714
  }
3709
3715
  async handleStreamingResponse(conversation, conversationSettings) {
3710
3716
  const messageId = nanoid();
3711
- 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;
3712
3719
  const assistantVoice = this.messageProcessingService.getVoice(mainVoice);
3713
- 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';
3714
3721
  let assistantMessage = {
3715
3722
  messageId,
3716
3723
  role: ChatRole.Assistant,
@@ -3882,7 +3889,7 @@ class ConversationService {
3882
3889
  isLoading: true,
3883
3890
  };
3884
3891
  // Process to get initial structure and ensure it's ready for display
3885
- const processedPlaceholder = this.messageProcessingService.processMessage(placeholder, this.conversationSettingsState());
3892
+ const processedPlaceholder = this.messageProcessingService.processMessage(placeholder, this.conversationSettingsState(), this.agentCardState.agentCard$()?.voice);
3886
3893
  // Use passed updateId or newly generated one from processing
3887
3894
  const messageId = updateId || processedPlaceholder.messageId || nanoid();
3888
3895
  processedPlaceholder.messageId = messageId;
@@ -3996,10 +4003,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
3996
4003
  const VIDEO_PLAYER_SERVICE_TOKEN = new InjectionToken('VIDEO_PLAYER_SERVICE_TOKEN');
3997
4004
  const BACKGROUND_SERVICE_TOKEN = new InjectionToken('BACKGROUND_SERVICE_TOKEN');
3998
4005
  const CHAT_OBSERVER_CONFIG = new InjectionToken('CHAT_OBSERVER_CONFIG');
4006
+ const LIVE2D_CONFIG = new InjectionToken('LIVE2D_CONFIG');
4007
+ function provideLive2D(config) {
4008
+ return {
4009
+ provide: LIVE2D_CONFIG,
4010
+ useValue: config
4011
+ };
4012
+ }
3999
4013
 
4000
4014
  class VideoPlayerService {
4001
4015
  constructor() {
4002
- this.conversationFlowStateService = inject(ConversationFlowStateService);
4003
4016
  this.useNativePlayer = false;
4004
4017
  this.isRewinding = false;
4005
4018
  this.videoQueue = [];
@@ -4009,17 +4022,6 @@ class VideoPlayerService {
4009
4022
  this.agentCard = signal(undefined, ...(ngDevMode ? [{ debugName: "agentCard" }] : /* istanbul ignore next */ []));
4010
4023
  console.log(' CONSTRUCTOR FOR VIDEO PLAYER...');
4011
4024
  this.playAndRewindedSubscription = this.playAndRewinded$.subscribe(() => { });
4012
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
4013
- console.log('CHANGING MOOD.. ', mood);
4014
- const card = this.agentCard();
4015
- if (mood && card?.assets?.motions) {
4016
- const motionUrl = card.assets.motions.find((m) => m.metadata?.moodState === mood)?.url;
4017
- if (motionUrl) {
4018
- this.addVideosToQueue([motionUrl]);
4019
- this.playAndRewind();
4020
- }
4021
- }
4022
- });
4023
4025
  }
4024
4026
  setAgentCard(card) {
4025
4027
  this.agentCard.set(card);
@@ -4151,9 +4153,6 @@ class VideoPlayerService {
4151
4153
  if (this.playAndRewindedSubscription) {
4152
4154
  this.playAndRewindedSubscription.unsubscribe();
4153
4155
  }
4154
- if (this.moodSubscription) {
4155
- this.moodSubscription.unsubscribe();
4156
- }
4157
4156
  }
4158
4157
  playNextInQueueNative() {
4159
4158
  if (this.videoQueue.length > 0) {
@@ -4241,25 +4240,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
4241
4240
 
4242
4241
  class VideoPlayerNativeService {
4243
4242
  constructor() {
4244
- this.conversationFlowStateService = inject(ConversationFlowStateService);
4245
4243
  this.videoQueue = [];
4246
4244
  this.defaultVideoUrl = null;
4247
4245
  this.pendingPlayingHandler = null;
4248
4246
  this.pendingPlayingTarget = null;
4249
4247
  this.agentCard = signal(undefined, ...(ngDevMode ? [{ debugName: "agentCard" }] : /* istanbul ignore next */ []));
4250
4248
  this.isVideoPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isVideoPlaying" }] : /* istanbul ignore next */ []));
4251
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
4252
- const card = this.agentCard();
4253
- if (!mood || !card?.assets?.motions)
4254
- return;
4255
- const motionUrl = card.assets.motions.find((m) => m.metadata?.moodState === mood)?.url;
4256
- if (!motionUrl)
4257
- return;
4258
- // Interrupt current playback. transitionToVideo loads the new clip
4259
- // into the hidden <video> and crossfades only when its first frame
4260
- // is ready, so the active clip keeps showing until then (no flash).
4261
- this.transitionToVideo(motionUrl);
4262
- });
4263
4249
  this.onVideoEndedBound = this.onVideoEnded.bind(this);
4264
4250
  }
4265
4251
  setAgentCard(card) {
@@ -4342,7 +4328,6 @@ class VideoPlayerNativeService {
4342
4328
  this.agentCard.set(undefined);
4343
4329
  }
4344
4330
  ngOnDestroy() {
4345
- this.moodSubscription?.unsubscribe();
4346
4331
  this.destroyPlayer();
4347
4332
  }
4348
4333
  onVideoEnded() {
@@ -4625,563 +4610,831 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
4625
4610
  args: [DCMicComponent]
4626
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"] }] } });
4627
4612
 
4628
- // Given a text that can be in markdown but only in asterisk like *italic* or **bold** or ***bold italic***
4629
- // Example Hola que tal es *Hey What's up* en inglés
4630
- // i want to extract in array every word like this.
4631
- // [{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: ''}]
4632
- function extractTags(text) {
4633
- const result = [];
4634
- const tagStack = [];
4635
- // Regex to match markdown markers (***, **, *) or spaces or sequences of non-space/non-asterisk characters (words with punctuation)
4636
- const regex = /(\*\*\*|\*\*|\*|\s+|[^\s\*]+)/g;
4637
- let match;
4638
- while ((match = regex.exec(text)) !== null) {
4639
- const token = match[0];
4640
- if (token.trim() === '') {
4641
- // Ignore spaces
4642
- continue;
4643
- }
4644
- if (token === '***') {
4645
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold italic') {
4646
- 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');
4647
4663
  }
4648
- else {
4649
- 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');
4650
4684
  }
4651
4685
  }
4652
- else if (token === '**') {
4653
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'bold') {
4654
- 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
+ }
4655
4766
  }
4656
- else {
4657
- 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);
4658
4773
  }
4659
- }
4660
- else if (token === '*') {
4661
- if (tagStack.length > 0 && tagStack[tagStack.length - 1] === 'italic') {
4662
- 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([]);
4663
4780
  }
4664
- else {
4665
- tagStack.push('italic'); // Opening italic
4781
+ if (messageSubject) {
4782
+ messageSubject.next([]);
4666
4783
  }
4667
4784
  }
4668
- else {
4669
- // It's a word (including punctuation)
4670
- const currentTag = tagStack.length > 0 ? tagStack[tagStack.length - 1] : '';
4671
- result.push({ word: token, tag: currentTag });
4672
- }
4673
4785
  }
4674
- // Note: This implementation assumes correctly matched opening and closing tags.
4675
- // Unclosed tags at the end of the string will be ignored.
4676
- return result;
4677
- }
4678
-
4679
- // Removed AudioState as it's replaced by AudioStatus from agent.models
4680
- class MessageContentDisplayer {
4681
- get hostHighlightColor() {
4682
- return this.highlightColor();
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');
4799
+ }
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);
4683
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 {
4684
4870
  constructor() {
4685
- // Inputs
4686
- this.message = input.required(...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
4687
- this.markWord = input(...(ngDevMode ? [undefined, { debugName: "markWord" }] : /* istanbul ignore next */ []));
4688
- this.highlightColor = input(...(ngDevMode ? [undefined, { debugName: "highlightColor" }] : /* istanbul ignore next */ []));
4689
- // Outputs
4690
- this.playAudio = output();
4691
- this.audioCompleted = output();
4692
- this.audioStatusChanged = output();
4693
- this.wordClicked = output();
4694
- // Signal States
4695
- this.wordWithMeta = signal([], ...(ngDevMode ? [{ debugName: "wordWithMeta" }] : /* istanbul ignore next */ []));
4696
- this.localAudioStatus = signal(null, ...(ngDevMode ? [{ debugName: "localAudioStatus" }] : /* istanbul ignore next */ []));
4697
- // Signals State computed from the input message
4698
- this.wordsWithMetaState = [];
4699
- this.lastHasTranscription = false;
4700
- this.audioElement = signal(null, ...(ngDevMode ? [{ debugName: "audioElement" }] : /* istanbul ignore next */ []));
4701
- this.destroyRef = inject(DestroyRef);
4702
- this.iconState = computed(() => {
4703
- const msg = this.message();
4704
- if (msg?.isLoading) {
4705
- return 'loading';
4706
- }
4707
- const status = this.localAudioStatus() || msg?.audioStatus;
4708
- if (status === 'playing') {
4709
- return 'playing';
4710
- }
4711
- if (status === 'stopped') {
4712
- return 'paused';
4713
- }
4714
- if (status === 'finished') {
4715
- return 'finished';
4716
- }
4717
- if (status === 'pending') {
4718
- return 'pending';
4719
- }
4720
- if (msg?.audioUrl) {
4721
- return 'playable';
4722
- }
4723
- return 'idle';
4724
- }, ...(ngDevMode ? [{ debugName: "iconState" }] : /* istanbul ignore next */ []));
4725
- this.isPendingAudio = computed(() => !!this.message() && this.message()?.audioStatus === 'pending', ...(ngDevMode ? [{ debugName: "isPendingAudio" }] : /* istanbul ignore next */ []));
4726
- this.messageText = computed(() => {
4727
- const msg = this.message();
4728
- return msg?.text || msg?.content || 'N/A';
4729
- }, ...(ngDevMode ? [{ debugName: "messageText" }] : /* istanbul ignore next */ []));
4730
- this.classTag = computed(() => this.message()?.tag, ...(ngDevMode ? [{ debugName: "classTag" }] : /* istanbul ignore next */ []));
4731
- // When message is updated, check is has transcriptions whisper format.
4732
- this.transcriptionTimestamps = computed(() => {
4733
- const msg = this.message();
4734
- if (!msg || !msg.transcription) {
4735
- return [];
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
+ });
4736
4918
  }
4737
- return matchTranscription(msg.text || msg.content, msg.transcription.words);
4738
- }, ...(ngDevMode ? [{ debugName: "transcriptionTimestamps" }] : /* istanbul ignore next */ []));
4739
- // if transcriptionTimestamps is calculated then hasTranscription is true.
4740
- this.hasTranscription = computed(() => {
4741
- return this.transcriptionTimestamps().length > 0;
4742
- }, ...(ngDevMode ? [{ debugName: "hasTranscription" }] : /* istanbul ignore next */ []));
4743
- effect(() => {
4744
- const currentMsg = this.message();
4745
- if (!currentMsg)
4746
- return;
4747
- const hasTranscription = this.hasTranscription();
4748
- const idChanged = currentMsg.messageId !== this.currentMessageId;
4749
- const audioUrlChanged = currentMsg.audioUrl !== this.lastAudioUrl;
4750
- const transcriptionChanged = hasTranscription !== this.lastHasTranscription;
4751
- if (idChanged || audioUrlChanged || transcriptionChanged) {
4752
- this.currentMessageId = currentMsg.messageId;
4753
- this.lastAudioUrl = currentMsg.audioUrl;
4754
- this.lastHasTranscription = hasTranscription;
4755
- this.localAudioStatus.set(null); // Reset local state on message/audio change
4756
- this.initializeBasedOnMessage(currentMsg);
4919
+ else {
4920
+ this.messages.set([...messages]);
4757
4921
  }
4758
- });
4759
- effect(() => {
4760
- if (this.isPendingAudio()) {
4761
- 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
+ }
4762
4942
  }
4763
4943
  });
4764
- effect(() => {
4765
- this.wordsWithMetaState = this.wordWithMeta();
4766
- });
4767
- }
4768
- trackByIndex(index, item) {
4769
- return index;
4770
4944
  }
4771
- ngOnDestroy() {
4772
- 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
+ }
4773
4960
  }
4774
- initializeBasedOnMessage(msg) {
4775
- if (msg.audioUrl) {
4776
- 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
+ })));
4777
4982
  }
4778
- if (this.hasTranscription()) {
4779
- const wordsAndTimestamps = [];
4780
- this.transcriptionTimestamps()?.forEach((word, index) => {
4781
- 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,
4782
4991
  });
4783
- const firstState = this.transcriptionTimestamps()?.map((word, index) => ({
4784
- word: word.word,
4785
- index,
4786
- isHighlighted: false,
4787
- tag: '',
4788
- marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
4789
- }));
4790
- console.log('firstState', firstState);
4791
- this.wordWithMeta.set(firstState);
4792
- // Para mostrar las transcripciones iliminadas: el audio tiene que sonar y en la subscripción apareceran los cambios.
4793
4992
  }
4794
- else {
4795
- 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);
4796
5012
  }
4797
- // Removal of shouldPlayAudio check - playback now handled by isPendingAudio effect
4798
5013
  }
4799
- initializeAudio(audioUrl) {
4800
- let audio = this.audioElement();
4801
- if (!audio) {
4802
- audio = new Audio(audioUrl);
4803
- this.audioElement.set(audio);
4804
- this.setupAudioEventListeners(audio);
5014
+ async processNextInQueue() {
5015
+ if (this.audioQueue.length === 0 || this.isGenerating() || this.currentPlayingIndex() !== null) {
5016
+ return;
4805
5017
  }
4806
- else if (audio.src && audioUrl && !audio.src.endsWith(audioUrl)) {
4807
- 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);
4808
5028
  }
5029
+ this.preGenerateNextIfNeeded();
4809
5030
  }
4810
- setupAudioEventListeners(audioElement) {
4811
- fromEvent(audioElement, 'timeupdate')
4812
- .pipe(takeUntilDestroyed(this.destroyRef), map(() => audioElement.currentTime || 0))
4813
- .subscribe((currentTime) => {
4814
- if (currentTime === 0) {
4815
- 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) {
4816
5038
  return;
4817
5039
  }
4818
- if (this.hasTranscription()) {
4819
- const wordsAndTimestamps = [];
4820
- this.transcriptionTimestamps()?.forEach((word, index) => {
4821
- wordsAndTimestamps.push({ ...word, ...this.wordsWithMetaState[index], index });
4822
- });
4823
- const updatedWords = wordsAndTimestamps.map((word, index) => ({
4824
- word: word.word,
4825
- index,
4826
- isHighlighted: currentTime >= word.start - 0.15 && currentTime < word.end + 0.15,
4827
- tag: word.tag,
4828
- marked: word.word.toLowerCase() == this.markWord()?.toLowerCase(),
4829
- }));
4830
- 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);
4831
5049
  }
4832
- });
4833
- fromEvent(audioElement, 'ended')
4834
- .pipe(takeUntilDestroyed(this.destroyRef))
4835
- .subscribe(() => {
4836
- const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
4837
- this.wordWithMeta.set(resetWords);
4838
- const currentMsg = this.message();
4839
- if (currentMsg) {
4840
- // No direct mutation of input!
4841
- this.updateAudioStatus('finished');
4842
- this.audioCompleted.emit(currentMsg);
5050
+ finally {
5051
+ this.preGenerationInProgress.set(false);
4843
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;
4844
5091
  });
4845
5092
  }
4846
- updateAudioStatus(status) {
4847
- const currentMsg = this.message();
4848
- // Update local state for immediate feedback
4849
- this.localAudioStatus.set(status);
4850
- // Only update if the status is actually changing relative to the current input
4851
- if (currentMsg && currentMsg.audioStatus !== status) {
4852
- // NOTE: We do NOT mutate currentMsg.audioStatus here.
4853
- // The parent orchestration service will update the state and propagate it back via signals.
4854
- 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;
4855
5103
  }
4856
- }
4857
- cleanupAudio() {
4858
- const audioElement = this.audioElement();
4859
- if (audioElement) {
4860
- audioElement.pause();
4861
- audioElement.src = '';
4862
- const resetWords = this.wordWithMeta().map((word) => ({ ...word, isHighlighted: false }));
4863
- this.wordWithMeta.set(resetWords);
5104
+ finally {
5105
+ this.isGenerating.set(false);
4864
5106
  }
4865
5107
  }
4866
- onPlayMessage() {
4867
- console.log('onPlayMessage');
4868
- const currentMsg = this.message();
4869
- const audioElement = this.audioElement();
4870
- if (audioElement) {
4871
- if (audioElement.paused) {
4872
- 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
4873
5134
  }
4874
5135
  else {
4875
- audioElement.pause();
4876
- this.updateAudioStatus('stopped');
5136
+ tagStack.push('bold italic'); // Opening bold italic
4877
5137
  }
4878
5138
  }
4879
- else if (currentMsg?.audioUrl) {
4880
- console.log('onPlayMessage initializeAudio with url');
4881
- this.initializeAudio(currentMsg.audioUrl);
4882
- this.startAudioPlayback();
4883
- }
4884
- else if (currentMsg) {
4885
- console.log('onPlayMessage emit playAudio');
4886
- 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
+ }
4887
5146
  }
4888
- }
4889
- initializePlainTextWords(text) {
4890
- const words = extractTags(text);
4891
- const plainWords = words.map((word, index) => ({
4892
- word: word.word,
4893
- index: index,
4894
- isHighlighted: false,
4895
- tag: word.tag,
4896
- }));
4897
- this.wordWithMeta.set(plainWords);
4898
- }
4899
- onWordClick(wordData) {
4900
- const currentMsg = this.message();
4901
- if (!currentMsg)
4902
- return;
4903
- const clickedWord = 'isHighlighted' in wordData
4904
- ? { word: wordData.word, index: wordData.index, messageId: currentMsg.messageId }
4905
- : { ...wordData, messageId: currentMsg.messageId };
4906
- this.wordClicked.emit(clickedWord);
4907
- }
4908
- startAudioPlayback() {
4909
- const audioElement = this.audioElement();
4910
- if (!audioElement)
4911
- return;
4912
- try {
4913
- audioElement.play();
4914
- 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
+ }
4915
5154
  }
4916
- catch (error) {
4917
- 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 });
4918
5159
  }
4919
- // Important: we move it to playing status immediately to avoid re-triggering the effect
4920
- this.updateAudioStatus('playing');
4921
- }
4922
- debug() {
4923
- console.log('debug', this.wordWithMeta());
4924
5160
  }
4925
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4926
- 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;
4927
5164
  }
4928
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageContentDisplayer, decorators: [{
4929
- type: Component,
4930
- 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"] }]
4931
- }], 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: [{
4932
- type: HostBinding,
4933
- args: ['style.--highlight-bg-color']
4934
- }], playAudio: [{ type: i0.Output, args: ["playAudio"] }], audioCompleted: [{ type: i0.Output, args: ["audioCompleted"] }], audioStatusChanged: [{ type: i0.Output, args: ["audioStatusChanged"] }], wordClicked: [{ type: i0.Output, args: ["wordClicked"] }] } });
4935
5165
 
4936
5166
  /**
4937
- * @Injectable
5167
+ * @Component
4938
5168
  *
4939
5169
  * @description
4940
- * This service orchestrates the playback of chat messages, ensuring that they are played sequentially.
4941
- * It manages a queue of messages and handles the generation and playback of audio for each message.
5170
+ * Variante "atada al chat" de `MessageContentDisplayer`.
4942
5171
  *
4943
- * @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)
4944
5186
  */
4945
- class MessageOrchestrationService {
4946
- constructor() {
4947
- // Services
4948
- this.conversationService = inject(ConversationService);
4949
- this.chatMonitorService = inject(ChatMonitorService);
4950
- this.userService = inject(UserService);
4951
- this.messagesStateService = inject(MessagesStateService);
4952
- // State Signals
4953
- this.messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
4954
- this.messageRole = signal(ChatRole.User, ...(ngDevMode ? [{ debugName: "messageRole" }] : /* istanbul ignore next */ []));
4955
- this.messageToPlay = signal(null, ...(ngDevMode ? [{ debugName: "messageToPlay" }] : /* istanbul ignore next */ []));
4956
- // Public Signals for components to consume
4957
- this.messagesSignal = this.messages.asReadonly();
4958
- this.messageToPlay$ = this.messageToPlay.asReadonly();
4959
- // Audio queue management
4960
- this.audioQueue = [];
4961
- this.isGenerating = signal(false, ...(ngDevMode ? [{ debugName: "isGenerating" }] : /* istanbul ignore next */ []));
4962
- this.currentPlayingIndex = signal(null, ...(ngDevMode ? [{ debugName: "currentPlayingIndex" }] : /* istanbul ignore next */ []));
4963
- this.preGenerationInProgress = signal(false, ...(ngDevMode ? [{ debugName: "preGenerationInProgress" }] : /* istanbul ignore next */ []));
4964
- console.log('MessageOrchestrationService initialized');
5187
+ class MessageContentReactive {
5188
+ get hostHighlightColor() {
5189
+ return this.highlightColor();
4965
5190
  }
4966
- /**
4967
- * @description
4968
- * Starts the orchestration process for a list of messages.
4969
- *
4970
- * @param messages - An array of `MessageContent` objects to be orchestrated.
4971
- * @param role - The `ChatRole` of the messages' author.
4972
- */
4973
- startOrchestration(messages, role) {
4974
- untracked(() => {
4975
- const currentMessages = this.messages();
4976
- const isAssistant = role === ChatRole.Assistant;
4977
- // Check if it's a continuation of the same orchestration (e.g., streaming)
4978
- const isContinuation = currentMessages.length > 0 &&
4979
- messages.length >= currentMessages.length &&
4980
- messages[0].messageId === currentMessages[0].messageId;
4981
- // Update internal state signal with new messages
4982
- // If it's a continuation, we want to merge properties carefully to avoid flickering or resetting local state like 'playing' status
4983
- if (isContinuation) {
4984
- this.messages.update(prev => {
4985
- return messages.map((newMsg, idx) => {
4986
- const prevMsg = prev[idx];
4987
- if (prevMsg) {
4988
- // Merge properties: keep local transient state like audioStatus if it's currently managed by this service
4989
- return { ...newMsg, ...prevMsg, ...newMsg }; // prioritize newMsg but keep prevMsg as base if needed
4990
- }
4991
- return newMsg;
4992
- });
4993
- });
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';
4994
5215
  }
4995
- else {
4996
- this.messages.set([...messages]);
5216
+ const status = msg?.audioStatus;
5217
+ if (status === 'playing') {
5218
+ return 'playing';
4997
5219
  }
4998
- this.messageRole.set(role);
4999
- if (isAssistant) {
5000
- const chatSettings = this.userService.user()?.settings?.conversation;
5001
- if (chatSettings?.synthVoice) {
5002
- if (!isContinuation) {
5003
- console.log('Starting new orchestration');
5004
- this.initializeAudioQueue();
5005
- this.processNextInQueue();
5006
- }
5007
- else {
5008
- console.log('Updating existing orchestration queue');
5009
- // Update the queue with new indices
5010
- for (let i = currentMessages.length; i < messages.length; i++) {
5011
- if (messages[i].audioStatus !== 'finished' && !this.audioQueue.includes(i)) {
5012
- this.audioQueue.push(i);
5013
- }
5014
- }
5015
- this.processNextInQueue();
5016
- }
5017
- }
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();
5018
5268
  }
5019
5269
  });
5270
+ effect(() => {
5271
+ this.wordsWithMetaState = this.wordWithMeta();
5272
+ });
5020
5273
  }
5021
- audioCompleted(message) {
5022
- const index = this.currentPlayingIndex();
5023
- if (index !== null) {
5024
- console.log(`Audio completed for index ${index}, setting status to finished`);
5025
- this.updateAudioStatus(message, 'finished', index);
5026
- }
5027
- else {
5028
- console.warn('Audio completed but currentPlayingIndex is null');
5029
- }
5030
- this.conversationService.currentAudioStatus.set({ message, completed: true });
5031
- this.currentPlayingIndex.set(null);
5032
- this.messageToPlay.set(null);
5033
- if (this.audioQueue.length > 0) {
5034
- this.processNextInQueue();
5035
- }
5274
+ trackByIndex(index, item) {
5275
+ return index;
5036
5276
  }
5037
- /**
5038
- * @description
5039
- * Updates the audio status of a message.
5040
- *
5041
- * @param message - The `MessageContent` object whose status has changed.
5042
- * @param status - The new `AudioStatus`.
5043
- * @param index - The index of the message in the orchestration queue.
5044
- */
5045
- updateAudioStatus(message, status, index) {
5046
- const currentMessage = this.messages()[index];
5047
- console.log(`Updating audio status for index ${index} to ${status}. MessageId: ${message.messageId}`);
5048
- if (status === 'playing') {
5049
- if (message.messageId) {
5050
- 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');
5051
5290
  }
5052
- // Update local state: set this one as active, others as inactive
5053
- this.messages.update((messages) => messages.map((m, idx) => ({
5054
- ...m,
5055
- isActive: idx === index,
5056
- // Also update the current one's status
5057
- ...(idx === index ? { audioStatus: status } : {}),
5058
- })));
5059
- }
5060
- else {
5061
- // For other statuses (stopped, finished), preserve isActive and update status
5062
- this.changeStates(index, { ...message, audioStatus: status });
5063
5291
  }
5064
- // 4. Update global state
5065
- if (message.messageId) {
5066
- this.messagesStateService.updateMultiMessage(message.messageId, index, {
5067
- audioStatus: status,
5068
- });
5292
+ else if (currentMsg?.audioUrl) {
5293
+ this.initializeAudio(currentMsg.audioUrl);
5294
+ this.startAudioPlayback();
5069
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
+ });
5070
5307
  }
5308
+ // --- Reporte de estado directo al orquestador (sin @Output) ---
5071
5309
  /**
5072
- * @description
5073
- * Notifies the `ConversationService` when a word is clicked.
5074
- *
5075
- * @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).
5076
5312
  */
5077
- wordClicked(wordData) {
5078
- this.conversationService.notifyWordClicked(wordData);
5079
- }
5080
- initializeAudioQueue() {
5081
- const messages = this.messages();
5082
- if (messages && messages.length > 0) {
5083
- this.audioQueue = messages
5084
- .map((m, index) => ({ status: m.audioStatus, index }))
5085
- .filter((item) => item.status !== 'finished' && item.status !== 'skip')
5086
- .map((item) => item.index);
5087
- 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());
5088
5317
  }
5089
5318
  }
5090
- async processNextInQueue() {
5091
- if (this.audioQueue.length === 0 || this.isGenerating() || this.currentPlayingIndex() !== null) {
5092
- return;
5319
+ // --- Audio lifecycle ---
5320
+ initializeBasedOnMessage(msg) {
5321
+ if (msg.audioUrl) {
5322
+ this.initializeAudio(msg.audioUrl);
5093
5323
  }
5094
- const nextIndex = this.audioQueue.shift();
5095
- this.isGenerating.set(true);
5096
- try {
5097
- 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);
5098
5333
  }
5099
- catch (error) {
5100
- console.error('Error generating audio:', error);
5334
+ else {
5335
+ this.initializePlainTextWords(msg.text || msg.content || '');
5101
5336
  }
5102
- finally {
5103
- 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;
5104
5347
  }
5105
- this.preGenerateNextIfNeeded();
5106
5348
  }
5107
- async preGenerateNextIfNeeded() {
5108
- if (this.audioQueue.length > 0 && !this.preGenerationInProgress()) {
5109
- const nextIndex = this.audioQueue[0];
5110
- const messages = this.messages();
5111
- const message = messages[nextIndex];
5112
- // Skip if audio is already generated or generation is not needed
5113
- if (message.audioUrl || message.isLoading) {
5114
- 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);
5115
5366
  }
5116
- this.preGenerationInProgress.set(true);
5117
- const loadingMessage = { ...message, isLoading: true };
5118
- this.changeStates(nextIndex, loadingMessage);
5119
- try {
5120
- const messageAudio = await this.generateAudio(message, null);
5121
- messageAudio.isLoading = false;
5122
- // Pre-generated audio stays in the background, no status update needed here or set to 'stopped'/'playable'
5123
- // Actually, if it's pre-rendered it should probably stay as whatever it was but with audioUrl
5124
- 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');
5125
5380
  }
5126
- finally {
5127
- 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);
5128
5391
  }
5129
- }
5392
+ });
5130
5393
  }
5131
- async generateAudioForIndex(index) {
5132
- const messages = this.messages();
5133
- if (messages[index].audioUrl) {
5134
- const messageAudio = { ...messages[index], audioStatus: 'pending' };
5135
- this.changeStates(index, messageAudio);
5136
- this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5137
- this.messageToPlay.set(messageAudio);
5138
- this.currentPlayingIndex.set(index);
5394
+ startAudioPlayback() {
5395
+ const audioElement = this.audioElement();
5396
+ if (!audioElement)
5397
+ return;
5398
+ const currentMsg = this.message();
5399
+ if (!currentMsg)
5139
5400
  return;
5140
- }
5141
- const message = { ...messages[index], isLoading: true };
5142
- this.changeStates(index, message);
5143
5401
  try {
5144
- const messageAudio = await this.generateAudio(messages[index], null);
5145
- messageAudio.isLoading = false;
5146
- messageAudio.audioStatus = 'pending';
5147
- this.changeStates(index, messageAudio);
5148
- this.chatMonitorService.logMessageAudioWillPlay(messageAudio);
5149
- this.messageToPlay.set(messageAudio);
5402
+ this.audioTextSyncService.playAudio(audioElement, currentMsg);
5150
5403
  }
5151
- finally {
5152
- const message = { ...messages[index], isLoading: false };
5153
- this.changeStates(index, message);
5404
+ catch (error) {
5405
+ console.error('Error playing audio:', error);
5154
5406
  }
5155
- this.currentPlayingIndex.set(index);
5156
- }
5157
- changeStates(index, messageAudio) {
5158
- this.messages.update((messages) => {
5159
- const newMessages = [...messages];
5160
- newMessages[index] = { ...messages[index], ...messageAudio };
5161
- return newMessages;
5162
- });
5407
+ this.reportStatus('playing');
5163
5408
  }
5164
- async generateAudio(message, overwriteText = null) {
5165
- if (message.audioUrl) {
5166
- return message;
5167
- }
5168
- try {
5169
- const text = overwriteText || message.text || message.content;
5170
- const ttsObject = buildObjectTTSRequest({ ...message, text }, { highlightWords: true });
5171
- const speechAudio = await this.conversationService.getTTSFile(ttsObject);
5172
- message = extractAudioAndTranscription(message, speechAudio);
5173
- return message;
5174
- }
5175
- finally {
5176
- 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);
5177
5416
  }
5178
5417
  }
5179
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5180
- 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 }); }
5181
5430
  }
5182
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageOrchestrationService, decorators: [{
5183
- type: Injectable
5184
- }], 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
+ }] } });
5185
5438
 
5186
5439
  /**
5187
5440
  * @Component
@@ -5190,8 +5443,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
5190
5443
  * This component orchestrates the display and audio playback of chat messages.
5191
5444
  * It uses the `MessageOrchestrationService` to manage the message queue and audio playback.
5192
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
+ *
5193
5450
  * @see MessageOrchestrationService
5194
- * @see MessageContentDisplayer
5451
+ * @see MessageContentReactive
5195
5452
  */
5196
5453
  class ChatMessageOrchestratorComponent {
5197
5454
  constructor() {
@@ -5217,41 +5474,12 @@ class ChatMessageOrchestratorComponent {
5217
5474
  this.orchestrationService.startOrchestration(this.messages(), this.messageRole());
5218
5475
  });
5219
5476
  }
5220
- /**
5221
- * @description
5222
- * Handles the `audioCompleted` event from the `MessageContentDisplayer`.
5223
- *
5224
- * @param message - The `MessageContent` object whose audio has finished playing.
5225
- */
5226
- onAudioCompleted(message) {
5227
- this.orchestrationService.audioCompleted(message);
5228
- this.audioCompleted.emit(message);
5229
- }
5230
- /**
5231
- * @description
5232
- * Handles the `audioStatusChanged` event from the `MessageContentDisplayer`.
5233
- *
5234
- * @param event - The event object containing the `MessageContent` and its new `AudioStatus`.
5235
- * @param index - The index of the message in the array.
5236
- */
5237
- onAudioStatusChanged(event, index) {
5238
- this.orchestrationService.updateAudioStatus(event.message, event.status, index);
5239
- }
5240
- /**
5241
- * @description
5242
- * Handles the `wordClicked` event from the `MessageContentDisplayer`.
5243
- *
5244
- * @param wordData - The `WordData` object containing information about the clicked word.
5245
- */
5246
- onWordClicked(wordData) {
5247
- this.orchestrationService.wordClicked(wordData);
5248
- }
5249
5477
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMessageOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5250
- 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 }); }
5251
5479
  }
5252
5480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ChatMessageOrchestratorComponent, decorators: [{
5253
5481
  type: Component,
5254
- 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"] }]
5255
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"] }] } });
5256
5484
 
5257
5485
  const EMOJI_TO_LABEL = new Map(MoodStateOptions.map((o) => [o.emoji, o.label]));
@@ -5567,169 +5795,262 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
5567
5795
  args: ['tooltipRef']
5568
5796
  }] } });
5569
5797
 
5570
- 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
+ }
5571
5803
  constructor() {
5572
- // Maps to store message-specific signals and observables
5573
- this.highlightedWordsSignalMap = new Map();
5574
- this.highlightedWords$Map = new Map();
5575
- // Maps for cleanup and active audio elements
5576
- this.cleanup$Map = new Map();
5577
- 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 */ []));
5578
5820
  this.destroyRef = inject(DestroyRef);
5579
- // Ensure cleanup when service is destroyed
5580
- this.destroyRef.onDestroy(() => {
5581
- 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();
5582
5885
  });
5583
5886
  }
5584
- /**
5585
- * Synchronizes audio playback with text transcription
5586
- * @param audioElement The audio element to sync with
5587
- * @param transcriptionTimestamps Array of word timestamps
5588
- * @param messageId Unique identifier for the message
5589
- */
5590
- syncAudioWithText(audioElement, transcriptionTimestamps, messageId) {
5591
- // Stop any existing sync for this message
5592
- this.stopSync(messageId);
5593
- // Create new signal and subject for this message if they don't exist
5594
- if (!this.highlightedWordsSignalMap.has(messageId)) {
5595
- 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);
5596
5896
  }
5597
- if (!this.highlightedWords$Map.has(messageId)) {
5598
- 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.
5599
5912
  }
5600
- // Create cleanup subject
5601
- const cleanup$ = new Subject();
5602
- this.cleanup$Map.set(messageId, cleanup$);
5603
- // Store the active audio element
5604
- this.activeAudioMap.set(messageId, audioElement);
5605
- // Get the signal and subject for this message
5606
- const messageSignal = this.highlightedWordsSignalMap.get(messageId);
5607
- const messageSubject = this.highlightedWords$Map.get(messageId);
5608
- // Initialize the highlighted words state
5609
- const initialWords = transcriptionTimestamps.map((word, index) => ({
5610
- word: word.word,
5611
- index,
5612
- isHighlighted: false,
5613
- }));
5614
- // Update both signal and observable
5615
- messageSignal.set(initialWords);
5616
- messageSubject.next(initialWords);
5617
- // 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) {
5618
5930
  fromEvent(audioElement, 'timeupdate')
5619
- .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$), map(() => audioElement.currentTime))
5931
+ .pipe(takeUntilDestroyed(this.destroyRef), map(() => audioElement.currentTime || 0))
5620
5932
  .subscribe((currentTime) => {
5621
- const updatedWords = transcriptionTimestamps.map((word, index) => {
5622
- const isHighlighted = currentTime >= word.start - 0.15 && currentTime < word.end + 0.15;
5623
- 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) => ({
5624
5943
  word: word.word,
5625
5944
  index,
5626
- isHighlighted,
5627
- };
5628
- });
5629
- // Update both signal and observable for this message
5630
- messageSignal.set(updatedWords);
5631
- 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
+ }
5632
5951
  });
5633
- // Listen to ended event for cleanup
5634
5952
  fromEvent(audioElement, 'ended')
5635
- .pipe(takeUntilDestroyed(this.destroyRef), takeUntil(cleanup$))
5953
+ .pipe(takeUntilDestroyed(this.destroyRef))
5636
5954
  .subscribe(() => {
5637
- // Reset highlighting when audio ends
5638
- const resetWords = initialWords.map((word) => ({
5639
- ...word,
5640
- isHighlighted: false,
5641
- }));
5642
- messageSignal.set(resetWords);
5643
- messageSubject.next(resetWords);
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);
5962
+ }
5644
5963
  });
5645
5964
  }
5646
- /**
5647
- * Stops the sync for a specific message and cleans up resources
5648
- * @param messageId The ID of the message to stop syncing
5649
- */
5650
- stopSync(messageId) {
5651
- if (this.activeAudioMap.has(messageId)) {
5652
- // Get the cleanup subject for this message
5653
- const cleanup$ = this.cleanup$Map.get(messageId);
5654
- if (cleanup$) {
5655
- cleanup$.next();
5656
- cleanup$.complete();
5657
- this.cleanup$Map.delete(messageId);
5658
- }
5659
- this.activeAudioMap.delete(messageId);
5660
- // Reset state for this message
5661
- const messageSignal = this.highlightedWordsSignalMap.get(messageId);
5662
- const messageSubject = this.highlightedWords$Map.get(messageId);
5663
- if (messageSignal) {
5664
- messageSignal.set([]);
5665
- }
5666
- if (messageSubject) {
5667
- messageSubject.next([]);
5668
- }
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 });
5669
5974
  }
5670
5975
  }
5671
- /**
5672
- * Stops all syncs and cleans up all resources
5673
- */
5674
- stopAllSyncs() {
5675
- // Get all message IDs and stop each sync
5676
- for (const messageId of this.activeAudioMap.keys()) {
5677
- this.stopSync(messageId);
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);
5678
5983
  }
5679
- // Clear all maps
5680
- this.highlightedWordsSignalMap.clear();
5681
- this.highlightedWords$Map.clear();
5682
- this.cleanup$Map.clear();
5683
- this.activeAudioMap.clear();
5684
5984
  }
5685
- /**
5686
- * Returns the highlighted words signal for a specific message
5687
- * @param messageId The ID of the message
5688
- */
5689
- getHighlightedWordsSignal(messageId) {
5690
- // Create a new signal for this message if it doesn't exist
5691
- if (!this.highlightedWordsSignalMap.has(messageId)) {
5692
- this.highlightedWordsSignalMap.set(messageId, signal([]));
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
+ }
5693
5997
  }
5694
- return this.highlightedWordsSignalMap.get(messageId);
5695
- }
5696
- /**
5697
- * Returns the highlighted words observable for a specific message
5698
- * @param messageId The ID of the message
5699
- */
5700
- getHighlightedWords$(messageId) {
5701
- // Create a new subject for this message if it doesn't exist
5702
- if (!this.highlightedWords$Map.has(messageId)) {
5703
- this.highlightedWords$Map.set(messageId, new BehaviorSubject([]));
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);
5704
6006
  }
5705
- return this.highlightedWords$Map.get(messageId).asObservable();
5706
6007
  }
5707
- /**
5708
- * Checks if a word at a specific index is currently highlighted for a specific message
5709
- * @param messageId The ID of the message
5710
- * @param index The index of the word to check
5711
- */
5712
- isWordHighlighted(messageId, index) {
5713
- const messageSignal = this.getHighlightedWordsSignal(messageId);
5714
- 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);
5715
6017
  }
5716
- /**
5717
- * Returns an observable that emits true when a word at a specific index is highlighted for a specific message
5718
- * @param messageId The ID of the message
5719
- * @param index The index of the word to observe
5720
- */
5721
- isWordHighlighted$(messageId, index) {
5722
- 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);
5723
6026
  }
5724
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5725
- 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 }); }
5726
6046
  }
5727
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudioTextSyncService, decorators: [{
5728
- type: Injectable,
5729
- args: [{
5730
- providedIn: 'root',
5731
- }]
5732
- }], 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"] }] } });
5733
6054
 
5734
6055
  class DcImmersiveChatComponent {
5735
6056
  constructor() {
@@ -6133,16 +6454,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
6133
6454
 
6134
6455
  class MessagesStateInspectorComponent {
6135
6456
  constructor() {
6136
- this.messagesStateService = inject(MessagesStateService);
6137
- 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 */ []));
6138
6460
  }
6139
6461
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessagesStateInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6140
- 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" }] }); }
6141
6463
  }
6142
6464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessagesStateInspectorComponent, decorators: [{
6143
6465
  type: Component,
6144
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" }]
6145
- }] });
6467
+ }], propDecorators: { service: [{ type: i0.Input, args: [{ isSignal: true, alias: "service", required: false }] }] } });
6146
6468
 
6147
6469
  class DcAgentCardDetailComponent extends EntityBaseDetailComponent {
6148
6470
  constructor() {
@@ -6182,11 +6504,11 @@ class DcAgentCardDetailComponent extends EntityBaseDetailComponent {
6182
6504
  this.selectedMotion.set(null);
6183
6505
  }
6184
6506
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6185
- 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 }); }
6186
6508
  }
6187
6509
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardDetailComponent, decorators: [{
6188
6510
  type: Component,
6189
- 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"] }]
6190
6512
  }], ctorParameters: () => [], propDecorators: { entityData: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityData", required: false }] }] } });
6191
6513
 
6192
6514
  class ConversationSummaryComponent {
@@ -6288,11 +6610,11 @@ class ConversationInspector {
6288
6610
  this.viewMode.set(this.viewMode() === 'markdown' ? 'regular' : 'markdown');
6289
6611
  }
6290
6612
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ConversationInspector, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6291
- 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" }] }); }
6292
6614
  }
6293
6615
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ConversationInspector, decorators: [{
6294
6616
  type: Component,
6295
- 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"] }]
6296
6618
  }], ctorParameters: () => [], propDecorators: { completeEvent: [{ type: i0.Output, args: ["completeEvent"] }] } });
6297
6619
 
6298
6620
  class ConversationInfoService {
@@ -6343,7 +6665,9 @@ class DCChatComponent {
6343
6665
  // 📥 Input Signals
6344
6666
  this.parseDict = input({}, ...(ngDevMode ? [{ debugName: "parseDict" }] : /* istanbul ignore next */ []));
6345
6667
  // 📤 Outputs
6668
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
6346
6669
  this.chatEvent = output(); // Notifies about various events happening inside the chat
6670
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
6347
6671
  this.goalCompleted = output(); // notifies when user completes goal (score reaches 100)
6348
6672
  // readonly wordClicked = output<WordData>(); // Replaced by sendMessage with ChatEventType.WordClicked
6349
6673
  // Signals States
@@ -6356,6 +6680,7 @@ class DCChatComponent {
6356
6680
  effect(() => {
6357
6681
  const score = this.conversationFlowStateService.flowState().goal.value;
6358
6682
  if (score >= 100) {
6683
+ // TODO: check if i can use the monitor in the father so subscribe to this.
6359
6684
  this.goalCompleted.emit();
6360
6685
  }
6361
6686
  });
@@ -6369,19 +6694,6 @@ class DCChatComponent {
6369
6694
  // this.conversationService.notifyWordClicked(null);
6370
6695
  }
6371
6696
  });
6372
- // Effect to watch for audio playback events
6373
- effect(() => {
6374
- const message = this.chatMonitorService.messageAudioWillPlay$();
6375
- if (message) {
6376
- this.chatEvent.emit({ type: ChatEventType.AudioStarted, payload: message });
6377
- }
6378
- });
6379
- // Subscribe to mood updates
6380
- this.moodSubscription = this.conversationFlowStateService.moodUpdated$.subscribe((mood) => {
6381
- if (mood) {
6382
- this.chatEvent.emit({ type: ChatEventType.MoodDetected, payload: mood });
6383
- }
6384
- });
6385
6697
  }
6386
6698
  async ngOnInit() {
6387
6699
  this.conversationService.setDestroyed(false);
@@ -6402,7 +6714,6 @@ class DCChatComponent {
6402
6714
  // Mark conversation as destroyed to prevent async operations
6403
6715
  this.conversationService.setDestroyed(true);
6404
6716
  this.chatMonitorService.cleanMonitorVars();
6405
- this.moodSubscription?.unsubscribe();
6406
6717
  // Ensure the microphone is stopped when the chat component is destroyed
6407
6718
  this.chatFooterComponent?.stopMic();
6408
6719
  this.checkPerformanceAnalysis();
@@ -6472,7 +6783,8 @@ class DCChatComponent {
6472
6783
  ConversationFlowStateService,
6473
6784
  DynamicFlowService,
6474
6785
  AgentCardStateService,
6475
- ChatMonitorService,
6786
+ // ChatMonitorService removido del providers[] local: ahora la instancia la provee el
6787
+ // componente ancestro (ConversationCardDetailsComponent) para compartirla con dc-agent-card-avatar.
6476
6788
  ContextEngineService,
6477
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 }); }
6478
6790
  }
@@ -6499,7 +6811,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
6499
6811
  ConversationFlowStateService,
6500
6812
  DynamicFlowService,
6501
6813
  AgentCardStateService,
6502
- ChatMonitorService,
6814
+ // ChatMonitorService removido del providers[] local: ahora la instancia la provee el
6815
+ // componente ancestro (ConversationCardDetailsComponent) para compartirla con dc-agent-card-avatar.
6503
6816
  ContextEngineService,
6504
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"] }]
6505
6818
  }], ctorParameters: () => [], propDecorators: { chatFooterComponent: [{
@@ -7492,7 +7805,7 @@ class CharacterFormGroupService {
7492
7805
  conversationFlow: this.createConversationFlowFormGroup(),
7493
7806
  manageable: this.formUtils.createManageableFormGroup(),
7494
7807
  learnable: this.formUtils.createLearnableFormGroup(),
7495
- voiceCloning: this.createVoiceCloningFormGroup(),
7808
+ voice: this.createAgentVoiceFormGroup(),
7496
7809
  agenticConfig: this.createAgenticConfigFormGroup(),
7497
7810
  live2d: [null],
7498
7811
  avatarType: [null],
@@ -7500,6 +7813,22 @@ class CharacterFormGroupService {
7500
7813
  }
7501
7814
  patchFormWithConversationData(form, agentCard) {
7502
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
+ }
7503
7832
  // Patch dynamicConditions FormArray after the main form is patched
7504
7833
  const dynamicConditionsFormArray = form.get('conversationFlow.dynamicConditions');
7505
7834
  dynamicConditionsFormArray.clear(); // Clear any existing default items
@@ -7669,8 +7998,6 @@ class CharacterFormGroupService {
7669
7998
  userMustStart: [false],
7670
7999
  streamResponses: [false],
7671
8000
  displayMode: ['chat'],
7672
- mainVoice: this.createVoiceTTSFormGroup(),
7673
- secondaryVoice: this.createVoiceTTSFormGroup(),
7674
8001
  model: createAIModelFormGroup(),
7675
8002
  rules: this.fb.array([]),
7676
8003
  chatMode: ['regular'],
@@ -7773,11 +8100,14 @@ class CharacterFormGroupService {
7773
8100
  emoji: [criteria?.emoji || ''],
7774
8101
  });
7775
8102
  }
7776
- createVoiceCloningFormGroup() {
8103
+ createAgentVoiceFormGroup(voice) {
7777
8104
  return this.fb.group({
7778
- sample: [],
7779
- transcription: [''],
7780
- 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
+ })
7781
8111
  });
7782
8112
  }
7783
8113
  createAgenticConfigFormGroup() {
@@ -8324,47 +8654,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8324
8654
  type: Output
8325
8655
  }] } });
8326
8656
 
8327
- class DcVoiceTtsFormComponent {
8328
- constructor() {
8329
- this.dialogService = inject(DialogService);
8330
- this.cdr = inject(ChangeDetectorRef);
8331
- this.form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
8332
- this.title = input('Voice TTS Settings', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
8333
- this.voiceTTSOptions = input([], ...(ngDevMode ? [{ debugName: "voiceTTSOptions" }] : /* istanbul ignore next */ []));
8334
- this.fullForm = input(false, ...(ngDevMode ? [{ debugName: "fullForm" }] : /* istanbul ignore next */ []));
8335
- this.providerOptions = [
8336
- { label: 'Google', value: 'google' },
8337
- { label: 'Fish Audio', value: 'fish-audio' },
8338
- ];
8339
- }
8340
- openVoiceSelector() {
8341
- const voiceControl = this.form().get('voice');
8342
- const dialogRef = this.dialogService.open(VoiceSelectorComponent, {
8343
- header: 'Select Voice',
8344
- width: '50vw',
8345
- maximizable: true,
8346
- height: '500px',
8347
- contentStyle: { 'max-height': '90vh', overflow: 'auto', 'min-height': '500px' },
8348
- baseZIndex: 10000,
8349
- closable: true,
8350
- modal: true,
8351
- data: { currentVoiceId: voiceControl?.value },
8352
- });
8353
- dialogRef.onClose.subscribe((selectedVoiceId) => {
8354
- if (selectedVoiceId) {
8355
- voiceControl?.setValue(selectedVoiceId);
8356
- this.cdr.detectChanges();
8357
- }
8358
- });
8359
- }
8360
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8361
- 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 }] }); }
8362
- }
8363
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcVoiceTtsFormComponent, decorators: [{
8364
- type: Component,
8365
- 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" }]
8366
- }], 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 }] }] } });
8367
-
8368
8657
  class PromptConversationTypesDialogComponent {
8369
8658
  constructor() {
8370
8659
  this.dialogRef = inject(DynamicDialogRef);
@@ -8434,12 +8723,6 @@ class DcConversationSettingsFormComponent {
8434
8723
  get rulesFormArray() {
8435
8724
  return this.form.get('rules');
8436
8725
  }
8437
- get mainVoiceFormGroup() {
8438
- return this.form.get('mainVoice');
8439
- }
8440
- get secondaryVoiceFormGroup() {
8441
- return this.form.get('secondaryVoice');
8442
- }
8443
8726
  get modelFormGroup() {
8444
8727
  return this.form.get('model');
8445
8728
  }
@@ -8471,7 +8754,7 @@ class DcConversationSettingsFormComponent {
8471
8754
  this.getRules();
8472
8755
  }
8473
8756
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcConversationSettingsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
8474
- 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"] }] }); }
8475
8758
  }
8476
8759
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcConversationSettingsFormComponent, decorators: [{
8477
8760
  type: Component,
@@ -8485,8 +8768,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8485
8768
  PopoverModule,
8486
8769
  DynamicDialogModule,
8487
8770
  ModelSelectorComponent,
8488
- DcVoiceTtsFormComponent,
8489
- ], 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" }]
8490
8772
  }], propDecorators: { form: [{
8491
8773
  type: Input
8492
8774
  }], textEngineOptions: [{
@@ -8497,6 +8779,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8497
8779
  type: Input
8498
8780
  }] } });
8499
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
+
8500
8823
  class DCAgentCardFormComponent extends EntityBaseFormComponent {
8501
8824
  constructor() {
8502
8825
  super(...arguments);
@@ -8535,14 +8858,14 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8535
8858
  this.isGenerating = signal(false, ...(ngDevMode ? [{ debugName: "isGenerating" }] : /* istanbul ignore next */ []));
8536
8859
  this.isCloningVoice = signal(false, ...(ngDevMode ? [{ debugName: "isCloningVoice" }] : /* istanbul ignore next */ []));
8537
8860
  this.canCloneVoice = computed(() => {
8538
- const sampleUrl = this.form.get('voiceCloning.sample')?.value?.url;
8539
- 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;
8540
8863
  const canCloneVoiceVar = !!sampleUrl && !voiceId;
8541
8864
  return canCloneVoiceVar;
8542
8865
  }, ...(ngDevMode ? [{ debugName: "canCloneVoice" }] : /* istanbul ignore next */ []));
8543
8866
  this.fishModelId = computed(() => {
8544
- const voiceId = this.form.get('voiceCloning.main.voice')?.value;
8545
- 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;
8546
8869
  return voiceId && provider === 'fish-audio' ? voiceId : null;
8547
8870
  }, ...(ngDevMode ? [{ debugName: "fishModelId" }] : /* istanbul ignore next */ []));
8548
8871
  }
@@ -8765,15 +9088,15 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8765
9088
  }
8766
9089
  onFileUploaded(event) {
8767
9090
  console.log('File uploaded', event);
8768
- this.form.controls.voiceCloning.patchValue({ sample: event });
9091
+ this.form.get('voice.cloning')?.patchValue({ sample: event });
8769
9092
  }
8770
9093
  async cloneVoice() {
8771
- const sample = this.form.get('voiceCloning.sample')?.value;
9094
+ const sample = this.form.get('voice.cloning.sample')?.value;
8772
9095
  if (!sample?.url) {
8773
9096
  this.toastService?.error({ title: 'Falta el sample', subtitle: 'Sube un audio antes de crear el modelo' });
8774
9097
  return;
8775
9098
  }
8776
- const text = this.form.get('voiceCloning.transcription')?.value || '';
9099
+ const text = this.form.get('voice.cloning.transcription')?.value || '';
8777
9100
  const agentName = this.form.get('name')?.value || 'Polilan';
8778
9101
  const title = `${agentName} — voice`;
8779
9102
  this.isCloningVoice.set(true);
@@ -8784,9 +9107,9 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8784
9107
  text,
8785
9108
  trainMode: 'fast',
8786
9109
  });
8787
- const mainCtrl = this.form.get('voiceCloning.main');
8788
- mainCtrl?.patchValue({ voice: res._id, provider: 'fish-audio' });
8789
- mainCtrl?.markAsDirty();
9110
+ const newMainCtrl = this.form.get('voice.main');
9111
+ newMainCtrl?.patchValue({ voice: res._id, provider: 'fish-audio' });
9112
+ newMainCtrl?.markAsDirty();
8790
9113
  this.toastService?.success({ title: 'Modelo de voz creado', subtitle: res._id });
8791
9114
  }
8792
9115
  catch (e) {
@@ -8807,7 +9130,7 @@ class DCAgentCardFormComponent extends EntityBaseFormComponent {
8807
9130
  this.router.navigate(['../../cards-creator'], { relativeTo: this.route });
8808
9131
  }
8809
9132
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DCAgentCardFormComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
8810
- 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"] }] }); }
8811
9134
  }
8812
9135
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DCAgentCardFormComponent, decorators: [{
8813
9136
  type: Component,
@@ -8835,7 +9158,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
8835
9158
  SimpleUploaderComponent,
8836
9159
  TextareaModule,
8837
9160
  ModelSelectorComponent,
8838
- ], 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"] }]
8839
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"] }] } });
8840
9163
 
8841
9164
  class TruncatePipe {
@@ -9010,7 +9333,7 @@ class AgentCardListComponent extends EntityBaseListV2Component {
9010
9333
  const adminFilters = [
9011
9334
  {
9012
9335
  name: 'Voz Única',
9013
- field: 'voiceCloning.sample',
9336
+ field: 'voice.cloning.sample',
9014
9337
  type: 'select',
9015
9338
  operator: '$not_null',
9016
9339
  options: [
@@ -9116,6 +9439,7 @@ class Live2dModelComponent {
9116
9439
  // Outputs (using new Angular output API)
9117
9440
  this.modelLoaded = output();
9118
9441
  this.viewStateChanged = output();
9442
+ this.live2dConfig = inject(LIVE2D_CONFIG, { optional: true });
9119
9443
  this.modelParameters = null;
9120
9444
  this.modelParts = null;
9121
9445
  this.motionGroups = [];
@@ -9156,7 +9480,12 @@ class Live2dModelComponent {
9156
9480
  this.isViewInitialized.set(true);
9157
9481
  }
9158
9482
  async initializeCanvas() {
9159
- this.app = new Application();
9483
+ if (!this.live2dConfig) {
9484
+ console.warn('Live2D configuration not provided. Please call provideLive2D in your app bootstrap.');
9485
+ return;
9486
+ }
9487
+ const PIXI = await this.live2dConfig.PIXI();
9488
+ this.app = new PIXI.Application();
9160
9489
  if (typeof this.app.init === 'function') {
9161
9490
  await this.app.init({
9162
9491
  canvas: this.canvasRef.nativeElement,
@@ -9173,6 +9502,13 @@ class Live2dModelComponent {
9173
9502
  }
9174
9503
  }
9175
9504
  async loadModel(modelPath) {
9505
+ if (!this.live2dConfig) {
9506
+ console.warn('Live2D configuration not provided. Please call provideLive2D in your app bootstrap.');
9507
+ return;
9508
+ }
9509
+ const PIXI = await this.live2dConfig.PIXI();
9510
+ const live2dEngine = await this.live2dConfig.Live2DModel();
9511
+ const Live2DModel = live2dEngine.Live2DModel;
9176
9512
  if (this.model) {
9177
9513
  this.app.stage.removeChild(this.model);
9178
9514
  this.model.destroy();
@@ -9254,7 +9590,7 @@ class Live2dModelComponent {
9254
9590
  this.playAnimation(group);
9255
9591
  }
9256
9592
  }
9257
- speak(audioUrl, options) {
9593
+ async speak(audioUrl, options) {
9258
9594
  if (!this.model)
9259
9595
  return;
9260
9596
  if (options?.focus !== false) {
@@ -9263,8 +9599,21 @@ class Live2dModelComponent {
9263
9599
  this.viewStateChanged.emit(transform);
9264
9600
  }
9265
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
+ }
9266
9615
  this.model.speak(audioUrl, {
9267
- volume: 0,
9616
+ volume: targetVolume,
9268
9617
  crossOrigin: options?.crossOrigin || 'anonymous',
9269
9618
  });
9270
9619
  }
@@ -9423,7 +9772,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
9423
9772
  args: ['canvas']
9424
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"] }] } });
9425
9774
 
9426
- class DcAgentCardConverseComponent {
9775
+ class DcAgentCardAvatarComponent {
9427
9776
  set isConversationActive(val) {
9428
9777
  this._isConversationActive.set(val);
9429
9778
  }
@@ -9445,8 +9794,8 @@ class DcAgentCardConverseComponent {
9445
9794
  this.videoPlayerService = inject(VideoPlayerNativeService);
9446
9795
  this.agentCardId = '';
9447
9796
  this.useNativePlayer = true;
9448
- this.activeAudioMessage = null;
9449
9797
  this._isConversationActive = signal(false, ...(ngDevMode ? [{ debugName: "_isConversationActive" }] : /* istanbul ignore next */ []));
9798
+ /** @deprecated Use ChatMonitorService instead — el canal oficial es el servicio compartido. */
9450
9799
  this.onStartConversation = output();
9451
9800
  this.videoPlayerA = viewChild('videoPlayerA', ...(ngDevMode ? [{ debugName: "videoPlayerA" }] : /* istanbul ignore next */ []));
9452
9801
  this.videoPlayerB = viewChild('videoPlayerB', ...(ngDevMode ? [{ debugName: "videoPlayerB" }] : /* istanbul ignore next */ []));
@@ -9527,25 +9876,48 @@ class DcAgentCardConverseComponent {
9527
9876
  this.videoPlayerService.setAgentCard(card);
9528
9877
  }
9529
9878
  });
9530
- // Live2D Speak (Lip-Sync) effect
9879
+ // Live2D Speak (Lip-Sync) effect — listening directly to ChatMonitorService's activeAudioMessage
9531
9880
  effect(() => {
9532
- 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);
9533
9886
  const type = this.avatarType();
9534
9887
  const l2d = this.live2dModel();
9535
- if (type === 'live2d' && audioMsg?.audioUrl && l2d) {
9536
- l2d.stopSpeaking();
9537
- 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
+ }
9538
9898
  }
9539
9899
  });
9540
9900
  // Live2D Mood state emotion updates mapping
9541
9901
  effect(() => {
9542
- const mood = this.conversationFlowStateService.flowState().moodState?.value;
9902
+ const mood = this.chatMonitorService.currentMood();
9543
9903
  const type = this.avatarType();
9544
9904
  const l2d = this.live2dModel();
9545
9905
  if (type === 'live2d' && mood && l2d) {
9546
9906
  l2d.setExpression(mood);
9547
9907
  }
9548
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
+ });
9549
9921
  }
9550
9922
  ngOnDestroy() {
9551
9923
  this.videoPlayerService.cleanUp();
@@ -9609,6 +9981,18 @@ class DcAgentCardConverseComponent {
9609
9981
  cardTranslated.conversationSettings.secondaryVoice.voice = transformedSecondaryVoice;
9610
9982
  }
9611
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
+ }
9612
9996
  return cardTranslated;
9613
9997
  }
9614
9998
  _transformVoice(voice, locale) {
@@ -9639,18 +10023,16 @@ class DcAgentCardConverseComponent {
9639
10023
  toggleInfoLayer() {
9640
10024
  this.showInfoLayer.update((value) => !value);
9641
10025
  }
9642
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DcAgentCardConverseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
9643
- 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" }] }); }
9644
10028
  }
9645
- 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: [{
9646
10030
  type: Component,
9647
- 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"] }]
9648
10032
  }], ctorParameters: () => [], propDecorators: { agentCardId: [{
9649
10033
  type: Input
9650
10034
  }], useNativePlayer: [{
9651
10035
  type: Input
9652
- }], activeAudioMessage: [{
9653
- type: Input
9654
10036
  }], isConversationActive: [{
9655
10037
  type: Input
9656
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 }] }] } });
@@ -10272,5 +10654,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
10272
10654
  * Generated bundle index. Do not edit.
10273
10655
  */
10274
10656
 
10275
- 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, 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, 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 };
10276
10658
  //# sourceMappingURL=dataclouder-ngx-agent-cards.mjs.map