import { Component, input, output, ContentChild, TemplateRef, Type, ChangeDetectionStrategy, ViewEncapsulation, computed, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { CopilotSlot } from "../../slots/copilot-slot"; import type { Message } from "@ag-ui/core"; import { CopilotChatAssistantMessage } from "./copilot-chat-assistant-message"; import { CopilotChatUserMessage } from "./copilot-chat-user-message"; import { CopilotChatMessageViewCursor } from "./copilot-chat-message-view-cursor"; import { cn } from "../../utils"; /** * CopilotChatMessageView component - Angular port of the React component. * Renders a list of chat messages with support for custom slots and layouts. * DOM structure and Tailwind classes match the React implementation exactly. */ @Component({ selector: "copilot-chat-message-view", standalone: true, host: { "data-copilotkit": "" }, imports: [ CommonModule, CopilotSlot, CopilotChatAssistantMessage, CopilotChatUserMessage, CopilotChatMessageViewCursor, ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: ` @if (customLayoutTemplate) { } @else {
@for (message of messagesValue(); track trackByMessageId($index, message)) { @if (message && message.role === "assistant") { @if (assistantMessageComponent() || assistantMessageTemplate()) { } @else { } } @else if (message && message.role === "user") { @if (userMessageComponent() || userMessageTemplate()) { } @else { } } } @if (showCursorValue()) { @if (cursorComponent() || cursorTemplate()) { } @else { } }
} `, }) export class CopilotChatMessageView { // Core inputs matching React props messages = input([]); showCursor = input(false); isLoading = input(false); inputClass = input(); // Handler availability handled via DI service // Assistant message slot inputs assistantMessageComponent = input | undefined>(); assistantMessageTemplate = input | undefined>(); assistantMessageClass = input(); // User message slot inputs userMessageComponent = input | undefined>(); userMessageTemplate = input | undefined>(); userMessageClass = input(); // Cursor slot inputs cursorComponent = input | undefined>(); cursorTemplate = input | undefined>(); cursorClass = input(); // Custom layout template (render prop pattern) @ContentChild("customLayout") customLayoutTemplate?: TemplateRef; // Output events (bubbled from child components) assistantMessageThumbsUp = output<{ message: Message }>(); assistantMessageThumbsDown = output<{ message: Message }>(); assistantMessageReadAloud = output<{ message: Message }>(); assistantMessageRegenerate = output<{ message: Message }>(); userMessageCopy = output<{ message: Message }>(); userMessageEdit = output<{ message: Message }>(); // Default components for slots protected readonly defaultAssistantComponent = CopilotChatAssistantMessage; protected readonly defaultUserComponent = CopilotChatUserMessage; protected readonly defaultCursorComponent = CopilotChatMessageViewCursor; // Derived values from inputs protected messagesValue = computed(() => this.messages()); protected showCursorValue = computed(() => this.showCursor()); protected isLoadingValue = computed(() => this.isLoading()); // Computed class matching React: twMerge("flex flex-col", className) computedClass = computed(() => cn("flex flex-col", this.inputClass())); // Layout context for custom templates (render prop pattern) layoutContext = computed(() => ({ isLoading: this.isLoadingValue(), messages: this.messagesValue(), showCursor: this.showCursorValue(), messageElements: this.messagesValue().filter( (m) => m && (m.role === "assistant" || m.role === "user"), ), })); // Slot resolution computed signals assistantMessageSlot = computed( () => this.assistantMessageComponent() || this.assistantMessageClass(), ); userMessageSlot = computed( () => this.userMessageComponent() || this.userMessageClass(), ); cursorSlot = computed(() => this.cursorComponent() || this.cursorClass()); // Props merging helpers mergeAssistantProps(message: Message) { return { message, messages: this.messagesValue(), isLoading: this.isLoadingValue(), inputClass: this.assistantMessageClass(), }; } mergeUserProps(message: Message) { return { message, inputClass: this.userMessageClass(), }; } // TrackBy function for performance optimization trackByMessageId(index: number, message: Message): string { return message?.id || `index-${index}`; } constructor() {} // Event handlers - just pass them through handleAssistantThumbsUp(event: { message: Message }): void { this.assistantMessageThumbsUp.emit(event); } handleAssistantThumbsDown(event: { message: Message }): void { this.assistantMessageThumbsDown.emit(event); } handleAssistantReadAloud(event: { message: Message }): void { this.assistantMessageReadAloud.emit(event); } handleAssistantRegenerate(event: { message: Message }): void { this.assistantMessageRegenerate.emit(event); } }