import {
Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges,
ElementRef, ViewChild
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
LucideAngularModule, Send, Sparkles, Trash2, BookmarkPlus, Square
} from 'lucide-angular';
import { Subscription } from 'rxjs';
import { AiChatService, ChatMessage } from '../../services/ai-chat.service';
/**
* Panneau de chat IA pour le mode jeu.
*
*
Diffère du {@link AiChatDrawerComponent} :
* - conversation 100% éphémère (le journal joue le rôle de mémoire persistante)
* - intégré dans le panneau latéral, pas en drawer
* - chaque réponse peut être ajoutée au journal en un clic (event {@link saveToJournal})
*
* Le backend reçoit le contexte complet via {@code /api/ai/chat/stream-session} :
* lore + campagne + GameSystem + journal — l'IA "sait" tout ce qui s'est passé.
*/
@Component({
selector: 'app-session-ai-chat-panel',
imports: [FormsModule, LucideAngularModule],
templateUrl: './session-ai-chat-panel.component.html',
styleUrls: ['./session-ai-chat-panel.component.scss']
})
export class SessionAiChatPanelComponent implements OnChanges, OnDestroy {
readonly Send = Send;
readonly Sparkles = Sparkles;
readonly Trash2 = Trash2;
readonly BookmarkPlus = BookmarkPlus;
readonly Square = Square;
@Input() sessionId!: string;
@Input() canSaveToJournal = true;
/** Émis quand le MJ clique "Ajouter au journal" sur une réponse. */
@Output() saveToJournal = new EventEmitter();
@ViewChild('messagesContainer') messagesContainer?: ElementRef;
messages: ChatMessage[] = [];
currentAssistantText = '';
input = '';
isStreaming = false;
error: string | null = null;
private streamSub: Subscription | null = null;
constructor(private aiChat: AiChatService) {}
ngOnChanges(changes: SimpleChanges): void {
// Reset complet si on change de session (changement d'instance jouée).
if (changes['sessionId'] && !changes['sessionId'].firstChange) {
this.cancelStream();
this.messages = [];
this.currentAssistantText = '';
this.error = null;
}
}
send(): void {
const text = this.input.trim();
if (!text || this.isStreaming || !this.sessionId) return;
this.messages = [...this.messages, { role: 'user', content: text }];
this.input = '';
this.error = null;
this.startStream();
}
private startStream(): void {
this.isStreaming = true;
this.currentAssistantText = '';
this.scrollToBottomSoon();
this.streamSub = this.aiChat.streamChatForSession(this.sessionId, this.messages).subscribe({
next: (event) => {
if (event.type === 'token') {
this.currentAssistantText += event.value;
this.scrollToBottomSoon();
} else if (event.type === 'done') {
this.finishAssistantMessage();
}
},
error: (err: unknown) => {
const message = err instanceof Error ? err.message : 'Erreur inconnue';
this.error = `Erreur IA : ${message}`;
this.isStreaming = false;
this.streamSub = null;
},
complete: () => {
this.finishAssistantMessage();
}
});
}
private finishAssistantMessage(): void {
if (this.currentAssistantText.trim()) {
this.messages = [...this.messages, { role: 'assistant', content: this.currentAssistantText }];
}
this.currentAssistantText = '';
this.isStreaming = false;
this.streamSub = null;
this.scrollToBottomSoon();
}
cancelStream(): void {
if (this.streamSub) {
this.streamSub.unsubscribe();
this.streamSub = null;
}
// On garde ce qui a déjà été streamé : utile si l'IA partait dans le mur.
if (this.currentAssistantText.trim()) {
this.messages = [...this.messages, { role: 'assistant', content: this.currentAssistantText + ' [interrompu]' }];
}
this.currentAssistantText = '';
this.isStreaming = false;
}
clearConversation(): void {
this.cancelStream();
this.messages = [];
this.error = null;
}
onSaveToJournal(content: string): void {
if (!this.canSaveToJournal) return;
this.saveToJournal.emit(content);
}
/** Scroll vers le bas après cycle de change detection — preuve d'affichage du dernier token. */
private scrollToBottomSoon(): void {
queueMicrotask(() => {
const el = this.messagesContainer?.nativeElement;
if (el) el.scrollTop = el.scrollHeight;
});
}
ngOnDestroy(): void {
this.cancelStream();
}
}