@@ -7,6 +7,11 @@ import {
77} from "@angular/core" ;
88import { CommonModule } from "@angular/common" ;
99import { cn } from "../../utils" ;
10+ import type { BinaryInputContent , InputContent } from "@ag-ui/client" ;
11+ import {
12+ getUserMessageBinaryContents ,
13+ getUserMessageTextContent ,
14+ } from "@copilotkitnext/shared" ;
1015
1116@Component ( {
1217 selector : "copilot-chat-user-message-renderer" ,
@@ -17,10 +22,54 @@ import { cn } from "../../utils";
1722 host : {
1823 "[class]" : "computedClass()" ,
1924 } ,
20- template : `{{ content() }}` ,
25+ template : `
26+ @if (textContent()) {
27+ <span>{{ textContent() }}</span>
28+ }
29+ @if (attachments().length) {
30+ <div [class]="attachmentsClass()">
31+ @for (attachment of attachments(); track trackAttachment(attachment, $index)) {
32+ <ng-container *ngIf="isImage(attachment); else fileTemplate">
33+ <figure class="flex flex-col gap-1">
34+ <img
35+ [src]="resolveSource(attachment)"
36+ [alt]="attachment.filename || attachment.id || attachment.mimeType"
37+ class="max-h-64 rounded-lg border border-border object-contain"
38+ />
39+ @if (attachment.filename || attachment.id) {
40+ <figcaption class="text-xs text-muted-foreground">
41+ {{ attachment.filename || attachment.id }}
42+ </figcaption>
43+ }
44+ </figure>
45+ </ng-container>
46+ <ng-template #fileTemplate>
47+ <div class="rounded-md border border-dashed border-border bg-muted/70 px-3 py-2 text-xs text-muted-foreground">
48+ {{ attachment.filename || attachment.id || 'Attachment' }}
49+ <span class="block text-[10px] uppercase tracking-wide text-muted-foreground/70">
50+ {{ attachment.mimeType }}
51+ </span>
52+ @if (resolveSource(attachment) && !isImage(attachment)) {
53+ <a
54+ [href]="resolveSource(attachment)"
55+ target="_blank"
56+ rel="noreferrer"
57+ class="mt-1 block text-xs text-primary underline"
58+ >
59+ Open
60+ </a>
61+ }
62+ </div>
63+ </ng-template>
64+ }
65+ </div>
66+ }
67+ ` ,
2168} )
2269export class CopilotChatUserMessageRenderer {
2370 readonly content = input < string > ( "" ) ;
71+ readonly contents = input < InputContent [ ] > ( [ ] ) ;
72+ readonly attachments = input < BinaryInputContent [ ] | undefined > ( undefined ) ;
2473 readonly inputClass = input < string | undefined > ( ) ;
2574
2675 readonly computedClass = computed ( ( ) => {
@@ -29,4 +78,44 @@ export class CopilotChatUserMessageRenderer {
2978 this . inputClass ( )
3079 ) ;
3180 } ) ;
81+
82+ readonly textContent = computed ( ( ) => {
83+ const explicit = this . content ( ) ;
84+ if ( explicit && explicit . length > 0 ) {
85+ return explicit ;
86+ }
87+ return getUserMessageTextContent ( this . contents ( ) ) ;
88+ } ) ;
89+
90+ readonly attachments = computed ( ( ) => {
91+ const provided = this . attachments ( ) ?? [ ] ;
92+ if ( provided . length > 0 ) {
93+ return provided ;
94+ }
95+ return getUserMessageBinaryContents ( this . contents ( ) ) ;
96+ } ) ;
97+
98+ readonly attachmentsClass = computed ( ( ) =>
99+ this . textContent ( ) . trim ( ) . length > 0
100+ ? "mt-3 flex flex-col gap-2"
101+ : "flex flex-col gap-2" ,
102+ ) ;
103+
104+ resolveSource ( attachment : BinaryInputContent ) : string | null {
105+ if ( attachment . url ) {
106+ return attachment . url ;
107+ }
108+ if ( attachment . data ) {
109+ return `data:${ attachment . mimeType } ;base64,${ attachment . data } ` ;
110+ }
111+ return null ;
112+ }
113+
114+ isImage ( attachment : BinaryInputContent ) : boolean {
115+ return attachment . mimeType . startsWith ( "image/" ) && ! ! this . resolveSource ( attachment ) ;
116+ }
117+
118+ trackAttachment ( attachment : BinaryInputContent , index : number ) : string {
119+ return attachment . id ?? attachment . url ?? attachment . filename ?? index . toString ( ) ;
120+ }
32121}
0 commit comments